fix(sandbox): attestation must not depend on the template's join - #352
Merged
Conversation
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>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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by UAT tier 3, running the autonomy loop live: one sweep, one objective, a real
qwen2.5-coder:7bproposer, hold admission left at its shipped default.What happened
The campaign crashed at attestation, after the proposer had already produced a candidate:
InspectFormatread the two list fields with the template'sjoin, which requires[]string.Isolating it
Four hypotheses were eliminated before the real one, each behaving correctly:
joinworks fine — host CLI, cap-dropped containerCapDropbreaksjoin[], joins to empty, no error--rmteardownno such object, exit 1, no template errorThe 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 thatrun-first-flight.ps1:130pins, 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:
ParseInspectLineis 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:
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 assertingjoinnever returns to that template.🤖 Generated with Claude Code