Skip to main content

10. Capability model: closed set, manifest-declared, target-enforced

Status: research note. Date: 2026-05-29 (GMT+7). Mirrors: deployed to /docs/research/0057/capability-model.

This note specifies the package-boundary capability model. The "why capabilities" rationale is in 02-design-philosophy §6; comparable systems are surveyed below.

1. The closed capability set​

Mochi-57 ships with exactly nine capability identifiers in v1:

IdentifierMeaning
fs.readRead files (any path)
fs.writeCreate / modify / delete files
net.dialOutbound network connections (TCP, UDP, HTTPS clients)
net.listenInbound network sockets (servers, listeners)
envRead / write process environment variables
ffiCall into target-language FFI (Go, Python, JS, JVM, ...)
clockRead the wall clock / monotonic clock
randomRead non-deterministic randomness (OS RNG)
proc.spawnStart subprocesses

Closed set: extension requires a MEP. Open extension is rejected for the same reasons Deno's permissions, Pony's reference capabilities, and Wasm WASI's interface set are closed: a fragmented vocabulary is unauditable.

1.1 Capabilities specifically not included v1​

  • fs.read.path, net.dial.host: scoped capabilities are tempting but explode the vocabulary. Held for v2 after measuring real usage.
  • crypto: cryptographic primitives are pure in v1 (no key material is a capability). Held for v2 when secure-enclave APIs land.
  • gpu, audio, display: target-specific concerns deferred to target MEPs (Swift MEP-49 for iOS, Kotlin MEP-50 for Android).
  • time.realtime: covered by clock in v1; specialised real-time capability deferred.
  • db, http.server, kafka: library-level concerns, not OS-level. Stay in the library API surface, not the capability vocabulary.

The v1 set is the minimum that catches the supply-chain delta classes documented by NodeShield's CBOM paper (2025): network access added, file write added, subprocess spawn added.

2. Declaring capabilities​

A package declares the set of capabilities it requires in mochi.toml:

[capabilities]
required = ["fs.read", "net.dial"]
optional = ["proc.spawn"]
  • required capabilities must be available at runtime; their absence is a startup error.
  • optional capabilities are documented but not asserted; their absence triggers a capability_missing runtime fault if the dep tries to use one.

Declaration is per-package, not per-module. A package author cannot say "module X uses fs.read but module Y does not"; the capability is asserted at the package boundary. This matches Roc's platform model and Pony's package-level capability annotations.

2.1 Capability inference is out of scope​

Static derivation of capabilities (scan the source for import std/fs and infer fs.read) is not part of MEP-57. Reasons:

  • Requires a Mochi-level effect system, which is a separate MEP candidate.
  • Cannot detect FFI-introduced effects without target-specific analysis.
  • Conservatism would over-declare; under-declaration would be unsound.

Instead, the declared set is a publisher promise. mochi lint capabilities is a future linter (deferred) that detects obvious gaps; v1 is publisher discipline + consumer audit.

3. Consumer-side pinning​

A consumer can pin the capability subset a dep is permitted to use:

[dependencies]
"@mochi/json" = { version = "^1.2", capabilities = ["fs.read"] }

If a candidate version's [capabilities].required set is not a subset of ["fs.read"], the solver rejects it with a cap_excluded incompatibility. See 05-solver-design §5.1.

The consumer's manifest can also pin the global allowed set:

[capabilities]
allowed = ["fs.read", "net.dial"]

Now any transitive dep whose required set escapes ["fs.read", "net.dial"] triggers a solver failure. This is the "I am not OK with anything in my tree opening a network socket, full stop" surface.

4. The lockfile capabilities_seen annotation​

After a successful resolution, the lockfile records the per-package required capability set:

[capabilities_seen]
"@mochi/strings" = []
"@mochi/json" = ["fs.read"]

On mochi update, if a candidate version would add a capability not in capabilities_seen for that package, the solver fails until the user explicitly accepts:

$ mochi update @mochi/json
warning: @mochi/json 1.3.0 newly requires capability "net.dial"
Previously seen capabilities for @mochi/json: ["fs.read"]
Accept with: mochi lock --accept-capabilities=@mochi/json
Or pin to a version not adding the capability: mochi update @mochi/json --max=1.2

This is the supply-chain delta signal: a previously safe library adding network access (the xz-utils pattern) becomes a visible event in CI.

5. Enforcement per target​

The capability declaration is informational unless a target enforces it. The four enforcement points:

5.1 TypeScript / Deno target​

The Deno permission model maps directly:

CapabilityDeno flag
fs.read--allow-read
fs.write--allow-write
net.dial--allow-net=<host:port>,...
net.listen--allow-net=<host:port>,...
env--allow-env
ffi--allow-ffi
clock(no Deno flag; always available)
random(no Deno flag; always available)
proc.spawn--allow-run=<cmd>

The publish pipeline emits a deno.json with the inferred permission block:

{
"tasks": {
"start": "deno run --allow-read --allow-net main.ts"
}
}

Consumers running the artifact get the Deno permission prompt at startup if any required permission is not granted.

For Node.js / Bun targets: capabilities are documented in the package README and package.json. v1 does not enforce at runtime (Node 22's experimental permission model is still flag-gated as of 2026). v2 will adopt when stable.

5.2 Python target​

A runtime shim wraps capability-sensitive calls:

# generated mochi_runtime/caps.py
from mochi_runtime import _check_capability

def fs_read(path):
_check_capability("fs.read")
return open(path, "rb").read()

The runtime reads the capability set from a mochi_caps.json sidecar at process start. Missing capabilities raise MochiCapabilityError (a subclass of PermissionError).

This is not a sandbox: a determined adversary in Python can bypass via raw syscalls. The enforcement is audit enforcement, not security enforcement. Pair with OS-level sandboxing (Linux user namespaces, macOS sandbox-exec) for security.

5.3 Wasm component target​

When MEP-55 (Wasm component target) lands, capabilities map to component-model imports:

Capabilitywasi: interface
fs.readwasi:filesystem/types
net.dialwasi:sockets/tcp
envwasi:cli/environment
clockwasi:clocks/wall-clock
randomwasi:random/random

The host provides only the interfaces declared; the Wasm component cannot import others. This is the strongest enforcement model and the v2 direction for the runtime.

5.4 VM3 (Mochi VM) path​

V1: capabilities are logged (mochi run --trace-caps shows usage). Not enforced. V2 candidate: a Mochi-level effect system that statically tracks capability usage and enforces at VM dispatch.

The v1 behaviour is transparency, not security: the user can see what their program asked for. The transition to enforcement is a separate MEP.

6. Comparable systems​

6.1 Deno permissions​

Deno (2018+) ships flag-gated permissions on the CLI: --allow-read, --allow-write, --allow-net, --allow-env, --allow-run, --allow-ffi. Path / host scoping via --allow-read=/etc,/var, --allow-net=api.example.com.

Mochi-57 borrows the closed-set design. The path / host scoping is held for v2 (vocabulary explosion vs benefit not yet justified for Mochi's use cases).

6.2 Roc platforms​

Roc's compile-time effects are declared per platform: a platform decides which effects are available. Packages declare effects they require; the linker verifies subset.

Mochi-57 borrows the platform-declares-effects model conceptually but uses manifest declaration rather than type-level effects (effect system is a separate MEP).

6.3 Pony reference capabilities​

Pony (2014+) has reference capabilities (val, ref, iso, tag) at the type system level. These are about shared-memory concurrency, not OS effects, but the "every reference carries an explicit capability" idea inspired Roc and Mochi.

6.4 Wasm Component Model​

The Component Model (W3C, 2024 stable) is the most powerful model: imports are declared in the component's WIT (WebAssembly Interface Type) and the host provides only the declared interfaces. Enforcement is at link time, not runtime.

Mochi-57 aligns: in the Wasm target (MEP-55 candidate), the capability declaration in mochi.toml becomes the import list in the WIT.

6.5 Lavamoat / NodeShield (npm)​

Lavamoat (MetaMask, 2019+) and NodeShield (2025) instrument require/import to enforce per-package permission boundaries in Node.js. NodeShield's 2025 paper proposes a "Capability Bill of Materials" (CBOM) extending SBOM with capability data.

Mochi-57's capabilities_seen lockfile annotation is the CBOM. The Sigstore-bound SBOM emitted by mochi build --sbom includes the capability set (Phase 15).

6.6 Java SecurityManager (deprecated 2021)​

JDK's SecurityManager (1.0, 1995; deprecated JEP 411, 2021) enforced per-class permissions at runtime. Deprecation reasons:

  • Performance: every system call paid a permission check.
  • Complexity: policy files were unworkable in practice.
  • Bypass: reflection routinely defeated the model.

Lessons for Mochi-57:

  • Capabilities at the package boundary (declared once per dep) avoid the per-call overhead.
  • Manifest format avoids policy-file fragmentation.
  • The audit-only stance in v1 sidesteps the bypass problem; security enforcement waits for a target with strong sandbox primitives (Wasm components, Deno).

6.7 Cargo's [features] model​

Cargo features are additive opt-in for code paths. They are not capabilities (they do not bound OS effects). Mochi's [features] is parallel: it enables optional code paths; [capabilities] is the separate concern.

7. The xz-utils pattern check​

The supply-chain attack on xz-utils (CVE-2024-3094, March 2024) added behaviour to 5.6.0 that 5.5.x did not have: the malicious code disabled ifunc resolution to inject into sshd. From a capability perspective, 5.6.0 added effects (proc-level memory write via ifunc, network behaviour via the sshd hook) that previous versions did not have.

If xz-utils had shipped with a capability declaration, the 5.6.0 release would have either:

  • Declared the new capabilities, making the addition visible in mochi update warnings, or
  • Falsely under-declared, in which case audit infrastructure could catch the divergence between declared and observed capabilities.

Mochi-57's v1 catches the first case. The second case is a v2 candidate: a runtime check that observed capabilities are a subset of declared. The check is feasible at the Wasm component target and the Deno target; harder elsewhere.

8. Capability monotonicity policy​

The registry enforces a monotonicity policy on patch and minor versions:

  • Patch (x.y.Z): capability set must not change. Adding net.dial in 1.2.5 when 1.2.4 had only fs.read is rejected with M057_PUB_E009.
  • Minor (x.Y.z): capability set may grow. New capabilities are recorded in the index entry.
  • Major (X.y.z): capability set may grow or shrink.

This matches semver: patches must not change observable behaviour; minors may add behaviour; majors may break.

Consumers see capability additions as "minor version bumped, capabilities expanded from X to Y" advisories.

9. Tooling​

  • mochi audit capabilities: walks the resolved tree, prints per-package capability declarations and any deltas since the last lockfile.
  • mochi why-capability <cap>: shows which packages in the tree require a given capability and what the consumer-pin policy allows.
  • mochi caps suggest: experimental v2 surface that infers a suggested capability set by static analysis of the source (high-confidence cases only).

10. Failure modes​

CodeMeaning
M057_CAP_E001Unknown capability identifier in manifest
M057_CAP_E002Consumer pin denies a required capability of a dep
M057_CAP_E003Lockfile records a capability addition not yet accepted
M057_CAP_E004Patch version added a capability (monotonicity violation)
M057_CAP_E005Runtime: a package called a capability it did not declare

M057_CAP_E005 is the runtime audit signal; on the Deno target it's the Deno permission denial; on Python it's the runtime shim error; on Wasm it's the component-model link failure.

11. Cross-references​