[
     {
          "Id": "29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d",
          "Created": "2026-08-14T16:05:54.336323174+03:00",
          "Path": "/usr/bin/tini",
          "Args": [
               "--",
               "/bin/sh",
               "-c",
               "[ -n \"${HOME-}\" ] || { echo 'agent_cred: HOME unset in container' >&2; exit 10; }\nagent_cred_target=\"${HOME%/}/.codex/auth.json\"\ntrap 'rm -f \"$agent_cred_target\"' EXIT HUP INT TERM\nmkdir -p \"${agent_cred_target%/*}\" || { echo 'agent_cred: cannot create credential directory' >&2; exit 10; }\n[ -r '/sandbox-secrets/codex/auth.json' ] || { echo 'agent_cred: staging mount missing: /sandbox-secrets/codex/auth.json' >&2; exit 10; }\ncp '/sandbox-secrets/codex/auth.json' \"$agent_cred_target\" || { echo 'agent_cred: credential copy failed' >&2; exit 10; }\nchmod 600 \"$agent_cred_target\" || { echo 'agent_cred: cannot restrict credential mode' >&2; exit 10; }\n\"$@\"",
               "sh",
               "codex",
               "exec",
               "--sandbox",
               "danger-full-access",
               "--skip-git-repo-check",
               "-m",
               "gpt-5.6-terra",
               "-c",
               "model_reasoning_effort=medium",
               "Repo: /home/user/Projects/overdeck/.worktrees/factory-pi-remote (git worktree, branch wt/factory-pi-remote).\nRead /home/user/Projects/overdeck/.worktrees/factory-pi-remote/docs/plans/2026-08-14-factory-pi-remote.md FIRST — it is the authoritative spec with all verified facts and resolved design decisions. Implement exactly what it describes. Do not re-derive facts it already states; trust them.\n\nSummary of what to build (full detail is in the plan file — read it, this is just an index):\n\n1. `modules/workstation/bin/pi-remote` — a bash CLI wrapper that runs the `pi` coding-agent binary on a buildbox over ssh instead of locally, reusing `modules/systray/remote_dispatch.py`'s public Python functions (host selection, registry, mirroring machinery) by shelling out to a small Python helper module (e.g. `modules/workstation/bin/pi_remote_dispatch.py` or extend remote_dispatch's own call sites — your call, keep remote_dispatch.py's existing contract intact, do not break command_router.py/cdx.py callers). It must:\n   - Target a bare `agent-seat` role host (NOT the podman sandbox container — see plan for why).\n   - Forward all argv to `pi` unchanged, except: detect info-only pi invocations (`--list-models`, `--version`, `--help`, or no `-p`/`--session-id` present) and exec local `pi` directly for those — never round-trip those to a buildbox.\n   - For a real agent turn, mirror the git repo tree (cwd) via remote_dispatch's existing open_session/push/pull machinery, AND additionally mirror the `--session-dir` argument's directory (raw_output_path stays local — VERIFY this by reading agent_pi.py's run() function before implementing; do not assume, read the actual code to confirm raw_output_path is written by the LOCAL Popen's stdout consumer thread, not by the remote pi process).\n   - Push `--session-dir`'s directory to the remote host before running pi there, pull it back after, every invocation (correctness over speed, no caching).\n   - Preserve real-time line-by-line stdout streaming (pi flushes per-line; do not buffer).\n   - Propagate SIGTERM/SIGKILL sent to the local pi-remote process to the remote pi process (use ssh -tt or a remote trap+wait pattern; verify empirically that killing pi-remote's local process also kills the remote pi process, e.g. via a follow-up ssh pgrep check in the test).\n   - Record a containment entry in ~/.overdeck/containment.jsonl on success/failure/abort, reusing command_router.py's `_append_containment_record` pattern (tool label \"pi\" or \"factory-pi\", runtime \"pi\") — do not duplicate its logic if it can be imported/reused cleanly; if not cleanly importable (it's currently a private function in command_router.py), extract a small shared helper into remote_dispatch.py or a new tiny module and have both command_router.py and pi-remote use it — keep this change minimal, do not refactor unrelated command_router.py code.\n   - Fail CLOSED on: registry unreadable, no reachable host, mirror push/pull failure — clear nonzero exit (reuse remote_dispatch.EXIT_* codes), detail to stderr, NEVER fall back to local pi silently.\n\n2. Factory config: add a `defaults` key to `modules/harness/factory/adw_modules/data_types.py`'s `ConfigDefaults` (e.g. `pi_spawn: Literal[\"local\", \"remote\"] = \"local\"`) and wire it through `modules/harness/factory/adw_modules/agents.py` so when `\"remote\"`, the agent_pi.run() spawn path uses the installed `pi-remote` binary's absolute path instead of the bare `PI_PATH`/\"pi\" default — WITHOUT modifying agent_pi.py's protocol/tracer/gate logic; the only touch to agent_pi.py, if any, should be widening how PI_PATH is resolved (e.g. read an additional env var or accept an explicit path parameter threaded from agents.py) — keep this diff minimal and follow the existing PI_PATH env-var precedence (explicit env var override still wins). Follow the config layering idiom shown in `modules/harness/factory/tests/test_config_layering.py`. Default stays \"local\" — zero behavior change unless a project opts in.\n\n3. Register `pi` as a new `vendor` entry in `modules/buildbox/devtools.json` so `buildbox bootstrap` installs it fleet-wide (pi ships as npm package `@earendil-works/pi-coding-agent`; the workstation already has it in `~/.npm-global`; check what \"kind\" values devtools.json already supports for npm-based installs — study the `codex` vendor entry (kind: mise, tool: npm:@openai/codex) as the closest pattern and adapt).\n\n4. Auth: extend `modules/systray/pi_auth_sync.py` (or a thin new remote counterpart) so an authenticated `~/.pi/agent/auth.json` reaches the buildboxes via the existing claude-parity/buildbox-parity convergence path (study `modules/buildbox/lib/claude-parity.sh` and `modules/workstation/claude/bin/buildbox-parity.sh` for the convergence pattern this should follow) — codex's own `~/.codex/auth.json` is already present on the boxes (verified this session on debian1), so pi_auth_sync.py's existing `_derive_credential` logic (which already derives pi creds FROM codex's auth.json) can plausibly run box-side too, or push its derived output — your design call, follow the plan file's \"next executable action\" section, keep the change narrowly scoped to landing an authenticated pi on the boxes via the established mechanism, not hand-rolling a new one.\n\n5. Tests:\n   - `pi-remote` / its Python helper: unit tests for fail-closed branches (registry absent, host down, mirror failure) near `modules/systray/tests/test_remote_dispatch*` patterns (FT_FROM_HOOK=1 FT_HINTS=1 ft find and follow that file's exact test structure/fixtures).\n   - One factory test proving the `pi_spawn: remote` config key changes the resolved spawn path passed into agent_pi.run — mock the wrapper entirely, no real ssh/dispatch. Add near the existing agent_pi/agents tests in `modules/harness/factory/tests/`.\n   - `python3 -m pytest modules/harness/factory/tests/ -q` MUST stay green (189/189 baseline recorded in the plan file — re-verify exact count before you start and report the delta).\n   - devtools.json / claude-parity changes: follow whatever existing test pattern covers devtools.json vendor entries (check `modules/buildbox/test/vendor.test.sh` and `modules/buildbox/test/claude-parity.test.sh`) and extend if a new vendor entry needs coverage.\n\nConstraints (from repo CLAUDE.md, non-negotiable):\n- No hand-scp of unmanaged binaries — pi reaches the boxes only through devtools.json + buildbox bootstrap.\n- Never touch agent_pi.py's protocol handling / tracer / gate logic / runner commit flow beyond the minimal PI_PATH-resolution widening described in step 2.\n- No stubs/placeholders — everything must actually work end to end for the parts you can verify locally (wrapper logic, config wiring, unit tests). Live ssh-to-buildbox proof will be run by the reviewer separately once pi is actually installed+authenticated on a box — you do not need to wait for that to land this code, but the code must be genuinely correct, not aspirational.\n- Comments: WHAT only, never reasoning/why, per repo CLAUDE.md; keep them minimal.\n- Do not create any new documentation/README files.\n- Do not run `git commit`/push — leave the worktree with the changes staged/unstaged for the reviewer (me) to inspect, test, and commit myself.\n\nWhen done, report: files changed/created, test counts before/after for both `modules/harness/factory/tests/` and any new test files you added, any design decisions you made that the plan file left open, and anything you could NOT verify (e.g. real ssh dispatch, since pi is not yet installed on any box)."
          ],
          "State": {
               "OciVersion": "1.2.1",
               "Status": "exited",
               "Running": false,
               "Paused": false,
               "Restarting": false,
               "OOMKilled": false,
               "Dead": false,
               "Pid": 0,
               "ExitCode": 0,
               "Error": "",
               "StartedAt": "2026-08-14T16:05:54.530771397+03:00",
               "FinishedAt": "2026-08-14T16:13:17.317704915+03:00",
               "CheckpointedAt": "0001-01-01T00:00:00Z",
               "RestoredAt": "0001-01-01T00:00:00Z"
          },
          "Image": "a7eb5250818c802b103779019e19d4ada20a636aef9054930ec30016516b9aaf",
          "ImageDigest": "sha256:05abf7962fb582174c8c33f712fb82c38951ccac266621d7b40e652b67d81097",
          "ImageName": "localhost/overdeck-agent-sandbox:d1c7f17660b1",
          "Rootfs": "",
          "Pod": "",
          "ResolvConfPath": "/run/user/1000/containers/overlay-containers/29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d/userdata/resolv.conf",
          "HostnamePath": "/run/user/1000/containers/overlay-containers/29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d/userdata/hostname",
          "HostsPath": "/run/user/1000/containers/overlay-containers/29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d/userdata/hosts",
          "StaticDir": "/home/user/.local/share/containers/storage/overlay-containers/29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d/userdata",
          "OCIConfigPath": "/home/user/.local/share/containers/storage/overlay-containers/29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d/userdata/config.json",
          "OCIRuntime": "crun",
          "ConmonPidFile": "/run/user/1000/containers/overlay-containers/29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d/userdata/conmon.pid",
          "PidFile": "/run/user/1000/containers/overlay-containers/29aa54e6d3b695ac5faf09fa849dc814354f8aad42a07187eadcef9fce10ad8d/userdata/pidfile",
          "Name": "overdeck-sandbox-factory-pi-remote-9b291304a43a",
          "RestartCount": 0,
          "Driver": "overlay",
          "MountLabel": "",
          "ProcessLabel": "",
          "AppArmorProfile": "",
          "EffectiveCaps": null,
          "BoundingCaps": null,
          "ExecIDs": [],
          "GraphDriver": {
               "Name": "overlay",
               "Data": {
                    "LowerDir": "/home/user/.local/share/containers/storage/overlay/3e8801912cbeb9ee125d408c661688b63aa79edff23bb1d465e0592cd7aee30e/diff:/home/user/.local/share/containers/storage/overlay/fa6614715059bf58e1ff8042c3ceac8c70948a9fdfebbca3f10e99cb426c9116/diff:/home/user/.local/share/containers/storage/overlay/e1440254663234040539164f7031e3e60276ea61c8128a827269453309a19b08/diff:/home/user/.local/share/containers/storage/overlay/66dcc187f20c9adfa3e3d9076e59f32b9fee63ddc435137e2ed586dce5ad0b0a/diff:/home/user/.local/share/containers/storage/overlay/e54fd5490a1c3dc0ddfcc9b31e08dca344fa2211e9ef34dae4788878a3ab069b/diff:/home/user/.local/share/containers/storage/overlay/291c74a8d55109cf4a6ca696cbdc6affc42e1de28dde5422ae494fb8abd87195/diff:/home/user/.local/share/containers/storage/overlay/aada397d822a1a53d2824a0c53a996d4159e8b7aade3d4ce28397f77feadb6e0/diff:/home/user/.local/share/containers/storage/overlay/e4b696f23f3708113e735eae64e9b39c39250decdbee572c781e6e817d6e53fb/diff:/home/user/.local/share/containers/storage/overlay/94f8cd5e058475b44b2454e8efb618afdb9f213c8b17d737aecd4d8f68f4f103/diff:/home/user/.local/share/containers/storage/overlay/b5f84eefc76473ba4d51f4aac92c04dc9ab54ffef84ca96d737e8d6e7dfc923f/diff:/home/user/.local/share/containers/storage/overlay/89fa520c6803c2724c744bf1ef5ce0f6b61121cff78c107ed751055f880c6bde/diff:/home/user/.local/share/containers/storage/overlay/8bba409efaa55e20fd14dab87cd1d2fc6c02f0eddd668a8731251e16a87b0842/diff:/home/user/.local/share/containers/storage/overlay/1c9774c7f21648d1e3974dbed4f761882063e0c59b45565ea0a20ee1e79c3015/diff:/home/user/.local/share/containers/storage/overlay/7de836d8555b12af133986347e4d0d5ad97230067302caa638ce6c01cb807330/diff:/home/user/.local/share/containers/storage/overlay/33abc4ad76e2099a56041549ac0643312ed831660ea7f28e07f05bc228e12f78/diff",
                    "UpperDir": "/home/user/.local/share/containers/storage/overlay/fbe05fcfefd7b8af4f36d580e50f5defcbb92729dafa856cf7af15b42a5f002f/diff",
                    "WorkDir": "/home/user/.local/share/containers/storage/overlay/fbe05fcfefd7b8af4f36d580e50f5defcbb92729dafa856cf7af15b42a5f002f/work"
               }
          },
          "Mounts": [
               {
                    "Type": "bind",
                    "Source": "/home/user/.local/state/overdeck-sandbox/creds/1786712742858041650-3482895-21518/codex/auth.json",
                    "Destination": "/sandbox-secrets/codex/auth.json",
                    "Driver": "",
                    "Mode": "",
                    "Options": [
                         "rbind"
                    ],
                    "RW": false,
                    "Propagation": "rprivate"
               },
               {
                    "Type": "bind",
                    "Source": "/home/user/sandbox",
                    "Destination": "/sandbox",
                    "Driver": "",
                    "Mode": "",
                    "Options": [
                         "rbind"
                    ],
                    "RW": true,
                    "Propagation": "rprivate"
               },
               {
                    "Type": "bind",
                    "Source": "/home/user/.local/share/overdeck-sandbox/secrets/e2e_key",
                    "Destination": "/sandbox-secrets/e2e_key",
                    "Driver": "",
                    "Mode": "",
                    "Options": [
                         "rbind"
                    ],
                    "RW": false,
                    "Propagation": "rprivate"
               },
               {
                    "Type": "bind",
                    "Source": "/home/user/.local/share/overdeck-sandbox/secrets/e2e_known_hosts",
                    "Destination": "/sandbox-secrets/e2e_known_hosts",
                    "Driver": "",
                    "Mode": "",
                    "Options": [
                         "rbind"
                    ],
                    "RW": false,
                    "Propagation": "rprivate"
               },
               {
                    "Type": "bind",
                    "Source": "/home/user/cdx-offload/git-common/overdeck-73dd7c243420",
                    "Destination": "/home/user/cdx-offload/git-common/overdeck-73dd7c243420",
                    "Driver": "",
                    "Mode": "",
                    "Options": [
                         "rbind"
                    ],
                    "RW": true,
                    "Propagation": "rprivate"
               },
               {
                    "Type": "bind",
                    "Source": "/home/user/cdx-offload/git-common/overdeck-73dd7c243420/worktrees/factory-pi-remote",
                    "Destination": "/home/user/cdx-offload/git-common/overdeck-73dd7c243420/worktrees/factory-pi-remote",
                    "Driver": "",
                    "Mode": "",
                    "Options": [
                         "rbind"
                    ],
                    "RW": true,
                    "Propagation": "rprivate"
               }
          ],
          "Dependencies": [],
          "NetworkSettings": {
               "EndpointID": "",
               "Gateway": "",
               "IPAddress": "",
               "IPPrefixLen": 0,
               "IPv6Gateway": "",
               "GlobalIPv6Address": "",
               "GlobalIPv6PrefixLen": 0,
               "MacAddress": "",
               "Bridge": "",
               "SandboxID": "",
               "HairpinMode": false,
               "LinkLocalIPv6Address": "",
               "LinkLocalIPv6PrefixLen": 0,
               "Ports": {},
               "SandboxKey": "",
               "Networks": {
                    "pasta": {
                         "EndpointID": "",
                         "Gateway": "",
                         "IPAddress": "",
                         "IPPrefixLen": 0,
                         "IPv6Gateway": "",
                         "GlobalIPv6Address": "",
                         "GlobalIPv6PrefixLen": 0,
                         "MacAddress": "",
                         "NetworkID": "pasta",
                         "DriverOpts": null,
                         "IPAMConfig": null,
                         "Links": null
                    }
               }
          },
          "Namespace": "",
          "IsInfra": false,
          "IsService": false,
          "KubeExitCodePropagation": "invalid",
          "lockNumber": 0,
          "Config": {
               "Hostname": "sandbox-factory-pi-remote-9b291304a43a",
               "Domainname": "",
               "User": "agent",
               "AttachStdin": false,
               "AttachStdout": false,
               "AttachStderr": false,
               "Tty": false,
               "OpenStdin": true,
               "StdinOnce": false,
               "Env": [
                    "BASH_ENV=/etc/overdeck-sandbox/toolgap.bash",
                    "SANDBOX_TOOLGAP_FILE=/sandbox/toolgap/gaps.jsonl",
                    "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1",
                    "PLAYWRIGHT_BROWSERS_PATH=/ms-playwright",
                    "PNPM_HOME=/sandbox/home/.local/share/pnpm",
                    "HOME=/sandbox/home",
                    "SANDBOX_ID=factory-pi-remote-9b291304a43a",
                    "SANDBOX_E2E_TARGET=user@host.containers.internal",
                    "SANDBOX_E2E_PORT=2222",
                    "PNPM_STORE_DIR=/sandbox/store/pnpm",
                    "COREPACK_ENABLE_DOWNLOAD_PROMPT=0",
                    "SANDBOX_IMAGE=localhost/overdeck-agent-sandbox:d1c7f17660b1",
                    "PATH=/usr/local/node/bin:/sandbox/home/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                    "container=podman",
                    "DISABLE_AUTOUPDATER=1",
                    "DEBIAN_FRONTEND=noninteractive",
                    "LANG=C.UTF-8",
                    "HOSTNAME=sandbox-factory-pi-remote-9b291304a43a"
               ],
               "Cmd": [
                    "/bin/sh",
                    "-c",
                    "[ -n \"${HOME-}\" ] || { echo 'agent_cred: HOME unset in container' \u003e\u00262; exit 10; }\nagent_cred_target=\"${HOME%/}/.codex/auth.json\"\ntrap 'rm -f \"$agent_cred_target\"' EXIT HUP INT TERM\nmkdir -p \"${agent_cred_target%/*}\" || { echo 'agent_cred: cannot create credential directory' \u003e\u00262; exit 10; }\n[ -r '/sandbox-secrets/codex/auth.json' ] || { echo 'agent_cred: staging mount missing: /sandbox-secrets/codex/auth.json' \u003e\u00262; exit 10; }\ncp '/sandbox-secrets/codex/auth.json' \"$agent_cred_target\" || { echo 'agent_cred: credential copy failed' \u003e\u00262; exit 10; }\nchmod 600 \"$agent_cred_target\" || { echo 'agent_cred: cannot restrict credential mode' \u003e\u00262; exit 10; }\n\"$@\"",
                    "sh",
                    "codex",
                    "exec",
                    "--sandbox",
                    "danger-full-access",
                    "--skip-git-repo-check",
                    "-m",
                    "gpt-5.6-terra",
                    "-c",
                    "model_reasoning_effort=medium",
                    "Repo: /home/user/Projects/overdeck/.worktrees/factory-pi-remote (git worktree, branch wt/factory-pi-remote).\nRead /home/user/Projects/overdeck/.worktrees/factory-pi-remote/docs/plans/2026-08-14-factory-pi-remote.md FIRST — it is the authoritative spec with all verified facts and resolved design decisions. Implement exactly what it describes. Do not re-derive facts it already states; trust them.\n\nSummary of what to build (full detail is in the plan file — read it, this is just an index):\n\n1. `modules/workstation/bin/pi-remote` — a bash CLI wrapper that runs the `pi` coding-agent binary on a buildbox over ssh instead of locally, reusing `modules/systray/remote_dispatch.py`'s public Python functions (host selection, registry, mirroring machinery) by shelling out to a small Python helper module (e.g. `modules/workstation/bin/pi_remote_dispatch.py` or extend remote_dispatch's own call sites — your call, keep remote_dispatch.py's existing contract intact, do not break command_router.py/cdx.py callers). It must:\n   - Target a bare `agent-seat` role host (NOT the podman sandbox container — see plan for why).\n   - Forward all argv to `pi` unchanged, except: detect info-only pi invocations (`--list-models`, `--version`, `--help`, or no `-p`/`--session-id` present) and exec local `pi` directly for those — never round-trip those to a buildbox.\n   - For a real agent turn, mirror the git repo tree (cwd) via remote_dispatch's existing open_session/push/pull machinery, AND additionally mirror the `--session-dir` argument's directory (raw_output_path stays local — VERIFY this by reading agent_pi.py's run() function before implementing; do not assume, read the actual code to confirm raw_output_path is written by the LOCAL Popen's stdout consumer thread, not by the remote pi process).\n   - Push `--session-dir`'s directory to the remote host before running pi there, pull it back after, every invocation (correctness over speed, no caching).\n   - Preserve real-time line-by-line stdout streaming (pi flushes per-line; do not buffer).\n   - Propagate SIGTERM/SIGKILL sent to the local pi-remote process to the remote pi process (use ssh -tt or a remote trap+wait pattern; verify empirically that killing pi-remote's local process also kills the remote pi process, e.g. via a follow-up ssh pgrep check in the test).\n   - Record a containment entry in ~/.overdeck/containment.jsonl on success/failure/abort, reusing command_router.py's `_append_containment_record` pattern (tool label \"pi\" or \"factory-pi\", runtime \"pi\") — do not duplicate its logic if it can be imported/reused cleanly; if not cleanly importable (it's currently a private function in command_router.py), extract a small shared helper into remote_dispatch.py or a new tiny module and have both command_router.py and pi-remote use it — keep this change minimal, do not refactor unrelated command_router.py code.\n   - Fail CLOSED on: registry unreadable, no reachable host, mirror push/pull failure — clear nonzero exit (reuse remote_dispatch.EXIT_* codes), detail to stderr, NEVER fall back to local pi silently.\n\n2. Factory config: add a `defaults` key to `modules/harness/factory/adw_modules/data_types.py`'s `ConfigDefaults` (e.g. `pi_spawn: Literal[\"local\", \"remote\"] = \"local\"`) and wire it through `modules/harness/factory/adw_modules/agents.py` so when `\"remote\"`, the agent_pi.run() spawn path uses the installed `pi-remote` binary's absolute path instead of the bare `PI_PATH`/\"pi\" default — WITHOUT modifying agent_pi.py's protocol/tracer/gate logic; the only touch to agent_pi.py, if any, should be widening how PI_PATH is resolved (e.g. read an additional env var or accept an explicit path parameter threaded from agents.py) — keep this diff minimal and follow the existing PI_PATH env-var precedence (explicit env var override still wins). Follow the config layering idiom shown in `modules/harness/factory/tests/test_config_layering.py`. Default stays \"local\" — zero behavior change unless a project opts in.\n\n3. Register `pi` as a new `vendor` entry in `modules/buildbox/devtools.json` so `buildbox bootstrap` installs it fleet-wide (pi ships as npm package `@earendil-works/pi-coding-agent`; the workstation already has it in `~/.npm-global`; check what \"kind\" values devtools.json already supports for npm-based installs — study the `codex` vendor entry (kind: mise, tool: npm:@openai/codex) as the closest pattern and adapt).\n\n4. Auth: extend `modules/systray/pi_auth_sync.py` (or a thin new remote counterpart) so an authenticated `~/.pi/agent/auth.json` reaches the buildboxes via the existing claude-parity/buildbox-parity convergence path (study `modules/buildbox/lib/claude-parity.sh` and `modules/workstation/claude/bin/buildbox-parity.sh` for the convergence pattern this should follow) — codex's own `~/.codex/auth.json` is already present on the boxes (verified this session on debian1), so pi_auth_sync.py's existing `_derive_credential` logic (which already derives pi creds FROM codex's auth.json) can plausibly run box-side too, or push its derived output — your design call, follow the plan file's \"next executable action\" section, keep the change narrowly scoped to landing an authenticated pi on the boxes via the established mechanism, not hand-rolling a new one.\n\n5. Tests:\n   - `pi-remote` / its Python helper: unit tests for fail-closed branches (registry absent, host down, mirror failure) near `modules/systray/tests/test_remote_dispatch*` patterns (FT_FROM_HOOK=1 FT_HINTS=1 ft find and follow that file's exact test structure/fixtures).\n   - One factory test proving the `pi_spawn: remote` config key changes the resolved spawn path passed into agent_pi.run — mock the wrapper entirely, no real ssh/dispatch. Add near the existing agent_pi/agents tests in `modules/harness/factory/tests/`.\n   - `python3 -m pytest modules/harness/factory/tests/ -q` MUST stay green (189/189 baseline recorded in the plan file — re-verify exact count before you start and report the delta).\n   - devtools.json / claude-parity changes: follow whatever existing test pattern covers devtools.json vendor entries (check `modules/buildbox/test/vendor.test.sh` and `modules/buildbox/test/claude-parity.test.sh`) and extend if a new vendor entry needs coverage.\n\nConstraints (from repo CLAUDE.md, non-negotiable):\n- No hand-scp of unmanaged binaries — pi reaches the boxes only through devtools.json + buildbox bootstrap.\n- Never touch agent_pi.py's protocol handling / tracer / gate logic / runner commit flow beyond the minimal PI_PATH-resolution widening described in step 2.\n- No stubs/placeholders — everything must actually work end to end for the parts you can verify locally (wrapper logic, config wiring, unit tests). Live ssh-to-buildbox proof will be run by the reviewer separately once pi is actually installed+authenticated on a box — you do not need to wait for that to land this code, but the code must be genuinely correct, not aspirational.\n- Comments: WHAT only, never reasoning/why, per repo CLAUDE.md; keep them minimal.\n- Do not create any new documentation/README files.\n- Do not run `git commit`/push — leave the worktree with the changes staged/unstaged for the reviewer (me) to inspect, test, and commit myself.\n\nWhen done, report: files changed/created, test counts before/after for both `modules/harness/factory/tests/` and any new test files you added, any design decisions you made that the plan file left open, and anything you could NOT verify (e.g. real ssh dispatch, since pi is not yet installed on any box)."
               ],
               "Image": "localhost/overdeck-agent-sandbox:d1c7f17660b1",
               "Volumes": null,
               "WorkingDir": "/sandbox/workspaces/factory-pi-remote-9b291304a43a",
               "Entrypoint": [
                    "/usr/bin/tini",
                    "--"
               ],
               "OnBuild": null,
               "Labels": {
                    "io.buildah.version": "1.43.2"
               },
               "Annotations": {
                    "io.container.manager": "libpod",
                    "io.podman.annotations.cid-file": "/home/user/.local/share/overdeck-sandbox/runs/factory-pi-remote-9b291304a43a/cid",
                    "io.podman.annotations.pids-limit": "512",
                    "io.podman.annotations.userns": "keep-id",
                    "org.opencontainers.image.stopSignal": "15",
                    "org.systemd.property.KillSignal": "15",
                    "org.systemd.property.TimeoutStopUSec": "uint64 10000000"
               },
               "StopSignal": "SIGTERM",
               "HealthcheckOnFailureAction": "none",
               "HealthLogDestination": "local",
               "HealthcheckMaxLogCount": 5,
               "HealthcheckMaxLogSize": 500,
               "CreateCommand": [
                    "podman",
                    "run",
                    "--replace",
                    "--interactive",
                    "--cidfile",
                    "/home/user/.local/share/overdeck-sandbox/runs/factory-pi-remote-9b291304a43a/cid",
                    "--name",
                    "overdeck-sandbox-factory-pi-remote-9b291304a43a",
                    "--hostname",
                    "sandbox-factory-pi-remote-9b291304a43a",
                    "--userns=keep-id",
                    "--cap-drop=ALL",
                    "--security-opt=no-new-privileges",
                    "--pids-limit",
                    "512",
                    "--memory",
                    "12g",
                    "--memory-swap",
                    "12g",
                    "--cpus",
                    "4",
                    "--cpu-shares",
                    "256",
                    "--volume",
                    "/home/user/sandbox:/sandbox:rw",
                    "--volume",
                    "/home/user/.local/share/overdeck-sandbox/secrets/e2e_key:/sandbox-secrets/e2e_key:ro",
                    "--volume",
                    "/home/user/.local/share/overdeck-sandbox/secrets/e2e_known_hosts:/sandbox-secrets/e2e_known_hosts:ro",
                    "--volume",
                    "/home/user/cdx-offload/git-common/overdeck-73dd7c243420:/home/user/cdx-offload/git-common/overdeck-73dd7c243420:rw",
                    "--volume",
                    "/home/user/cdx-offload/git-common/overdeck-73dd7c243420/worktrees/factory-pi-remote:/home/user/cdx-offload/git-common/overdeck-73dd7c243420/worktrees/factory-pi-remote:rw",
                    "--volume",
                    "/home/user/.local/state/overdeck-sandbox/creds/1786712742858041650-3482895-21518/codex/auth.json:/sandbox-secrets/codex/auth.json:ro",
                    "--workdir",
                    "/sandbox/workspaces/factory-pi-remote-9b291304a43a",
                    "--env",
                    "SANDBOX_ID=factory-pi-remote-9b291304a43a",
                    "--env",
                    "SANDBOX_IMAGE=localhost/overdeck-agent-sandbox:d1c7f17660b1",
                    "--env",
                    "SANDBOX_E2E_TARGET=user@host.containers.internal",
                    "--env",
                    "SANDBOX_E2E_PORT=2222",
                    "localhost/overdeck-agent-sandbox:d1c7f17660b1",
                    "/bin/sh",
                    "-c",
                    "[ -n \"${HOME-}\" ] || { echo 'agent_cred: HOME unset in container' \u003e\u00262; exit 10; }\nagent_cred_target=\"${HOME%/}/.codex/auth.json\"\ntrap 'rm -f \"$agent_cred_target\"' EXIT HUP INT TERM\nmkdir -p \"${agent_cred_target%/*}\" || { echo 'agent_cred: cannot create credential directory' \u003e\u00262; exit 10; }\n[ -r '/sandbox-secrets/codex/auth.json' ] || { echo 'agent_cred: staging mount missing: /sandbox-secrets/codex/auth.json' \u003e\u00262; exit 10; }\ncp '/sandbox-secrets/codex/auth.json' \"$agent_cred_target\" || { echo 'agent_cred: credential copy failed' \u003e\u00262; exit 10; }\nchmod 600 \"$agent_cred_target\" || { echo 'agent_cred: cannot restrict credential mode' \u003e\u00262; exit 10; }\n\"$@\"",
                    "sh",
                    "codex",
                    "exec",
                    "--sandbox",
                    "danger-full-access",
                    "--skip-git-repo-check",
                    "-m",
                    "gpt-5.6-terra",
                    "-c",
                    "model_reasoning_effort=medium",
                    "Repo: /home/user/Projects/overdeck/.worktrees/factory-pi-remote (git worktree, branch wt/factory-pi-remote).\nRead /home/user/Projects/overdeck/.worktrees/factory-pi-remote/docs/plans/2026-08-14-factory-pi-remote.md FIRST — it is the authoritative spec with all verified facts and resolved design decisions. Implement exactly what it describes. Do not re-derive facts it already states; trust them.\n\nSummary of what to build (full detail is in the plan file — read it, this is just an index):\n\n1. `modules/workstation/bin/pi-remote` — a bash CLI wrapper that runs the `pi` coding-agent binary on a buildbox over ssh instead of locally, reusing `modules/systray/remote_dispatch.py`'s public Python functions (host selection, registry, mirroring machinery) by shelling out to a small Python helper module (e.g. `modules/workstation/bin/pi_remote_dispatch.py` or extend remote_dispatch's own call sites — your call, keep remote_dispatch.py's existing contract intact, do not break command_router.py/cdx.py callers). It must:\n   - Target a bare `agent-seat` role host (NOT the podman sandbox container — see plan for why).\n   - Forward all argv to `pi` unchanged, except: detect info-only pi invocations (`--list-models`, `--version`, `--help`, or no `-p`/`--session-id` present) and exec local `pi` directly for those — never round-trip those to a buildbox.\n   - For a real agent turn, mirror the git repo tree (cwd) via remote_dispatch's existing open_session/push/pull machinery, AND additionally mirror the `--session-dir` argument's directory (raw_output_path stays local — VERIFY this by reading agent_pi.py's run() function before implementing; do not assume, read the actual code to confirm raw_output_path is written by the LOCAL Popen's stdout consumer thread, not by the remote pi process).\n   - Push `--session-dir`'s directory to the remote host before running pi there, pull it back after, every invocation (correctness over speed, no caching).\n   - Preserve real-time line-by-line stdout streaming (pi flushes per-line; do not buffer).\n   - Propagate SIGTERM/SIGKILL sent to the local pi-remote process to the remote pi process (use ssh -tt or a remote trap+wait pattern; verify empirically that killing pi-remote's local process also kills the remote pi process, e.g. via a follow-up ssh pgrep check in the test).\n   - Record a containment entry in ~/.overdeck/containment.jsonl on success/failure/abort, reusing command_router.py's `_append_containment_record` pattern (tool label \"pi\" or \"factory-pi\", runtime \"pi\") — do not duplicate its logic if it can be imported/reused cleanly; if not cleanly importable (it's currently a private function in command_router.py), extract a small shared helper into remote_dispatch.py or a new tiny module and have both command_router.py and pi-remote use it — keep this change minimal, do not refactor unrelated command_router.py code.\n   - Fail CLOSED on: registry unreadable, no reachable host, mirror push/pull failure — clear nonzero exit (reuse remote_dispatch.EXIT_* codes), detail to stderr, NEVER fall back to local pi silently.\n\n2. Factory config: add a `defaults` key to `modules/harness/factory/adw_modules/data_types.py`'s `ConfigDefaults` (e.g. `pi_spawn: Literal[\"local\", \"remote\"] = \"local\"`) and wire it through `modules/harness/factory/adw_modules/agents.py` so when `\"remote\"`, the agent_pi.run() spawn path uses the installed `pi-remote` binary's absolute path instead of the bare `PI_PATH`/\"pi\" default — WITHOUT modifying agent_pi.py's protocol/tracer/gate logic; the only touch to agent_pi.py, if any, should be widening how PI_PATH is resolved (e.g. read an additional env var or accept an explicit path parameter threaded from agents.py) — keep this diff minimal and follow the existing PI_PATH env-var precedence (explicit env var override still wins). Follow the config layering idiom shown in `modules/harness/factory/tests/test_config_layering.py`. Default stays \"local\" — zero behavior change unless a project opts in.\n\n3. Register `pi` as a new `vendor` entry in `modules/buildbox/devtools.json` so `buildbox bootstrap` installs it fleet-wide (pi ships as npm package `@earendil-works/pi-coding-agent`; the workstation already has it in `~/.npm-global`; check what \"kind\" values devtools.json already supports for npm-based installs — study the `codex` vendor entry (kind: mise, tool: npm:@openai/codex) as the closest pattern and adapt).\n\n4. Auth: extend `modules/systray/pi_auth_sync.py` (or a thin new remote counterpart) so an authenticated `~/.pi/agent/auth.json` reaches the buildboxes via the existing claude-parity/buildbox-parity convergence path (study `modules/buildbox/lib/claude-parity.sh` and `modules/workstation/claude/bin/buildbox-parity.sh` for the convergence pattern this should follow) — codex's own `~/.codex/auth.json` is already present on the boxes (verified this session on debian1), so pi_auth_sync.py's existing `_derive_credential` logic (which already derives pi creds FROM codex's auth.json) can plausibly run box-side too, or push its derived output — your design call, follow the plan file's \"next executable action\" section, keep the change narrowly scoped to landing an authenticated pi on the boxes via the established mechanism, not hand-rolling a new one.\n\n5. Tests:\n   - `pi-remote` / its Python helper: unit tests for fail-closed branches (registry absent, host down, mirror failure) near `modules/systray/tests/test_remote_dispatch*` patterns (FT_FROM_HOOK=1 FT_HINTS=1 ft find and follow that file's exact test structure/fixtures).\n   - One factory test proving the `pi_spawn: remote` config key changes the resolved spawn path passed into agent_pi.run — mock the wrapper entirely, no real ssh/dispatch. Add near the existing agent_pi/agents tests in `modules/harness/factory/tests/`.\n   - `python3 -m pytest modules/harness/factory/tests/ -q` MUST stay green (189/189 baseline recorded in the plan file — re-verify exact count before you start and report the delta).\n   - devtools.json / claude-parity changes: follow whatever existing test pattern covers devtools.json vendor entries (check `modules/buildbox/test/vendor.test.sh` and `modules/buildbox/test/claude-parity.test.sh`) and extend if a new vendor entry needs coverage.\n\nConstraints (from repo CLAUDE.md, non-negotiable):\n- No hand-scp of unmanaged binaries — pi reaches the boxes only through devtools.json + buildbox bootstrap.\n- Never touch agent_pi.py's protocol handling / tracer / gate logic / runner commit flow beyond the minimal PI_PATH-resolution widening described in step 2.\n- No stubs/placeholders — everything must actually work end to end for the parts you can verify locally (wrapper logic, config wiring, unit tests). Live ssh-to-buildbox proof will be run by the reviewer separately once pi is actually installed+authenticated on a box — you do not need to wait for that to land this code, but the code must be genuinely correct, not aspirational.\n- Comments: WHAT only, never reasoning/why, per repo CLAUDE.md; keep them minimal.\n- Do not create any new documentation/README files.\n- Do not run `git commit`/push — leave the worktree with the changes staged/unstaged for the reviewer (me) to inspect, test, and commit myself.\n\nWhen done, report: files changed/created, test counts before/after for both `modules/harness/factory/tests/` and any new test files you added, any design decisions you made that the plan file left open, and anything you could NOT verify (e.g. real ssh dispatch, since pi is not yet installed on any box)."
               ],
               "Umask": "0022",
               "Timeout": 0,
               "StopTimeout": 10,
               "Passwd": true,
               "sdNotifyMode": "container"
          },
          "HostConfig": {
               "Binds": [
                    "/home/user/.local/state/overdeck-sandbox/creds/1786712742858041650-3482895-21518/codex/auth.json:/sandbox-secrets/codex/auth.json:ro,rprivate,rbind",
                    "/home/user/sandbox:/sandbox:rw,rprivate,rbind",
                    "/home/user/.local/share/overdeck-sandbox/secrets/e2e_key:/sandbox-secrets/e2e_key:ro,rprivate,rbind",
                    "/home/user/.local/share/overdeck-sandbox/secrets/e2e_known_hosts:/sandbox-secrets/e2e_known_hosts:ro,rprivate,rbind",
                    "/home/user/cdx-offload/git-common/overdeck-73dd7c243420:/home/user/cdx-offload/git-common/overdeck-73dd7c243420:rw,rprivate,rbind",
                    "/home/user/cdx-offload/git-common/overdeck-73dd7c243420/worktrees/factory-pi-remote:/home/user/cdx-offload/git-common/overdeck-73dd7c243420/worktrees/factory-pi-remote:rw,rprivate,rbind"
               ],
               "CgroupManager": "systemd",
               "CgroupMode": "private",
               "ContainerIDFile": "/home/user/.local/share/overdeck-sandbox/runs/factory-pi-remote-9b291304a43a/cid",
               "LogConfig": {
                    "Type": "journald",
                    "Config": null,
                    "Path": "",
                    "Tag": "",
                    "Size": "-1B"
               },
               "NetworkMode": "pasta",
               "PortBindings": {},
               "RestartPolicy": {
                    "Name": "no",
                    "MaximumRetryCount": 0
               },
               "AutoRemove": false,
               "AutoRemoveImage": false,
               "Annotations": {
                    "io.container.manager": "libpod",
                    "io.podman.annotations.cid-file": "/home/user/.local/share/overdeck-sandbox/runs/factory-pi-remote-9b291304a43a/cid",
                    "io.podman.annotations.pids-limit": "512",
                    "io.podman.annotations.userns": "keep-id",
                    "org.opencontainers.image.stopSignal": "15",
                    "org.systemd.property.KillSignal": "15",
                    "org.systemd.property.TimeoutStopUSec": "uint64 10000000"
               },
               "VolumeDriver": "",
               "VolumesFrom": null,
               "CapAdd": [],
               "CapDrop": [
                    "CAP_CHOWN",
                    "CAP_DAC_OVERRIDE",
                    "CAP_FOWNER",
                    "CAP_FSETID",
                    "CAP_KILL",
                    "CAP_NET_BIND_SERVICE",
                    "CAP_SETFCAP",
                    "CAP_SETGID",
                    "CAP_SETPCAP",
                    "CAP_SETUID",
                    "CAP_SYS_CHROOT"
               ],
               "Dns": [],
               "DnsOptions": [],
               "DnsSearch": [],
               "ExtraHosts": [],
               "HostsFile": "",
               "GroupAdd": [],
               "IpcMode": "shareable",
               "Cgroup": "",
               "Cgroups": "default",
               "Links": null,
               "OomScoreAdj": 0,
               "PidMode": "private",
               "Privileged": false,
               "PublishAllPorts": false,
               "ReadonlyRootfs": false,
               "SecurityOpt": [
                    "no-new-privileges"
               ],
               "Tmpfs": {},
               "UTSMode": "private",
               "UsernsMode": "private",
               "IDMappings": {
                    "UidMap": [
                         "0:1:1000",
                         "1000:0:1",
                         "1001:1001:64536"
                    ],
                    "GidMap": [
                         "0:1:1000",
                         "1000:0:1",
                         "1001:1001:64536"
                    ]
               },
               "ShmSize": 65536000,
               "Runtime": "oci",
               "ConsoleSize": [
                    0,
                    0
               ],
               "Isolation": "",
               "CpuShares": 256,
               "Memory": 12884901888,
               "NanoCpus": 4000000000,
               "CgroupParent": "user.slice",
               "BlkioWeight": 0,
               "BlkioWeightDevice": null,
               "BlkioDeviceReadBps": null,
               "BlkioDeviceWriteBps": null,
               "BlkioDeviceReadIOps": null,
               "BlkioDeviceWriteIOps": null,
               "CpuPeriod": 100000,
               "CpuQuota": 400000,
               "CpuRealtimePeriod": 0,
               "CpuRealtimeRuntime": 0,
               "CpusetCpus": "",
               "CpusetMems": "",
               "Devices": [],
               "DiskQuota": 0,
               "KernelMemory": 0,
               "MemoryReservation": 0,
               "MemorySwap": 12884901888,
               "MemorySwappiness": -1,
               "OomKillDisable": false,
               "PidsLimit": 512,
               "Ulimits": [
                    {
                         "Name": "RLIMIT_NOFILE",
                         "Soft": 524288,
                         "Hard": 524288
                    },
                    {
                         "Name": "RLIMIT_NPROC",
                         "Soft": 127162,
                         "Hard": 127162
                    }
               ],
               "CpuCount": 0,
               "CpuPercent": 0,
               "IOMaximumIOps": 0,
               "IOMaximumBandwidth": 0,
               "CgroupConf": null
          },
          "UseImageHosts": false,
          "UseImageHostname": false
     }
]
