Skip to content

feat(autonomy): in-session execution + the recorded model-proposed flight (P5) - #306

Merged
IanFrelinger merged 12 commits into
masterfrom
feat/autonomy-session-execution
Aug 14, 2026
Merged

feat(autonomy): in-session execution + the recorded model-proposed flight (P5)#306
IanFrelinger merged 12 commits into
masterfrom
feat/autonomy-session-execution

Conversation

@IanFrelinger

@IanFrelingerIanFrelinger commented Aug 14, 2026

Copy link
Copy Markdown
Owner

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.

  • One batched candidate execution (repeats=2) serves witness AND determinism, observed before mutation so a mutant run can never poison the candidate's own evidence.
  • Mutants still compile in-process (Roslyn is trusted; the mutant never executes here) and execute in one batch — in the session, each unit gets its own load context (fresh statics), mirroring the in-process ALC isolation without any collectible-context ceremony, because the whole process is disposable.
  • Backend infrastructure failures throw. They are not candidate evidence; neither a vacuous kill nor a vacuous pass may come from the backend falling over. (The T2 vacuous-kill lesson, applied to the new seam.)
  • SessionExecutionBackend: a deliberately dumb runner — zero package references, offline restore, built once per session from embedded source, executes reflectively via the wrapper-injected CertAuditContext, prints raw JSON. It never sees expected outputs.
  • ExecuteCandidateInSession requires BuildCandidateInSession (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

RecordedProposal carries 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's generation-depth input — provenance in evidence, not in prose.

The --proposed flight refuses to run without full session containment, and the proposal's in-process handle throws on ExecuteAsync: 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):

  1. BudgetExhausted at the 600s ceiling — a mutate-int-literal mutant 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).
  2. BudgetExhausted again — the first fix raced the brick's returned task, but a synchronous brick executes inside MethodInfo.Invoke, so the race never started. Fixed by racing the whole invoke inside Task.Run.
  3. REJECT 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).
  4. REJECT mutation, escape 0.05 — the leading-newline sentinel survivor.
  5. PASSAdmittedAndSwapped, 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

  • In-container, 66/66 across eight suites: gate execution-seam facts (honest observations admit with the backend recorded; tampered observations reject at correctness; nondeterministic repeats reject at determinism; surviving-mutant observations reject at mutation with honest escape rate; backend failure throws, minting no verdict), backend orchestration facts, teeth + campaign + harness + composition + watch + digest regression suites
  • Real flight -SessionBuild -SessionExecute: PASS (12.3s @ 2-case witness; re-flown PASS @ hardened witness)
  • Real flight -SessionBuild -SessionExecute -Proposed: PASS after the campaign above
  • make kernel-coverage-gate — runs in CI

🤖 Generated with Claude Code

PlzTouchGrassand others added 12 commits August 13, 2026 23:48
…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>
@cursor

cursorBot commented Aug 14, 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 changed the base branch from feat/autonomy-watch-digest-decisions to masterAugust 14, 2026 16:03
@IanFrelinger
IanFrelinger merged commit 4ab2da7 into masterAug 14, 2026
17 checks passed
@IanFrelinger
IanFrelinger deleted the feat/autonomy-session-execution branch August 14, 2026 16:04
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