Skip to content

<objective> Build the control-channel wire format and socket guards: NDJSON protocol with request ids and catalogue-code errors, plus the 0700/0600 permission enforcement. No daemon yet.

Purpose: Deliver P6-D2, P6-D3, P6-D10, and P6-D11, the foundation plans 06-03 and 06-04 build against, with no dependency on any other Phase 6 work. Output: Channel 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 </context>

<tasks>

<task type="tdd"> <name>Task 1: NDJSON protocol with ids and catalogue-code errors (P6-D2, P6-D10)</name> <files>src/daemon/channel.ts, test/daemon/channel.test.ts</files> <read_first> - src/session/paths.ts lines 30-34 (controlSocketPath: join(stateDir, "run", "control.sock")) - src/core/errors.ts lines 6-31 (ERROR_CODES, exactly 22; no code is added) - src/core/errors.ts lines 143-162 (RealmError and makeRealmError) - src/core/timeout.ts (withTimeout signature) - src/mcp/server.ts lines 185-222 (newline framing and fixed-text rejection precedent) </read_first> <behavior> - Test (RT-06): encode a status request with an id, decode the reply, and assert the reply carries the same id - Test (RT-06): each of CONTROL_METHODS status, up, down, doctor round-trips; an unknown method replies with a catalogue code, not a raw message - Test (RT-06): a malformed line is rejected with fixed text and never throws across the boundary - Test (RT-06): a socket operation that never answers rejects within SOCKET_TIMEOUT_MS (short injected value) - Test titles cite RT-06 </behavior> <action>Write the failing tests first per behavior, then implement src/daemon/channel.ts per P6-D2 and P6-D10: export CONTROL_METHODS, the request/reply encode/decode helpers, SOCKET_TIMEOUT_MS (default 5000 per OQ-P6-01), and send/receive helpers that bound every socket operation with withTimeout over node:net only. Replies carry the request id plus either a result or an existing catalogue ErrorCode; malformed input is rejected with fixed text. No daemon logic, no process spawn, no process.env read. Test titles cite RT-06.</action> <acceptance_criteria> - test/daemon/channel.test.ts asserts id round-trip for all four methods, catalogue-code-only errors, malformed-line rejection, and the timeout bound - src/daemon/channel.ts imports node:net, node:fs, src/core/errors.ts, src/core/timeout.ts, and src/session/paths.ts only </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/daemon/channel.test.js</automated> </verify> <done>Wire format round-trips all four methods with ids, catalogue codes, and bounded operations.</done> </task>

<task type="auto"> <name>Task 2: Socket directory guards 0700/0600 with refusal (P6-D3, P6-D11)</name> <files>src/daemon/channel.ts, test/daemon/channel.test.ts</files> <read_first> - src/daemon/channel.ts (from Task 1 of this plan; the module the guards extend) - src/session/store.ts lines 19-21 (DIRECTORY_MODE 0700 precedent and RECORD_SUFFIX handling) - src/doctor/checks.ts lines 87-88 (STATE_DIR_MODE and PERMISSION_BITS precedent) </read_first> <action>Extend the Task 1 files without changing the wire format: add ensureSocketDir (creates stateDir/run with mode 0700 when absent; when present asserts (mode & 0o777) === 0o700 and uid === process.getuid(), refusing with makeRealmError("SCHEMA_INVALID") per P6-D11 and naming the expected mode and observed state) and a chmodSocket helper applying SOCKET_MODE 0o600 after listen, both over node:fs with injected stat/chmod seams so tests stay hermetic. Add structural assertions that the module references no Podman or Docker socket path and no container module. Test titles cite RT-06.</action> <acceptance_criteria> - The guards create-or-check 0700, chmod 0600, and refuse wrong-mode or foreign-owner directories with SCHEMA_INVALID - The structural test pins that channel.ts touches no container or Podman socket surface </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/daemon/channel.test.js</automated> </verify> <done>Socket filesystem guards enforced and pinned; refusal uses only the existing catalogue.</done> </task>

</tasks>

<threat_model>

Trust Boundaries ​

BoundaryDescription
socket bytes -> decodeUntrusted NDJSON crosses here; malformed lines are rejected with fixed text, never parsed speculatively
run directory stat -> ensureSocketDirDirectory mode and owner cross here; wrong mode or foreign owner refuses before any socket is bound

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-06-01S (Spoofing)src/daemon/channel.tshighmitigateForeign-owner directory refuses with SCHEMA_INVALID, so another user's socket cannot be adopted (P6-D3)
T-06-02T (Tampering)src/daemon/channel.tshighmitigate0700 directory plus 0600 socket; any other mode refuses before listen (P6-D3)
T-06-03D (Denial of Service)src/daemon/channel.tsmediummitigateEvery socket operation is bounded by SOCKET_TIMEOUT_MS via withTimeout; a hung peer cannot block the caller (P6-D2)
</threat_model>

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

<success_criteria> Channel wire format plus socket guards complete with ids, catalogue codes, bounds, and 0700/0600 enforcement; no daemon code present. </success_criteria>

Artifacts this phase produces (contribution of this plan) ​

  • src/daemon/channel.ts (new): CONTROL_METHODS, encode/decode, SOCKET_TIMEOUT_MS, ensureSocketDir, chmodSocket (P6-D2, P6-D3, P6-D10, P6-D11).
  • test/daemon/channel.test.ts (new): RT-06 framing, error, timeout, permission, and no-socket-path cases.

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

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