`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]>
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.