Skip to content

fix(sandbox): attestation must not depend on the template's join - #352

Merged
IanFrelinger merged 1 commit into
masterfrom
fix/attestation-inspect-robust
Aug 17, 2026
Merged

fix(sandbox): attestation must not depend on the template's join#352
IanFrelinger merged 1 commit into
masterfrom
fix/attestation-inspect-robust

Conversation

@IanFrelinger

Copy link
Copy Markdown
Owner

Found by UAT tier 3, running the autonomy loop live: one sweep, one objective, a real qwen2.5-coder:7b proposer, hold admission left at its shipped default.

What happened

The campaign crashed at attestation, after the proposer had already produced a candidate:

InvalidOperationException: Sandbox session 'nexo-session-909c…' cannot be attested (inspect exit 1):
template parsing error: executing "" at <.HostConfig.CapDrop>:
wrong type for value; expected []string; got []interface {}
at DockerSandboxedSessionRunner.DockerSandboxedSession.AttestAsync … :303
at AutonomousIterationHarness.RunBudgetedAsync … AutonomyLoopService.SweepAsync
exit 134

InspectFormat read the two list fields with the template's join, which requires []string.

Isolating it

Four hypotheses were eliminated before the real one, each behaving correctly:

hypothesisresult
daemon 29.7.2 is incompatiblejoin works fine — host CLI, cap-dropped container
pinned CLI 27.5.1 is incompatibleworks too, when the container was created by the host CLI
unset CapDrop breaks joinrenders [], joins to empty, no error
inspect races --rm teardownclean no such object, exit 1, no template error

The trigger is narrower: a container created by an older CLI than its daemon reports those fields as []interface{}. Creating and inspecting through the CLI 27.5.1 that run-first-flight.ps1:130 pins, against the 29.7.2 daemon, reproduces it every time.

Why this is worth more than a version bump

Bumping the spike's pinned CLI would have made my machine green and left the defect in place. Two things were actually wrong:

  • Brittleness in the wrong place. Attestation is the leg that turns "we asked for containment" into "the engine says this is the containment". It should be the last code to care how the engine typed a field.
  • The failure mode.ParseInspectLine is written so that absent evidence reads as unverified rather than contained — deliberately fail-closed, and documented as such. But a template error throws before any of that parsing runs. So a session that could not be attested killed the whole campaign instead of refusing one iteration. Safe — nothing was admitted — but not operable for a loop whose entire point is running unattended.

The fix reads the fields as Go renders them ([ALL], []) and parses that. Both decodings print identically, so the parse cannot be broken by the CLI's typing choice. The old comma-separated form still parses, so existing expectations are untouched.

Verification

The spike's old CLI pin is deliberately left alone, so the live re-flight proves the fix works across the version gap rather than dodging it. Same script, same mode, same model:

sandbox-session attested … digest=sha256:e1fc6e423f543119c406d24e2e687d67c569f18f04a37a8b0005d80ad0dcee80
Candidate for tag-scan-classifier compiled in session (toolchain 10.0.400)
Certification ADMIT tag-scan-classifier escape_rate=0
Autonomy iteration for tag-scan-classifier (attempt 1): CertifiedButHeld —
certified; the operator holds admission (loop is in hold mode, no unattended swap)
SWEEP: attempted 1 objective(s) in 22.5s
exit 0

That is also the first end-to-end confirmation of the hold-admission claim against a live proposer rather than the TestKit fake: a real model proposal, compiled inside an attested session, certified at escape_rate=0, and held.

Unit tests: 19/19 in DockerSessionAttestationTests, including three new cases for the bracketed output the engine actually prints (nothing covered it before — every test fed the comma form the code produced) and a guard asserting join never returns to that template.

🤖 Generated with Claude Code

A live autonomy sweep crashed the whole campaign at attestation:
InvalidOperationException: Sandbox session 'nexo-session-909c…' cannot be
attested (inspect exit 1): template parsing error: executing "" at
<.HostConfig.CapDrop>: wrong type for value; expected []string; got
[]interface {}
at DockerSandboxedSessionRunner.DockerSandboxedSession.AttestAsync
… AutonomyLoopService.SweepAsync … exit 134
InspectFormat read the two list fields with the template's join, which requires
[]string. A container CREATED by an older CLI than the daemon it talks to
reports those fields as []interface{}, and join then fails the entire inspect.
Reproduced deterministically: with the docker CLI 27.5.1 that the first-flight
spike pins, against this box's 29.7.2 daemon, creating and inspecting through
that CLI fails, while the same inspect of a container created by the matching
29.7.2 CLI succeeds. Four other hypotheses were eliminated first (daemon
version alone, CLI version alone, unset CapDrop, and inspect racing --rm
teardown; each behaves correctly).
Two things were wrong, one of them serious:
- The brittleness itself. Attestation is the leg that turns "we asked for
containment" into "the engine says this is the containment", so it is the
last place that should care how the engine happened to type a field.
- Worse, the failure mode. ParseInspectLine is written so absent evidence reads
as unverified rather than contained, but a TEMPLATE error throws before any
of that parsing runs. A session that could not be attested therefore killed
the process instead of refusing one iteration -- safe (nothing was admitted)
but not operable for a loop meant to run unattended.
The fix reads the fields as Go renders them ("[ALL]", "[]") and parses that.
Both decodings print identically, so the parse cannot be broken by the CLI's
typing choice. The old comma-separated form still parses, so existing
expectations are unchanged.
Found by UAT tier 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursorBot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@IanFrelinger
IanFrelinger merged commit 173ea9a into masterAug 17, 2026
6 checks passed
@IanFrelinger
IanFrelinger deleted the fix/attestation-inspect-robust branch August 17, 2026 19:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@IanFrelinger@PlzTouchGrass