Skip to content

A shared cycle runner for two disposable backends, and the three defects running it found - #56

Closed
kacperpaczos wants to merge 42 commits into
hieuphung97:mainfrom
kacperpaczos:kacperpaczos/shared-python-runner-distrobox
Closed

kacperpaczos wants to merge 42 commits into
hieuphung97:mainfrom
kacperpaczos:kacperpaczos/shared-python-runner-distrobox

Conversation

@kacperpaczos

Copy link
Copy Markdown

One shared Python runner drives one minimal proof cycle on two disposable
backends — Distrobox Assemble and a Pulumi/libvirt/QEMU virtual machine — from
one configuration, with one run_id, real Orca in the environment, one Claude
Code worker on a clean copy of the project, one independent check, artifacts
exported to the host, export confirmed before anything is destroyed, and only
per-run resources removed.

Everything lives under experiments/minimal-cycle/. Nothing outside it changes
except docs/decisions.md.

What is actually proved, and where it stops

The container backend completes a cycle. A worker read the prompt, wrote
the marker byte for byte, reported worker_done with outcome succeeded, and
an independent check run outside that session agreed. SETTLED, exported,
destroyed. The whole run is in evidence/distrobox-settled-cycle/, including
the reply that decided it.

The machine backend does not. Same commit, same host, same task: the domain
is created, a distinct kernel is proved, Orca reports ready, a coordinator
terminal opens, the dispatch is made — and the worker's turn is never observed.
evidence/vm-unobserved-turn/ carries the agent's own terminal, and lists what
that run ruled out (network, shell, submission, first-run state) and what it did
not (the agent's own silence in that guest, which is a lead, not a finding).

That asymmetry is the honest state of this work. No run is marked green on the
machine backend, and the runner classifies it TIMEOUT, never SETTLED.

The three defects that running it found

A fresh agent home asks four questions. Claude Code asks a home it has never
run in to finish onboarding and sign in, trust the folder, allow the external
imports the project's own instructions declare, and accept the permission mode
it was launched with. Each is a modal a dispatch cannot answer, so the agent
holds the task unsent and the plane reports a turn that never started — the same
symptom as a dead worker with none of the cause. cycle_runner/firstrun.py
answers them by writing two small state files, from what the configuration
already says; nothing is copied from the host beyond the one allowlisted token.
The receipt names the four questions in words, because a pre-approval is a real
decision and a reader should be able to disagree with it.

The completion wait was read from the wrong half of the reply. The delivery
is nested under result; the top level is the request envelope. A worker that
had finished and reported was recorded as one that never answered, on a run
whose artifact was already correct on disk. The fixtures carried an invented
flat shape and so agreed with the defect; they now carry what the plane sends,
and all three orchestration replies are exported beside the manifest.

A disposable environment is not disposable until nothing of it is still
running.
Cleanup destroyed the container and the directory it declared and
reported DESTROYED, truthfully — and never looked for what the run had
started inside them. On a backend that shares the host's process namespace
those survive: application instances from six separate runs were found alive on
the host, each holding a user data directory that no longer existed, each still
drawing windows on the operator's screen. A related escape put two terminals
belonging to a per-run directory inside the operator's own application.

Both now have rails. Cleanup surveys for processes tied to this run — by
environment, working directory and process tree, never by program name, so it
cannot reach the operator's own application — stops them, and reports a
survivor as RESIDUE rather than DESTROYED. The coordinator terminal is
asked which machine it is on before anything is dispatched from it.

Corrections to earlier evidence in this branch

evidence/distrobox-full-cycle/README.md claimed a virtual display left the
host's desktop untouched. It did not: Distrobox shares the host's Wayland
socket, the application chose it over the DISPLAY it was given, and the
container's PID namespace is the host's. The record now says so and says why the
claim stood — nothing in that run observed where the application actually drew.

evidence/README.md still said no Orca had ever started in either backend. It
has been rewritten to say what each run showed and where the work stops.

Shape of the change

  • cycle_runner/ — one lifecycle, two adapters behind one interface, phases
    from prepare to close, with BLOCKED / UNKNOWN / TIMEOUT dispositions
    that refuse to claim knowledge nobody has.
  • host/ — pinned versions, an idempotent host preparation script, and the
    Packer build for the tool image.
  • tests/ — 549 tests, and counterexamples.py with 46 rows. Each row replaces
    one rail with an implementation that is present, runs and returns a pass, then
    runs the tests that must reject it; every row is currently RED.
  • evidence/ — what each run actually produced, scrubbed of the host's home,
    user name and machine identifiers and of nothing else.

Secrets

Exactly one declared auth method runs per cycle. No credential value, length or
prefix reaches an image, a seed, IaC state, a log, a receipt or this repository.
The one allowlisted file is copied into the per-run home, removed before
cleanup, and the removal is verified.

Gates

git diff --check, bash -n tests/contracts.sh, jq on every plugin manifest,
bash tests/contracts.sh, the line budget, the absence checks and both
disclosure greps all pass. Nothing under skills/ changes, so no version pin
moves.

… statuses, pattern redaction, result contract and a schema-validated manifest.
…the identity probe, project export and diff, export receipts, cleanup rails, auth bootstrap and the Orca worker launcher.
…t and export before stop, and destroy only after both are confirmed.
…it would mount and blocks on an unacknowledged host home.
…n overlay whose base is only a backing file, a seed carrying one public key, and a preflight that refuses an unverified provider schema.
…harness, and the evidence one host produced; require an orca the environment actually carries.
…ema, pool volumes, a user-mode egress interface and address discovery the runner owns.
…, and report a failed command by its tail and exit code.
…ng, and give the guest a video device its display server can use.
…gh to the real process runner launched the Orca application on the developer's own desktop.
…play first, and stop the image racing the runner for it.
…tions apart by what the path resolves to rather than by the path.
…replace the fixtures that had invented a flatter reply.
…d rejects the sender flag the other two take.
…nd the command the plane composed for the dispatch.
…an environment; a container inherited it and attested as the host's terminal.
A disposable environment gives Claude Code a home it has never run in, and
it asks four things before its first turn: finish onboarding and sign in,
trust the project copy, allow the external imports this project's own
instructions declare, and accept the permission mode it was launched with.
Each is a modal the dispatch cannot answer, so the agent holds the task
unsent and the execution plane reports a turn that never started — the same
symptom as a worker that died, with none of the cause.

The answers are written from what the configuration already says rather than
copied from the host, so no account, machine or history travels with them.
Auth now names its settings contribution in one place, because the first-run
state and the key helper share that file.
…decided the run.

The completion wait answers with a delivery nested under the reply's result;
the reply's top level is the request envelope. Reading the envelope found no
messages, so a worker that had finished its task and reported worker_done was
recorded as one that never answered, and the independent check was skipped on
a run whose artifact was already correct on disk.

The test fixtures carried the invented flat shape, so they agreed with the
defect. They now carry the shape the plane really sends, and each of the three
orchestration replies is exported beside the manifest, redacted. A verdict
with no reply behind it cannot be checked by the next reader.
The worker read the prompt, wrote the marker byte for byte, and reported
worker_done with outcome succeeded; the check ran outside that session and
agreed; the artifacts were exported and confirmed before the box and its
per-run home were destroyed.

The reported outcome now comes from the worker's own payload rather than from
the message type, which only repeated that it had finished.
An unverifiable dispatch says the turn was not observed and nothing more,
and the same reply names the terminal it created for the agent and the
command to read it. What that screen holds is the difference between an
agent still thinking, one stopped at a question nobody can answer, and a
launch line that was never submitted. The run now keeps it.
…ing is dispatched from it.

A command line inside an environment can reach a runtime outside it. The
binary, the user data path and the project path all look right, the reply
carries a handle and says the surface is visible, and the terminal belongs
to the host — which on this host meant tabs opening on the operator's own
desktop for work the environment never did. Nothing in the reply says so.

The terminal is now asked, once, and has to answer with the name of the
environment it is supposed to be in. A terminal that answers with another
machine, or does not answer at all, stops the run before a worker is
dispatched to it.
The record said a virtual display left the host's desktop untouched. It did
not. Distrobox shares the host's Wayland socket, the application chose it over
the DISPLAY it was given, and because the container shares the host's PID
namespace, destroying the container did not end those processes: instances from
seven runs were found still alive on the host, drawing on the operator's screen.

Setting DISPLAY is not isolation from a desktop, and nothing in that run
observed where the application actually drew — which is why the claim stood.
…work stops.

The index still said no Orca had started in either backend and no worker had
been driven through one. Both are now false, and one backend completes a cycle
while the other does not. The index says which is which, and keeps the earlier
runs because each of them moved a rail.
The same commit, host and task that settle in a container stop here at a
worker whose turn is never observed. The capture is the agent's own terminal:
the launch line and nothing after it. Network, shell, submission and the
first-run questions are ruled out by measurement; the agent's own silence in
that guest is a lead, and the measurement that would confirm it was
interrupted, so it is written as a lead.
…it is.

Cleanup declared a container and a directory and destroyed both, truthfully.
It never looked for what the run had started inside them, and on a backend that
shares the host's process namespace those survive: instances from six runs were
found alive on the host, each holding a user data directory that no longer
existed, each still drawing on the operator's screen.

The survey reads more than argv, because the application names no path in its
own: it reads the process environment, the working directory, and the tree
under the identifier the start script reported. It matches paths, never program
names, so it cannot reach the operator's own application. A process it cannot
stop makes the run RESIDUE rather than DESTROYED.

A process identifier only means something where the environment's process table
is the host's, so the adapter declares that and the machine backend declares it
false. The survey is passed in rather than defaulted, so a test that does not
ask for it can never signal a process on the machine running the tests.
@kacperpaczos

Copy link
Copy Markdown
Author

Opened by mistake against the upstream repository; this belongs on the fork. Closing straight away — sorry for the noise.

Sign up for free to 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.

1 participant