Skip to content

feat(server): prune sandbox export artifacts and document the sandbox runtime - #52

Merged
awtprod merged 3 commits into
mainfrom
impl/sandbox-artifact-retention
Aug 22, 2026
Merged

feat(server): prune sandbox export artifacts and document the sandbox runtime#52
awtprod merged 3 commits into
mainfrom
impl/sandbox-artifact-retention

Conversation

@awtprod

@awtprod awtprod commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Problem

Three gaps from the sandbox audit (items 13, 14, 24):

  1. Unbounded artifact retention. Every sandbox stop/expire exports <stateDir>/sandbox-artifacts/<sha256(threadId)>.{bundle,json,store.tar} and nothing ever pruned the directory — a code comment on T3_SANDBOX_STORE_MAX_BYTES admitted as much. Per-thread growth is bounded (exports overwrite the same set), but sets for threads deleted out-of-band or settled long ago accumulated forever.
  2. The manifest advertised an unservable file. The export manifest listed store: <name>.store.tar, but the artifact HTTP route serves only bundle|manifest — a client following the manifest got a guaranteed 404.
  3. Undocumented env flags. The nine sandbox runtime flags existed only as code comments; there was no operator-facing runtime doc at all (sandbox-host.md is host-bootstrap-only).

Fix

  • Age-based sweep (sweepExpiredArtifacts on SandboxRuntimeManager), riding the lifecycle reactor's existing minutely pass — no new timer. Deletes artifact sets whose newest file mtime exceeds T3_SANDBOX_ARTIFACT_MAX_AGE_SECONDS (default 30 days, explicit 0 disables). Sets for threads in a non-terminal sandbox lifecycle are protected regardless of age, since a young export seeds re-provision (deleting one degrades restore to a plain clone and loses the provider's conversation). Per-run deletions capped at 1000 sets (logged if hit); sweep failures are logged with cause and never stall expiry; in-flight .tmp export temporaries are never eligible.
  • Manifest annotation rather than serving the store: the store is transcript data used server-side for restore, so the manifest now marks it storeServed: false. The only manifest reader (the restore path) reads digests from the thread projection, not the manifest, and the HTTP manifest route serves raw bytes — no schema change needed.
  • Docs: sandbox env block in .env.example (each flag verified against its usage site) and a new operator-focused docs/operations/sandbox-runtime.md (enable gate, lifecycle, export artifacts, retention, deletion cleanup), linked from sandbox-host.md without touching the regions PR fix(sandbox): pin podman-static v5.8.4 host bootstrap and harden re-runs #49 rewrites.

Tests

7 new tests: sweep old-deleted/young-kept, newest-file dating, active-thread protection, 0 disables, temporaries/foreign files ignored, unconfigured/missing-dir no-ops, manifest storeServed round-trip, and a reactor test proving the periodic pass drives the sweep with the right protected set and survives a sweep failure. Verified the new tests fail with the sweep stashed. Full server suite: 3282 passed (baseline 3275).

Built with Claude Fable 5 via Claude Code.

🤖 Generated with Claude Code

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.6 KiB 13.6 KiB −27 B (−0.2%) 15.1 KiB
Codex Thread snapshot wire 6.9 KiB 6.9 KiB −1 B (−0.0%) 7.3 KiB
Codex Live turn WebSocket wire 6.7 KiB 6.7 KiB −26 B (−0.4%) 7.8 KiB
Codex Live turn WebSocket decoded 54.2 KiB 54.2 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 17 17 0 (0.0%) 21
Claude Total thread wire 13.7 KiB 13.7 KiB −7 B (−0.0%) 15.1 KiB
Claude Thread snapshot wire 7.0 KiB 7.0 KiB −5 B (−0.1%) 7.3 KiB
Claude Live turn WebSocket wire 6.7 KiB 6.7 KiB −2 B (−0.0%) 7.8 KiB
Claude Live turn WebSocket decoded 55.0 KiB 55.0 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 17 17 0 (0.0%) 21

Baseline: ceb12db · PR result: c2f2aed · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 107.8 KiB
  • Claude decoded thread snapshot: 108.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@awtprod
awtprod force-pushed the impl/sandbox-hardening branch from 9458301 to c27f869 Compare August 22, 2026 02:24
awtprod and others added 3 commits August 22, 2026 02:37
…pass

Nothing ever pruned <stateDir>/sandbox-artifacts: every stop/expire wrote a
bundle + manifest + provider store set, and sets for threads deleted
out-of-band or settled long ago accumulated forever. Add an age-based sweep
(T3_SANDBOX_ARTIFACT_MAX_AGE_SECONDS, default 30 days, explicit 0 disables)
that rides the sandbox lifecycle reactor's existing minutely pass. Sets whose
sha matches a thread in a non-terminal sandbox lifecycle are kept regardless
of age -- their export may seed a re-provision. A set is dated by its newest
file, per-run deletions are capped at 1000 sets, and sweep failures are
logged with cause without stalling expiry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… file

The manifest listed store: <name>.store.tar, but the artifact HTTP route
serves only bundle|manifest -- a client following the manifest got a 404 on
an advertised file. The store is transcript data used server-side for
restore, so rather than serving it, mark it storeServed: false in the
manifest. The manifest is written as plain JSON and its only schema-validated
reader is the restore path, which ignores unknown fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sandbox env flags existed only as code comments spread across the server.
Add the sandbox block to .env.example (all flags with defaults, including the
new artifact-retention cap) and an operator-focused
docs/operations/sandbox-runtime.md covering the two-flag enable gate, the
provision/stop/expire/re-provision lifecycle, export artifacts and where they
live, the retention policy, and the store's exclusion from HTTP serving.
Linked from sandbox-host.md, which stays host-bootstrap-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@awtprod
awtprod force-pushed the impl/sandbox-artifact-retention branch from 8362077 to c2f2aed Compare August 22, 2026 02:38
@awtprod
awtprod changed the base branch from impl/sandbox-hardening to main August 22, 2026 02:38
@awtprod
awtprod merged commit 8d02ec0 into main Aug 22, 2026
19 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant