feat(autonomy): in-session execution + the recorded model-proposed flight (P5) - #306
Merged
Conversation
…pe docs Phase 0 (honesty): AutonomousIterationHarness and SessionEnvironmentInputs now state precisely what a sandbox session does today - started, attested (image digest, engine version, effective caps, refused if weaker than requested), recorded onto the certificate, torn down with the iteration - and what it does NOT do: the harness never calls ISandboxedSession.ExecAsync, so the candidate's compile, witness, and mutation runs happen in the harness process. Those certificate inputs are provisioning evidence, not a containment guarantee; the write surface is confined separately and genuinely by ProposerConfinement. docs/certification-evidence.md records this as a known v0 limitation alongside a second one: the loop has never run against a live container engine or a real proposer. Phase 1 (composition): AddNexoAutonomy registers the swap host with its quarantine/watch/cadence surfaces, the iteration harness, ClusterBudget, the Docker session runner and reaper, and provenance sinks for both swap and session events (R2.1 - no unlogged path). Options bind from Nexo:Autonomy, default Enabled=false, and ValidateOnStart refuses an enabled-but-incoherent configuration (sessions without an image cannot be attested; retention below 1 leaves no rollback target, violating I-3). Deliberately NO air-gapped refusal, unlike the MCP/A2A protocol surfaces: R6.3 requires autonomy controls to work fully offline, so refusing an air-gapped host would enforce the reverse of the invariant. The reaper ships as a separate opt-in hosted service so one-shot CLI processes are not forced to run a sweeper. The DI resolution test walks every edge rather than resolving the harness alone, because this codebase has already produced a factory-laundered cycle that passed ValidateOnBuild and then recursed at resolution time. It also surfaced a pre-existing defect: AddCertificationInfrastructure alone registers ICompositionCertificationGate without the IBrickRegistry it needs, so it fails ValidateOnBuild - tests compose via AddCertificationGate, and the defect is tracked separately rather than changed here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Docker The acceptance step recorded as outstanding in docs/certification-evidence.md: the loop has never run against a live container engine. This spike is that flight, deliberately OUTSIDE Nexo.sln and every CI gate - its output is a run recorded in the evidence doc, not a shipped artifact. Program.cs composes AddCertificationGate + AddNexoAutonomy exactly the way a host would (ValidateOnBuild/ValidateScopes), hand-authors a Triage objective, projects it to a ProposalIterationContext in the host glue (the one place BackgroundAgents and Infrastructure types may meet), and runs ONE iteration: tier classification, attested sandbox session, the real certification chain, autonomous Tier-0 swap, three post-swap executions clearing the watch window, and the digest render. The SandboxSpec carries no mounts, deliberately: sessions are SIBLING containers on the host daemon (docker.sock pass-through), so container-local paths would be meaningless - and the session's certified role today is provisioning attestation only. Mount handling gets proven when build/test move in-container, not smuggled in here untested. --dry swaps in the TestKit fake runner (same wiring, no daemon) and asserts zero leaked sessions. run-first-flight.ps1 mirrors test-in-container.ps1 (Smart App Control blocks fresh host DLLs) plus the docker.sock mount and a static docker CLI fetch. The spike includes build/GlobalUsings.DomainBrick.cs directly because spikes/ has no Directory.Build.props tree to inject it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…engine The acceptance step is no longer outstanding. One real iteration flew from commit 1afac86: Triage objective -> Tier0Autonomous -> attested session on the live daemon (alpine:3.20 @ sha256:d9e853e8..., engine 29.7.2, effective caps verified) -> full certification chain ADMIT (signed, escape_rate=0, all five inputs recorded) -> autonomous Tier-0 swap as generation 1 in 4.4s -> 3/3 post-swap invocations clearing the watch window -> digest. Zero nexo-session-* containers left on the host daemon afterwards. The --dry leg (fake runner, same wiring) passed with an explicit zero-leaked-sessions assertion. Known limitation 5 is rewritten to what actually remains: the loop has still never admitted a MODEL-produced candidate - the flight brick was hand-authored to the gate-teeth shape. Limitation 4 (session attests, does not contain) is unchanged by the flight and the section says so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e compile The in-container toolchain change the known-limitation docs tracked. With Nexo:Autonomy:BuildCandidateInSession=true, every iteration must compile its candidate INSIDE the attested session via ISandboxedSession.ExecAsync; an iteration without a session refuses fail-closed rather than silently building on the host, and the validator rejects the flag without UseSandboxSessions at boot (an always-refusing loop is a configuration error, not a policy). SessionCandidateBuild compiles the EXACT bytes every certification-path compile sees (CandidateSourceWrapper.Wrap - A1.2 extended to the session): source, reference assemblies, NuGet.config, and the generated project all stream through ExecAsync as base64 argv chunks, because sessions may be sibling containers on a remote daemon where host bind mounts are meaningless. The generated project has zero PackageReferences and cleared package sources, so restore succeeds offline from the SDK's installed packs alone - anything needing a download fails loudly instead of waiting on a network the session does not have. A passed build becomes a session-build certificate input binding the toolchain version and candidate source hash to the session id. A failed build terminates the iteration as an explained failure with the step's diagnostic tail; the candidate never reaches the gate. Honesty boundary, stated in the harness docs: session-build claims COMPILATION containment only. The witness and mutation runs still execute in the harness process; sandbox-spec/attestation inputs remain provisioning evidence. The flight spike gains --session-build (pinned mcr.microsoft.com/dotnet/sdk:9.0 session image) and run-first-flight.ps1 gains -SessionBuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-session The flight re-flown from d71d045 with -SessionBuild: the candidate compiled INSIDE the attested mcr.microsoft.com/dotnet/sdk:9.0 session (digest sha256:35048e3a..., engine 29.7.2) on the live daemon, offline, and the signed certificate carries six input kinds including session-build. AdmittedAndSwapped in 7.0s; zero sessions leaked; the dry leg and the 20/20 in-container suite passed alongside. Known limitation 4 is rewritten to the narrowed truth: session containment now covers COMPILATION (opt-in, fail-closed); witness and mutation execution still run in the harness process, and closing that means shipping a certification runner into the image - a separate step, not an increment. SessionEnvironmentInputs' remarks updated to match: the three environment inputs stay provisioning evidence; session-build is the separate compilation-containment claim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…led HostServicesOnly The final phase of the post-merge plan - three small closures. Watch window (R5.2): WatchThresholds.MaxInvocationDuration is an ABSOLUTE per-invocation wall-clock ceiling, judged like the contract leg - no baseline, no minimum invocation count - because a first-generation deploy has no baseline for the relative legs and a pathological single invocation must not hide inside a healthy mean. Wall clock is the one per-invocation resource honestly measurable in a shared host process; per-brick memory or CPU attribution is not, and the doc says so instead of pretending. Configured via Nexo:Autonomy:WatchMaxInvocationSeconds (0 = off). Digest (spec 7): AddNexoAutonomyDigestJob retains swap provenance in a bounded ring (RecordingBrickSwapProvenanceSink - still logs every event, so R2.1 holds) and renders the human digest on cadence (DigestIntervalSeconds, 0 disables). The sink takeover is order-independent with AddNexoAutonomy, and a saturated ring names what fell off rather than presenting a truncated history as complete. Single-op controls stay on the authority services - a rendering job is not a control surface. HostServicesOnly: the fail-closed refusal is now documented as the settled posture, not a v1 gap. The one workload that seemed to need the mode - in-session package restore - was solved offline in P3, and a model server belongs on the proposer side of the boundary. The enum member stays for a backend that can genuinely realize per-session egress; none is planned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndidate The two closures the evidence doc still recorded as open. Execution containment (P5a): CertificationRequest.ExecutionBackend routes every EXECUTION of candidate and mutant code through a backend; null keeps today's in-process path byte-for-byte. The gate stays the judge - backends return RAW observations (outputs, summary, threw) and WitnessRunner's new judges compare them with exactly the in-process semantics, unwrapping JSON transport values so a transport artifact can never masquerade as a kill. One batched candidate execution (repeats=2) serves witness AND determinism, observed before mutation so mutant runs cannot poison candidate evidence. Mutants still COMPILE in-process (Roslyn is trusted) but execute in the batch; non-compiling mutants stay dead-on-arrival. Backend infrastructure failures THROW - they are not candidate evidence and must never become a signed verdict in either direction (the vacuous-kill lesson, applied). SessionExecutionBackend implements the port over ISandboxedSession: a deliberately DUMB runner (zero package references, offline restore, built once per session from embedded source) loads each unit in its own load context (fresh statics, mirroring in-process mutant isolation), executes reflectively using the wrapper-injected CertAuditContext, and prints raw JSON observations. It never sees expected outputs. ExecuteCandidateInSession option requires BuildCandidateInSession (the leg loads the session-built assembly); the validator refuses the rest at boot. Recorded model proposal (P5b): RecordedProposal carries the first candidate in this repository authored by a model (Claude Fable 5, 2026-08-14, from the objective and interface contract only - never the witness values), a deliberately different implementation shape from the hand-authored brick. The model identity rides the lineage proposer signature (R4.1), hash-bound into the generation-depth input. The --proposed flight requires full session containment, and the proposal's in-process handle THROWS on ExecuteAsync: model-proposed code never runs in the harness process, and if anything tries, the flight fails loudly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… flight
The first model-proposed flight terminated BudgetExhausted at the 600s
ceiling (R4.6 doing its job) because the proposed source is a
position-advancing while loop, and mutants like `position = end + 1` ->
`end - 1` (or `+ 0`) never terminate. In-session that hung the runner exec
until the iteration ceiling killed the flight.
The runner now races every execution against a 10s per-execution ceiling:
a timeout becomes a threw-OBSERVATION ("execution timed out after 10s"),
which the judges read as a killed mutant or a rejected candidate - a
nonterminating execution is a behavioral fact, reported as such. Once one
execution of a unit times out the unit is dead and its remaining slots
report timed-out without executing, bounding spinning threads to one per
hung unit; the runner process exits at batch end and takes any spinners
with it - an isolation move the in-process path could never afford.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>…flight lesson The re-flight still hit the 600s ceiling. The first fix raced Task.WhenAny against the brick's RETURNED task - but a synchronous brick (the Task.FromResult shape both flight candidates use) executes its entire body INSIDE MethodInfo.Invoke, so a nonterminating mutant spins before any task exists and the race never starts. The invoke now runs entirely inside Task.Run and the race covers it. The timed-out unit's spinner is a threadpool thread that dies with the runner process at batch end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The third proposed flight produced the first honest REJECT of a
model-produced candidate: mutation, escape_rate=0.16, three surviving
mutate-int-literal mutants - all boundary-index equivalents
(skip-first-character shapes) that the two-case witness could not
distinguish because it never pinned an ERROR marker at the start of the
text or of a line.
That is a real witness gap, named by the gate exactly as designed. The
hardening case ("ERROR first\nERROR second" -> count 2, first message
"first") is contract-derived, not implementation-fitting: any correct
scanner must count edge-positioned markers and extract their messages,
and the case kills all three survivor classes for any implementation
shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>…l proposal admitted The evidence section tells the whole five-flight story, because the rejections ARE the evidence: BudgetExhausted x2 (a nonterminating mutant found the runner's missing per-execution timeout, then the race-the- returned-task bug), REJECT at mutation 0.16 (three boundary-index survivors naming a real witness gap), REJECT 0.05 (the leading-newline sentinel), and only then AdmittedAndSwapped at escape 0 against the contract-hardened witness - all with in-session build AND execution on the live engine, all in ~23s each once the runner was honest. Limitation 4 rewritten to its end state: session containment covers compilation + execution opt-in; the host keeps orchestration, trusted mutant compilation, and ALL judgment (a backend that judged its own executions could be lied to once and would then lie forever). Limitation 5 rewritten: one recorded model proposal has traversed the full loop; what remains is live-API proposing at iteration time - operational work on existing seams, not a missing mechanism. Also the hand-authored full-containment flight re-flown PASS at the hardened four-case witness, and 66/66 across the eight in-container suites after everything. Co-Authored-By: Claude Fable 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. |
IanFrelinger
changed the base branch from
feat/autonomy-watch-digest-decisions
to
masterAugust 14, 2026 16:03
Uh oh!
There was an error while loading. Please reload this page.
3 tasks
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.
Summary
The two closures the evidence doc still recorded as open, executed: candidate code now executes inside the attested session (P5a), and a model-produced candidate has traversed the full autonomous loop (P5b). Stacked on #305.
P5a — execution containment
CertificationRequest.ExecutionBackend(null = today's in-process path, byte-for-byte): when present, the gate's witness, determinism, and mutation legs route every execution of candidate/mutant code through the backend — and the gate stays the judge. Backends return raw observations (outputs, summary, threw);WitnessRunner's new judges compare them with exactly the in-process semantics, unwrapping JSON transport values so a transport artifact can never masquerade as a kill.SessionExecutionBackend: a deliberately dumb runner — zero package references, offline restore, built once per session from embedded source, executes reflectively via the wrapper-injectedCertAuditContext, prints raw JSON. It never sees expected outputs.ExecuteCandidateInSessionrequiresBuildCandidateInSession(the leg loads the session-built assembly); the validator refuses the rest at boot.With both legs on, untrusted candidate code never runs in the harness process: compile in-session, witness in-session, mutants in-session, determinism in-session. What remains on the host: orchestration, Roslyn compilation of mutants, and judgment.
P5b — the recorded model proposal
RecordedProposalcarries the first candidate in this repository authored by a model (Claude Fable 5, 2026-08-14, recorded — the same record/replay discipline as the agent-composer arc). Authored from the objective text and interface contract only, never shown the witness values; a deliberately different implementation shape (index scanning) so a byte-copy cannot masquerade as a proposal. The model identity rides the lineage proposer signature (R4.1), hash-bound into the certificate'sgeneration-depthinput — provenance in evidence, not in prose.The
--proposedflight refuses to run without full session containment, and the proposal's in-process handle throws onExecuteAsync: if anything ever tries to run model-proposed code in the harness process, the flight fails loudly instead of quietly executing untrusted code on the host.The five-flight campaign — the rejections ARE the evidence
The model-proposed candidate was not admitted on the first try, and the campaign it took is the strongest teeth evidence the ledger has (full table in
docs/certification-evidence.md):BudgetExhaustedat the 600s ceiling — amutate-int-literalmutant turned the proposal's position-advancing loop nonterminating and hung the in-session runner. R4.6 caught it; the runner gained a per-execution timeout (a timed-out execution is now an observation, judged as a killed mutant — and the spinner dies with the disposable runner process, an isolation move the in-process path could never afford).BudgetExhaustedagain — the first fix raced the brick's returned task, but a synchronous brick executes insideMethodInfo.Invoke, so the race never started. Fixed by racing the whole invoke insideTask.Run.mutation, escape 0.16 — three surviving boundary-index mutants named a real witness gap (no case pinned an ERROR marker at the start of the text or of a line).mutation, escape 0.05 — the leading-newline sentinel survivor.AdmittedAndSwapped, escape 0 against the contract-hardened four-case witness, 23.5s, zero sessions leaked.The witness hardening was contract-derived, not implementation-fitting: edge-positioned markers and leading newlines are behaviors any correct scanner must pin, and the cases kill those survivor classes for every implementation shape. The hand-authored flight was re-flown PASS at the hardened witness.
Testing
-SessionBuild -SessionExecute: PASS (12.3s @ 2-case witness; re-flown PASS @ hardened witness)-SessionBuild -SessionExecute -Proposed: PASS after the campaign abovemake kernel-coverage-gate— runs in CI🤖 Generated with Claude Code