sandbox: the S7 audit trail can outlive the process that wrote it #309

Merged
dcharlot merged 1 commit from fix/sandbox-s7-audit-trail-survives-restart into main 2026-10-18 07:47:45 -04:00

2026-10-18

Transaction Science
sandbox: the S7 audit trail can outlive the process that wrote it
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 17s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m25s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m49s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 1m24s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 1m34s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 1m2s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m59s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m11s
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 2m25s
SPEC §2.2 grades tier S7 as "S6 + geographic pinning + **immutable
audit trail**", and §7.7 states: "`audit_prev` — the head of the host's
append-only, hash-chained audit log **before** this run ... The chain is
tamper-evident — reordering or removing any entry breaks it — so the
sequence of sovereign runs is itself attestable." `sandbox-attest`'s
audit module repeats it, and SPEC.md:352 grades the log Engineering
with "the hash-chained, tamper-evident audit log are implemented and
signed into the attestation; vectored" — disclosing nothing about
durability.

The reference state was a `Mutex<AuditLog>` that `Host::new` always
built fresh, with no constructor taking an existing log and
`audit_head()` the only accessor. `AuditLog` derives Serialize and
Deserialize, and nothing in the tree ever wrote or read one.

So the trail did not survive a restart, and nothing showed it had not.
`AuditLog::verify` recomputes the head from the entries it holds, so a
freshly reset log verifies perfectly: after a restart the next sovereign
run is sealed with `audit_prev` = BLAKE3("sandbox.audit/1"), the genesis
head — byte-identical to what a host's very first sovereign run would
carry. A data-residency auditor given the post-restart host sees a short
chain that verifies from genesis, while the runs before the restart have
left the "immutable audit trail" without breaking anything. The one
property §7.7 sells is the one that did not survive.

The pattern was already in the repo and unused here:
`sandbox-build::FileBuildLedger` is a file-backed, verify-on-open JSONL
ledger. This follows it.

- `AuditLog::load_jsonl` / `append_jsonl` give the trail a file.
- The file stores **only the entry list**; the head is replayed from it,
  never stored. So the file cannot assert a head it did not earn, and a
  loaded log verifies by construction.
- `Host::with_durable_audit(path)` opens and replays the existing chain.
- The append writes the line **before** the in-memory head advances. A
  crash between the two leaves the file ahead of memory and the next
  load replays to the head the attestation already committed to; the
  other order would seal a sovereign run against a head no file records.
- A missing file is the first run. A file that does not parse is an
  error, not a shorter history: the runs whose lines are unreadable are
  exactly the ones the trail exists to hold. `AttestError::Audit` is
  fatal by design.

What this does not claim: a log is not self-authenticating. A truncation
that drops entries *and* leaves a consistent list still verifies on its
own. What detects that is the `audit_prev` each S7 attestation carries —
the attestations are the evidence and the log is the index into them,
which is exactly why the index has to survive. Said plainly in the
module doc rather than left to be assumed.

`Host::new` and every existing call site are unchanged: a host that does
not ask for a durable trail behaves as before, and the limit is now
documented on `with_durable_audit` and pinned by a test.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-18 00:47:20 -04:00