Skip to content

<objective> Complete the lifecycle controller (all seven operations with timeout budgets and idempotency), prove no host environment reaches the sandbox, and land the doctor core check runner. Depends on all wave 1-2 plans.

Purpose: Close CORE-06, RT-07 teardown semantics, AUD-02, and the doctor core so the phase goal holds with no container present. Output: Controller completion, fake scriptability, doctor runner, and three 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/02-policy-state-and-filesystem-lockdown/02-CONTEXT.md @.planning/phases/02-policy-state-and-filesystem-lockdown/02-PATTERNS.md </context>

<tasks>

<task type="tdd"> <name>Task 1: Full lifecycle operations with budgets and idempotency</name> <files>src/lifecycle/controller.ts, test/support/fake-execution.ts, test/lifecycle/idempotent.test.ts, test/lifecycle/no-host-env.test.ts</files> <read_first> - src/lifecycle/controller.ts (from plan 02-01; extend the create path, do not rewrite it) - src/core/timeout.ts (from plan 02-02; wrap every ExecutionService call) - src/contract/validate.ts (Policy type for startupBudgetMs; no new codes needed per Q-1) - test/support/fake-execution.ts (from plan 02-01; add scripted delay and failure setters here) </read_first> <behavior> - Test: every operation repeated returns the same snapshot with no duplicate fake resources and no second record write - Test: startup overrun gives STARTUP_BUDGET_EXCEEDED with state error; teardown overrun returns REALM_STOPPING, session stays stopping, repeated destroy resumes - Test: probe healthy changes nothing, probe unhealthy in active moves to error with reason probe failed, probe elsewhere returns the snapshot; attach and detach idempotent with no state or record change; stop of unbound or stopping is a no-op; destroy of absent session is a no-op; destroy releases the session locks and deletes the record - Test: sentinel env value set in the test process never appears in fake inputs, the session record file, or any audit line; a sample port request object has no environment key by runtime key-set check - Test titles cite CORE-06, RT-07, and AUD-02 </behavior> <action>Write the failing tests first per behavior, then extend the controller per D-38 and D-28: start valid in connecting and error via create; startup create, start, and probe wrapped with policy.startupBudgetMs mapping expiry to STARTUP_BUDGET_EXCEEDED and state error; stop valid in connecting, active, degraded, and error moving to stopping, calling exec.stop wrapped with the single TEARDOWN_TIMEOUT_MS 5000 constant owned in controller.ts, mapping expiry to a REALM_STOPPING return with the session staying in stopping so repeated destroy resumes teardown (Q-1 carried: no new catalogue code); destroy stopping first when needed then exec.destroy, session lock release via the D-33 table, and record delete, with absent-session no-op; probe only in active or degraded with unhealthy moving active to error reason probe failed and never producing degraded (ANDA-03 is Phase 3); attach and detach idempotent against the in-memory host set; controller calls the state machine only with checked transitions. Extend the fake with per-operation delay and failure setters the timeout tests drive. Keep F-7: the fake stores request objects visibly so the no-env assertion is a runtime key check, never a suppression comment.</action> <acceptance_criteria> - test/lifecycle/idempotent.test.ts repeats all seven operations asserting identical snapshots and single fake resources - test/lifecycle/no-host-env.test.ts asserts sentinel absence in fake inputs, record file, and audit lines plus the no-environment-key runtime check - Destroy of an absent session and stop of unbound or stopping are verified no-ops </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/lifecycle/idempotent.test.js dist/test/lifecycle/no-host-env.test.js dist/test/lifecycle/controller.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>All operations complete, idempotent, budgeted, and env-free with tests green.</done> </task>

<task type="auto"> <name>Task 2: Doctor core check runner</name> <files>src/doctor/checks.ts, test/doctor/checks.test.ts</files> <read_first> - src/contract/validate.ts (the policy-file check validates through validate policy; never hand-rolls schema rules) - src/session/paths.ts (from plan 02-01; state-dir default resolution shared with the store) </read_first> <action>Implement runChecks in src/doctor/checks.ts returning CheckResult objects with id, status of pass, warn, or fail, detail, and fix per D-44: policy-file absent yields warn with defaults-in-effect detail, invalid yields fail with the schema problem named as fix input, policy inside a workspace root yields fail with a move fix; state-dir missing yields warn created-on-first-use, wrong mode yields fail with chmod 700 fix; session-store not writable yields fail with ownership fix using access W_OK and no probe file. Register only these three checks (flag D-44 as costly per the phase brief so later phases extend the same runner deliberately); Podman, OpenShell, AgentOS, Anda, and control-channel items stay unregistered so the runner never reports on components it cannot inspect. Keep the F-10 status shape: pass, warn, fail, distinct from the validate ok shape.</action> <acceptance_criteria> - test/doctor/checks.test.ts asserts pass, warn, and fail outcomes for each of the three checks, each with its fix - No check references Podman, OpenShell, AgentOS, Anda, or control-channel state </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/doctor/checks.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Doctor core runner green with only the three Phase 2 checks registered.</done> </task>

<task type="auto"> <name>Task 3: Startup-budget overrun and teardown resume spot coverage</name> <files>test/lifecycle/controller.test.ts</files> <read_first> - src/lifecycle/controller.ts (as extended in Task 1 of this plan; append only the budget cases here) </read_first> <action>Append to test/lifecycle/controller.test.ts from plan 02-01 only the timeout-mapping cases: create with a scripted fake delay beyond startupBudgetMs yields STARTUP_BUDGET_EXCEEDED with state error; destroy with a scripted stop delay beyond the teardown bound returns REALM_STOPPING with the session in stopping and a repeated destroy resuming teardown. No other file changes in this task.</action> <acceptance_criteria> - Both budget cases assert the exact code and the resulting state - No duplicate coverage of the Task 1 idempotency cases </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/lifecycle/controller.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Budget and teardown-resume cases green in the controller suite.</done> </task>

</tasks>

<threat_model>

Trust Boundaries ​

BoundaryDescription
host env -> ExecutionServiceHost secrets cross into the sandbox boundary; the port must carry no env by type
hung runtime -> controllerAn unresponsive runtime crosses into teardown; budgets keep it bounded

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-02-10I (Information Disclosure)src/core/ports.tshighmitigatePort carries no environment field by type; no-host-env test sets a sentinel and asserts absence in fake inputs, record, and audit lines
T-02-11D (Denial of Service)src/lifecycle/controller.tsmediummitigateEvery ExecutionService call wrapped: startupBudgetMs for startup, single 5000 teardown constant; expiry maps to STARTUP_BUDGET_EXCEEDED or REALM_STOPPING, never hangs
T-02-12D (Denial of Service)src/fs/locks.tslowmitigateDestroy releases the session locks; stale locks vanish with the in-memory table, never persisted
</threat_model>

<verification> npx tsc -p tsconfig.json && node --test dist/test/lifecycle/idempotent.test.js dist/test/lifecycle/no-host-env.test.js dist/test/lifecycle/controller.test.js dist/test/doctor/checks.test.js; grep the test files for their requirement IDs. </verification>

<success_criteria> All seven operations idempotent and budgeted, sentinel env absent everywhere, and doctor core reporting only what it can inspect. </success_criteria>

<output> Create .planning/phases/02-policy-state-and-filesystem-lockdown/02-05-SUMMARY.md when done </output>

Artifacts this phase produces ​

  • src/lifecycle/controller.ts (extended: start, probe, attach, detach, stop, destroy; TEARDOWN_TIMEOUT_MS 5000; budget wraps per D-28)
  • src/doctor/checks.ts (runChecks with policy-file, state-dir, session-store checks per D-44)
  • test/support/fake-execution.ts (extended: scripted per-operation delay and failure)
  • test/lifecycle/idempotent.test.ts (CORE-06: all seven operations repeated without duplication)
  • test/lifecycle/no-host-env.test.ts (AUD-02: sentinel absence in fake, record, and audit; no-environment-key runtime check)
  • test/doctor/checks.test.ts (D-44: pass, warn, fail outcomes with fixes; Phase 3 and 6 items unregistered)

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