<objective> Move the version parser to its Phase 3 home, build the injected prerequisite probe, and land RuntimeConfig validation. No live Podman call in this plan.
Purpose: Deliver the RT-02 probe path and the RT-01 image and URL load rules that session-execution and doctor build on. Output: Three source modules, three unit test files, and deletion of the old parser location with no re-export. </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/03-container-runtime-and-bridges/03-CONTEXT.md @.planning/phases/03-container-runtime-and-bridges/03-PATTERNS.md </context>
<tasks>
<task type="tdd"> <name>Task 1: Move the version parser to src/runtime</name> <files>src/runtime/podman-version.ts, test/runtime/podman-version.test.ts, test/integration/podman-version.ts</files> <read_first> - test/integration/podman-version.ts (move the whole file; OPENSHELL_MIN_PODMAN_MAJOR, parsePodmanVersion, PodmanProbe, and podmanSkipReason keep their bodies unchanged per D-49) - test/integration/podman-version.test.ts (move with only the import rewritten to ../../src/runtime/podman-version.js) </read_first> <behavior> - Test: moved assertions hold unchanged (parse cases and skip-reason mapping) - Test titles cite RT-02 </behavior> <action>Write the move first, then the test: copy test/integration/podman-version.ts to src/runtime/podman-version.ts and test/integration/podman-version.test.ts to test/runtime/podman-version.test.ts per D-49, D-74, and pattern-map row 3, changing only the test import to ../../src/runtime/podman-version.js and rewriting the parser header comment (it must no longer say the probe lives in the Podman gate; D-53 moves the I/O to podman-probe.ts). Delete test/integration/podman-version.ts with no re-export left behind (clean cutover per the brief). Do not touch test/integration/podman-gate.test.ts; plan 03-10 owns it.</action> <acceptance_criteria> - test/integration/podman-version.ts no longer exists and no file re-exports it - Moved test passes with only its import changed </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/podman-version.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Parser lives in exactly one place under src/runtime with moved tests green.</done> </task>
<task type="tdd"> <name>Task 2: Prerequisite probe with injected I/O</name> <files>src/runtime/podman-probe.ts, test/runtime/podman-probe.test.ts</files> <read_first> - src/runtime/podman-version.ts (from Task 1 of this plan; parse and gate the version through it) - src/core/errors.ts (PODMAN_MISSING, PODMAN_MACHINE_DOWN, PODMAN_UNSUPPORTED_VERSION catalogue entries; makeRealmError is the only construction path) - test/integration/podman-gate.test.ts (probe mapping lines 14-24: execFile, 5000 ms timeout, failures to unavailable; keep the same mapping, F-P3-07) </read_first> <behavior> - Test: ENOENT, non-zero exit, or timeout gives PODMAN_MISSING with a human-readable reason - Test: 4.9.3 output gives PODMAN_UNSUPPORTED_VERSION naming the detected version and that OpenShell requires 5.x (OQ-P3-12 default: pin 0.1.2 verbs, gate on 5.x) - Test: refused socket or missing socket gives PODMAN_MACHINE_DOWN with the systemctl --user start podman.socket fix (OQ-P3-13 default: rootless API socket mapping on Linux) - Test: 5.x output with a connectable socket passes; probing stops at the first failure - Test titles cite RT-02 </behavior> <action>Write the failing tests first per behavior, then implement probePodman({ runCommand, connectSocket, env, timeoutMs }) in src/runtime/podman-probe.ts per D-53: run podman --version as argv with no shell, 5000 ms timeout, explicit minimal env (PATH, HOME, XDG_RUNTIME_DIR passed in, never process.env); parse via the moved parser; connect to $XDG_RUNTIME_DIR/podman/podman.sock. Result is { ok: true, version } or { ok: false, code, reason } with reason from ERROR_CATALOGUE plus the specific detail. Sequence the version and socket checks so the first failure short-circuits. OQ-P3-08 (confirm flags against the 4.9.3 binary) is recorded in the runtime decision note (plan 03-03); this task only reads the version string, never installs or upgrades.</action> <acceptance_criteria> - test/runtime/podman-probe.test.ts covers all five mappings above with assert.match on each reason - No test spawns a real process or opens a real socket (injected runCommand and connectSocket only) </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/podman-version.test.js dist/test/runtime/podman-probe.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Probe maps every failure to its code and reason with unit tests green and no real I/O.</done> </task>
<task type="tdd"> <name>Task 3: RuntimeConfig load validation</name> <files>src/runtime/config.ts, test/runtime/config.test.ts</files> <read_first> - src/core/policy.ts loadPolicyFile (load-then-check shape to mirror) - src/fs/root.ts bindRoot (absolute-path rule lines 20-22; relative paths fail) - src/core/errors.ts (SCHEMA_INVALID catalogue entry; relative stateDir and workspaceRoot fail with SCHEMA_INVALID per D-73, resolving F-P3-02) </read_first> <behavior> - Test: tag-only image rejected with SCHEMA_INVALID; digest image matching the pinned regex accepted - Test: andaUrl with userinfo rejected; non-http(s) scheme rejected; empty workloadArgv rejected - Test: relative stateDir and relative workspaceRoot rejected with SCHEMA_INVALID (D-73) - Test titles cite RT-01 </behavior> <action>Write the failing tests first per behavior, then implement the RuntimeConfig type and loadRuntimeConfig validator in src/runtime/config.ts per D-52: image must match ^[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$; andaUrl must parse as http: or https: with no userinfo; workloadArgv is a non-empty string array; stateDir and workspaceRoot must be absolute. No defaults for image or workload (the caller and the gated suite provide them); no environment read. Numeric limits live only in HARDENING_PROFILE (plan 03-07) and the backoff defaults, never here.</action> <acceptance_criteria> - test/runtime/config.test.ts covers every accept and reject pair above with exact codes - No schema file added and no ajv use for this validator (hand-written per the D-52 text) </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/config.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Config validation green with digest pinning and absolute-path rules enforced.</done> </task>
</tasks>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| podman --version stdout -> parser | Untrusted version text crosses into gating; unparseable output must fail to UNSUPPORTED, never pass |
| caller config -> RuntimeConfig | Image references and URLs cross into load; tag-only and userinfo forms must be rejected before any use |
| socket path -> connector | Filesystem path crosses into the socket dial; only the XDG-derived podman.sock path is ever dialed |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-03-02 | S (Spoofing) | src/runtime/podman-version.ts | medium | mitigate | Unparseable or major-below-5 output maps to PODMAN_UNSUPPORTED_VERSION; unknown means blocked, never assumed 5.x |
| T-03-03 | T (Tampering) | src/runtime/config.ts | high | mitigate | Digest-pinned image regex enforced at load with SCHEMA_INVALID; tag-only refs never reach the runner; userinfo URLs rejected so no credential leaks into andaUrl |
| T-03-04 | I (Information Disclosure) | src/runtime/podman-probe.ts | low | mitigate | Probe takes an explicit minimal env argument; test asserts the argv carries no extra variables |
| </threat_model> |
<verification> npx tsc -p tsconfig.json && node --test dist/test/runtime/podman-version.test.js dist/test/runtime/podman-probe.test.js dist/test/runtime/config.test.js; grep the three test files for their requirement IDs. </verification>
<success_criteria> Version parser moved with no re-export, probe maps every failure to a coded reason, and digest pinning plus URL and path rules hold at load. </success_criteria>
<output> Create .planning/phases/03-container-runtime-and-bridges/03-02-SUMMARY.md when done </output>