Skip to content

Commit f248cf8

Browse files
committed
feat(run)!: execute fail-closed hermetic Cargo checks
Add an explicit locked fetch boundary, isolated offline Cargo execution, global compiler observation, and exact output manifests for the graduated pure-Rust current-host check class. Model build-script actions and results without circular identity, preserve normal execution for unsupported classes, and report every unproven boundary. BREAKING CHANGE: Action plans and decision receipts now use schema version 4.
1 parent 450e720 commit f248cf8

28 files changed

Lines changed: 8416 additions & 130 deletions

.changes/hermetic-action-identities-9b40.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
"cargo-rail" = "major"
33
---
44

5-
Add fail-closed ActionKey diagnostics, exact compiler-observation evidence, and stable Cargo configuration contracts
6-
without enabling artifact reuse. Incomplete executable/runtime observations explicitly bypass diagnostic reuse. Action
7-
plans and decision receipts now use schema version 3. Executable identity hashes exact bytes in a bounded-memory stream.
5+
Add fail-closed action-key diagnostics, exact compiler observations, executable identity, and stable Cargo
6+
configuration contracts without enabling artifact reuse. Transparent rustdoc observation preserves the selected tool
7+
and HTML output while recording stable dep-info; incomplete documentation output trees remain uncacheable.
8+
9+
Build-script compilation now retains the exact executable and separates the non-circular pre-execution action key from
10+
the ordered instructions, environment reads, generated tree, and execution evidence in its result digest. Verified
11+
results propagate to every dependent compilation unit; incomplete stable Cargo observations keep normal scripts
12+
executable but explicitly non-reusable.
13+
14+
Add `cargo rail run --all --action build --hermetic` for the graduated pure-Rust Cargo-check class. An explicit locked
15+
fetch action produces an exact immutable crates.io, remote registry-mirror, or Git dependency inventory from a reviewed
16+
`Cargo.lock`; no full metadata call can acquire dependencies before that boundary, and warm inventories make zero
17+
registry requests. Checks then run locked/offline in fresh read-only source and isolated output roots with logical path
18+
remapping and a controlled environment. Exact preinstalled sysroot tools are pinned without invoking ambient wrappers
19+
or allowing rustup to install/update a toolchain. An observation-only global rustc wrapper covers workspace and external
20+
dependency units; Cargo artifacts, compiler observations, emitted outputs, and dep-info must agree exactly. macOS
21+
enforces filesystem and network denial and can issue a verified action key/result manifest; other hosts report
22+
platform-limited. Build scripts, proc macros, docs, linked/native/cross-target work, custom tool boundaries, and sccache
23+
remain fail-closed. Workspace/package boundary overrides, redirected outputs, configured compiler wrappers, unstable
24+
Cargo semantics, and raw rustc arguments fail before fetch state. Declared outputs are re-digested immediately before
25+
publication. No compiler artifact or Cargo fingerprint is restored before P7. Action plans and decision receipts now
26+
use schema version 4.

docs/architecture.md

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Commands do not reload metadata independently.
2121
|---|---|
2222
| `commands/` | CLI handlers and command dispatch |
2323
| `action` | shell-free action declarations and deterministic request expansion |
24+
| `hermetic` | isolated fetch/check execution and exact output proof |
25+
| `build_script` | non-circular build-script action/result identities |
2426
| `workspace/` | `WorkspaceContext` construction and shared state |
2527
| `graph/` | dependency graph queries |
2628
| `cargo/` | metadata, manifests, unify logic |
@@ -84,17 +86,118 @@ the class is not reusable.
8486

8587
During workspace-only `rustc` diagnostics, cargo-rail records argv-declared inputs before execution and correlates the
8688
completed invocation with Cargo's stable JSON artifact messages. Rustc dep-info supplies observed file and environment
87-
reads. The immutable result manifest keeps declared inputs, observed reads, dependency artifacts, emitted outputs, and
89+
reads. Cargo has no rustdoc-wrapper setting, so the observation profile instead places a transparent proxy in Cargo's
90+
selected `RUSTDOC` slot and retains the selected rustdoc as the inner executable. The proxy discovers the selected
91+
rustdoc's supported default emit set, adds stable dep-info without dropping HTML output, and preserves response-file,
92+
doctest, and unsupported-tool invocations unchanged with an explicit bypass.
93+
94+
The immutable result manifest keeps declared inputs, observed reads, dependency artifacts, emitted outputs, and
8895
execution metadata in separate fields. Every file is SHA-256 digested from its bytes and re-digested before diagnostic
89-
evidence can be reused. Cargo's `fresh` flag is retained only as execution metadata; it never authorizes a hit.
96+
evidence can be reused. Cargo's `fresh` flag is retained only as execution metadata; it never authorizes a hit. Cargo's
97+
documentation artifact message names an index file rather than the complete HTML tree, so documentation output remains
98+
explicitly uncacheable until the isolated output boundary can enumerate and verify that tree.
99+
100+
Custom-build compilation retains Cargo's exact executable output separately from the other compiler artifacts. A
101+
versioned `BuildScriptActionKey` can be issued only at the next process boundary, after the executable and source bytes
102+
are revalidated and the relevant manifest/lock closure, toolchain, host and target identities, profile, features,
103+
configuration, complete non-secret environment, exact declared dependency-action/result set, logical working
104+
directory, and isolated launch layout are known. The stable action ID cannot appear in its own dependency set. The
105+
script's instruction stream, runtime reads, and generated output tree are deliberately absent because they are results
106+
of that action. Normal Cargo execution still inherits ambient state, so compiler observation records a stable
107+
explanation but does not issue a build-script key. Cargo leaves the optional `executable` field empty for
108+
custom-build artifacts, so cargo-rail accepts exactly one target-named program from `filenames`; zero or multiple
109+
matches fail closed.
110+
111+
`BuildScriptResult` version 1 is the separate post-execution identity. Its digest frames the Cargo instruction lines
112+
in emitted order, including rerun declarations, link libraries/search paths/arguments, `rustc-cfg`,
113+
`rustc-check-cfg`, `rustc-env`, metadata, warnings, and errors. It also binds the sorted set of non-secret environment
114+
reads by value digest, a canonical logical generated-output tree with file bytes, executable modes, and symlink
115+
targets, plus execution success and platform identity. Modern `cargo::KEY=VALUE` and legacy `cargo:KEY=VALUE`
116+
instructions follow Cargo's distinct parsing rules; legacy unreserved keys are metadata. Physical checkout paths,
117+
escaping output symlinks, malformed observations, failed execution, secrets, or any missing domain withhold the
118+
digest. Serialized analysis retains only counts, capability names, and stable reasons, never raw instruction or
119+
environment values.
120+
121+
Cargo's stable `build-script-executed` JSON is useful but incomplete post-execution evidence. Cargo can replay the
122+
message without running the script, and the message omits instruction order, rerun declarations, link arguments,
123+
metadata, warnings, runtime environment reads, the generated tree, and execution freshness. The normal collector
124+
therefore stores only redacted counts for linked libraries/paths, cfgs, `rustc-env` entries, and whether `OUT_DIR` was
125+
reported. It records every missing proof domain and never issues a result digest from that subset. When a complete
126+
result is available, its verified digest is attached to the producer package's ordinary compilation units and every
127+
transitive consumer unit. The build-script unit never consumes its own result. Missing action/result evidence or an
128+
incomplete dependency graph produces a stable bypass instead of omitting the edge. The current hermetic profile blocks
129+
build scripts before execution, so normal Cargo remains executable while that complete runtime boundary is still
130+
ungraduated.
90131

91132
Selected and underlying Cargo, rustc, and rustdoc implementations, wrappers, configured linkers and runners, and
92133
repository executables are content-addressed when relevant. Scripts also bind direct interpreters. Response-file
93-
expansion, dynamic libraries, SDK inputs, default linkers, incomplete platform images, and missing stable rustdoc
94-
invocation evidence produce explicit bypasses. Any observation bypass prevents diagnostic-evidence reuse; collector
95-
semantics are versioned so older evidence cannot silently gain authority. Observation manifests never become
96-
pre-execution `ActionKey` inputs, and cargo-rail neither stores result artifacts nor writes or restores Cargo
97-
fingerprint state.
134+
expansion, dynamic libraries, SDK inputs, default linkers, incomplete platform images, and incomplete rustdoc output
135+
trees produce explicit bypasses. Any observation bypass prevents diagnostic-evidence reuse; collector
136+
semantics are versioned so older evidence cannot silently gain authority. An observation manifest never identifies its
137+
own producing action; exact verified artifacts and result digests may become inputs only to later dependent actions.
138+
Cargo-rail neither stores result artifacts nor writes or restores Cargo fingerprint state.
139+
140+
## Hermetic execution profile
141+
142+
`cargo rail run --all --action build --hermetic` is an explicit proof profile. It does not alter ordinary `run`
143+
execution and it is not yet an action-result cache.
144+
145+
Actual hermetic execution currently requires the explicit built-in `--action build`. Default, profile, workflow, and
146+
other action dispatch is rejected before workspace context or hermetic state is created; dry-run remains a planning
147+
preview rather than an execution-boundary proof.
148+
149+
Trailing Cargo arguments may refine modeled features, targets, target kinds, and profiles. They may not replace the
150+
workspace/lockfile, expand package scope outside cargo-rail's selection, redirect outputs, inject Cargo configuration,
151+
enable unstable Cargo semantics, or pass raw rustc arguments; those boundary overrides fail before fetch state.
152+
153+
The profile requires an existing exact `Cargo.lock` and has one network boundary: `cargo fetch --locked`. Before that
154+
boundary, cargo-rail captures and classifies Cargo configuration, rejects configured compiler/rustdoc wrappers and
155+
ambient `RUSTC`/`RUSTDOC`, and performs only locked/offline local-package metadata preflight. Toolchain discovery
156+
disables rustup auto-install/update behavior, ignores ambient compiler wrappers, and pins the exact sysroot Cargo,
157+
rustc, and rustdoc; wrappers, rustup staging homes, and a newly downloaded toolchain cannot enter the fetch identity.
158+
The fetch binds the lockfile, acquisition configuration, credential capability names, and exact Cargo implementation,
159+
captures locked registry or Git packages as an immutable source inventory, and runs full metadata locked/offline
160+
against that inventory. A warm run exactly revalidates and reuses this dependency inventory without contacting the
161+
registry. It does not restore compiler output.
162+
163+
Each check runs with `--locked --offline` in a fresh root containing a streamed, byte-verified, read-only source tree,
164+
a fresh mutable Cargo bookkeeping area, an isolated `target-dir`, stable `build.build-dir`, temporary and home
165+
directories, and a controlled environment. Effective supported Cargo `[env]`, remote registry source replacement,
166+
profile environment, target rustflags, and repository-relative values are materialized explicitly. Cargo dep-info and
167+
rustc output paths are remapped to logical workspace, target, build, Cargo-home, toolchain, and run roots. The source,
168+
dependency inventory, and exact toolchain/platform read boundary are revalidated after observation and immediately
169+
before the manifest is published.
170+
171+
Cargo invokes cargo-rail as a global observation-only rustc wrapper for this profile, so registry and Git dependency
172+
compilations are observed with the same exactness as workspace units. Rustc version/help/print probes are excluded
173+
from the unit set. Before a key can be issued, the multiset of Cargo compiler artifacts must match the raw observed
174+
crate invocations exactly, every invocation must have observed outputs, and dep-info outputs must survive into the
175+
declared output manifest. Missing or extra coverage fails closed instead of producing a partial key.
176+
177+
On macOS, `sandbox-exec` denies network and defaults filesystem access to denied. The policy admits only the isolated
178+
run, immutable dependency inventory, exact Cargo/rustc/driver and host sysroot inputs, the observer executable, and the
179+
small sealed host file set required by the selected toolchain. Other operating systems still get offline Cargo and
180+
isolated roots, but receive `platform_limited` and no authorizing action key until an equivalent filesystem/network
181+
boundary is implemented.
182+
183+
The result is a versioned manifest of every declared compiler-output file, directory, symlink, mode, digest, and byte
184+
count under the isolated Cargo build directory. Cargo's internal fingerprints and incremental state are intentionally
185+
excluded: their layout is unstable, they are never synthesized, and P7 must not restore a whole Cargo build directory
186+
as if it were valid state. After source, inventory, toolchain, and platform revalidation, every declared output is
187+
re-read and compared with that manifest immediately before the report is published.
188+
189+
| Action class | macOS proof | Other hosts | Current contract |
190+
|---|---|---|---|
191+
| Pure current-host `cargo check` for libraries and binaries | `eligible` | `platform_limited` | Two-root action key and output manifest converge. |
192+
| Pure test, example, and bench compilation via `cargo check --all-targets` | `eligible` | `platform_limited` | Compilation only; no test or benchmark process runs. |
193+
| Locked crates.io, remote registry-mirror, or Git dependencies | input supported | input supported | Network only during fetch; full metadata/build are locked and offline, and a warm inventory performs zero registry requests. Local directory/source replacement remains ungraduated. |
194+
| Build scripts, proc macros, native/generated code | `uncacheable` | `uncacheable` | Dynamic runtime/tool inputs are not yet sandboxed as complete action classes; normal execution remains available. |
195+
| Documentation, actual test execution, linked build/package artifacts | `uncacheable` | `uncacheable` | Output/runtime boundaries are incomplete or not implemented by the profile. |
196+
| Cross/custom targets, configured linker/runner, repository wrappers, sccache | `uncacheable` | `uncacheable` | Tool/SDK/wrapper coexistence has not passed the per-class proof gate. |
197+
198+
Physical checkout roots, unrelated files/packages/environment, and Cargo's mutable cache representation do not affect
199+
the graduated key. Exact source, resolution, profile, feature, target, flags, environment, toolchain, platform, and
200+
dependency-content changes do.
98201

99202
## Mutation authority
100203

docs/commands.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ Options:
108108
--dry-run
109109
Preview selected execution without spawning subprocesses
110110
111+
--hermetic
112+
Execute supported Rust actions in fresh isolated roots
113+
111114
-f, --format <FORMAT>
112115
Dry-run action plan format (json/github require --dry-run)
113116
@@ -172,6 +175,7 @@ Examples:
172175
cargo rail run --workflow commit # Resolve profile from [run.workflow.commit]
173176
cargo rail run --profile bench # User-defined profile from [run.profile.bench]
174177
cargo rail run --all --action test # Force full test run
178+
cargo rail run --all --action build --hermetic # Prove a locked/offline Cargo check
175179
cargo rail run --dry-run --print-cmd # Preview exact execution
176180
cargo rail run --dry-run -f json # Versioned CI action plan
177181
cargo rail run --dry-run -f github # GitHub Actions key=value plan

0 commit comments

Comments
 (0)