S6: the launcher boots, the composition layer closes the listener seam, and the diagonal runs - #193
Merged
Merged
Conversation
`guest_main._default_harden` called `apply_tier0()` with no policy, so the bare `Tier0Policy()` default applied: read-only roots of /usr, /lib, /lib64, /bin, /sbin and /etc. Two things a real guest needs are outside that set. `/proc` is the one S5 already paid for. `apply_tier0` installs the Landlock ruleset and only afterwards reads /proc/self/status to fill in `seccomp_mode` and `no_new_privs`, so without the grant a guest that hardened correctly reports `Seccomp: -1` — evidence-gathering denied by the ruleset it was verifying. S5 fixed it in the probe's own local policy and named it "not optional" there; the default was left alone, so the next caller inherited the gap. The source root is the new half. The launcher unpacks `repl_sandbox` into a directory under none of the default roots, and module-scope imports have already run by the time Tier-0 is applied — it is the lazy ones that would raise EACCES, deep in a later turn rather than at startup. Neither was reachable before: `guest_main` has no non-test caller, which is what `KataLauncher.boot` is about to change. Each of the three checks was watched failing against the code as it stood — both grants absent, and `_default_harden` calling `apply_tier0()` bare. pytest 1042 -> 1045.
INTERFACES §3.1a corrected the design records in July: under the ratified VMM's hybrid vsock the host accepts on an AF_UNIX socket, which carries no peer CID, so identity is the host-assigned id bound to that sandbox's own socket path. The records were followed; these two source comments were not. `session.py`'s header said the CID "comes from the kernel at `accept()`" and `capabilities.py` said guest CIDs are "kernel-assigned per microVM" — both unqualified, both in the modules whose actual behaviour that correction rewrote. Each now names what supplies the value under each transport and points at §3.1a rather than restating it, following the same one-place-to-read discipline the correction itself asks for. Also ties the two copies of the guest-CID floor together. `MIN_GUEST_CID` (derived) and `FIRST_GUEST_CID` (a literal) are one fact in two modules, and each was checked only against its own literal — so a change to either would leave two enforcing surfaces disagreeing about which CIDs are admissible with both modules' tests still green. Asserting them against each other is the point; a literal-vs-literal check passes while the copies drift, which is the shape S6-entry found in the reserved names. The new check was watched failing against a planted drift (FIRST_GUEST_CID 3 -> 4, reverted). pytest 1045 -> 1046.
`boot` ran the G1 gate and then raised. It now mints a sandbox, ensures the pinned image, launches under the Kata runtime, refuses when containerd reported success without a VM, discovers the vsock socket, ships the package, and releases everything it allocated if any of that fails. Three things were measured on the reference host rather than reasoned about, and each changed the code. `pgrep` excludes its own PID but not its ancestors, so on a host with zero VMMs a `pgrep -af cloud-hypervisor` from any process whose command line carries that string matches itself. A launcher whose job is refusing a boot that produced no VM cannot use a check that invents one, so identity is `/proc/<pid>/exe`. `comm` is not usable for it: TASK_COMM_LEN truncates at 15 characters, so a real VMM reads `cloud-hyperviso` and the obvious equality test would refuse every genuine boot while looking correct. Containerd image stores are per-namespace. `ctr -n trellis run` fails with `image "...": not found` against a namespace that has not pulled, so owning a namespace means owning the pull — which is why the digest is now a config value with `test_config.py` asserting it tracks the provisioner's copy. The namespace is not `default` because rule 19(a) asks a session to confirm a destructive step over its whole reach, and Kata puts the namespace literally in the leaked-cgroup path. By the time `ctr run -d` returns, the VMM and its socket already exist — the first poll 15 ms later found both. So an absent VMM is "never booted", not "not booted yet", and the refusal needs no polling window. `start_bridge` refuses instead of passing. The guest needs no in-guest forwarder — there is no rlms in the guest, and the stubs dial AF_VSOCK directly — but who owns a session's LM_PORT/DB_PORT listeners is not settled: `kata_repl` assigns them to the LM handler and the broker, `KataLauncher` takes no host, and no code outside tests and probes binds one. A silent no-op would assert a bridge that does not exist, since setup() calls this step "the bridge, before any untrusted worker process". Observed on the AX41: G1 PASS at ratio 12.7; boot in 34.8s; VMM pid found; socket discovered at /run/vc/vm/<sandbox>/clh.sock; the guest imported the shipped package and reported its source root as /run/trellis. Negative control — VMM detection blinded against a real, successful `ctr run` — the refusal fired and left zero containers, tasks, VMMs, vm dirs and namespace cgroups behind. pytest 1047 -> 1048, vitest 1416/119.
BUILD_PLAN §5.6 said `KataLauncher.boot` raises and the package has no guest entry point. Both were false as of this branch and the second was false before it — #190 built `guest_main.py`. §5.6 now carries the observed run, the three corrections the build owes to running rather than reading, and the gate ledger's S6 row says where it stopped. The eighth item is recorded as open rather than resolved. `start_bridge` refuses: the guest needs no in-guest forwarder, but who owns a session's LM_PORT/DB_PORT listeners is unsettled, and no item 1-7 named it. That reading went to a composed judge panel rather than being settled by the session that benefits from it — the no-forwarder half promoted unanimously, the proposed replacement was refused by all three seats on independent grounds, and the audit seat found the filing had omitted CONFORMANCE §2.1's still-standing "Option A ... is viable unchanged". C12's density-chain row asserted the same two falsified claims in the markdown and in the render's inline data. Both are densified in place rather than appended, and the render's script block was re-checked for parse — a hand-maintained artifact carrying executable data needs a syntax gate, not a proofread, and this one shipped broken once before. check:repo-surface PASS, density-trellis contract PASS, pytest 1048.
The previous commit updated C12's row in the class-overview table, which is not the section the staleness check watches — the tier body at the bottom of the file is. Three of its claims were false: that the launch path is absent, that the lesson about portable enforcing surfaces had been paid twice, and that `boot` still raises with no guest entry point behind it. Densified in place rather than appended: T1 carries the third instance of the surface-moves lesson, T2 carries the launcher that boots and the observation that the host-to-guest composition exists only as a block hand-written five times outside the package, and T5 replaces the "next real build" paragraph with what the build found — the three measured corrections, and the refusal it stopped at.
The lifetime question asked in the last session presupposed that the durable thing and the executing thing are one object, which made a scope that ends look like a session that is lost. They are two objects. A workspace survives because its state is in the substrate; the microVM is compute borrowed for one stretch of work. The diagram places the REPL at the centre and draws that separation: many swappable workspaces on durable storage, one loaded into a persistent namespace, an ephemeral VM shell around it carrying the three ports, the model holding addresses rather than bytes, and the artifact loop closing back into the store through the judges and the user gate. It also marks the open seam honestly: the host end of LM and DB has no owner.
An owner exchange established that a session's question about the sandbox composition's lifetime rested on a false premise: it treated the REPL and the microVM as one object, which makes a closing scope look like a lost session. They are two objects, and the record now says so before a later session presupposes it again. THE_REPL_IN_TRELLIS.md documents the system diagram and the understanding behind it: workspaces are many, swappable and durable (a repository is one, which is how Trellis edits Trellis); one workspace per session; the namespace holds handles rather than payloads; the microVM is torn down at session end and nothing that matters is lost when it goes. It answers the persistence question the owner put back to the session. A workspace does not remember by snapshotting its namespace, on four grounds of which the first is decisive: the namespace holds model-authored objects, and deserialising them outside the sandbox is an arbitrary-code-execution primitive running with the host's privileges. It remembers by reconstruction over its own store plus a manifest -- which means the artifact loop is not a feature on top of persistence, it IS the persistence mechanism, and a result that was never filed was never meant to survive. Concurrency is restated correctly after two wrong framings in as many sessions. Instances run in parallel, one per database; workspaces are what must not be shared, and checkout is the lock. That forces a correction on the launcher's own commit message: the containerd namespace is a constant, so parallel instances share it, and a cleanup sweep must key on the sandbox name it minted rather than on the namespace. Owed and not done here: an index row for the new record. docs/README.md has ~28 bytes of headroom against a checked budget, so it needs a densifying pass rather than an append.
`_kill_shim` found its target with `pgrep -f 'containerd-shim-kata-v2.*<name>'` and SIGKILLed whatever came back. During cleanup on the reference host an operator ran that same pattern from a shell and killed the shell itself: pgrep matches any process whose command line contains the pattern, and the invoking command line did. That is the fourth time this trap has fired here and the first time it destroyed something. `vmm_pids_carrying` already avoided it by reading `/proc/<pid>/exe`. The fix is to make that the only way this module identifies a process: `_pids_by_executable` carries the walk, `vmm_pids_carrying` delegates, and `_kill_shim` uses it. The `carrying` filter matches the sandbox name as a whole path component rather than a substring, so `sess-1` cannot claim `sess-10`'s shim. Three checks over a fake `/proc`, each watched failing against the behaviour they replace. The first draft of two of them passed the planted breakage -- their impostors were being caught by the name filter rather than by the executable check, so they proved nothing about the fix. The impostors now carry the sandbox name as a real path component, which leaves the executable check as the only thing that can catch them; planting the pgrep behaviour reddens exactly those two. pytest 1048 -> 1051.
Owner-directed companion to trellis_repl_system.svg. It records what the diagram claims and where each claim comes from, so a later session checks the picture against the code rather than against prose. The reason it exists is the error at its top: a session asked whether the sandbox composition should be a context manager, framing it as unmissable cleanup versus a session outliving its scope. That tension was manufactured by treating the REPL and the microVM as one object. They are three lifetimes -- a durable workspace, a per-session namespace, a disposable VM -- and a scope ending releases the VM without losing anything, because what persists is in the substrate. rlms already ends a non-persistent run with cleanup(). Also records, with measurements: that Cloud Hypervisor snapshot/restore is real and cheap (0.2s, 174 MB on disk for a 2 GiB guest) but desynchronises the Kata shim when driven behind containerd's back, so it is a scoping fact for the workspace-persistence question rather than a ready mechanism; and that this session's own containerd-namespace default is justified in config.py by a property -- everything under this path is mine -- that is false once two Trellis instances share a machine, which the owner has now confirmed they will.
`session_host.open_workspace_session` is the seam the eighth item of BUILD_PLAN 5.6 named and nothing owned. It is not new: the block already worked in five places -- every probe and the CLI selftest each build a host, open a session, bind the listeners and drive the backend -- but each proved one half by substituting a stand-in for the other, and the diagonal never ran. This promotes it out of `scripts/` and gives it a name. A context manager, because the durable thing and the executing thing are different objects. A workspace persists through the substrate; the microVM is compute borrowed for one stretch of work. So a closing scope is a VM released, never a session lost, and leaking a live VM stops being possible by forgetting. rlms already models this -- its driver ends a non-persistent run with `cleanup()`. The manifest ships now and ships empty, so reconstruction is a no-op on day one and no later session retrofits a mechanism around an absence. Its field set is pinned as a test: it carries addresses and never a namespace, because restoring model-authored objects outside the sandbox would be an arbitrary-code-execution primitive with the host's privileges. A turn's durable output is the filed artifact; the artifact loop is not a feature on top of persistence, it is the mechanism. Identity is two values, not one. A workspace id is stable -- physics is the same workspace next Tuesday -- and a session id is unique per opening, dated, with entropy, because one user opening two sessions in a day is ordinary and a collision there shares a ledger. The user component is operational identity only: it names a lease holder on this machine and never reaches the substrate, where FEATURE_LIST 1.6 is closed on the ground that nothing names an owner. Checkout auto-reclaims on an observation rather than a timeout. A lease names the sandbox its holder booted, so liveness is answerable by asking whether a real VMM still carries it -- the same exe-verified check written to refuse a boot that produced no VM now proves a real one is gone. A timeout would be wrong in both directions. `start_bridge` no longer refuses unconditionally: it refuses when nobody attached a bridge, which stays the honest default for a caller that boots a guest and never composes a session. Four properties watched failing against planted breakage before being trusted: reclaiming a live holder, asking liveness about the claimant's sandbox instead of the holder's, and leaking the lease on each of the two exit paths. pytest 1051 -> 1067, vitest 1416/119.
A live Kata guest was paused and snapshotted through Cloud Hypervisor's own API socket. Every step reported success; afterwards the container could not be re-entered and teardown could not reap the VMM. The cause is that a Kata sandbox has two control planes reaching one virtual machine and neither knows about the other. containerd's authority runs through kata-agent, a process *inside* the guest; pausing the VM freezes that process, the shim's RPC times out, and the channel is torn down. Resuming brings the guest back and re-establishes nothing. The mistake errored at no point -- reaching past containerd did not do something it forbade, it did something it could not see. That is the fifth instance of this program's recurring shape, and the first stated as a rule about operations rather than about enforcing surfaces: an operation is only as safe as the layer it is issued at. The reconciliation is recorded because it looked like a fork and was not. Workspace persistence appeared to want VM snapshots while the launch path is containerd. The owner's ruling that the manifest ships empty settles it at the application layer instead: the VM returns to being disposable, which is what the context-manager lifetime already assumed, containerd stays the launch path so nothing measured on hardware is invalidated, and snapshot becomes a future warm-start optimisation rather than the foundation of persistence. Also records the four rulings the next build carries, and flags that the workspace-identity composition is proposed rather than ratified -- Matt raised it as a revision to the standing plan and deferred to Cnid, and that agreement is not on the record yet.
Owner direction (Matt, 2026-07-25): a workspace identifier carries a timestamp at the finest granularity the system offers plus a UUID, and the human-facing label moves to metadata. That dissolved the ambiguity rather than managing it. Deriving an identifier from a workspace's name and the current date forces a choice between stable and unique -- a date breaks it as a lock key, no date makes two workspaces called "physics" the same workspace. Minting once at creation gives both. "physics" is now a display name: no lease, ledger, manifest or audit line refers to it, so a rename is a one-field edit. The user component stays operational identity only -- it names a lease holder on this machine and never reaches the substrate, where FEATURE_LIST 1.6 is closed on the ground that nothing names an owner. A stored Trellis user id is a live proposal needing its own dated entry; this build forecloses neither choice. Then the diagonal ran on the AX41 for the first time: the real backend composition against the real microVM boundary, rather than either half against a stand-in. 7/7 on the second attempt -- a real VMM carrying the session's sandbox, the host end bound at <uds>_5001, an empty manifest reconstructing to a no-op, the workspace checked out for the life of the scope, a second session on the same live workspace refused, the scope closing to zero VMMs, containers, VM directories and no lease, and a crashed holder's lease auto-reclaimed against a real liveness check. The first attempt returned 6/7 and found what no off-host test could see. The module header lists step 5 as "bind the listeners"; the code attached the bridge and left starting it to KataREPL.setup(). Every off-host test passed, because none of them opens a session without also driving a backend -- so a session composed without one came up with no host end at all, and the guest's first tool call would have met a closed connection. A step described in prose and merely prepared in code reads as done; third instance of that shape here. The repair makes SessionBridge.start idempotent by design rather than defensively. The composition layer binds at step 5 because it owns the failure -- a second listener can fail after the first is bound and a VM is running, and only the party holding both can unwind them -- while KataREPL.setup is equally right to assert the bridge is up before any untrusted worker. Both callers are correct, so the second call confirms rather than rebinding a path already held. pytest 1067 -> 1071, vitest 1416/119, repo-surface PASS, density-trellis PASS.
.claude/launch.json is scaffolded by the Claude Code preview tooling when the browser pane is used, not authored. It appeared mid-session in one worktree, had never been in git history, and was not ignored -- so a blanket `git add -A` swept it into a commit, which would have made one session's incidental choice of dev server the default every later session loads. The repo already states this convention at the line above: committed harness config is deliberate (.claude/settings.json, rules, skills, ceremonies) and per-machine harness state is ignored (.claude/settings.local.json). This is the second kind and had no rule yet.
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 free
to 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.
KataLauncher.bootraised after the G1 gate. It now claims a real microVM, andsession_host.open_workspace_sessionjoins it to a host for the first time —the diagonal that every earlier proof avoided by substituting a stand-in for
one half.
What was unbuilt, and is now
KataLauncher.boot. Mints a sandbox, ensures the pinned image in its owncontainerd namespace, launches under the Kata runtime, refuses when containerd
reports success without creating a VM, discovers the vsock socket from the
running VMM's argv, ships the package, and releases everything it allocated if
any step fails.
session_host.open_workspace_session. The composition layer — a contextmanager over six forced steps: lease, host,
open_session, boot, listeners,scaffold. Not a new seam: the block already worked in five places (every probe,
the CLI selftest), each proving one half against a stand-in for the other. This
promotes it out of
scripts/and names it.Three things measured rather than reasoned about
Each changed the code.
pgrepcannot carry the refusal. It excludes its own PID but not itsancestors, so on a host with zero VMMs a
pgrep -af cloud-hypervisorfrom anyprocess whose command line contains that string matches itself.
commis notthe fix:
TASK_COMM_LENtruncates at 15 characters, so a real VMM readscloud-hypervisoand the obvious equality test refuses every genuine boot whilelooking correct. Identity is
/proc/<pid>/exe.Image stores are per-namespace.
ctr -n trellis runfails withimage "…": not founduntil that namespace has pulled, so owning a namespacemeans owning the pull — which is why the digest is now a config value with a
test asserting it tracks
provision_kata_host.sh.Readiness does not gate the refusal. By the time
ctr run -dreturns, theVMM and its socket already exist (first poll at 15 ms found both). An absent VMM
is never booted, not not booted yet.
The eighth item, found and closed
Section 5.6 listed seven gaps. An eighth existed: nothing joined a launched
guest to the
TrellisSandboxHostits own stubs dial. The reading went to acomposed judge panel rather than being settled by the session that benefits from
it — the no-forwarder half promoted unanimously, the proposed replacement was
refused by all three seats on independent grounds, and the audit seat found the
evidence bundle had omitted a still-standing record passage. Closed by owner
ruling in favour of a composition layer.
Verification
Every check was watched failing against planted breakage before being trusted.
bound at
<uds>_5001; an empty manifest reconstructing to a no-op; theworkspace checked out for the life of the scope; a second session on the same
live workspace refused; the scope closing to zero VMMs, containers, VM
directories and no lease; a crashed holder's lease auto-reclaimed against a
real liveness check.
ctr run— the refusal fired and left nothing behind.could see: step 5 was described in the module header and only prepared in the
code, so a session composed without a backend came up with no host end at all.
A step described in prose and merely prepared in code reads as done.
pytest1042 → 1071 ·vitest1416/119 ·check:repo-surfacePASS ·density-trellis contract PASS.
Records
BUILD_PLAN§5.6 carries the observed run and the closed eighth item;THE_REPL_IN_TRELLIS.mdis new and documents the system diagram, the identityrules, and what running it changed; C12's density-chain row tracks the code.
Scope carried honestly
S6's
[R]half now has its enforcing surface. The equivalence harness againstCONFORMANCE §6's twelve predicted-FALSE clauses is unbuilt, and the[A]half— the metered real-model run — has not been proposed or spent. Nine
density-chain classes remain stale from earlier commits on master and are
deliberately left for their own session.