Skip to content

<objective> Extend the session execution runner with bounded exec capture and egress plumbing while preserving every Phase 3 behavior.

Purpose: Deliver the D-94 runner half and the C-3 egress validation point that the dispatcher exec path depends on. Output: Extended session-execution module, updated runner tests, and the fake-openshell capture fields. </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="tdd"> <name>Task 1: Bounded exec capture in the OpenShell runner (D-94, C-6, D-108)</name> <files>src/runtime/session-execution.ts, test/support/fake-openshell.ts, test/runtime/session-execution.test.ts</files> <read_first> - src/runtime/openshell.ts lines 41-55 (captureOutput flag, widened outcome, OpenShellRunnerError from plan 04-03) - src/runtime/session-execution.ts lines 241-275 (spawnOpenShellRunner: stdio ignore, RUNNER_FAILED and RUNNER_TIMED_OUT rejections) - test/support/fake-openshell.ts lines 17-20 and 65-84 (logged argv, timeoutMs, exitCode returns, and the hang step the TOOL-04 kill test uses) - test/runtime/session-execution.test.ts lines 269-310 (existing runner tests that must keep passing) </read_first> <behavior> - Test (TOOL-03): exec with capture returns stdout and stderr bounded at 262144 bytes with truncated set when the output exceeds the bound - Test (TOOL-03): a runner rejection during exec maps to REALM_NOT_ACTIVE; a deadline expiry maps to TOOL_TIMEOUT and the runner receives its kill so nothing keeps running - Test (TOOL-03): a non-zero remote exit code is a successful call carrying exitCode, not an error - Test titles cite TOOL-03 </behavior> <action>Write the failing tests first per behavior, then extend spawnOpenShellRunner in src/runtime/session-execution.ts per D-94 and C-6: when captureOutput is true, collect stdout/stderr up to 262144 bytes each with truncated set on overflow; when false keep stdio ignore so the create path is byte-identical. Map OpenShellRunnerError kind failed to REALM_NOT_ACTIVE (sandbox not reachable) and kind timed-out to TOOL_TIMEOUT via makeRealmError, with no child message text crossing the boundary; a timed-out exec kills the process tree in its own timer (the dispatcher's withTimeout race is a second bound, not the kill, per D-91). Only exec reads the captured fields; the create path ignores them. Extend test/support/fake-openshell.ts so its exitCode returns gain stdout, stderr, and truncated while keeping the logged argv and timeoutMs (its lines 17-20 and 65-67). Keep every existing runner test passing. (Implements D-94 runner half and C-6; OQ-P4-05 default: remote exit code via CLI status.)</action> <acceptance_criteria> - test/runtime/session-execution.test.ts asserts bounded capture with truncation, both error mappings with the kill, and non-zero exit as success - All pre-existing runner tests still pass unchanged in behavior </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/session-execution.test.js</automated> </verify> <done>Bounded exec capture green with kill-on-timeout and correct mappings.</done> </task>

<task type="tdd"> <name>Task 2: Egress validation and policy plumbing at create (D-81, C-3, D-106)</name> <files>src/runtime/session-execution.ts, test/runtime/session-execution.test.ts</files> <read_first> - src/runtime/session-execution.ts lines 52 and 87-110 (SessionExecutionOptions and the create() policy-write-before-create sequence with the line-91 writePolicyDocument call) - src/runtime/config.ts (from plan 04-03; absolute workloadArgv[0] rule already enforced at load) - src/runtime/egress.ts (from plan 04-01; validateEgressAllow to call) - test/runtime/session-execution.test.ts line 164 (the buildPolicyDocument() call to update) </read_first> <behavior> - Test (RT-05): create() passes the egress list and runtime.workloadArgv[0] into the policy write, and the write lands before the create argv - Test (RT-05): an invalid allowlist throws SCHEMA_INVALID before any create call (no OpenShell invocation on the fake log) - Test titles cite RT-05 </behavior> <action>Write the failing tests first per behavior, then apply the C-3 recommended resolution in src/runtime/session-execution.ts: add egressAllow to SessionExecutionOptions and run validateEgressAllow in the SessionExecution constructor so an invalid list throws before any create (the earliest shared point; loadPolicyFile is not edited per the pattern map). Update create() to pass the egress list and runtime.workloadArgv[0] through writePolicyDocument into buildPolicyDocument(egress, workloadBinary) from plan 04-03, preserving the write-before-create order. Update the line-164 buildPolicyDocument() call in the session-execution test. (Implements C-3 and the create-side half of D-81; planner decision recorded: constructor validation rather than changing loadPolicyFile. OQ-P4-10 default: egress fixed at create; change means destroy and recreate.)</action> <acceptance_criteria> - test/runtime/session-execution.test.ts asserts egress and workload binary reach the policy write in order and that invalid lists fail with zero create calls - The write-before-create ordering assertion still holds </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/session-execution.test.js</automated> </verify> <done>Egress plumbed to the policy write with constructor validation.</done> </task>

<task type="auto"> <name>Task 3: D-80 argv refusal coverage on the execution path</name> <files>test/runtime/session-execution.test.ts</files> <read_first> - test/runtime/session-execution.test.ts lines 26-80 (execution fixture) - test/runtime/openshell-argv.test.ts (from plan 04-03; the refusal-table assertions this test mirrors at the execution layer) </read_first> <action>Extend test/runtime/session-execution.test.ts to assert that no argv the execution path produces (create or exec) contains --env, --provider, --driver-config-json, --forward, --expose, --template, or --auto-providers, mirroring the plan 04-03 refusal table at the layer that actually spawns. Test titles cite RT-05. (Implements the execution-layer half of D-80.)</action> <acceptance_criteria> - The execution test asserts every D-80 forbidden flag is absent from spawned argv - No live process, socket, or container is touched </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/session-execution.test.js</automated> </verify> <done>Execution-layer argv refusals proven.</done> </task>

</tasks>

<threat_model>

Trust Boundaries ​

BoundaryDescription
sandbox process -> runnerUnbounded child output crosses into the daemon; the 262144-byte bound with truncation contains it
constructor options -> createThe egress list crosses into the policy write; constructor validation rejects it before any side effect

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-04-12D (Denial of Service)src/runtime/session-execution.tshighmitigateExec output is bounded per stream with a truncation flag so a chatty sandbox process cannot exhaust daemon memory; the bound test pins it (D-94)
T-04-13D (Denial of Service)src/runtime/session-execution.tshighmitigateA timed-out exec kills the process tree in the runner's own timer and maps to TOOL_TIMEOUT, so no orphan keeps running in the background; the kill test pins it (D-91, D-94)
</threat_model>

<verification> npx tsc -p tsconfig.json && node --test dist/test/runtime/session-execution.test.js; grep test/runtime/session-execution.test.ts for TOOL-03 and RT-05 in test titles. </verification>

<success_criteria> Bounded capture, error mappings with kill, egress plumbing, and execution-layer refusals all green with Phase 3 behavior preserved. </success_criteria>

Artifacts this phase produces (contribution of this plan) ​

  • spawnOpenShellRunner capture path in src/runtime/session-execution.ts (extended): bounded stdout/stderr with truncated, kinded error mapping, kill on timeout (D-94, C-6).
  • SessionExecutionOptions.egressAllow with constructor validateEgressAllow (new): earliest shared validation point (C-3).
  • create() egress plumbing (extended): policy write receives the egress list and workloadArgv[0] before the create argv (D-81).
  • test/support/fake-openshell.ts (extended): stdout, stderr, truncated on returns; kept argv and timeoutMs logging.
  • test/runtime/session-execution.test.ts (extended): TOOL-03 capture/mapping cases, RT-05 plumbing and refusal cases.

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

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