Phase 3: Container Runtime and Bridges - Context
Gathered: 2026-10-21 Status: Ready for planning Mode: --auto (all gray areas auto-selected; each choice derived from docs/specs/realm-spec.xml, the settled user decisions S-07 to S-16 below, and the frozen Phase 1 and Phase 2 contracts. Decision IDs continue from Phase 2 (D-46 onward). Threat IDs use T-03-xx.)
<domain>
Phase Boundary
Phase 3 delivers the runtime and bridge layer behind the lifecycle controller, and the first gate on file-mutating tools:
- Prerequisite probe for Podman, the rootless Podman API socket, and the Podman version floor (RT-02).
- A closed, typed libpod client over a Unix socket, with no exec route, and the session-container hardening profile plus its verifier (RT-01).
- An OpenShell adapter that builds the pinned
sandbox createargv, writes a deny-all network policy document, and is the only path by which agent workload code is started (RT-03). - An AgentOS supervisor behind an
AgentOsDriverport, with bounded backoff andAGENTOS_CRASH_LOOP(RT-04). - A TypeScript HTTP client for Anda Engine with a shape handshake, reconnection with bounded exponential backoff and jitter, and state feed into the controller (ANDA-01, ANDA-02, ANDA-03, ANDA-04).
- The lifecycle gate for file-mutating tools (CORE-05),
realm downlabel sweep with zero orphans (RT-09), and the Phase 3 doctor checks that D-44 assigned to this phase. - Podman-gated integration bodies that skip with a stated reason on Podman 4.9.3 (
PODMAN_UNSUPPORTED_VERSION).
Out of this phase (see <deferred>): the reaper and stale-record cleanup (RT-08, Phase 4); default-deny egress enforcement and the egress allowlist mapping (RT-05, Phase 4); the dispatcher, queue wait, and tool timeouts (TOOL-03, TOOL-04, Phase 4); any per-call exec through OpenShell (Phase 4, because no exec verb is in the pinned OpenShell set); creation of the control socket (RT-06, Phase 6); the CLI and daemon (Phase 6); host snippets; the Omp, MCP, and ACP surfaces (Phase 5).
Verification boundary. The local host runs Podman 4.9.3, below the 5.x that OpenShell's Podman driver requires, so no live container or OpenShell run is possible in this phase. Every behavior is tested against in-process fakes and a fake Unix-socket libpod server. Live behavior is written as gated suites that skip with a stated reason. A phase verification can therefore only report "unit-verified, live-gated". It must not report live verification.
</domain>
<spec_lock>
Requirements Lock (source: .planning/REQUIREMENTS.md and docs/specs/realm-spec.xml)
Phase requirements: CORE-05, RT-01, RT-02, RT-03, RT-04, RT-09, ANDA-01, ANDA-02, ANDA-03, ANDA-04 (10 IDs). Their text lives in .planning/REQUIREMENTS.md and is not restated here. No phase-level *-SPEC.md exists, so spec_loaded = false. Where a requirement's literal wording conflicts with a settled input, the settled input governs and the conflict is recorded in D-46 and D-47. The requirement text is not edited in this phase.
</spec_lock>
<decisions>
Implementation Decisions
Settled inputs (from the user; not reopened in this discussion)
- S-07: Policy default is advisory with a visible warning. Fail-closed is an opt-in per project. Host write tools (Omp built-in write and edit) are disabled only in required mode, which is Phase 5 (OMP-05). Phase 3 gates mutation through the controller (CORE-05); it does not touch host tools.
- S-08: Runtime layering. The OpenShell gateway runs on the host, rootless, and creates one OpenShell sandbox per session through its Podman driver. The AgentOS library runs as the sandbox workload. Anda Engine runs as a separate service, reached over HTTP from the TypeScript client. Nothing nests OpenShell inside the Realm container, and nothing runs privileged.
- S-09: Host Podman is 4.9.3. The OpenShell Podman driver needs Podman 5.x. Live container and runtime integration tests stay gated with
PODMAN_UNSUPPORTED_VERSION. Unit tests use a fake Unix-socket HTTP server that emulates the libpod routes. The verified route set is confirmed on a 5.x host through the gated integration suite. The local Podman install is not changed. - S-10: OpenShell CLI is pinned to the locally verified 0.1.2 verbs:
sandbox create --name,--policy,--no-auto-providers,--no-keep,--detach,--env,--label,--provider,--cpu,--memory, and a trailing-- command. The docs describe 0.1.5. The drift is recorded in the runtime decision note and re-verified on upgrade. - S-11: AgentOS: the library mode inside the sandbox is primary. Its native sidecar requirement is unverified, so it is recorded as an open question. Hosted mode (Rivet engine and services images through Podman) is secondary and gated.
- S-12: Anda: a TypeScript client over the Anda Engine HTTP API (
POST /{id}, CBOR or JSON, Bearer CWT or ICP envelope). No Rust shim. A version handshake runs on connect. Reconnection uses bounded exponential backoff with jitter, and the connection state feeds the state machine. - Carried from Phase 2 (S-02 to S-05, still in force): one container per session; advisory locks live in the daemon, keyed by canonical path, with no daemon in Phase 3; the control channel is a user-only Unix socket and is not created in Phase 3; reaping keys off the session record and belongs to Phase 4.
Requirement-text conflicts resolved by the settled inputs
D-46: RT-03 and ROADMAP Phase 3 success criterion 3 say "OpenShell runs inside the container". S-08 places the OpenShell gateway on the host, and the Podman driver creates the sandbox container. The settled layering governs. RT-03's intent is kept: all agent execution is routed through OpenShell, and no code path bypasses it. This is proven by
test/runtime/openshell-routing.test.ts: the only workload start is the OpenShell adapter argv, the libpod client has no exec method, and AgentOS is reached only through its supervisor. The requirement text is not edited here. The mismatch is reported to the user as a requirement-wording decision. Reversibility: costly. Every runtime module depends on this layering.D-47: RT-01's "running realm container" is the session sandbox container created by the OpenShell Podman driver. Realm does not create it, because S-08 assigns creation to OpenShell. Realm verifies it after create through libpod inspect against the hardening profile (D-51). A mismatch destroys the container and moves the session to
errorwithOPENSHELL_START_FAILEDand a reason that names the first failed property. No fallback to a Realm-created container is allowed without a new decision. Reversibility: costly.D-48: Gating. Every live test skips with a reason that names the code (
PODMAN_UNSUPPORTED_VERSIONon 4.9.3,PODMAN_MISSINGwhen the binary is absent). The unit suite never depends on Podman. The libpod routes and their response fields are provisional (podman.md Q5: no route verified). They are isolated in one file,src/runtime/libpod-routes.ts, and marked provisional there. Reversibility: reversible, because the route file is the only place they live.
Module layout (extends D-19; spec n2 with the Phase 2 amendments)
D-49: New paths for Phase 3. Each file has one owner:
src/core/backoff.ts: pure bounded exponential backoff with full jitter. The random source and the sleep are injected. Shared by the Anda client and the AgentOS supervisor.src/core/ports.ts: add a sibling interfaceBridgePort(connect, current state, change subscription, close).ExecutionServiceis unchanged (D-39 is frozen).src/runtime/config.ts:RuntimeConfigtype and load-time validation (D-52).src/runtime/podman-version.ts: the version parser moved fromtest/integration/podman-version.ts(Phase 1 handed this move to Phase 3). Its unit test moves with it totest/runtime/podman-version.test.ts, and the imports intest/integration/podman-gate.test.tsare updated.src/runtime/podman-probe.ts: the prerequisite probe (D-53).src/runtime/libpod-routes.ts(provisional route table) andsrc/runtime/libpod-client.ts(closed client, D-54).src/runtime/hardening.ts: the profile constant andverifyInspect(D-51).src/runtime/openshell.ts: argv builder, policy document builder, and the bounded runner (D-55 to D-57).src/runtime/agentos.ts:AgentOsDriverport and the supervisor (D-59, D-60).src/runtime/session-execution.ts: theExecutionServiceimplementation that composes the above (D-67).src/anda/client.ts(the bridge,BridgePortimplementation) andsrc/anda/shape.ts(AppInformation shape check, D-61).src/lifecycle/mutation-gate.ts(CORE-05, D-63).src/lifecycle/controller.ts: extended (D-64, D-65). It is not replaced.src/doctor/checks.ts: extended with the D-44 Phase 3 checks (D-69).test/support/fake-libpod.ts(fake Unix-socket server emulating libpod),test/support/fake-anda.ts(fake HTTP server),test/support/fake-bridge.ts(in-memoryBridgePort),test/support/fake-agentos.ts(in-memoryAgentOsDriver),test/support/fake-openshell.ts(records argv and returns scripted results). Helpers only; they are not test files.- Not created in Phase 3:
src/anda/native/(ANDA-04: no shim),src/runtime/egress.ts(Phase 4),src/session/reap.ts(Phase 4),src/tools/(Phase 4),src/cli/(Phase 6), any control socket code (Phase 6). - Reversibility: reversible (placement), except the port shapes, which are costly.
D-50: Dependency direction, extending D-20.
coreimports nothing.runtimeimportscore,contract,fs(atomic replace for the policy document), andsession/paths.andaimportscoreandcontractonly.lifecycleimportscore,session,fs, andcontract, and receivesExecutionService,BridgePort, and any other port through its deps; it does not importruntimeoranda. The composition is wired by the caller, which is Phase 6.doctormay importruntimeandandafor its probes. No module outsidelifecycleimportslifecycle. Reversibility: reversible.
Libpod client and hardening (RT-01, RT-03, RT-09)
D-51: Hardening profile constant
HARDENING_PROFILEinsrc/runtime/hardening.ts. The properties thatverifyInspect(profile, inspect)checks on the session container are:- not privileged; rootless (from
info, not inferred); CapDropis exactly["ALL"], andCapAddis empty (no capability is verified yet);no-new-privilegesis set;- read-only root filesystem, with tmpfs mounts at
/tmpand/run; - PID limit 512, memory limit 1 GiB, CPU limit 2 CPUs (these are Claude's-discretion defaults, held in this one constant);
- network mode
none(RT-05 allowlist is Phase 4); - the image digest equals the pinned digest;
- the mount set is exactly the canonical workspace root and the realm state directory. No mount path may be or contain
docker.sockorpodman.sock.
verifyInspectreturns an empty list on conformance, or the list of failed property names. Any unknown or missing field counts as a failure (fail closed). Reversibility: costly. The profile is the RT-01 contract.- not privileged; rootless (from
D-52: Image pinning and config (RT-01).
RuntimeConfigrequiresimagematching^[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$. A tag-only reference is rejected at load withSCHEMA_INVALID.andaUrlmust parse as anhttp:orhttps:URL with no userinfo.workloadArgvis a non-empty string array with no default. Phase 3 supplies no default image and no default workload; the caller and the gated suite provide them.stateDirandworkspaceRootare absolute. Numeric limits live only inHARDENING_PROFILEand in the Anda and AgentOS backoff defaults (D-59, D-62). Reversibility: reversible.D-53: Prerequisite probe (RT-02).
probePodman({ runCommand, connectSocket, env, timeoutMs })takes injected I/O and is run in this order:- Run
podman --versionas argv with no shell, a timeout of 5000 ms, and an explicit minimal host environment (PATH,HOME,XDG_RUNTIME_DIR). ENOENT, a non-zero exit, or a timeout givesPODMAN_MISSING. - Parse the version with
podman-version.ts. A major below 5, or output that does not parse, givesPODMAN_UNSUPPORTED_VERSION. The reason names the detected version and that OpenShell requires 5.x. Realm does not install or upgrade anything. - Connect to the rootless API socket at
$XDG_RUNTIME_DIR/podman/podman.sock. A refused connection, a missing socket, or a timeout givesPODMAN_MACHINE_DOWN. The reason gives the fixsystemctl --user start podman.socket.
On Linux, "machine" means the rootless API service (podman.md Q4). This mapping is recorded in the runtime note as an assumption (OQ-P3-13). The result is
{ ok: true, version }or{ ok: false, code, reason }.reasoncomes fromERROR_CATALOGUE, with the specific detail appended. Probing stops at the first failure. Reversibility: reversible.- Run
D-54: Libpod client (
src/runtime/libpod-client.ts) is a closed class overnode:httpwithsocketPath. Its public methods are exactlyinfo(),listContainersByLabel(key, value),inspectContainer(id),stopContainer(id, timeoutMs), andremoveContainer(id, { force }). No public method takes a path, a method name, or a body from a caller. It has no exec, attach, or start-exec method, which is the structural RT-03 guard. Every request is raced against a timeout, and the socket is destroyed when the timeout fires. Route paths come only fromlibpod-routes.ts(D-48). Tests assert the method set by runtime key listing and the absence of anyexecroute string. Reversibility: costly.
OpenShell adapter and policy document (RT-03, RT-05 preparation)
D-55: OpenShell argv builder
buildCreateArgv(input)returns exactly this sequence for one session, with no shell:openshell sandbox create --name realm-<sessionId> --policy <policyPath> --no-auto-providers --no-keep --detach --label io.realm.managed=true --label io.realm.session=<sessionId> --cpu <cpus> --memory <memory> -- <workloadArgv...>The builder never emits
--envor--provider, because AUD-02 forbids host environment or credentials reaching the sandbox. Tests assert both absences, assert that the workload follows--, and assert the 0.1.2 verb list exactly. Semantics of--no-keepcombined with--detachare documented only for 0.1.5 and are recorded as an open question (OQ-P3-12). Reversibility: costly. The verbs are pinned (S-10).D-56: Create is idempotent by label (CORE-06). Flow: (1)
listContainersByLabelforio.realm.session=<id>. If a container exists, return its id and run nothing. (2) Otherwise run the create argv through the bounded runner. (3) PolllistContainersByLabelat 250 ms intervals until a container appears or the caller's deadline expires. Deadline expiry givesOPENSHELL_START_FAILED. A repeated create with a sandbox of the same name therefore never creates a second container. Reversibility: reversible.D-57: Policy document.
buildPolicyDocument(config)returns a plain object that is written as JSON (valid YAML 1.2, so no YAML dependency is added; the gated suite confirms OpenShell accepts it, OQ-P3-15). It contains:version: 1;filesystem_policy:read_only: [],read_write: ["/workspace"](the in-sandbox workspace path is a Realm convention, OQ-P3-02);landlock: { compatibility: "hard_requirement" }(fail closed when Landlock is unavailable; the docs require ABI v3 for either mode);process: { run_as_user: "sandbox", run_as_group: "sandbox" }(the docs reject root, so no root is requested);network_policies: {}, which is deny-all egress by the documented default. The allowlist mapping is Phase 4 (RT-05). The docs'binarieskey name is not recorded in the note, so no endpoint is written in Phase 3 (OQ-P3-16).
The document is written with the atomic replace from
src/fs/atomic-write.tsinto<stateDir>/runtime/<sessionId>.policy.jsonwith mode 0600. It holds no secret and no environment. Reversibility: costly, because the policy shape is a documented contract.
AgentOS supervision (RT-04)
D-59:
AgentOsDriverport insrc/runtime/agentos.ts:start(): Promise<void>,alive(): Promise<boolean>,stop(): Promise<void>. The supervisor owns restart policy:- a crash (
alivefalse, orstartrejects) restarts after a backoff fromsrc/core/backoff.ts(base 500 ms, cap 8000 ms, full jitter); - maximum consecutive attempts: 5. The fifth consecutive failure holds the supervisor and throws
AGENTOS_CRASH_LOOP(retryable, so an explicit retry can run again); - the consecutive counter resets after 60000 ms of continuous
alive; - every driver call is raced against a timeout (QUAL-01).
The concrete driver is not written in Phase 3, for two reasons. First, the library package
@rivet-dev/agentos-coreis not verified in its registry, and no dependency is added without that check (D-07 analogue, OQ-P3-06). Second, the in-sandbox channel between the host supervisor and the library process is not documented (OQ-P3-05).session-execution.tstakes the driver as a dependency, so the composition is complete and the gap is named. Reversibility: reversible.- a crash (
D-60: AgentOS lifecycle goes only through the supervisor, which uses the
AgentOsDriverport and its supported library API. Nothing else starts or stops the AgentOS process. Hosted mode is not implemented (S-11). Native sidecar support stays an open question (OQ-P3-07).
Anda bridge (ANDA-01, ANDA-02, ANDA-03, ANDA-04)
D-61: Boundary and handshake. The bridge is a TypeScript HTTP client over Anda Engine (S-12). Phase 3 uses only the documented anonymous route
GET /.well-known/information, which returnsAppInformation(anda.md Q1, server README). It does not usePOST /{id}, because the RPC body layout and credential format are undocumented (anda.md Q2, Q3). Credentials are therefore not needed in Phase 3. JSON only; no CBOR dependency is added.Anda documents no protocol or engine version on any route (anda.md Q4), so the ANDA-02 "version handshake" is a shape handshake.
shape.tschecks that the body is a JSON object withenginesas an array,default_engineas a string or null, andstart_time_msas a non-negative integer. Any mismatch givesANDA_VERSION_MISMATCH. When Anda publishes a version surface, it replaces the shape check, andanda-decision.mdrecords the change. This limitation is named in the decision note and in the phase report, not presented as a version check. Reversibility: reversible.D-62: Reconnection and states (ANDA-03).
AndaBridgeimplementsBridgePort:connect(deadlineMs)runs one handshake, bounded by a per-attempt timeout of 2000 ms, and retries withsrc/core/backoff.ts(base 500 ms, cap 30000 ms, full jitter) until the deadline. Exhaustion givesANDA_UNREACHABLE. A shape mismatch fails at once withANDA_VERSION_MISMATCHand is not retried.- After a successful connect, a background loop probes at the same backoff schedule. A failure publishes
downwithANDA_UNREACHABLEand keeps retrying in the background. A success publishesup. close()cancels every timer and aborts any in-flight request. No timer survives close.- The bridge never changes the state machine itself. It publishes events, and the controller decides (D-65).
Reversibility: reversible.
D-63 (CORE-05 gate).
src/lifecycle/mutation-gate.tsexposesgateMutation(policy, state), a synchronous wrapper overdecideMutation(D-23). It returns{ outcome: "proceed" },{ outcome: "proceed-with-warning", warning }, or{ outcome: "refused", code }.runGatedMutation(gate, fn)runsfnonly on proceed outcomes and returns the warning with the result. The gate never waits. Bounded queue wait is TOOL-04 (Phase 4), so a non-active call is decided in the same tick and cannot hang. The controller exposesmutationGate(), which reads the current state. The gate does not write audit entries; the dispatcher does that in Phase 4. Reversibility: reversible.D-64 (controller state rules). The controller gains a required
bridge: BridgePortdependency. Every existing caller is migrated, including the Phase 2 controller and idempotency tests, throughtest/support/fake-bridge.ts. The transitions are:- Startup (create): after
exec.start, callbridge.connect(deadline)inside the same startup budget. Success movesconnectingtoactive. Exhaustion movesconnectingtoerrorwithANDA_UNREACHABLE. A mismatch moves it toerrorwithANDA_VERSION_MISMATCH. Both are retryable through the existingerrortoconnectingedge, except that a mismatch is not retryable. - Bridge
downwhileactive: move todegradedwith reasonANDA_UNREACHABLE. Bridgeupwhiledegraded: move toactivewith reasonanda reachable. Bridgemismatchinactiveordegraded: move toerrorwith reasonANDA_VERSION_MISMATCH. - Listener guard: a bridge event moves the state only when the current state is
activeordegraded(orconnecting, for the startup path, which the create flow owns). In any other state the event is discarded by an explicit check, never by a caught throw. This avoidsREALM_INVALID_TRANSITIONfrom a background timer. probe()remains exec-only. Probe never producesdegradedfrom a container result. Degraded comes only from the bridge (Phase 2 D-38 said Phase 3 does this).stop()closes the bridge beforeexec.stop, inside the existing teardown bound.destroy()also closes it.
Reversibility: costly. The state transitions are user-visible through the two labels.
- Startup (create): after
D-65 (controller and bridge contract). The bridge emits
{ kind: "up" | "down" | "mismatch", code?, at }. The controller subscribes once in create and unsubscribes in destroy. The listener is registered before the first handshake, so no event is lost. Events do not carry the response body or any credential.D-66 (no shim, ANDA-04). No
src/anda/native/directory is created.docs/integration/anda-decision.mdrecords the no-shim decision and its evidence: a documented HTTP surface exists, so ANDA-04's condition for a shim is not met. The Anda licence conflict recorded inanda.mdQ6 is carried as an open item (OQ-P3-11). It does not block Phase 3, because no Anda code or dependency is added.
Session execution composition and teardown (CORE-06, RT-07, RT-09)
D-67:
src/runtime/session-execution.tsimplementsExecutionService:create({ idempotencyKey, workspaceRoot })builds the hardenedRuntimeConfigview, writes the policy document (D-57), runs the OpenShell create with label idempotency (D-56), resolves the container id, and runs the hardening verifier (D-51). A verification failure destroys the container and throwsOPENSHELL_START_FAILED.start(containerId)checks the container is running throughinspectContainer. It starts nothing itself, because OpenShell starts the sandbox with--detach. A non-running container givesOPENSHELL_START_FAILED.probe(containerId)giveshealthywhen the container is running and AgentOSalive()is true, andunhealthyotherwise.stop(containerId)callsstopContainer.destroy(containerId)callsremoveContainer(force).- Every method takes an injected
LibpodClient,OpenShellRunner, andAgentOsSupervisor. None readsprocess.env.
Reversibility: costly, because the controller depends on the port shape.
D-68:
realm down(RT-09) is a sweep.removeAllRealmContainers(client)lists every container withio.realm.managed=trueand removes each one withforce. It then lists again and fails if any remain, so the sweep proves zero orphans. Failure givesREALM_STOPPING, with the session left instopping. A repeated call resumes. Containers without the realm label are never touched. The controller's sessiondestroyremoves that session's container, and the sweep covers any leftover. Reversibility: reversible.
Doctor (D-44 Phase 3 registration; CLI-03 is Phase 6)
D-69: D-44 said Phase 3 registers its items into the same runner. Phase 3 registers only items it can inspect truthfully:
podman: pass whenpodman --versiongives 5.x or newer; fail withPODMAN_MISSINGorPODMAN_UNSUPPORTED_VERSIONand a fix for each.podman-service: fail withPODMAN_MACHINE_DOWNand thesystemctl --user start podman.socketfix when the socket refuses; pass otherwise.openshell: runsopenshell sandbox create --helpand passes only when the output mentions each pinned flag (--policy,--no-auto-providers,--no-keep,--detach,--label,--cpu,--memory). A missing binary is a fail with the fix "install the OpenShell CLI at the pinned 0.1.2 verbs". A binary with other flags is a warn with the drift note.anda-bridge: pass when/.well-known/informationreturns the shape (D-61); fail withANDA_UNREACHABLEand the fix "start the Anda engine at the configured URL"; warn "not configured" when no URL is set.
The AgentOS item is not registered in Phase 3, because no driver binding exists to inspect (D-59). The control-channel item stays with Phase 6. Each check returns
{ id, status, detail, fix }as in D-44. Reversibility: reversible.
Test suite taxonomy (QUAL-02; extends D-45)
D-70: Test layout follows D-45 with new
runtimeandandadirectories. Every Phase 3 requirement ID maps to at least one named test file:test/core/backoff.test.ts: bounded exponential schedule, cap, full jitter with an injected random source, zero jitter at the boundary (ANDA-03, AgentOS backoff).test/runtime/podman-version.test.ts: moved fromtest/integration/, unchanged assertions (RT-02 parse path).test/runtime/podman-probe.test.ts: ENOENT givesPODMAN_MISSING; 4.9.3 givesPODMAN_UNSUPPORTED_VERSION; refused socket givesPODMAN_MACHINE_DOWN; 5.x with socket gives pass; human-readable reason on each (RT-02).test/runtime/config.test.ts: tag-only image rejected; digest image accepted;andaUrlwith userinfo rejected; emptyworkloadArgvrejected; relative paths rejected (RT-01, RT-05 precursor).test/runtime/libpod-client.test.ts: closed method set by runtime key listing; no exec route string; timeout destroys the socket; fake server round trip (RT-03 guard, RT-09 support).test/runtime/hardening.test.ts: each property in D-51 checked, a conforming inspect yields an empty list, a missing field fails closed (RT-01).test/runtime/openshell-argv.test.ts: the exact pinned argv, the workload after--, no--env, no--provider(RT-03, AUD-02).test/runtime/openshell-policy.test.ts:version: 1,hard_requirement,run_as_usernot root,network_policiesempty, JSON that parses, file mode 0600 (RT-03).test/runtime/openshell-routing.test.ts: structural no-bypass: the session-execution workload start goes only through the OpenShell argv builder; the libpod client has no exec method; AgentOS starts only through the supervisor (RT-03, the D-46 guarantee).test/runtime/agentos-supervisor.test.ts: restarts with bounded backoff;AGENTOS_CRASH_LOOPon the fifth consecutive failure; counter reset after stable window; timeout on a hung driver (RT-04).test/runtime/session-execution.test.ts: create reuses the labelled container with no second create (CORE-06); verification failure destroys and givesOPENSHELL_START_FAILED; policy document written before create (RT-07 ordering).test/runtime/down-sweep.test.ts: three labelled containers and one foreign container; after sweep no labelled container remains and the foreign one is untouched; repeated sweep is a no-op; a remaining container givesREALM_STOPPING(RT-09).test/lifecycle/mutation-gate.test.ts: every state under both modes; synchronous return; warning text names the state; no wait (CORE-05).test/lifecycle/bridge-states.test.ts:activetodegradedondownwithANDA_UNREACHABLE;degradedtoactiveonup; mismatch toerror; startup exhaustion toerror; events in disallowed states discarded without a throw (ANDA-03 state feed, CORE-05 non-hang).test/anda/handshake.test.ts: fake server with correct shape gives connect; wrong shape givesANDA_VERSION_MISMATCHwith no retry; no credentials sent (ANDA-02).test/anda/reconnect.test.ts: unreachable givesANDA_UNREACHABLEand background retries with growing bounded delays; recovery publishesup;close()leaves no timer (ANDA-03).test/anda/boundary-decision.test.ts:docs/integration/anda-decision.mdexists and records the HTTP boundary, the shape-handshake limitation, the no-shim decision, and the licence conflict (ANDA-01).test/anda/no-shim.test.ts:src/anda/native/does not exist; the decision note cites the documented HTTP surface (ANDA-04).test/doctor/runtime-checks.test.ts: pass, warn, and fail outcomes forpodman,podman-service,openshell, andanda-bridge, each with its fix; no AgentOS item registered (D-69).test/integration/podman-gate.test.ts(extended in place): live bodies for RT-01 hardening, RT-03 routing, and RT-09 zero orphans. They run underskip: gateand skip on 4.9.3 withPODMAN_UNSUPPORTED_VERSION(RT-01, RT-03, RT-09).test/quality/suite-taxonomy.test.ts(extended): addsruntimeandandato the category map, and asserts all 10 Phase 3 requirement IDs are cited by at least one test file (QUAL-02 category part).
test/support/*files are helpers. They are exercised by the tests above and are not test files.D-71: No MCP or ACP test file is created in Phase 3 (Q-4 stands; Phase 5).
D-72: The Phase 2 doctor test (
test/doctor/checks.test.ts) asserts the three Phase 2 check IDs and that no Phase 3 name is registered. D-69 changes that registration on purpose, so its expected-ID list is migrated to the Phase 2 and Phase 3 checks. The absence assertion is narrowed to the AgentOS and control-channel names. This is a deliberate change, not an incidental edit. Reversibility: reversible.D-73: A relative
stateDirorworkspaceRootinRuntimeConfigfails at load withSCHEMA_INVALID, the same mechanism D-22 uses for a relative policy root (aSchemaInvalidError). The Anda shape check (D-61) is hand-written code insrc/anda/shape.ts. It adds no schema file and no ajv use. Reversibility: reversible.D-74:
test/integration/podman-gate.test.tskeeps its ownexecFileprobe, so the skip gate depends on no runtime module. The runtime probe (D-53) is tested separately intest/runtime/podman-probe.test.ts. The gate test imports only the parser fromsrc/runtime/podman-version.ts. Reversibility: reversible.
Claude's Discretion
- Exact helper names, internal splits inside a module, and the exact argument shape of
runCommandandconnectSocketinjection. - Test helper layout under
test/support/. - Number of plans and their wave grouping, subject to the file-ownership rule (each file has one owner per wave) and to the dependency order in
<canonical_refs>. - Exact warning and reason text beyond the frozen catalogue reasons, as long as the two display labels stay exact.
- Whether the probe's version and socket checks share one
Promise.allor run in sequence (sequence is the default, because the first failure must short-circuit).
Open questions (recorded; each has a default that the plan applies)
- OQ-P3-01 (blocking the live proof, not the unit build): Does OpenShell's Podman driver apply the
--labelvalues to the container it creates? Default: assume yes. If not, label lookup never finds the container, create times out toOPENSHELL_START_FAILED, and nothing is claimed. Gated suite decides. - OQ-P3-02: Can the OpenShell driver bind-mount the workspace, and at what in-sandbox path? Default:
/workspace(D-57). Gated. - OQ-P3-03: How is the digest-pinned image reference passed to OpenShell? No image verb is in the pinned set. Default: the verifier (D-51) refuses any container whose image digest differs. Gated.
- OQ-P3-04: Does an OpenShell sandbox record persist after its container is removed? No sandbox delete verb is pinned. Default: Phase 3 removes containers only. Phase 4 reaper handles records.
- OQ-P3-05 (blocks the concrete AgentOS driver): No documented channel between the host supervisor and the AgentOS library process in the sandbox. Default:
AgentOsDriverport only; no concrete binding. Reported as blocked. - OQ-P3-06 (blocks adding the AgentOS dependency):
@rivet-dev/agentos-coreversion, license, and Node floor are not verified in its registry. Default: no dependency added in Phase 3. Execution checksnpm viewread-only before any add. - OQ-P3-07: Whether AgentOS library mode needs a native sidecar, and whether native ELF binaries run (agentos.md Q5). Default: recorded as open; library mode primary (S-11).
- OQ-P3-08: Confirm the flags in
podman run --helpagainst the local 4.9.3 binary (podman.md open question 1). Run it read-only, with no install or upgrade, and record the result in the runtime note. - OQ-P3-09: Anda publishes no protocol version (anda.md Q4). Default: shape handshake (D-61). Recorded as a limitation.
- OQ-P3-10: Anda RPC body and credential format (anda.md Q2, Q3). Not needed in Phase 3. Blocks Phase 4 or 5 tool calls through Anda.
- OQ-P3-11: Anda licence conflict (anda.md Q6). Default: no Anda code or dependency added. Must be resolved before any is vendored.
- OQ-P3-12: OpenShell CLI drift: docs 0.1.5, pinned verbs 0.1.2 (S-10). Also the
--no-keepwith--detachsemantics, documented only for 0.1.5. Default: pin 0.1.2 and gate. - OQ-P3-13: On Linux, "Podman machine" is mapped to the rootless API socket (D-53). Default: this mapping, recorded.
- OQ-P3-14: Whether the OpenShell driver applies PID limit, read-only root, and capability drop. Default: the verifier (D-51) fails closed when it does not. Gated.
- OQ-P3-15: Whether OpenShell accepts the policy as JSON (valid YAML 1.2). Default: JSON. Gated.
- OQ-P3-16: The
binarieskey name and endpoint shape fornetwork_policies(openshell.md Q3 does not record it). Default: no endpoints in Phase 3; Phase 4 maps the allowlist after reading the schema.
Requirement text drift (reported, not edited)
- RT-03 and ROADMAP Phase 3 SC3 say "OpenShell runs inside the container". Settled S-08 contradicts it (D-46).
- RT-01 and ROADMAP Phase 3 SC2 describe a container Realm runs. Under S-08 Realm verifies a container that OpenShell creates (D-47).
Both are reported for a user decision. Neither is edited here.
</decisions>
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Product spec (source of truth; do not modify)
docs/specs/realm-spec.xml§REALM_SANDBOX (CONTAINER_LAYER_PODMAN, RUNTIME_LAYER_OPENSHELL, AGENT_OS_LAYER, ANDA_ENGINE_BRIDGE, LIFECYCLE), §STATES, §GRACEFUL_DEGRADATION (FAILURE_CASES: "Anda bridge unreachable: degraded, with a retry in the background"; "AgentOS crashes: restart with backoff, then error"), §GRAPH_OF_THOUGHT n2 (module map), n3 (defaults), n4 (Anda boundary and control channel), n5 (failure modes), n6 (frozen contracts), n7 (doc gates).
Planning state
.planning/REQUIREMENTS.md: CORE-05, RT-01, RT-02, RT-03, RT-04, RT-09, ANDA-01 to ANDA-04 text, and the traceability table..planning/ROADMAP.md: Phase 3 goal and five success criteria..planning/PROJECT.md: constraints (ESM, strict, no host paths, no telemetry, no unverified dependency, no secrets)..planning/STATE.md: Phase 3 blockers (Anda boundary, Podman 4.9.3 versus 5.x).
Integration notes (gates; read for the facts and open questions they record)
docs/integration/podman.md: Q1 hardening flags (confirm on 4.9.3), Q2 limits, Q3 no egress allowlist, Q4 API socket (never mount it), Q5 libpod routes not verified.docs/integration/openshell.md: Q1 the Podman driver creates its own container, Q2 Podman 5.x minimum, Q3 policy schema, Q4 enforcement, Q5--policy, Q7 TypeScript SDK (not used in Phase 3).docs/integration/agentos.md: Q1 library versus hosted, Q2 lifecycle API (AgentOs.create,dispose), Q5 native binaries not documented, Q8 hosted worker is Docker-based.docs/integration/anda.md: Q1 HTTP boundary and routes, Q2 RPC schema not documented, Q3 credentials not documented, Q4 no version surface, Q6 licence conflict.docs/integration/hosts.md: Node constraint behindengines(not changed in Phase 3).
Prior phases (frozen; read before touching related code)
.planning/phases/01-scaffold-and-frozen-contracts/01-CONTEXT.md(D-01..D-18) and01-06-SUMMARY.md(Podman gate and the helper move tosrc/runtime/)..planning/phases/02-policy-state-and-filesystem-lockdown/02-CONTEXT.md(D-19..D-45),02-VERIFICATION.md(what passed), and02-REVIEW.md(H-1 to H-2 fixed in the corrective pass; M-3 raw port errors leaving teardown).
Frozen code (extend beside it; never edit the frozen tables)
src/core/states.ts(six states, 13 allowed edges,checkTransition).src/core/errors.ts(22-code catalogue: the 20 spec codes plusPODMAN_UNSUPPORTED_VERSIONandAUDIT_WRITE_FAILED;ERROR_CATALOGUE;makeRealmError). No code is added in Phase 3.src/core/policy.schema.json,src/contract/tools.schema.json,src/contract/session-record.schema.json,src/contract/validate.ts.src/core/ports.ts(ExecutionService; D-39 frozen;BridgePortis added beside it).src/core/policy.ts(decideMutation, D-23),src/core/timeout.ts(withTimeout),src/core/events.ts,src/core/status.ts.src/lifecycle/controller.ts(extended in D-64; its operation names are frozen).src/fs/atomic-write.ts,src/fs/locks.ts,src/session/paths.ts,src/session/store.ts.test/integration/podman-gate.test.ts,test/integration/podman-version.ts,test/quality/suite-taxonomy.test.ts,test/support/fake-execution.ts.
No external specs or ADRs beyond the above are referenced. .planning/codebase/ does not exist for this repo.
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
src/core/policy.ts:decideMutation(policy, state)returnsallow,allow-with-warning, ordeny. D-63 wraps it and adds no new decision logic.src/core/timeout.ts:withTimeout(promise, ms, code). Every new async boundary uses it, including the libpod client, the OpenShell runner, the Anda client, and the AgentOS driver calls.src/fs/atomic-write.ts: the atomic replace for the policy document (D-57).src/session/paths.ts:resolveStateDir,controlSocketPath. The runtime derives itsruntime/directory from the state dir argument and never readsprocess.env.src/core/backoff.ts(new): reused by the Anda client and the AgentOS supervisor.test/integration/podman-version.tsandtest/integration/podman-gate.test.ts: the parser and the gate, which Phase 3 moves and extends.
Established Patterns
- Typed errors through
makeRealmError(code)andRealmError. Never a raw error across a boundary (Phase 2 review M-3). - Injected I/O for every module that touches the host or the network, so tests run with no environment.
FakeExecutionServiceis the model. - Idempotency keyed by session id (Phase 2 controller, fake execution).
- Test file per source file under
test/<dir>/, with requirement IDs cited in test titles.
Integration Points
src/lifecycle/controller.tsgains theBridgePortdependency (D-64). Every caller is migrated, including the Phase 2 controller tests andtest/support/fake-execution.ts.src/doctor/checks.tsgains the D-69 checks through the same runner. The Phase 2 doctor test's expected-ID assertion is migrated in Phase 3 (D-72). Its absence check for AgentOS and control-channel items is kept.- The spec's WORKFLOW file names (
src/runtime/execution-service.ts,src/core/controller.ts,src/policy/) are superseded by the Phase 2 layout (D-19). The Phase 2 names are used.
</code_context>
<specifics>
Specific Ideas
- Keep every unverified fact in one file that says so (
libpod-routes.ts, the OpenShell verb list inopenshell.ts), so the 5.x gated pass has a single place to confirm or correct. - The runtime note
docs/integration/runtime-decision.mdrecords: the layering (S-08) and its conflict with RT-01 and RT-03 wording (D-46, D-47); the Podman gate (S-09); the OpenShell pin and drift (S-10); the AgentOS mode choice (S-11); every OQ-P3 item with its default and the module it blocks. - The Anda note
docs/integration/anda-decision.mdrecords: the HTTP boundary (S-12); the evidence (anda.md Q1 and the routes); the shape-handshake limitation (D-61); the no-shim decision (D-66); the licence conflict (OQ-P3-11).
</specifics>
<deferred>
Deferred Ideas
- Reaping of orphan containers and stale session records on startup (RT-08): Phase 4.
- Default-deny egress enforcement and the allowlist mapping into OpenShell
network_policies(RT-05): Phase 4. Phase 3 writes deny-all only. - The single dispatcher, the bounded queue wait while connecting, and tool timeouts (TOOL-03, TOOL-04): Phase 4.
- Any per-call
execthrough OpenShell: Phase 4, after the sandbox exec verb is pinned and verified. No exec verb is in the pinned set. - The control socket, its permissions, and the daemon that hosts the lock table (RT-06, S-03): Phase 6.
realm up,down,status,doctorCLI wiring and the AgentOS doctor item (CLI-01, CLI-03): Phase 6.- Host config snippets (CLI-04): Phase 6.
- Omp, MCP, and ACP surfaces, host write-tool removal (spec q4): Phase 5.
- Hosted AgentOS mode (S-11): secondary, gated; not built in Phase 3.
- A Rust shim (ANDA-04): not needed; revisit only if the documented HTTP surface is found to be insufficient.
- Anda RPC calls and credential production (OQ-P3-10): the phase that first calls
POST /{id}. - Teardown-timeout catalogue code (Q-1): stays unresolved. Phase 3 keeps
TEARDOWN_TIMEOUT_MS= 5000 withREALM_STOPPING, so the Phase 2 mapping still applies.
Reviewed Todos (not folded)
- None. No todos matched this phase.
</deferred>
Context gathered: 2026-10-21 </content>