Skip to content

Commit 1343440

Browse files
Panopticon Agentclaude
andcommitted
Harden Codex unattended launch: apps connector, hook trust, flag order, alt-screen
Four fixes to make Codex spawn reliably without an operator in the container: - `features.apps = false` in config.toml disables the built-in apps connector, which cannot start in a container and stalls every spawn on its 30 s MCP timeout. The `[mcp_servers.codex_apps] enabled = false` alternative is invalid config that crash-loops Codex — the feature flag is the only safe disable. - `--dangerously-bypass-hook-trust` added to launch_argv (first run and resume): since PR #387 wired Stop/UserPromptSubmit hooks into config.toml, Codex prompts interactively to trust each hook hash; with no operator this hangs indefinitely. - Bypass flags moved after the `resume` subcommand (`codex resume --last --flags`), matching the reference implementation's required argument order. - `--no-alt-screen` on every launch so Codex renders into tmux scrollback instead of the alternate screen, keeping `tmux attach` history accessible. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f30683a commit 1343440

2 files changed

Lines changed: 54 additions & 17 deletions

File tree

src/panopticon/container/cli/codex.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,19 @@ def write_mcp_config(self, config_dir: Path, service_url: str) -> Path:
119119
plane claude connects to, at ``<service_url>/mcp`` — no auth token (the server is the
120120
container's own task service). Older codex builds only pick up HTTP MCP with
121121
``experimental_use_rmcp_client`` set, so we enable it defensively (a no-op on builds with
122-
native support). Merged into ``config.toml`` so it coexists with the trust/overview keys.
122+
native support). ``features.apps = false`` disables codex's built-in apps connector, which
123+
cannot start in the container and otherwise stalls every spawn on its 30 s MCP timeout
124+
(the ``[mcp_servers.codex_apps] enabled = false`` alternative is invalid config that
125+
crash-loops codex — the feature flag is the only safe disable). Merged into ``config.toml``
126+
so it coexists with the trust/overview keys.
123127
"""
124128
config = config_dir / self.CONFIG_FILE
125129
with update_toml_config(config) as data:
126130
servers = data.setdefault("mcp_servers", {})
127131
servers["panopticon"] = {"url": f"{service_url.rstrip('/')}/mcp"}
128-
data.setdefault("features", {})["experimental_use_rmcp_client"] = True
132+
features = data.setdefault("features", {})
133+
features["experimental_use_rmcp_client"] = True
134+
features["apps"] = False
129135
return config
130136

131137
def write_workflow_overview(self, config_dir: Path, overview: str) -> Path | None:
@@ -230,22 +236,36 @@ def launch_argv(
230236
instead of starting fresh. The config dir is a **per-task volume**, so this resumes both
231237
within a container's life and **across respawn/recreate**.
232238
233-
On a **first run** (no prior session) the ``starting_model`` tier is resolved via
234-
:meth:`resolve_model` and passed as ``--model`` (on resume codex uses the session's model),
235-
and an ``initial_prompt`` is appended as codex's first message. ``turn`` is accepted for
236-
signature parity with the claude adapter; auto-continuing a resumed session on the agent's
237-
turn (claude's interrupt prompt) is deferred with the rest of the turn wiring to M3.6, since
238-
injecting a prompt into a resumed codex session isn't yet verified.
239+
``--dangerously-bypass-hook-trust`` bypasses codex's per-hash interactive trust prompt for
240+
unrecognised hooks (our Stop/UserPromptSubmit hooks, wired in :meth:`write_settings`). On
241+
resume the bypass flags go **after** the subcommand (``codex resume --last --flags``), as
242+
codex's parser requires; on first run they are global flags before the prompt. ``--no-alt-screen``
243+
renders codex output into the tmux scrollback (not the alternate screen) so ``tmux attach``
244+
history stays useful. On a **first run** (no prior session) the ``starting_model`` tier is
245+
resolved via :meth:`resolve_model` and passed as ``--model`` (on resume codex uses the
246+
session's model), and an ``initial_prompt`` is appended as codex's first message. ``turn`` is
247+
accepted for signature parity with the claude adapter.
239248
"""
240-
argv = ["codex", "--dangerously-bypass-approvals-and-sandbox"]
241249
sessions = config_dir / self.SESSIONS_DIRNAME
242250
if sessions.exists() and any(sessions.rglob("*.jsonl")):
243-
argv += ["resume", "--last"] # resume the config dir's most recent session
244-
else:
245-
if starting_model: # first run only — on resume codex uses the session's model
246-
argv += ["--model", self.resolve_model(starting_model)]
247-
if initial_prompt:
248-
argv.append(initial_prompt) # positional: codex's first message
251+
return [
252+
"codex",
253+
"--no-alt-screen",
254+
"resume",
255+
"--last",
256+
"--dangerously-bypass-approvals-and-sandbox",
257+
"--dangerously-bypass-hook-trust",
258+
]
259+
argv = [
260+
"codex",
261+
"--dangerously-bypass-approvals-and-sandbox",
262+
"--dangerously-bypass-hook-trust",
263+
"--no-alt-screen",
264+
]
265+
if starting_model: # first run only — on resume codex uses the session's model
266+
argv += ["--model", self.resolve_model(starting_model)]
267+
if initial_prompt:
268+
argv.append(initial_prompt) # positional: codex's first message
249269
return argv
250270

251271
def launch(self, config_dir: Path) -> None: # pragma: no cover - real LLM; skipif-gated / live

tests/container/test_codex.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def test_write_mcp_config_points_codex_at_the_task_service_over_http(tmp_path: P
6464
assert data["mcp_servers"]["panopticon"] == {"url": "http://host.docker.internal:8000/mcp"}
6565
# older codex only picks up HTTP MCP with the rmcp client enabled (a no-op where it's native)
6666
assert data["features"]["experimental_use_rmcp_client"] is True
67+
# built-in apps connector cannot start in the container — disable it to avoid the 30 s stall
68+
assert data["features"]["apps"] is False
6769

6870

6971
def test_write_mcp_config_strips_a_trailing_slash(tmp_path: Path) -> None:
@@ -152,24 +154,35 @@ def test_launch_argv_starts_fresh_without_a_session(tmp_path: Path) -> None:
152154
assert CodexAgentCLI().launch_argv(tmp_path, Path("/workspace")) == [
153155
"codex",
154156
"--dangerously-bypass-approvals-and-sandbox",
157+
"--dangerously-bypass-hook-trust",
158+
"--no-alt-screen",
155159
]
156160

157161

158162
def test_launch_argv_resumes_when_a_session_transcript_exists(tmp_path: Path) -> None:
159163
sessions = tmp_path / "sessions" / "2026" / "08"
160164
sessions.mkdir(parents=True)
161165
(sessions / "rollout-abc.jsonl").write_text("{}")
166+
# bypass flags go *after* the subcommand on resume (codex's parser requires it)
162167
assert CodexAgentCLI().launch_argv(tmp_path, Path("/workspace")) == [
163168
"codex",
164-
"--dangerously-bypass-approvals-and-sandbox",
169+
"--no-alt-screen",
165170
"resume",
166171
"--last",
172+
"--dangerously-bypass-approvals-and-sandbox",
173+
"--dangerously-bypass-hook-trust",
167174
]
168175

169176

170177
def test_launch_argv_appends_initial_prompt_on_first_run(tmp_path: Path) -> None:
171178
argv = CodexAgentCLI().launch_argv(tmp_path, Path("/workspace"), initial_prompt="review plan")
172-
assert argv == ["codex", "--dangerously-bypass-approvals-and-sandbox", "review plan"]
179+
assert argv == [
180+
"codex",
181+
"--dangerously-bypass-approvals-and-sandbox",
182+
"--dangerously-bypass-hook-trust",
183+
"--no-alt-screen",
184+
"review plan",
185+
]
173186

174187

175188
def test_launch_argv_omits_initial_prompt_when_resuming(tmp_path: Path) -> None:
@@ -184,6 +197,8 @@ def test_launch_argv_passes_the_resolved_model_on_first_run(tmp_path: Path) -> N
184197
assert argv == [
185198
"codex",
186199
"--dangerously-bypass-approvals-and-sandbox",
200+
"--dangerously-bypass-hook-trust",
201+
"--no-alt-screen",
187202
"--model",
188203
"gpt-5.6-codex",
189204
]
@@ -203,6 +218,8 @@ def test_launch_argv_passes_model_before_initial_prompt_on_first_run(tmp_path: P
203218
assert argv == [
204219
"codex",
205220
"--dangerously-bypass-approvals-and-sandbox",
221+
"--dangerously-bypass-hook-trust",
222+
"--no-alt-screen",
206223
"--model",
207224
"gpt-5.6-codex",
208225
"start now",

0 commit comments

Comments
 (0)