All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 5s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 48s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m41s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m10s
standards workspaces build + test / proof-rs (proof-conformance, proof/proof-rs, proof-rs, linux, true) (pull_request) Successful in 2m18s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m38s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 2m43s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 5m23s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 13m57s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 4m28s
Sweep 2026-10-19 item #7, second half, by the owner's decision. Each checkpoint was decided alone: a unit could read data an attacker writes and later reach an endpoint it is granted, each call passing its own check, and nothing connected the read to the egress. The Enforcer is stateless by design (C2), so the connection had to be state the run carries explicitly. sandbox-capability (SPEC §5.8): - Grants.flow: FlowGrant { untrusted: [FlowSource { kind: Path|Kv|Program, pattern, origin }], tainted_may: [FlowSink { kind: Egress|Exec, target, accepts }] }. Omitted when empty, so a grant set without it keeps its bytes. - RunTaint: the origins of untrusted data a run has read; only grows. - Two pure Enforcer functions: taints(source), the origins a read adds; and may_reach(sink, &taint), where a tainted run reaches a sink only where tainted_may opens it to EVERY origin it carries (entries combine; `*` opens to any target or origin; an unopened sink is closed). Additional to the sink's capability, never instead. A decision stays a function of its inputs, the run's taint among them, as I/O counters already are. sandbox-exec-wasm (S2, where the live checkpoints are): HostState carries the taint. A storage or kv read that returns data, and a program's output, taint the run from a source the grant marks; egress and exec are refused with TAINTED (-6, distinct from DENIED) when the grant has not opened them to the run's origins. sandbox-exec-os is untouched: it refuses network and process grants outright, so no sink is reachable there. sandbox-runtime: grants_content_id binds flow_untrusted and flow_tainted_may when present, so the flow grant is part of the capability surface (SPEC §1.2); a grant without one keeps its id. Conformance class C13: sequence vectors (a run's calls in order; reads check the origins after, sinks whether reachable), in code and in the new conformance/v1/flow.json. The pack count goes to 13. SPEC §5.8, spec/capabilities.md (the enforcer contract: pure, with the run's history as an input), spec/conformance.md, conformance/v1/README.md; SPEC §10's class list was missing C11 and C12 and now lists C11–C13. Residuals, stated in §5.8: an unmarked source is read as the principal's data, and a tainted run's writes are not marked, so data can lose its taint between runs. Note: running gen_vectors also rewrites attestation.json and negative.json with "mechanism": null, a drift that predates this change; left out of it. Tests: - capability: three unit tests (a read carries every matching source's origin, kinds do not cross, `*` is any program; a tainted run reaches only what is opened to all it carries, entries combine; the taint only grows and an empty flow grant is not encoded). - exec-wasm: three end-to-end through real guests (an untrusted storage, kv or program read closes the egress that follows, a clean read leaves it open; an untrusted read closes the launch that follows; a sink opens only to the origins named, and no flow grant is untouched). - runtime: the flow grant is bound into the grant id, every part of it. - C13. Mutation testing: 17 mutants; 16 killed, 1 equivalent (the clean-run early return in may_reach: an empty taint already satisfies "every origin"). Verified on the build host: sandbox-rs 428 tests (--locked); every conformance class passes, C13 in code and from flow.json; clippy -D warnings clean on sandbox-capability, -exec-wasm, -runtime, -conformance. Repo checks pass. Co-Authored-By: Claude Opus 5.5 <[email protected]>
120 lines
9.2 KiB
Markdown
120 lines
9.2 KiB
Markdown
# 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<DeviceClass>, // 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`.
|