From 1240804e31141913c8d0d01d197bf1de3938bcf7 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Wed, 27 May 2026 02:37:01 -0700 Subject: [PATCH] fix(security): pin ACP npx launchers to reviewed versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3357. The three built-in ACP providers in openhands.sdk.settings.acp_providers launch with floating npx commands (`npx -y `), which resolve npm's `latest` tag at agent-launch time — after commit review, after the uv lock's `exclude-newer` guardrail, and after the agent-server image build. Because each provider also defaults to a permission-disabling session mode (`bypassPermissions`, `full-access`, `yolo`), a poisoned or unexpectedly broken `latest` publish would be fetched and executed under a high-trust mode on the next launch. This change pins each launch command to the same versions the agent-server Dockerfile already installs at image build time (`@agentclientprotocol/claude-agent-acp@0.30.0`, `@zed-industries/codex-acp@0.11.1`, `@google/gemini-cli@0.39.1`), so build-time and runtime versions cannot diverge. The gemini-cli pin is bumped from 0.38.0 → 0.39.1 in both the SDK and the Dockerfile, matching the patched release called out in the issue. The versions are declared as module-level constants (`CLAUDE_AGENT_ACP_VERSION`, `CODEX_ACP_VERSION`, `GEMINI_CLI_VERSION`) so future bumps are one-line edits and the intent is documented inline. The OpenAPI example in `conversation_router_acp.py` is also fixed: it previously launched `npx -y claude-agent-acp` (unscoped, 404 on npm, registerable by anyone — a dependency-confusion footgun). It now matches the provider default. No public API change: the `default_command` tuple shape is unchanged, only its contents. --- .../agent_server/conversation_router_acp.py | 8 +++- .../openhands/agent_server/docker/Dockerfile | 2 +- .../openhands/sdk/settings/acp_providers.py | 42 +++++++++++++++++-- tests/sdk/test_settings.py | 2 +- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/openhands-agent-server/openhands/agent_server/conversation_router_acp.py b/openhands-agent-server/openhands/agent_server/conversation_router_acp.py index d1e48df1d6..d4795a4e09 100644 --- a/openhands-agent-server/openhands/agent_server/conversation_router_acp.py +++ b/openhands-agent-server/openhands/agent_server/conversation_router_acp.py @@ -50,7 +50,13 @@ ), ).model_dump(exclude_defaults=True, mode="json"), StartACPConversationRequest( - agent=ACPAgent(acp_command=["npx", "-y", "claude-agent-acp"]), + agent=ACPAgent( + acp_command=[ + "npx", + "-y", + "@agentclientprotocol/claude-agent-acp@0.30.0", + ] + ), workspace=LocalWorkspace(working_dir="workspace/project"), initial_message=SendMessageRequest( role="user", diff --git a/openhands-agent-server/openhands/agent_server/docker/Dockerfile b/openhands-agent-server/openhands/agent_server/docker/Dockerfile index d4b59784bb..2fdbada531 100644 --- a/openhands-agent-server/openhands/agent_server/docker/Dockerfile +++ b/openhands-agent-server/openhands/agent_server/docker/Dockerfile @@ -173,7 +173,7 @@ RUN set -ux; \ if "$ACP_NODE_DIR/bin/npm" install -g \ @agentclientprotocol/claude-agent-acp@0.30.0 \ @zed-industries/codex-acp@0.11.1 \ - @google/gemini-cli@0.38.0; then \ + @google/gemini-cli@0.39.1; then \ # Create wrappers in /usr/local/bin that prepend ACP's Node 22 to PATH. # This ensures the ACP binary's #!/usr/bin/env node shebang resolves # to Node 22, while the repo's own node (NVM/system) stays untouched diff --git a/openhands-sdk/openhands/sdk/settings/acp_providers.py b/openhands-sdk/openhands/sdk/settings/acp_providers.py index f29684d93d..39ca454a6e 100644 --- a/openhands-sdk/openhands/sdk/settings/acp_providers.py +++ b/openhands-sdk/openhands/sdk/settings/acp_providers.py @@ -194,12 +194,39 @@ class ACPProviderInfo: ) +# --------------------------------------------------------------------------- +# Pinned npm versions for the built-in ACP launchers. +# +# These pair with the corresponding versions in +# ``openhands-agent-server/openhands/agent_server/docker/Dockerfile`` so that +# the version installed at image build time and the version launched at agent +# runtime cannot diverge. Bumping these requires also bumping the Dockerfile +# pins (and vice versa). +# +# Each provider runs with a permission-disabling default session mode, so the +# floating ``npx -y `` form (which would resolve npm's ``latest`` at +# launch time, after commit and lock review) is intentionally avoided here. +# +# - ``CLAUDE_AGENT_ACP_VERSION`` — pairs with Dockerfile ``@...@0.30.0`` +# - ``CODEX_ACP_VERSION`` — pairs with Dockerfile ``@...@0.11.1`` +# - ``GEMINI_CLI_VERSION`` — bumped to a patched release (>= 0.39.1) +# --------------------------------------------------------------------------- + +CLAUDE_AGENT_ACP_VERSION = "0.30.0" +CODEX_ACP_VERSION = "0.11.1" +GEMINI_CLI_VERSION = "0.39.1" + + ACP_PROVIDERS: Mapping[str, ACPProviderInfo] = MappingProxyType( { "claude-code": ACPProviderInfo( key="claude-code", display_name="Claude Code", - default_command=("npx", "-y", "@agentclientprotocol/claude-agent-acp"), + default_command=( + "npx", + "-y", + f"@agentclientprotocol/claude-agent-acp@{CLAUDE_AGENT_ACP_VERSION}", + ), api_key_env_var="ANTHROPIC_API_KEY", base_url_env_var="ANTHROPIC_BASE_URL", default_session_mode="bypassPermissions", @@ -212,7 +239,11 @@ class ACPProviderInfo: "codex": ACPProviderInfo( key="codex", display_name="Codex", - default_command=("npx", "-y", "@zed-industries/codex-acp"), + default_command=( + "npx", + "-y", + f"@zed-industries/codex-acp@{CODEX_ACP_VERSION}", + ), api_key_env_var="OPENAI_API_KEY", base_url_env_var="OPENAI_BASE_URL", default_session_mode="full-access", @@ -225,7 +256,12 @@ class ACPProviderInfo: "gemini-cli": ACPProviderInfo( key="gemini-cli", display_name="Gemini CLI", - default_command=("npx", "-y", "@google/gemini-cli", "--acp"), + default_command=( + "npx", + "-y", + f"@google/gemini-cli@{GEMINI_CLI_VERSION}", + "--acp", + ), api_key_env_var="GEMINI_API_KEY", base_url_env_var="GEMINI_BASE_URL", default_session_mode="yolo", diff --git a/tests/sdk/test_settings.py b/tests/sdk/test_settings.py index ff27ca5a1b..2fe683a6b9 100644 --- a/tests/sdk/test_settings.py +++ b/tests/sdk/test_settings.py @@ -559,7 +559,7 @@ def test_acp_create_agent_uses_server_default_command() -> None: assert agent.acp_command == [ "npx", "-y", - "@agentclientprotocol/claude-agent-acp", + "@agentclientprotocol/claude-agent-acp@0.30.0", ] assert agent.acp_model == "claude-opus-4-6"