Attach — operator attaches a TUI to a live agent
On-demand, end-to-end-encrypted attach to a running agent: an operator runs
pond run attach <run> and the real Codex TUI drives a live, headless agent
through pond’s relay. Pond is otherwise outbound-poll-only; this is its one
full-duplex transport, and it’s a zero-knowledge relay — the payload is
sealed (Noise/ChaCha20) inside glyph on the worker and in the operator’s client,
so pond only ever moves ciphertext.
Composes with glyph (the lean Codex
fork): the agent is codex-app-server, and the e2e + tunnel halves are glyph’s
attach-bridge (serve/connect) and attach-e2e crates. The RFC is
neotype-perf/REMOTE_ATTACH.md in that repo.
Code: app/routers/attach.py (rendezvous + trigger), app/attach_bridge.py
(operator ws⇄tcp), swarm/src/ws_client.py + swarm/src/core.py (worker
auto-dial), cli/commands/run.py::attach + cli/attach_bridge.py (the CLI).
Topology
Both ends dial inward (no inbound to a NAT’d worker); the e2e terminates in glyph, so the bridges and rendezvous see only ciphertext.
codex-tui --remote unix://OP codex-app-server (unix socket)
│ ▲
attach-bridge connect (e2e initiator) attach-bridge serve (e2e responder)
│ 127.0.0.1 │ 127.0.0.1
operator bridge ──ws──▶ /v1/attach/{sid} ◀──ws── worker-host bridge
(cli/CLI) (app/routers/attach.py) (swarm worker)
How a run becomes attachable
- Harness — the
codex-attachablepreset (app/harness_presets.py) runscodex-app-server --listen unix://{socket}(long-lived; the TUI drives turns). The worker fills{socket}with a short per-job path (build_job_spec); a deep$CODEX_HOMEpath overflows the unix-socketSUN_LEN(~108) limit. - Request —
POST /v1/runs/{id}/attachmints a one-shot rendezvous session, records it on the run’s live dispatch job (dispatch_jobs.attach_session_id), and returns the serving worker’s enrollment-pinned X25519 pubkey for the operator to pin as the Noise responder key. - Trigger —
GET /v1/orch/pollsurfacesattachRequestsfor the orch’s running jobs; the orchestrator builds the rendezvous ws url and relays it to the owning worker on its next heartbeat (swarm/src/orchestrator.py,backend.py). - Worker dials out —
core.beat()runs one_run_attach_bridgeper session: glyphattach-bridge serve(signed with the worker’s pinned X25519 key) against the agent’s socket, piped to the rendezvous via the stdlibws_client(swarm is stdlib-only — nowebsockets). - Operator —
pond run attachopens the rendezvous, runsattach-bridge connectpinning the worker key, and launchescodex-tui --remote. Quit to detach; the run keeps running.
Status
Verified end-to-end on a live profile:none pool: operator request → poll →
orchestrator relay → worker auto-dial → the operator reached the real
codex-app-server (HTTP/1.1 101 Switching Protocols). Tests: rendezvous
(tests/test_attach_rendezvous.py), the trigger, the {socket} substitution
(swarm/tests/test_sandbox.py), and the reproducible transport smoke
(scripts/attach_e2e.py). P1 (interactive) — no prompt, the agent is idle
until attached and the TUI drives turns — and P2 (headless + steerable) — the
stage carries a prompt, the worker runs the first turn headless, and an operator
attaches to observe/steer the same thread — both work (see the checklist below).
Remaining work (pick-up list)
Functionality is proven; these are real gaps a contributor can take independently.
- Attachable-stage lifecycle. (done) A stage is attachable when its
command template carries the
{socket}placeholder — the same signal the worker uses, so the two agree with no extra schema. The orchestrated executor (_run_tasks_orchestrated) now skips the wall-clock deadline for an attachable stage: it runs detach-driven, ending on cancel rather than being markedfailedattimeout_sec. The idle half is also done: the worker tracks attach activity per job and gracefully ends an agent that’s never attached past a connect window, or attached-then-detached past an idle window (re-attach resets it) —POND_ATTACH_CONNECT_SEC/POND_ATTACH_IDLE_SEC. Tripping the job’s cancel event SIGTERMs the app-server (exits 0 →done), and the executor no longer downgrades an attachable stage’s empty stdout tofailed. - Reap the agent on job end/timeout. (done)
NoneProvidernow launches the agent withstart_new_session=True, and the baseSandboxHandleterminate/kill signal the whole process group (guarded so we never killpg the worker’s own group).worker_run_jobalso reaps in afinallyon every exit path (normal / cancel / timeout / exception), so the long-livedcodex-app-serverand any children can’t be orphaned. - Multi-worker pubkey lookup. (done)
dispatch_jobs.claimed_by_workernow records the claiming worker’s name — the orchestrator reports it on therunningtransition (swarmbackend.pywrapsstate.claim;OrchJobStateIncarries an optionalworker)._worker_pubkey_for_runresolves that specific(orch_id, worker)’s pinned key, unambiguous on a multi-worker pool, with the single-worker heuristic kept as a fallback for older orchestrators. Verified live on a 2-worker pool (the claiming worker’s key is returned, not the other’s). - Pin the operator key worker-side. (done) The operator mints an
ephemeral X25519 keypair per attach and sends its public half with the request
(
POST /attachoperatorPubkey); it rides the orchestrator-poll relay (dispatch_jobs.attach_operator_pubkey→ orchset_attach/heartbeat → worker) and the worker passes it toattach-bridge serve --peer. glyph’sservegained--peer: in Noise IK the responder learns the initiator’s static key during the handshake, so it authorizes it post-handshake and drops a mismatch. Verified live: a matching operator key reaches101; a different key is rejected. - Multi-replica rendezvous. (done) Set
POND_ATTACH_REDIS_URLand the ws handler validates the session against the DB (the cross-replica source of truth) and relays the byte stream over Redis pub/sub keyed byattach_session_id— so the operator and worker ws connections can land on different replicas. Each peer subscribes before announcing presence and waits for the other (pub/sub has no buffering); duplicate roles are rejected via a RedisSET NX. Still zero-knowledge — only ciphertext crosses Redis. Unset = the in-process pairing (single replica), unchanged. Verified live across two replicas. - P2 — headless + steerable. (done) No
codex-app-serverauto-run mode was needed — the worker drives the first turn. An attachable stage that carries an explicitpromptis P2 (the prompt resolver’s built-in fallback assets do NOT count — else P1 would be unreachable): the executor ships the stage’s own prompt to the worker (POND_INITIAL_PROMPT), which — once the app-server binds its socket — runsinitialize → thread/start → turn/startover a stdlib WS-over-unix client (swarm/src/app_server_client.py), publishes thethread_idon the job’s stdout, and keeps the turn-owning connection open.POST /attachreturns thatagentThreadId;pond run attachlaunchescodex-tui --remote --resume <thread_id>so the operator lands on (and steers) the SAME thread — the app-server fans turn events to every subscribed connection. The only glyph change was exposingcodex-tui’s existingresume_session_idas--resume. No prompt ⇒ P1 (idle). Verified live through the e2e tunnel: the operator resumed the worker’s headless session and the agent recalled context from it. - Confined (
glyph-brokered) attach. Verified onprofile:none; the Docker/confined path ({socket}inside the container, the broker network) is untested (nested-container limits block it locally). Wire + verify on real infra.
Threat model notes
- Zero-knowledge relay: the rendezvous and both ws⇄tcp bridges carry only Noise-sealed frames; the control plane can’t read or forge session traffic.
- Mutual authentication. The worker is authenticated by its
enrollment-pinned X25519 key (the operator pins it as
connect --peer). The operator is authenticated by the ephemeral key it registers in the attach request, which the worker pins asserve --peer— so only that operator can complete the handshake (no longer TOFU). - The agent container gains no new inbound: glyph
servebinds localhost on the worker host; the worker host makes the outbound rendezvous connection.