Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions src/panopticon/container/cli/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,19 @@ def write_mcp_config(self, config_dir: Path, service_url: str) -> Path:
plane claude connects to, at ``<service_url>/mcp`` — no auth token (the server is the
container's own task service). Older codex builds only pick up HTTP MCP with
``experimental_use_rmcp_client`` set, so we enable it defensively (a no-op on builds with
native support). Merged into ``config.toml`` so it coexists with the trust/overview keys.
native support). ``features.apps = false`` disables codex's built-in apps connector, which
cannot start in the container and otherwise 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). Merged into ``config.toml``
so it coexists with the trust/overview keys.
"""
config = config_dir / self.CONFIG_FILE
with update_toml_config(config) as data:
servers = data.setdefault("mcp_servers", {})
servers["panopticon"] = {"url": f"{service_url.rstrip('/')}/mcp"}
data.setdefault("features", {})["experimental_use_rmcp_client"] = True
features = data.setdefault("features", {})
features["experimental_use_rmcp_client"] = True
features["apps"] = False
return config

def write_workflow_overview(self, config_dir: Path, overview: str) -> Path | None:
Expand Down Expand Up @@ -334,14 +340,24 @@ def launch_argv(
config dir is a **per-task volume**, so this resumes both within a container's life and
**across respawn/recreate**.

On **resume** with ``turn == "agent"`` (the agent was interrupted mid-turn), the interrupt
prompt ``"You were interrupted. Continue."`` is appended as codex's first positional message
so the agent picks up where it left off. On a **first run** (no resumable session) the
``starting_model`` tier is resolved via :meth:`resolve_model` and passed as ``--model`` (on
resume codex uses the session's model), and ``initial_prompt`` is appended as the first
message.
``--dangerously-bypass-hook-trust`` bypasses codex's per-hash interactive trust prompt for
unrecognised hooks (our Stop/UserPromptSubmit hooks, wired in :meth:`write_settings`). With
``session_id`` now a positional argument to ``resume``, all bypass flags are placed at the
global level (before the subcommand) so they parse correctly in both first-run and resume
paths. ``--no-alt-screen`` renders codex output into the tmux scrollback (not the alternate
screen) so ``tmux attach`` history stays useful. On **resume** with ``turn == "agent"`` (the
agent was interrupted mid-turn), the interrupt prompt ``"You were interrupted. Continue."``
is appended as codex's first positional message so the agent picks up where it left off. On a
**first run** (no resumable session) the ``starting_model`` tier is resolved via
:meth:`resolve_model` and passed as ``--model`` (on resume codex uses the session's model),
and ``initial_prompt`` is appended as the first message.
"""
argv = ["codex", "--dangerously-bypass-approvals-and-sandbox"]
argv = [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
]
sessions_dir = config_dir / self.SESSIONS_DIRNAME
session_id = _find_resume_target(sessions_dir) if sessions_dir.exists() else None
if session_id:
Expand Down
24 changes: 23 additions & 1 deletion tests/container/test_codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ def test_write_mcp_config_points_codex_at_the_task_service_over_http(tmp_path: P
assert data["mcp_servers"]["panopticon"] == {"url": "http://host.docker.internal:8000/mcp"}
# older codex only picks up HTTP MCP with the rmcp client enabled (a no-op where it's native)
assert data["features"]["experimental_use_rmcp_client"] is True
# built-in apps connector cannot start in the container — disable it to avoid the 30 s stall
assert data["features"]["apps"] is False


def test_write_mcp_config_strips_a_trailing_slash(tmp_path: Path) -> None:
Expand Down Expand Up @@ -239,6 +241,8 @@ def test_launch_argv_starts_fresh_without_a_session(tmp_path: Path) -> None:
assert CodexAgentCLI().launch_argv(tmp_path, Path("/workspace")) == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
]


Expand All @@ -250,14 +254,22 @@ def test_launch_argv_resumes_when_an_interactive_session_exists(tmp_path: Path)
assert CodexAgentCLI().launch_argv(tmp_path, Path("/workspace")) == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
"resume",
"sess-abc",
]


def test_launch_argv_appends_initial_prompt_on_first_run(tmp_path: Path) -> None:
argv = CodexAgentCLI().launch_argv(tmp_path, Path("/workspace"), initial_prompt="review plan")
assert argv == ["codex", "--dangerously-bypass-approvals-and-sandbox", "review plan"]
assert argv == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
"review plan",
]


def test_launch_argv_omits_initial_prompt_when_resuming(tmp_path: Path) -> None:
Expand All @@ -273,6 +285,8 @@ def test_launch_argv_passes_the_resolved_model_on_first_run(tmp_path: Path) -> N
assert argv == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
"--model",
"gpt-5.6-sol",
]
Expand All @@ -293,6 +307,8 @@ def test_launch_argv_passes_model_before_initial_prompt_on_first_run(tmp_path: P
assert argv == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
"--model",
"gpt-5.6-sol",
"start now",
Expand Down Expand Up @@ -409,6 +425,8 @@ def test_launch_argv_resumes_with_interrupt_prompt_when_agent_turn(tmp_path: Pat
assert argv == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
"resume",
"s1",
"You were interrupted. Continue.",
Expand All @@ -423,6 +441,8 @@ def test_launch_argv_resumes_without_interrupt_prompt_when_user_turn(tmp_path: P
assert argv == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
"resume",
"s1",
]
Expand All @@ -438,6 +458,8 @@ def test_launch_argv_falls_back_to_first_run_when_only_exec_sessions(tmp_path: P
assert argv == [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"--no-alt-screen",
"--model",
"gpt-5.6-sol",
"hi",
Expand Down
Loading