<objective> Build the closed libpod client over a Unix socket with its provisional route table and the fake socket server the whole phase tests against.
Purpose: Provide the structural RT-03 guard (no exec route exists to call) and the list and remove primitives that the down sweep needs. Output: Route table, closed client, fake server, and the client test. </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: Provisional route table and closed client</name> <files>src/runtime/libpod-routes.ts, src/runtime/libpod-client.ts</files> <read_first> - src/session/store.ts (SessionStore class shape: readonly field plus constructor argument, makeRealmError on failure) - src/core/timeout.ts (withTimeout is the outer bound; the client destroys its own socket on expiry per F-P3-05) - docs/integration/podman.md Q5 (no libpod route verified; every route here is provisional per D-48) </read_first> <behavior> - Test: the client prototype key set equals exactly info, listContainersByLabel, inspectContainer, stopContainer, removeContainer (runtime key-set check, no ts-expect-error per the F-7 rule) - Test: no exec substring appears in the route constants or the client source (structural RT-03 guard) - Test: a request exceeding its timeout destroys the socket and rejects with the timeout code; removeContainer treats 404 as success - Test: fake server round trip for list, inspect, stop, and remove - Test titles cite RT-03 and RT-09 </behavior> <action>Write the failing tests first per behavior, then implement per D-48 and D-54: LIBPOD_ROUTES in src/runtime/libpod-routes.ts as a frozen (Object.freeze plus as const) table holding only the routes the five methods need, with a header marking every route provisional (podman.md Q5, confirmed on a 5.x host through the gated suite); LibpodClient in src/runtime/libpod-client.ts as a closed class over node:http with socketPath whose public methods are exactly info(), listContainersByLabel(key, value), inspectContainer(id), stopContainer(id, timeoutMs), and removeContainer(id, { force }); no public method takes a path, method name, or body from a caller; every request is raced against a timeout and the socket is destroyed when it fires (F-P3-05: withTimeout only rejects, so the client owns cancellation). removeContainer treats 404 as success, matching the fake execution no-op destroy. Never mount or expose the socket path to a container (RT-06 precursor; podman.md Q4).</action> <acceptance_criteria> - The runtime key-set test pins the exact five-method set and fails if a sixth appears - A source grep for exec in both files returns only the absence assertion in the test - Timeout destroys the fake-recorded socket instead of only rejecting </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/libpod-client.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Closed client green with exactly five methods, no exec route, and socket destruction on timeout.</done> </task>
<task type="auto"> <name>Task 2: Fake Unix-socket libpod server</name> <files>test/support/fake-libpod.ts</files> <read_first> - src/runtime/libpod-routes.ts (from Task 1 of this plan; the fake imports the route strings so the provisional table stays single) - test/support/fake-execution.ts (call-log shape, setDelay, failNext, beforeCall to mirror) </read_first> <action>Implement the fake libpod server in test/support/fake-libpod.ts per the pattern map: a node:http server on a socketPath holding an in-memory container table; it imports its route strings from src/runtime/libpod-routes.ts; it records every request and every socket close so the timeout test can assert cancellation; it supports scripted per-route responses and delays in the FakeExecutionService style. This file is a helper, not a test file: it carries no requirement-ID title and is exercised by test/runtime/libpod-client.test.ts from Task 1 of this plan. Expose the shared container store so the plan 03-05 OpenShell fake can add a container on successful create.</action> <acceptance_criteria> - Fake serves list, inspect, stop, and remove against its table with request and close recording - Route strings come from the libpod routes module, never duplicated literals </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/runtime/libpod-client.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Fake server supports the client suite with shared-store access for the OpenShell fake.</done> </task>
</tasks>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| caller -> LibpodClient | Method arguments cross into socket requests; only id, label key/value, and timeout cross, never paths or method names |
| libpod socket -> client | Socket bytes cross into the controller path; malformed JSON fails with a typed error, never a raw throw |
| socket file -> container | The API socket grants full Podman access as the user (podman.md Q4); it must never be mounted into a session container |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-03-07 | E (Elevation of Privilege) | src/runtime/libpod-client.ts | critical | mitigate | Closed five-method set pinned by a runtime key-set test; no exec, attach, or start-exec route exists to call, so no code path bypasses OpenShell (RT-03) |
| T-03-08 | I (Information Disclosure) | src/runtime/libpod-client.ts | high | mitigate | Socket path is an explicit constructor argument, never read from the environment; socket bytes never carry credentials in Phase 3 |
| T-03-09 | D (Denial of Service) | src/runtime/libpod-client.ts | medium | mitigate | Every request raced against a timeout with socket destruction on expiry (F-P3-05); hung daemon cannot hang the caller |
| </threat_model> |
<verification> npx tsc -p tsconfig.json && node --test dist/test/runtime/libpod-client.test.js; grep the test file for RT-03 and RT-09. </verification>
<success_criteria> Five-method client only, no exec route anywhere, socket destroyed on timeout, and fake round trips green. </success_criteria>
<output> Create .planning/phases/03-container-runtime-and-bridges/03-04-SUMMARY.md when done </output>