Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6688d47
Record the shared cycle-runner decision and its plan as the review ba…
kacperpaczos Sep 14, 2026
9322cac
Add the cycle run contract: validated config, run identifiers, closed…
kacperpaczos Sep 14, 2026
a3bd464
Add the runner core: deadline-aware process capture, host snapshots, …
kacperpaczos Sep 14, 2026
115b543
Add the shared cycle lifecycle: identity gate before the task, collec…
kacperpaczos Sep 14, 2026
0f0c35b
Add the Distrobox adapter, with a preflight that asks Distrobox what …
kacperpaczos Sep 14, 2026
e631b5c
Add the machine adapter: a per-run domain declared with Pulumi over a…
kacperpaczos Sep 14, 2026
7b503bc
Add the command line, the runner's documentation, the counterexample …
kacperpaczos Sep 14, 2026
e025051
Reconcile the acceptance table into the runner's own documentation an…
kacperpaczos Sep 14, 2026
9d1708f
Redact values forwarded over the machine transport, and state the exp…
kacperpaczos Sep 14, 2026
69a0aeb
Record the resources a failed create may have left, instead of report…
kacperpaczos Sep 14, 2026
fc9d31b
Refresh the committed evidence from a cycle run by the final code.
kacperpaczos Sep 14, 2026
23cbe0c
Stop preflight leaving a per-run home behind: distrobox creates the c…
kacperpaczos Sep 14, 2026
2850c3b
Rebuild the machine adapter against the pinned provider: verified sch…
kacperpaczos Sep 15, 2026
2caf65b
Quote the remote command so an argument vector survives the transport…
kacperpaczos Sep 15, 2026
9ff4701
Wait for the guest to answer before calling the environment created; …
kacperpaczos Sep 15, 2026
34e98be
Add the host preparation, the tool-image build, and the evidence one …
kacperpaczos Sep 15, 2026
5ee0823
Prove Orca's runtime and open a coordinator terminal before dispatchi…
kacperpaczos Sep 15, 2026
90820b6
Stop the test doubles running anything they do not name: a fall-throu…
kacperpaczos Sep 15, 2026
dd2e373
Start the application only when the runtime is down, wait for the dis…
kacperpaczos Sep 15, 2026
4e402e5
Name the Orca command the environment must use, and tell two installa…
kacperpaczos Sep 15, 2026
0a5cd87
Make the project copy a repository Orca can register, and keep reposi…
kacperpaczos Sep 15, 2026
e4860de
Read the Run and Dispatch identifiers from where Orca puts them, and …
kacperpaczos Sep 15, 2026
9d0b943
Settle an unverifiable dispatch as unknown rather than failed, and re…
kacperpaczos Sep 15, 2026
25f161c
Record a container carrying its own Orca at the host's path, and the …
kacperpaczos Sep 15, 2026
beb9fc7
Wait for a dispatch the plane could not observe, instead of calling i…
kacperpaczos Sep 15, 2026
1b2c1b1
Give the completion wait the flag it accepts: it names a terminal, an…
kacperpaczos Sep 15, 2026
cd2ef92
Record the coordinator and the worker as two sessions in one guest, a…
kacperpaczos Sep 15, 2026
a1f8be9
Record a deadline firing on the machine backend with every artifact k…
kacperpaczos Sep 15, 2026
e2dd118
Provision the environment before anything that needs what provisionin…
kacperpaczos Sep 15, 2026
0ef7cae
Report an Orca refusal by its code and message instead of the tail of…
kacperpaczos Sep 15, 2026
d543079
Strip the host's own session identity before running anything inside …
kacperpaczos Sep 15, 2026
b0fab72
Record one full container cycle on a display of its own, reaching whe…
kacperpaczos Sep 15, 2026
57b73c6
Answer the questions a fresh agent home asks before it will work.
kacperpaczos Sep 15, 2026
4976600
Read the settling message from the delivery, and keep the reply that …
kacperpaczos Sep 15, 2026
7d6f220
Record the first cycle that settled, with the independent check passing.
kacperpaczos Sep 15, 2026
0ee077e
Read the agent's terminal when the plane could not see its turn start.
kacperpaczos Sep 15, 2026
d84b0fb
Make the coordinator terminal say which machine it is on before anyth…
kacperpaczos Sep 15, 2026
3b9f99d
Correct a claim in the container evidence that a later run disproved.
kacperpaczos Sep 15, 2026
120d104
Rewrite the evidence index to say what each run showed and where the …
kacperpaczos Sep 15, 2026
a819882
Record the machine backend not completing, and what that run ruled out.
kacperpaczos Sep 15, 2026
a205df4
End what a run started, and call a run with processes left over what …
kacperpaczos Sep 15, 2026
1114607
Name the new rails in the acceptance table and stop the README implyi…
kacperpaczos Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.py[cod]
experiments/minimal-cycle/.artifacts/
experiments/minimal-cycle/.state/
288 changes: 287 additions & 1 deletion docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,298 @@
What has been settled, what is still open, and what was rejected and why.
Rationale is kept because the reasons are the reusable part.

Last updated 2026-09-06.
Last updated 2026-09-15.

---

## Settled

### 2026-09-15 — A disposable environment is not disposable until nothing of it is still running on the host

#### Context

A container run gave its Orca a virtual framebuffer of its own and the evidence
recorded that the host's desktop was therefore untouched. It was not. Distrobox
shares the host's Wayland socket as well as its X socket, and the application
chose Wayland: it drew on the operator's own compositor and ignored the
`DISPLAY` it was handed. Because a Distrobox container also shares the host's
PID namespace, destroying the container did not end those processes. Instances
from seven separate runs were found alive on the host hours later, each holding
a `--user-data-dir` under its own destroyed per-run directory, and each still
putting windows on the operator's screen — twice, on two different days.

Cleanup had reported `DESTROYED` every time, truthfully: every resource the
adapter *declared* was gone. The declaration named a container and a directory.
It did not name the processes the run had started, because nothing in the run
had ever looked for them.

A second escape had the same shape. 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 terminal handle and reports its surface as
visible, and the terminal belongs to the host. Two such terminals were found in
the operator's own application, holding the host's shell in a per-run directory
that no longer existed.

#### Decision

An environment's resources include what it started. Cleanup surveys the host for
processes holding a path under this run's own state directory, stops them, and
reports what it stopped; a survivor is residue, not a detail.

Before anything is dispatched from it, the coordinator terminal is asked which
machine it is on 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, stops the run.

Neither check asks the backend to promise anything. Both read what is true after
the fact, which is the only form in which either of these was ever visible.

#### Alternatives considered

Trusting `DISPLAY`. It is what produced the false claim: an environment variable
records an intention, and the application is free to connect somewhere else.

Reading the desktop to see whether a window appeared. It answers the question
for the run that is watching and not for the one that already exited, and it
needs a desktop the runner is not supposed to touch.

Killing by process name. It would reach the operator's own application, which is
the failure this is meant to prevent, not a worse version of it.

#### Consequences

"Destroyed" now means something a reader can check, and it costs one survey per
run. A backend that shares the host's namespaces is recorded as sharing them:
the container backend mounts the host home, shares the host's PID namespace and
shares the host's Wayland socket, and the preflight and the evidence say so
rather than describing a sandbox.

The general form is worth keeping. A disposable environment that shares any
namespace with its host can leave something behind that outlives the resource
the run declared, and a cleanup that verifies only its own declarations will
report success while that thing is still on the screen.

#### Non-goals

Making Distrobox an isolation boundary. Managing the operator's own
application.

#### Deferred

The same survey for the machine backend's guest-side processes, triggered by a
backend whose transport can outlive its domain.

### 2026-09-15 — A test double that can run a command runs it on the developer's machine

#### Context

The cycle runner's fake backend adapter treated its "environment" as a
directory on the host, and answered a small set of commands from a table.
Anything it did not recognise fell through to the real process runner, which
meant it ran on the machine running the tests. That was a deliberate choice:
it made the fake honest about the commands it did handle, because the check
script and the auth teardown really executed and really observed their files.

Then the runner gained a step that starts the Orca desktop application inside
the environment. The fake did not recognise it, so it fell through. Every
lifecycle test that reached the identity phase — more than a dozen — launched a
real Orca on the developer's own desktop. The result was dozens of application
windows and a reboot to clear them.

Nothing about that was a bug in the runner. The runner did exactly what it was
asked. The fault was entirely in the double: its fall-through was a general
permission to execute, held open for the convenience of a few specific
commands.

#### Decision

A test double never runs a command it does not name. The fake adapter carries
an allowlist of program names and, for `sh -c`, an allowlist of the runner's
own script names; anything else is answered with a non-zero outcome and a
reason, never executed. Removals are refused outside the fake's own root. The
stub runner's passthrough is narrowed to key generation, which is the one thing
it genuinely needs a real process for.

`tests/test_doubles_are_contained.py` holds these as tests: starting the
application through the fake must be refused, an arbitrary program must be
refused, an unnamed shell script must be refused, and the runner's own scripts
must still run.

#### Alternatives considered

Removing execution from the fake entirely and scripting every answer. It would
be safe and it would make the check and the teardown prove nothing: those tests
are worth having precisely because a real file is really inspected.

Running the suite in a container or a virtual machine. It would contain the
blast radius and it would also hide the defect, which is that a double should
not be able to do this in the first place. It is worth doing later as defence
in depth; it is not the fix.

#### Consequences

The allowlist has to grow deliberately when the runner learns a new command the
fake should really execute, and a reviewer sees that growth. That is the point.

This generalises past this runner. Any test double standing in for a remote or
isolated environment, on a machine where the same commands would also work
locally, has this shape: the fall-through case is the dangerous one, and it is
usually written for convenience by someone who is thinking about the commands
they listed rather than the ones they did not.

#### Non-goals

Sandboxing the suite. Preventing the runner itself from starting applications
in a real environment — that is its job.

#### Deferred

Running the suite inside a disposable environment as defence in depth,
triggered by the suite needing to execute anything broader than the current
allowlist.

### 2026-09-14 — One shared cycle runner, two backend adapters, export before cleanup

#### Context

The delivery contract has never been exercised against a disposable
environment. Every observation so far comes from this checkout on this host,
so a run that silently used the host's own Orca, the host's own checkout, or
the host's own login would look exactly like a run that used an isolated one.
The design handed to this delivery asks a narrower question than a benchmark
does: can one cycle be created, driven, observed, exported and destroyed
without losing its evidence?

Two isolation mechanisms were on the table and neither subsumes the other.
Distrobox integrates deliberately with the host — home directory, graphical
sockets, audio, message bus — and its own documentation warns against reading
that as container-grade isolation. A virtual machine under `libvirt` with
`qemu` isolates far more and costs a base image, a tool image, a seed and an
address for reaching into the guest. Choosing one would have settled the
isolation question by deleting the other half of it.

The failure this record is built around is not a weak backend. It is a green
manifest produced by a run that never entered the environment, or a clean
cleanup that removed the evidence that would have shown it.

#### Decision

`experiments/minimal-cycle/` carries one Python runner with one lifecycle and
two backend adapters behind one interface. The runner is a coordinator and a
redactor; it is not a container manager, a virtual-machine manager, a
scheduler, or a second orchestration next to Orca. Each adapter drives the
documented interface of its own tool — Distrobox Assemble and the Distrobox
command line for one, Pulumi with the `libvirt` provider over `qemu` for the
other — and both return the same result contract.

Four rails are load-bearing and each is proved by an instrument that rejects a
present, running, passing implementation rather than an absent one.

An identity verdict precedes the task. The runner runs the same probe on the
host and inside the environment and refuses to continue when the environment's
answer carries no environment marker and repeats the host's own name, machine
identity and home. Finding no Orca inside the environment is a blocked run,
never a reason to use the host's.

Export precedes destruction, and the receipt is a re-read. Every required
artifact is written to the host, then read back from that host path and
digested again; the run is exportable only when every one of them matches.
Cleanup is attempted only after that confirmation. An unconfirmed export
leaves the environment standing and records residue with its reason.

Cleanup is a declared list, not a path expression. Each adapter names its
per-run resources and its shared ones, and the runner refuses any target
outside the per-run list. A shared base image, a shared tool image and a
shared configuration survive every terminal state by construction rather than
by the destroy command happening to point elsewhere.

Redaction is by pattern. A token the run has never seen is redacted because of
its shape, not because it was on a list, and an absolute path to a credential
file is replaced before it reaches a log, a manifest, a seed, a stack setting
or this repository.

Where a backend's own behaviour contradicts one of those rails, the runner
neither hides it nor works around it. It asks the tool what it would really do,
and turns the answer into a preflight finding that blocks until the
configuration records the compromise deliberately. Two exist. Distrobox mounts
the invoking user's home into every box and offers no flag to suppress it, so
`distrobox.accept_host_home_mount` has to say so before a run starts. The
libvirt provider's resource schema is not assumed from an example, so
`vm.provider_schema_verified` has to record that someone checked the rendered
program against the pinned version. An acknowledgement is recorded in the
manifest; it is not a way of making the fact go away.

#### Alternatives considered

One backend only, deferring the other. It would have halved the work and
settled by omission the one architectural question actually in play — whether
the lifecycle can be written once. Writing both adapters against one interface
is what tests that claim.

`libvirt-python` directly instead of Pulumi. It is the lower-level official
binding, and using it would have meant writing domain, disk, state and cleanup
management inside this runner: exactly the second manager the design forbids.

A single shell script per backend. It is smaller, and it cannot express the
one ordering that matters. Export-then-verify-then-destroy, with a refusal in
the middle, is control flow with state, and the rails above are testable only
because they are functions rather than lines in a script.

Writing the diff inside the environment with `diff` or `git`. It adds a tool
requirement to every image and makes the patch a property of the environment
rather than of the runner. The tree is fetched and the patch is computed on
the host with the standard library instead, identically for both backends.

#### Consequences

The repository now carries Python, which it did not before. It is confined to
`experiments/minimal-cycle/`, depends on the standard library at runtime, and
is not part of the dely plugin surface: no skill, no plugin manifest and no
structural contract check moves because of it.

The runner is ready for the separate acceptance test the design reserves, and
it has not passed that test. This host carries no `pulumi`, no Orca package
inside any container image, and no built tool image, so nothing here observes
Orca starting in either backend, a window bound to that instance, a Claude
Code worker driven through it, or a login surviving a run. Those remain
unproven, and the runner's rails are written so that the absence shows up as a
blocked run rather than as a green one.

This decision does not make Distrobox a sandbox and does not claim the two
backends are equivalent. It claims one lifecycle can drive both and that the
evidence survives every way the cycle can end.

Running it already moved one rail. The first real Distrobox cycle reached the
identity gate, was correctly judged to be in the environment, and was allowed
through because `command -v orca` resolved inside the box. It resolved to the
*host's* launcher: Distrobox mounts the host home and preserves `PATH`, so a
host installation answers from inside a container that has none. That run failed
later, at the launch, for an unrelated reason. Had the launcher been one that
works from inside a container, it would have driven the host and produced a
complete green manifest for work the environment never did.

Presence of a command is therefore not presence of an installation. Orca counts
as present only when the path found inside differs from the one the host probe
found and the binary reports a version there. The point generalises past Orca:
anything a disposable environment inherits from the host through a mount or
through `PATH` can answer a presence check without being present, and a check
that only asks whether a name resolves cannot tell the two apart.

#### Non-goals

Measuring start-up time, model quality, tester quality, cost, or any task
matrix. Building an image cache engine. Implementing the tester, builder,
reviewer and committer roles. Running the cycle under continuous integration.

#### Deferred

The real one-cycle acceptance on each backend, triggered by a host that
carries `pulumi`, a verified base image, a built tool image, and an Orca
package installable into the chosen image. A second fixture beyond the single
marker file, triggered by that acceptance passing. Running the runner's own
suite in continuous integration, triggered by a decision about whether this
repository's workflow should carry a Python step.

### 2026-09-06 — The dispatch prompt carries the acceptance row as written

#### Context
Expand Down
Loading