Skip to content

<objective> Build the dispatcher file handlers and the audit stage marker they depend on.

Purpose: Deliver D-98 workspace results and the C-4 audit phase field ahead of the dispatcher plan, keeping TOOL-03 handler logic testable in isolation. Output: New workspace module, extended audit entry, and two test files. </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/04-reaper-egress-guard-and-shared-dispatcher/04-CONTEXT.md @.planning/phases/04-reaper-egress-guard-and-shared-dispatcher/04-PATTERNS.md </context>

<tasks>

<task type="auto"> <name>Task 1: Audit entry phase field (C-4, D-89, D-90, D-107)</name> <files>src/audit/log.ts, test/audit/log.test.ts</files> <read_first> - src/audit/log.ts lines 34-41 (AuditEntryInput without a lock or stage marker; the gap C-4 names) - test/audit/log.test.ts (existing entry-shape assertions to keep passing) </read_first> <action>Apply the C-4 recommended resolution in src/audit/log.ts per D-89 and D-90: add an optional phase field valued admission or result to AuditEntryInput, leaving every other field unchanged. The admission line (outcome ok, meaning admitted) and the result line (outcome ok or the error code) are then distinguishable by phase, which repairs the D-90 "distinguish by outcome" claim without changing the D-42 field set. Keep all existing entry-shape tests passing. (Implements C-4; planner decision recorded: accept the optional phase field rather than dropping the lock field or the two-line claim.)</action> <acceptance_criteria> - AuditEntryInput carries optional phase admission or result and all existing audit tests still pass - No other audit field changes and the D-42 format is otherwise unchanged </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/audit/log.test.js</automated> </verify> <done>Audit phase field added with existing shape intact.</done> </task>

<task type="tdd"> <name>Task 2: Workspace read, list, and write handlers (D-88, D-98)</name> <files>src/tools/workspace.ts, test/tools/workspace.test.ts</files> <read_first> - src/fs/write.ts lines 27-43 (lockedWrite: authoritative acquire and hash check stay inside) - src/fs/read.ts lines 16-35 (readFileCapped and the read-mode canonicalize at line 35) - src/fs/list.ts lines 13-40 (listDir bounds: maxBytes at most 1 MiB, MAX_LIST_ENTRIES 1000) - src/fs/credentials.ts (assertCredentialAccess to call from each handler) - test/fs/write.test.ts lines 58-101 (ifMatch cases) and lines 130-141 (lock held by another owner) </read_first> <behavior> - Test (TOOL-03): write honors ifMatchSha256 and a mismatch yields LOCK_CONFLICT with the file unchanged (D-34) - Test (TOOL-03): audit lines for a write carry byte count and sha256 and no content (D-42) - Test (TOOL-03): read and list pass through the credential and root checks and return the Phase 2 result shapes (D-36) - Test titles cite TOOL-03 </behavior> <action>Write the failing tests first per behavior, then implement src/tools/workspace.ts per D-88 and D-98: the only module under src/tools/ importing lockedWrite, readFileCapped, and listDir; each handler canonicalizes with the mode it needs and calls assertCredentialAccess; write returns { sha256, bytes } plus the optional advisory warning from decideMutation (D-23, CORE-05); read and list return the Phase 2 shapes (D-36). A write under advisory policy in a non-active state returns the warning in the result with the audit admission line; the write itself is not reported as failed (D-98). Open the module with a header comment naming D-88 and D-98. Constructor-style dependencies arrive injected (boundRoot, locks, audit); the module reads no process.env (D-88). (Implements D-88 workspace half and D-98.)</action> <acceptance_criteria> - test/tools/workspace.test.ts asserts ifMatch/LOCK_CONFLICT with unchanged file, audit byte-count/sha256 without content, and read/list credential and root checks - No module under src/tools/ besides workspace.ts imports the three filesystem functions </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/tools/workspace.test.js</automated> </verify> <done>Workspace handlers green with Phase 2 result shapes and audit discipline.</done> </task>

<task type="auto"> <name>Task 3: TOOL-03 static no-bypass guard over src/tools (D-99, D-104)</name> <files>test/tools/no-bypass.test.ts</files> <read_first> - test/runtime/openshell-routing.test.ts lines 34-60 (fixture and source-read style) and line 21 (LIBPOD_ROUTE_NAMES pattern) - test/quality/suite-taxonomy.test.ts lines 181-184 (readFileSync source reads) - src/doctor/checks.ts lines 24, 381, and 497 (the OPENSHELL_BINARY and execFile references the guard must allowlist per C-5) </read_first> <action>Write test/tools/no-bypass.test.ts per D-99 using the D-46 routing-test method: assert that buildExecArgv, lockedWrite, readFileCapped, listDir, and spawnOpenShellRunner are referenced only from src/tools/*, src/runtime/openshell.ts, src/runtime/session-execution.ts, and their own modules; assert on spawn( in src/ with the doctor added to the allowlist for the OPENSHELL_BINARY literal only (C-5 resolution: the guard checks the D-99 symbols plus spawn(, and the doctor's read-only probe is the single literal exception). Test titles cite TOOL-03. (Implements D-99 over src/tools/; plan 04-06 extends the routing file for the dispatcher half.)</action> <acceptance_criteria> - test/tools/no-bypass.test.ts fails if any new module outside the allowlist references the guarded symbols or spawns a process - The doctor probe stays allowlisted for the OPENSHELL_BINARY literal only </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/tools/no-bypass.test.js</automated> </verify> <done>No-bypass guard green with the doctor exception pinned.</done> </task>

</tasks>

<threat_model>

Trust Boundaries ​

BoundaryDescription
tool input -> file handlersRaw tool params cross into filesystem writes; schema, canonicalization, credential, and lock checks all precede lockedWrite
handler outcome -> auditByte counts and hashes cross into the audit log; content never does

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-04-07T (Tampering)src/tools/workspace.tshighmitigateThe authoritative acquire and hash check stay inside lockedWrite so a pre-check race still yields LOCK_CONFLICT; the ifMatch test pins it (D-89, D-34)
T-04-08I (Information Disclosure)src/tools/workspace.tshighmitigateAudit lines carry byte count and sha256, never content; exec entries carry redacted argv or script length and sha256; the no-content test pins it (D-42, D-90)
</threat_model>

<verification> npx tsc -p tsconfig.json && node --test dist/test/audit/log.test.js dist/test/tools/workspace.test.js dist/test/tools/no-bypass.test.js; grep the tool test files for TOOL-03 in test titles. </verification>

<success_criteria> Workspace handlers, audit phase marker, and the no-bypass guard all green with Phase 2 shapes preserved. </success_criteria>

Artifacts this phase produces (contribution of this plan) ​

  • src/tools/workspace.ts (new): read, list, and write handlers over the Phase 2 filesystem layer; sole importer of the three file functions under src/tools/ (D-88, D-98).
  • AuditEntryInput.phase in src/audit/log.ts (extended): optional admission/result marker (C-4, D-89, D-90).
  • test/tools/workspace.test.ts (new): TOOL-03 handler, lock, and audit-content cases.
  • test/tools/no-bypass.test.ts (new): TOOL-03 D-99 static guard with the C-5 doctor exception.
  • test/audit/log.test.ts (extended): phase-field cases.

<output> Create .planning/phases/04-reaper-egress-guard-and-shared-dispatcher/04-04-SUMMARY.md when done </output>

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