open-standards/sandbox/spec/environment-manifest.md
Transaction Science d79332b061
Some checks failed
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 55s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 5s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 2m2s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 1m42s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m24s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 1m26s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Failing after 1m7s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m16s
standards workspaces build + test / proof-rs (proof-conformance, proof/proof-rs, proof-rs, linux, true) (pull_request) Successful in 1m32s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m31s
sandbox: a manifest is pinned only when its image AND every feature are digest-addressed
`EnvironmentManifest::is_pinned()` checked the base image's digest alone,
while its doc promised "fully pinned" and "same address ⇒ same bytes". Its
own test asserted it true for a manifest whose features were `rust:1` and
`node:1`. Those are tags, which resolve to whatever was last pushed under
them, so two hosts can run different toolchains under one address. The
spec repeated the overclaim: it called such a manifest pinned, and so fit
for a cross-host "same environment" claim.

- `Feature::is_pinned()`: the reference carries an `@sha256:` digest.
- `EnvironmentManifest::is_pinned()`: the image and every feature are
  digest-addressed. `image_pinned()` is the old image-only check.
- Its docs, the spec (spec/environment-manifest.md, SPEC §14): pinned means
  the whole toolchain. Mounted content is data and never part of the
  address, so a pinned manifest says nothing about what its mounts hold.
- C7 conformance checks the image (as it always said it did), that a
  feature by tag leaves the manifest unpinned, and that digest features pin
  it.

Pinned-ness is not in the manifest core, so no content address or vector
changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 20:50:37 -04:00

3.4 KiB

The environment manifest

A receipt says what a unit cost; an attestation says who ran it, where, at what tier. Neither says in what environment — which base image, which tool features, which variables and mounts. For short runs that gap is tolerable. For durable runs (§13) it is not: a run suspended on one host and resumed on another is only the same run if it resumes into the same toolchain, and "same" must be checkable by hash, not asserted by trust.

The environment manifest closes the gap: one canonical object whose BLAKE3 content address stands for the whole declared environment.

Ingestion: the devcontainer format

Environments are declared in the open Development Containers specification (devcontainer.json) — already the common declaration format across the industry. This standard ingests its declarative subset:

accepted meaning
image base image reference; a @sha256: digest pins the image
features tool features, normalized to (id, JCS-canonical options), sorted; a reference with a @sha256: digest is pinned, one by tag (:1) floats
containerEnv environment variables, sorted
mounts string or object form, normalized to (source, target, kind), sorted by target
remoteUser the user the unit runs as

Cosmetic and editor-facing fields (name, customizations, …) are accepted and excluded from identity. Networking fields (forwardPorts, appPort, …) are likewise excluded: the network surface is governed by the capability grant (§5), never by the environment declaration.

Refused fields. Anything that executes commands or widens the sandbox is refused with an error, not skipped: build, dockerFile, dockerComposeFile, the five lifecycle *Command hooks, runArgs, privileged, capAdd, securityOpt. Lifecycle automation belongs to the unit and its grants, where it is metered and attested — not smuggled into an identity object. Unknown fields are also refused: silently dropping a field a future devcontainer revision makes execution-relevant would mint a false identity.

Canonical form, address, signature

The manifest core (schema: "sandbox.env-manifest/1") is JCS-canonicalized; its BLAKE3 address is the environment's identity; it is signed Ed25519 over the canonical bytes, mirroring the attestation convention (§7.3). Key order and whitespace in the source document do not affect the address.

A manifest is pinned only when its image and every feature are addressed by digest. Otherwise it is unpinned: its address identifies a declaration, not bytes — a tag such as rust:1 resolves to whatever its publisher last pushed, so a pinned image beside a feature by tag does not fix the toolchain. Hosts SHOULD treat unpinned manifests the way the energy receipt treats estimates — usable, but labelled, and never the basis for a "same environment" claim across hosts.

Mounted content is data, not toolchain, and is never part of the address. A pinned manifest fixes what a unit runs with; it says nothing about what its mounts hold.

Use in durable runs

SnapshotRef.manifest (§13) records the manifest address at suspension. Resumption MUST be refused when the offered environment's manifest address differs — same filesystem plus a drifted toolchain is a different snapshot. Conformance class C7 exercises ingestion determinism, the refusals, and this linkage.

The reference implementation is sandbox_attest::manifest.