From a6ae6841bbfc524574d34fe78ce1d4bfda9f3b13 Mon Sep 17 00:00:00 2001 From: didericis Date: Thu, 25 Jun 2026 00:30:50 -0400 Subject: [PATCH] fix: route remote control through provider startup args --- bot_bottle/agent_provider.py | 3 +-- bot_bottle/backend/__init__.py | 3 +-- bot_bottle/cli/resume.py | 2 -- bot_bottle/cli/start.py | 14 ++++--------- bot_bottle/contrib/claude/agent_provider.py | 1 - bot_bottle/contrib/codex/Dockerfile | 15 ++++++++------ bot_bottle/contrib/codex/agent_provider.py | 1 - bot_bottle/contrib/pi/agent_provider.py | 1 - tests/unit/test_cli_start_settle.py | 21 ++++++++++++++++++++ tests/unit/test_contrib_codex_provider.py | 9 +++++++++ tests/unit/test_docker_bottle.py | 10 ++++++++++ tests/unit/test_docker_provision_git_user.py | 1 - tests/unit/test_plan_print_parity.py | 2 +- tests/unit/test_smolmachines_provision.py | 1 - 14 files changed, 56 insertions(+), 28 deletions(-) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 9179276..d519033 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -61,7 +61,6 @@ class AgentProviderRuntime: prompt_mode: PromptMode bypass_args: tuple[str, ...] resume_args: tuple[str, ...] - remote_control_args: tuple[str, ...] @dataclass(frozen=True) @@ -391,7 +390,7 @@ def prompt_args( if prompt_mode == "append_file": return ["--append-system-prompt-file", prompt_path] if prompt_mode == "read_prompt_file": - if argv and "resume" in argv: + if argv and ("resume" in argv or "remote-control" in argv): return [] return [f"Read and follow the instructions in {prompt_path}."] if prompt_mode == "print_read_prompt_file": diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index 06b279e..f47345d 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -109,9 +109,8 @@ class BottlePlan(ABC): def workspace_plan(self) -> WorkspacePlan: return workspace_plan(self.spec, guest_home=self.guest_home) - def print(self, *, remote_control: bool) -> None: + def print(self) -> None: """Render the y/N preflight summary to stderr.""" - del remote_control spec = self.spec manifest = self.manifest agent = manifest.agent diff --git a/bot_bottle/cli/resume.py b/bot_bottle/cli/resume.py index 557c293..88b082f 100644 --- a/bot_bottle/cli/resume.py +++ b/bot_bottle/cli/resume.py @@ -28,7 +28,6 @@ from .start import _launch_bottle def cmd_resume(argv: list[str]) -> int: parser = argparse.ArgumentParser(prog=f"{PROG} resume", add_help=True) parser.add_argument("--dry-run", action="store_true") - parser.add_argument("--remote-control", action="store_true") parser.add_argument( "identity", help="bottle identity from a prior `start` (see its session-end output)", @@ -56,6 +55,5 @@ def cmd_resume(argv: list[str]) -> int: return _launch_bottle( spec, dry_run=args.dry_run, - remote_control=args.remote_control, backend_name=backend_name, ) diff --git a/bot_bottle/cli/start.py b/bot_bottle/cli/start.py index c752cfc..d773e9f 100644 --- a/bot_bottle/cli/start.py +++ b/bot_bottle/cli/start.py @@ -42,7 +42,6 @@ def cmd_start(argv: list[str]) -> int: parser = argparse.ArgumentParser(prog=f"{PROG} start", add_help=True) parser.add_argument("--dry-run", action="store_true") parser.add_argument("--cwd", action="store_true", help="copy host cwd into the running bottle") - parser.add_argument("--remote-control", action="store_true") parser.add_argument( "--backend", choices=known_backend_names(), @@ -89,7 +88,6 @@ def cmd_start(argv: list[str]) -> int: return _launch_bottle( spec, dry_run=dry_run, - remote_control=args.remote_control, backend_name=backend_name, ) @@ -134,7 +132,7 @@ def prepare_with_preflight( def attach_agent( - bottle: Bottle, *, remote_control: bool = False, resume: bool = False, + bottle: Bottle, *, resume: bool = False, agent_provider_template: str = "claude", startup_args: tuple[str, ...] = (), ) -> int: @@ -153,8 +151,6 @@ def attach_agent( "(Ctrl-D or 'exit' to leave; container will be removed)" ) agent_args = list(runtime.bypass_args) - if remote_control: - agent_args.extend(runtime.remote_control_args) agent_args.extend(startup_args) if resume: agent_args.extend(runtime.resume_args) @@ -218,9 +214,9 @@ def _text_prompt_yes() -> bool: return reply in ("y", "Y", "yes", "YES") -def _text_render_preflight(*, remote_control: bool): +def _text_render_preflight(): def _render(plan: DockerBottlePlan) -> None: - plan.print(remote_control=remote_control) + plan.print() return _render @@ -228,7 +224,6 @@ def _launch_bottle( spec: BottleSpec, *, dry_run: bool, - remote_control: bool, backend_name: str | None = None, ) -> int: """Shared launch core for `start` and `resume`. Builds the plan, @@ -240,7 +235,7 @@ def _launch_bottle( plan, identity = prepare_with_preflight( spec, stage_dir=stage_dir, - render_preflight=_text_render_preflight(remote_control=remote_control), + render_preflight=_text_render_preflight(), prompt_yes=_text_prompt_yes, dry_run=dry_run, backend_name=backend_name, @@ -253,7 +248,6 @@ def _launch_bottle( agent_provider_template = getattr(plan, "agent_provider_template", "claude") exit_code = attach_agent( bottle, - remote_control=remote_control, agent_provider_template=agent_provider_template, startup_args=plan.agent_provision.startup_args, ) diff --git a/bot_bottle/contrib/claude/agent_provider.py b/bot_bottle/contrib/claude/agent_provider.py index 14e010d..1eee6ee 100644 --- a/bot_bottle/contrib/claude/agent_provider.py +++ b/bot_bottle/contrib/claude/agent_provider.py @@ -91,7 +91,6 @@ _RUNTIME = AgentProviderRuntime( prompt_mode="append_file", bypass_args=("--dangerously-skip-permissions",), resume_args=("--continue",), - remote_control_args=("--remote-control",), ) diff --git a/bot_bottle/contrib/codex/Dockerfile b/bot_bottle/contrib/codex/Dockerfile index a2a246f..cabaf05 100644 --- a/bot_bottle/contrib/codex/Dockerfile +++ b/bot_bottle/contrib/codex/Dockerfile @@ -1,12 +1,12 @@ # bot-bottle Codex provider image. # # Mirrors the default Claude image shape: Node LTS, git/network tooling, -# non-root node user, and the provider CLI installed globally. +# non-root node user, and the provider CLI installed for that user. FROM node:22-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends git ca-certificates curl \ + && apt-get install -y --no-install-recommends git ca-certificates curl procps \ && rm -rf /var/lib/apt/lists/* # App-specific deps. Python isn't required by codex itself @@ -17,12 +17,15 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* -RUN npm install -g --no-fund --no-audit @openai/codex@0.136.0 \ - && npm cache clean --force - USER node WORKDIR /home/node -RUN mkdir -p /home/node/.codex +ENV PATH="/home/node/.local/bin:${PATH}" + +# Remote-control support requires the standalone Codex install layout +# under ~/.codex/packages/standalone/current. The npm package can run +# the TUI, but remote-control commands expect this installer-owned path. +RUN mkdir -p /home/node/.codex \ + && curl -fsSL https://chatgpt.com/codex/install.sh | sh CMD ["codex"] diff --git a/bot_bottle/contrib/codex/agent_provider.py b/bot_bottle/contrib/codex/agent_provider.py index cca0e47..ac13b46 100644 --- a/bot_bottle/contrib/codex/agent_provider.py +++ b/bot_bottle/contrib/codex/agent_provider.py @@ -55,7 +55,6 @@ _RUNTIME = AgentProviderRuntime( prompt_mode="read_prompt_file", bypass_args=("--dangerously-bypass-approvals-and-sandbox",), resume_args=("resume", "--last"), - remote_control_args=(), ) diff --git a/bot_bottle/contrib/pi/agent_provider.py b/bot_bottle/contrib/pi/agent_provider.py index c3d47a5..2c9bf38 100644 --- a/bot_bottle/contrib/pi/agent_provider.py +++ b/bot_bottle/contrib/pi/agent_provider.py @@ -166,7 +166,6 @@ _RUNTIME = AgentProviderRuntime( prompt_mode="append_system_prompt", bypass_args=(), resume_args=(), - remote_control_args=(), ) diff --git a/tests/unit/test_cli_start_settle.py b/tests/unit/test_cli_start_settle.py index 826e8b0..860d13f 100644 --- a/tests/unit/test_cli_start_settle.py +++ b/tests/unit/test_cli_start_settle.py @@ -102,6 +102,27 @@ class TestAttachAgent(unittest.TestCase): bottle.argv, ) + def test_remote_control_is_provider_startup_arg(self): + class Bottle: + argv: list[str] = [] + + def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: + self.argv = list(argv) + return 0 + + bottle = Bottle() + exit_code = start_mod.attach_agent( + bottle, # type: ignore[arg-type] + agent_provider_template="codex", + startup_args=("remote-control",), + ) + + self.assertEqual(0, exit_code) + self.assertEqual( + ["--dangerously-bypass-approvals-and-sandbox", "remote-control"], + bottle.argv, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_contrib_codex_provider.py b/tests/unit/test_contrib_codex_provider.py index 54e2b2c..1788e8c 100644 --- a/tests/unit/test_contrib_codex_provider.py +++ b/tests/unit/test_contrib_codex_provider.py @@ -29,6 +29,9 @@ from bot_bottle.supervise import SupervisePlan _URL = "http://supervise:9100/" +_CODEX_DOCKERFILE = ( + Path(__file__).resolve().parents[2] / "bot_bottle/contrib/codex/Dockerfile" +) def _make_bottle(exec_result: ExecResult | None = None) -> MagicMock: @@ -276,6 +279,12 @@ class TestCodexProvision(unittest.TestCase): ) +class TestCodexDockerfile(unittest.TestCase): + def test_installs_procps_for_remote_control_pid_management(self): + dockerfile = _CODEX_DOCKERFILE.read_text() + self.assertIn("procps", dockerfile) + + class TestCodexSuperviseMcp(unittest.TestCase): def test_noop_when_supervise_disabled(self): bottle = _make_bottle() diff --git a/tests/unit/test_docker_bottle.py b/tests/unit/test_docker_bottle.py index 289421a..0f16000 100644 --- a/tests/unit/test_docker_bottle.py +++ b/tests/unit/test_docker_bottle.py @@ -136,6 +136,16 @@ class TestClaudeArgv(unittest.TestCase): argv, ) + def test_codex_remote_control_startup_arg_does_not_receive_initial_prompt(self): + argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").agent_argv( + ["--dangerously-bypass-approvals-and-sandbox", "remote-control"], + ) + self.assertEqual( + ["docker", "exec", "-it", "bot-bottle-dev-abc", "codex", + "--dangerously-bypass-approvals-and-sandbox", "remote-control"], + argv, + ) + def test_codex_resume_does_not_append_initial_prompt(self): argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").agent_argv( ["--dangerously-bypass-approvals-and-sandbox", "resume", "--last"], diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index 2fb7466..ac858de 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -31,7 +31,6 @@ class _Provider(AgentProvider): return AgentProviderRuntime( template="test", command="test", image="", prompt_mode="append_file", bypass_args=(), resume_args=(), - remote_control_args=(), ) def provision_plan(self, **kwargs): # type: ignore[override] raise NotImplementedError diff --git a/tests/unit/test_plan_print_parity.py b/tests/unit/test_plan_print_parity.py index f8b5648..fbe7025 100644 --- a/tests/unit/test_plan_print_parity.py +++ b/tests/unit/test_plan_print_parity.py @@ -130,7 +130,7 @@ def _capture_print(plan: DockerBottlePlan | SmolmachinesBottlePlan) -> list[str] orig = sys.stderr sys.stderr = buf try: - plan.print(remote_control=False) + plan.print() finally: sys.stderr = orig return buf.getvalue().splitlines() diff --git a/tests/unit/test_smolmachines_provision.py b/tests/unit/test_smolmachines_provision.py index e3fbbfc..b790cf5 100644 --- a/tests/unit/test_smolmachines_provision.py +++ b/tests/unit/test_smolmachines_provision.py @@ -44,7 +44,6 @@ class _Provider(AgentProvider): return AgentProviderRuntime( template="test", command="test", image="", prompt_mode="append_file", bypass_args=(), resume_args=(), - remote_control_args=(), ) def provision_plan(self, **kwargs): # type: ignore[override] raise NotImplementedError