# The capability-grant model Normative companion to [`SPEC.md`](../SPEC.md) §5. RFC 2119 keywords apply. A **grant set** is the complete, explicit description of what a unit may touch. The model is **deny-by-default**: any access to a resource not named by a grant **MUST** be denied. The grant set is content-addressed before the run (its `grants_id`, SPEC §3) and **MUST NOT** widen during the run (SPEC §1.2). ``` Grants { compute : ComputeGrant, storage : StorageGrant, network : NetworkGrant, kv : KvGrant, host : HostGrant, } ``` The **default grant set is empty**: no devices beyond `Cpu`, no storage, no network, no kv, no host services. Every capability is opt-in. ## ComputeGrant — the heterogeneity clause ``` ComputeGrant { devices : Set, // Cpu | Simd | Gpu | Npu | Tpu | Lpu | Photonic | Quantum energy_budget_uj : u64, cpu_time_ms : u64, memory_bytes : u64, } ``` Rules: 1. **Dispatch is gated by class.** A unit **MUST NOT** dispatch work to a device class absent from `devices`; the host **MUST** deny it. `Cpu` is implicitly present. 2. **The host must isolate AND meter every granted class.** A host **MUST** refuse the grant (decline the run) if `devices` names a class it cannot both confine at the tier in force **and** attribute energy to in the receipt (SPEC §6.2). A class the host can run but cannot meter is not grantable — metering is not optional. 3. **Class minimums bind the tier.** Any class beyond `Cpu`/`Simd` requires ≥ S2; direct device assignment requires ≥ S5; confidential compute requires ≥ S6 (see [`tiers.md`](tiers.md)). 4. **Budgets are hard.** On exceeding `energy_budget_uj`, `cpu_time_ms`, or `memory_bytes`, the host **MUST** terminate the unit and record the breached bound as `outcome = Terminated{ breach }` in the attestation (SPEC §7.1). The energy budget is denominated in the same unit as the receipt — integer microjoules — so a unit's grant and its account are directly comparable. ## StorageGrant ``` StorageGrant { read : [Glob], write : [Glob], delete : [Glob], max_iops : u64?, // optional sustained-I/O ceiling: ops/sec max_read_bps : u64?, // optional sustained-I/O ceiling: bytes/sec } ``` A path operation **MUST** match at least one allow-glob for its mode (`read`/`write`/`delete`) or be denied. Globs use `*` (single segment) and `**`/`/*` (prefix) matching; the matching algorithm is fixed in the reference `sandbox-capability` crate and vectored. Write and delete do not imply read. The empty lists deny all storage. ### I/O ceilings — the contention lever A grant **MAY** carry sustained-I/O ceilings. A request to sustain a rate is permitted **iff** it stays within every ceiling that is set; an unset ceiling (`null`) does not bound that dimension. This is a deny-by-default rate control, and it is the standard's lever against **storage-contention side channels** (see `SECURITY.md`): a unit whose I/O is capped cannot drive the high-rate, page-cache-busting random-read pattern such a channel needs to read co-tenant activity off shared-device latency. A deployment that colocates a contention-sensitive workload at S0–S2 (shared physical I/O) **MUST** set a ceiling on untrusted neighbors, or raise the floor to **S3**, where device I/O is partitioned rather than shared. The ceilings serialize **only when set** (`skip_serializing_if`): a grant that omits them is byte-identical to one written before they existed, so its content id and the published conformance vectors are unchanged. The reference enforces them in `sandbox-capability` (`Enforcer::may_sustain_io`, `io_ceilings`); a ceiling is a property of the **grant**, so it is bound into `grants_content_id` and attested. ## NetworkGrant ``` NetworkGrant { outbound : [Endpoint], inbound : bool, credentials : [EgressCredential] } // SPEC §5.3 EgressCredential { endpoint : Endpoint, secret_ref : Handle, inject : Injection } ``` `outbound` is an allowlist of `host:port` strings; an egress to any endpoint not on the list **MUST** be denied. `inbound = false` denies all inbound connections. The empty grant denies all network. Per SPEC §1.4, the host's own operation never depends on network; a denied egress is a unit-level denial, not a host failure. ## KvGrant ``` KvGrant { read : bool, write : bool, prefixes : [Bytes] } ``` Key-value access is gated by direction (`read`/`write`) and by key `prefixes`: a key operation **MUST** match a granted prefix and direction or be denied. Empty `prefixes` with `read`/`write` true grants the whole keyspace; the conservative default is all-false. ## HostGrant ``` HostGrant { logging : bool, clock : bool, random : bool, inference : bool } ``` - `logging` — the unit may emit log lines to the host sink. - `clock` — the unit may read wall-clock time. When false, time calls return a normalized constant (anti-evasion, SPEC §7.5). - `random` — the unit may draw host randomness. When a replay journal is recorded (SPEC §8), draws are journaled. - `inference` — the unit may call an on-host model endpoint. When granted, the energy of each inference **MUST** be metered to the device class that served it (SPEC §6.2) and **MUST NOT** be hidden inside an opaque host-call cost. ## ProcessGrant — shell and code-interpreter ``` ProcessGrant { exec : bool, programs : [String], max_processes : u32 } ``` The substrate for **shell access** and **code-interpreter** capabilities. A unit **MUST NOT** spawn a subprocess unless `exec` is true and the program is on the `programs` allowlist — deny-by-default, so an empty allowlist permits nothing even with `exec` true. `max_processes` bounds concurrency. Granting `exec` lifts the required tier to at least **S2** ([`tiers.md`](tiers.md)): subprocess execution crosses a brokered host boundary. The reference WASM executor brokers this as a `sandbox::exec` host call that consults the `Enforcer` before spawning and journals the subprocess output (a nondeterministic input, SPEC §8). ## BrowserGrant — browser and GUI agents ``` BrowserGrant { enabled : bool, origins : [Origin], headless : bool } ``` The substrate for **browser-automation** and **GUI-agent** capabilities. A navigation **MUST** be denied unless `enabled` is true and the target origin (`scheme://host[:port]`) is on the `origins` allowlist. `headless` forbids on-screen rendering. Granting `enabled` lifts the required tier to at least **S2**. ## DisplayGrant — desktop and VNC ``` DisplayGrant { enabled : bool, vnc : bool, max_width : u32, max_height : u32 } ``` The substrate for **desktop** capabilities. Framebuffer access **MUST** be denied unless `enabled` is true; a VNC export **MUST** additionally require `vnc`; the surface is bounded by `max_width × max_height`. Granting `enabled` lifts the required tier to at least **S2**. The **filesystem** capability is `storage`; a **code-interpreter** is `process` + `storage`; an **editor/IDE** is `display` + `process` + `storage`. The standard models the underlying resources, never the composed product — so a new agent surface is expressed as a combination of grants, not a new primitive. ## Enforcement contract The reference `sandbox-capability` crate exposes an `Enforcer` that, given a `Grants` set, answers each access query (`may_dispatch(class)`, `may_path(mode, path)`, `may_egress(endpoint)`, `may_kv(dir, key)`, `may_host(service)`, `may_exec(program)`, `may_navigate(origin)`, `may_display()`, `may_vnc()`) with allow/deny. The enforcer is **pure and deterministic**: the same grant set and query always yield the same decision, with no float and no ambient state. Conformance vector class **C2** (SPEC §10, [`conformance.md`](conformance.md)) fixes the decisions for a canonical grant set, including the deny-by-default negatives. What a run has already read is state, and it is not ambient (SPEC §5.8). The run carries it as a `RunTaint`, the origins of the untrusted data it has read, which only grows. `taints(source)` says which origins a read adds, and `may_reach(sink, taint)` decides a sink against what the run carries, so a decision is still a function of its inputs, the run's history among them. Conformance vector class **C13** fixes these decisions for whole runs, step by step, since what a run may do depends on what it did before. At tier S2 the enforcement is **live**, not advisory: the reference WASM executor (`sandbox-exec-wasm`) brokers every capability the guest exercises through a host-call boundary — `kv_get`/`kv_put`, `storage_read`/`storage_write`, `egress`, `clock_now`, `random_u64`, `log` — and each call consults the `Enforcer` before acting. A denied call returns an error code to the guest (denial is not a crash), so capability enforcement is exercised through real execution. The run's flow state crosses these calls with it: a storage or kv read, or a program's output, from a source the flow grant marks untrusted taints the run, and an `egress` or `exec` the grant has not opened to the run's origins is refused with `TAINTED`, distinct from `DENIED`. Each nondeterministic input the guest receives (a kv or storage read, the clock, a random draw, an outbound call) is recorded in the run's replay journal (SPEC §8), so the attestation carries a `journal_root`.