Skip to content

feat(sessions): drive Codex sessions via Remote Control (issue #103)#134

Open
defangdevs wants to merge 2 commits into
masterfrom
fix/codex-remote-control
Open

feat(sessions): drive Codex sessions via Remote Control (issue #103)#134
defangdevs wants to merge 2 commits into
masterfrom
fix/codex-remote-control

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

What

Wires codex into the existing remoteControl option instead of ignoring it. Codex sessions can now be driven from the Codex desktop/mobile apps, closing issue #103.

How

Unlike claude's --remote-control TUI flag, codex ships a dedicated app-server daemon (codex remote-control {start,stop,pair}) that the Codex apps drive. codex remote-control start detaches (reparents to init) and returns immediately, so it can't be a session's foreground command — the tmux session would exit at once and the reconcile loop would respawn it every ~2s while the real daemon ran unsupervised.

  • Supervisor wrapper (codexRemoteControl): the actual foreground command for a remote-controlled codex session. It clears any leftover daemon, (re)starts a fresh one, then blocks as long as the control socket answers, so the session's life tracks the daemon's. On session kill/restart (SIGHUP/INT/TERM) it tears the daemon down, so no detached daemon is ever leaked.
  • skipPermissions rides in as -c approval_policy=never -c sandbox_mode=danger-full-access, since the remote-control start subcommand rejects the TUI's --dangerously-bypass-approvals-and-sandbox flag.
  • remoteControlName stays claude-only — the codex daemon names itself from the hostname. Pair a running box with codex remote-control pair.
  • The codex daemon is a per-user singleton (one control socket per user); enable remoteControl on at most one codex session per user.

Docs & tests

  • Updated README.md, aws/template.yaml parameter/output descriptions, and the module option descriptions to reflect that both agents support Remote Control.
  • Added a NixOS test (tests/sessions.nix) asserting the supervisor wrapper runs with the autonomy -c overrides (and not the rejected --dangerously-bypass flag), and that the daemon's control socket answers.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EdKG7KmEiwGB4AtLfddYLT

@defangdevs

Copy link
Copy Markdown
Owner Author

Let me confirm the one thing that determines what the fix looks like — whether codex has an offline daemon-start path that the PR bypassed.

That's the decisive test. Here's my analysis.

Root cause: the PR uses the wrong codex primitive

Codex exposes two ways to bring up the app-server daemon, and they differ in exactly the dimension that matters here:

Command | Needs login + network? | Brings up local socket? -- | -- | -- codex remote-control start (what the PR wires) | Yes — eagerly opens the backend relay connection; aborts if it errors | No, if the connection fails codex app-server daemon start (what it should use) | No — just starts the local daemon | Yes, offline

I proved both, offline, no login, on this box:

  • remote-control start  Error: Remote control is enabled on nixos but the connection is errored. (wrapper exits 1)
  • app-server daemon start  {"status":"started",...}, and the version probe returns {"status":"running",...} exit 0. enable-remote-controleven succeeds offline (remoteControlEnabled:true is a capability flag, not a live connection).

So remote-controllability is: start the local daemon (offline-safe) → mark it remote-control-enabled → the relay connection attaches opportunistically once the user logs in. The PR collapsed that into remote-control start, which insists the relay be live at start time — the one variant that can't work headless.

The tell: the wrapper contradicts itself

The supervisor starts with remote-control start (backend-eager) but health-checks with codex app-server daemon version (local-only). The two halves are at different layers. If it started with app-server daemon start, start and liveness would agree, it'd come up offline, and CI's assertion (app-server daemon version succeeds) would pass as written — because that assertion is already testing for the local daemon, not the connection. The test author's mental model ("starting the daemon needs no login — that's separate from pairing") is correct for app-server daemon and wired to the one command where it's false.

Why this is worse than a failing test — it's a default-path regression

  • remoteControl defaults to true. Pre-PR, codex ignored it and ran its TUI → worked out of the box, reachable via the browser terminal. Post-PR, the default flips every fresh codex box into remote-control start, which exits 1 immediately, and the reconcile loop respawns it (~2s per the PR's own description) → a permanent crash-loop(CPU + log spam) on any box that isn't already logged in.
  • Bootstrapping deadlock: codex remote control replaces the TUI rather than layering onto it (as claude's --remote-control flag does). But you need codex login to make remote control work — and there's no interactive surface left to log in through, because the daemon took the TUI's place. Claude avoids this precisely because its remote control is additive. This asymmetry is the design mistake underneath the symptom.

The fix

Swap the primitive in codexRemoteControl: app-server daemon start (+ enable-remote-control) instead of remote-control start. That:

  1. brings the daemon up offline → session stays alive, no crash-loop;
  2. makes the wrapper's start and liveness probe consistent;
  3. lets remote control light up when the user later codex logins / remote-control pairs — which is what the docs already tell people to do;
  4. makes the CI assertion pass without weakening it.

Secondary, worth a comment but not blocking: even after the fix, app-server daemon version confirms the local daemon is up, not that the relay connection is live — so "supervisor healthy" ≠ "drivable from the app." The wrapper tracks the daemon's lifecycle, not its reachability. That's a fine contract; it should just be stated.

Want me to write this up as a PR review comment, or draft the wrapper change against the branch?

codex and others added 2 commits July 22, 2026 09:27
Wire codex into the remoteControl option instead of ignoring it. Unlike
claude's `--remote-control` TUI flag, codex ships a dedicated app-server
daemon (`codex remote-control {start,stop,pair}`) that the Codex apps
drive. `start` detaches, so it can't be a session's foreground command
directly; add a supervisor wrapper that (re)starts the daemon, blocks on
its control socket, and tears it down on session kill/restart so no
detached daemon is leaked.

skipPermissions rides in as `-c approval_policy=never -c
sandbox_mode=danger-full-access` since the subcommand rejects the TUI's
--dangerously-bypass flag. remoteControlName stays claude-only (the codex
daemon names itself from the hostname). Update README, AWS template docs,
and add a NixOS test asserting the wrapper runs and the daemon answers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdKG7KmEiwGB4AtLfddYLT
@defangdevs
defangdevs force-pushed the fix/codex-remote-control branch from 144f7b9 to f76ec35 Compare July 22, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants