Skip to content

<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 ​

BoundaryDescription
podman --version stdout -> parserUntrusted version text crosses into gating; unparseable output must fail to UNSUPPORTED, never pass
caller config -> RuntimeConfigImage references and URLs cross into load; tag-only and userinfo forms must be rejected before any use
socket path -> connectorFilesystem path crosses into the socket dial; only the XDG-derived podman.sock path is ever dialed

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-03-02S (Spoofing)src/runtime/podman-version.tsmediummitigateUnparseable or major-below-5 output maps to PODMAN_UNSUPPORTED_VERSION; unknown means blocked, never assumed 5.x
T-03-03T (Tampering)src/runtime/config.tshighmitigateDigest-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-04I (Information Disclosure)src/runtime/podman-probe.tslowmitigateProbe 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>

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