Skip to content

<objective> Build the daemon: single-owner start, startup reaper wiring, method dispatch over the channel, and graceful shutdown, with a shared handle builder the CLI mcp command reuses.

Purpose: Deliver the long-running side of CLI-01 with the deferred Phase 4 reaper wiring (P6-D4), on top of the 06-01 channel. Output: Daemon module plus its test file. </objective>

<execution_context> @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md </execution_context>

<context> @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/06-cli-daemon-and-host-packaging/06-CONTEXT.md @.planning/phases/06-cli-daemon-and-host-packaging/06-01-PLAN.md </context>

<tasks>

<task type="tdd"> <name>Task 1: buildDaemonHandle plus single-owner start (P6-D1, P6-D12)</name> <files>src/daemon/daemon.ts, test/daemon/daemon.test.ts</files> <read_first> - src/daemon/channel.ts (from plan 06-01; CONTROL_METHODS, ensureSocketDir, SOCKET_TIMEOUT_MS) - src/realm/compose.ts lines 56-85 (createRealmHandle signature and the RealmHandle shape) - src/runtime/session-execution.ts lines 59-85 (SessionExecution construction seam) - src/anda/client.ts lines 62-77 and 138-141 (AndaBridge options and BridgePort surface) - src/runtime/openshell.ts lines 292-295 (spawnOpenShellRunner entry) - src/lifecycle/controller.ts lines 58-76 (LifecycleDeps and the controller surface) </read_first> <behavior> - Test (CLI-01): buildDaemonHandle returns a RealmHandle whose controller snapshot and dispatcher status answer through the shared mapping - Test (RT-06): startDaemon with no owner binds the socket and answers status over a real socket pair - Test (RT-06): a second startDaemon against the same state directory refuses (single owner) - Test titles cite CLI-01 or RT-06 </behavior> <action>Write the failing tests first per behavior, then implement src/daemon/daemon.ts per P6-D1 and P6-D12: export buildDaemonHandle (wires createRealmHandle with the production adapters: SessionExecution over the libpod client, AndaBridge over the configured URL, spawnOpenShellRunner, Date clock; tests inject the existing fakes at the same seam) and startDaemon (ensureSocketDir from 06-01, stale-socket connect-then-remove per OQ-P6-03, listen plus 0600 chmod, serve the channel). No CLI parsing, no process spawning, no process.env read. Test titles cite CLI-01 or RT-06.</action> <acceptance_criteria> - buildDaemonHandle shares the exact createRealmHandle path the tests already cover; no second wiring exists - A second concurrent owner refuses; the refusal carries a catalogue code </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/daemon/daemon.test.js</automated> </verify> <done>Handle builder plus single-owner serving works over the channel.</done> </task>

<task type="auto"> <name>Task 2: Reap-before-serve and method dispatch (P6-D4)</name> <files>src/daemon/daemon.ts, test/daemon/daemon.test.ts</files> <read_first> - src/daemon/daemon.ts (from Task 1 of this plan; the start path the reap precedes) - src/session/reap.ts lines 24-40 (reapStartup with REAP_SWEEP_TIMEOUT_MS; single-owner precondition note) - src/runtime/sweep.ts lines 25-50 (removeAllRealmContainers: list, remove, proving second listing) - src/doctor/checks.ts lines 106-124 (runChecks shared-probe pattern to copy for method dispatch) </read_first> <action>Extend the Task 1 files: call reapStartup with the SessionStore and removeAllRealmContainers sweep before the daemon serves its first request (P6-D4; a failed sweep rejects startup before any record is touched, per the reap module), and dispatch the four CONTROL_METHODS (status reads the controller snapshot through displayStatus; up runs the probe-then-start path; down tears down; doctor calls runChecks). Add a test with a recording sweep fake proving the reap completed before the first served request, plus one dispatch test per method. Test titles cite CLI-01 or RT-06.</action> <acceptance_criteria> - The reap runs before serving with the bounded sweep; the ordering test pins it - All four methods dispatch over the socket with catalogue-code failures </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/daemon/daemon.test.js</automated> </verify> <done>Reaper wiring complete; all four methods serve over the channel.</done> </task>

<task type="auto"> <name>Task 3: Graceful shutdown on down (P6-D1)</name> <files>src/daemon/daemon.ts, test/daemon/daemon.test.ts</files> <read_first> - src/daemon/daemon.ts (from Task 2 of this plan; the down path to complete) - src/realm/compose.ts lines 80-84 (close delegates to controller stop, safe to call twice) </read_first> <action>Complete the down path in the Task 2 files: on down, stop accepting new requests, drain in-flight ones within SOCKET_TIMEOUT_MS, delegate teardown to the RealmHandle close path, remove the socket file, and resolve the daemon run promise so the process exits. Add tests proving a second down is a no-op and the socket file is gone after down. Test titles cite CLI-01.</action> <acceptance_criteria> - down drains, closes, removes the socket, and resolves; repeated down is a no-op </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/daemon/daemon.test.js</automated> </verify> <done>Graceful shutdown pinned; daemon exits cleanly after down.</done> </task>

</tasks>

<threat_model>

Trust Boundaries ​

BoundaryDescription
socket request -> method dispatchUntrusted method and params cross here; unknown methods and bad params fail with catalogue codes
second process -> socket bindA rival daemon crosses here; the single-owner bind plus directory guards refuse it

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-06-06E (Elevation of Privilege)src/daemon/daemon.tshighmitigateThe daemon calls reapStartup before serving, so a crashed session's containers and records cannot survive into the new owner (P6-D4, D-84)
T-06-07D (Denial of Service)src/daemon/daemon.tsmediummitigateSingle-owner start refuses a second daemon instead of splitting lock ownership; down drains within SOCKET_TIMEOUT_MS so shutdown cannot hang (P6-D1)
T-06-08T (Tampering)src/daemon/daemon.tsmediummitigatebuildDaemonHandle is the one wiring shared with realm mcp, so no second tool path can bypass the dispatcher (P6-D12, P5-D11)
</threat_model>

<verification> npx tsc -p tsconfig.json && node --test dist/test/daemon/daemon.test.js; grep test/daemon/daemon.test.ts for CLI-01 and RT-06 in test titles. </verification>

<success_criteria> Daemon starts once per state directory, reaps before serving, dispatches all four methods, and shuts down gracefully on down. </success_criteria>

Artifacts this phase produces (contribution of this plan) ​

  • src/daemon/daemon.ts (new): buildDaemonHandle, startDaemon, graceful shutdown (P6-D1, P6-D4, P6-D12).
  • test/daemon/daemon.test.ts (new): CLI-01 and RT-06 ownership, reap-order, dispatch, and shutdown cases.

<output> Create .planning/phases/06-cli-daemon-and-host-packaging/06-03-SUMMARY.md when done </output>

AGPL-3.0-only. Copyright (C) 2026 Spectrum Web Co/swcstudiospace.