Skip to content

Commit dfef936

Browse files
fix: add ACP authenticate step for codex-acp
codex-acp requires an explicit `conn.authenticate()` call after initialize and before session creation. Auto-detect the auth method from environment variables (CODEX_API_KEY or OPENAI_API_KEY). Without this, codex-acp returns "Authentication required" when trying to create a session in the remote runtime container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6337423 commit dfef936

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

openhands-sdk/openhands/sdk/agent/acp_agent.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,30 @@ def _make_dummy_llm() -> LLM:
112112
}
113113
_DEFAULT_BYPASS_MODE = "full-access"
114114

115+
# ACP auth method ID → environment variable that supplies the credential.
116+
# When the server reports auth_methods, we pick the first method whose
117+
# required env var is set.
118+
_AUTH_METHOD_ENV_MAP: dict[str, str] = {
119+
"codex-api-key": "CODEX_API_KEY",
120+
"openai-api-key": "OPENAI_API_KEY",
121+
}
122+
123+
124+
def _select_auth_method(
125+
auth_methods: list[Any],
126+
env: dict[str, str],
127+
) -> str | None:
128+
"""Pick an auth method whose required env var is present.
129+
130+
Returns the ``id`` of the first matching method, or ``None`` if no
131+
env-var-based method is available (the server may not require auth).
132+
"""
133+
method_ids = {m.id for m in auth_methods}
134+
for method_id, env_var in _AUTH_METHOD_ENV_MAP.items():
135+
if method_id in method_ids and env_var in env:
136+
return method_id
137+
return None
138+
115139

116140
def _resolve_bypass_mode(agent_name: str) -> str:
117141
"""Return the session mode ID that bypasses all permission prompts.
@@ -540,6 +564,25 @@ async def _init() -> tuple[Any, Any, Any, str, str]:
540564
agent_name = init_response.agent_info.name or ""
541565
logger.info("ACP server initialized: agent_name=%r", agent_name)
542566

567+
# Authenticate if the server requires it. Some ACP servers
568+
# (e.g. codex-acp) require an explicit authenticate call
569+
# before session creation. We auto-detect the method from
570+
# the env vars that are available to the process.
571+
auth_methods = init_response.auth_methods or []
572+
if auth_methods:
573+
method_id = _select_auth_method(auth_methods, env)
574+
if method_id is not None:
575+
logger.info(
576+
"Authenticating with ACP method: %s", method_id
577+
)
578+
await conn.authenticate(method_id=method_id)
579+
else:
580+
logger.warning(
581+
"ACP server offers auth methods %s but no matching "
582+
"env var is set — session creation may fail",
583+
[m.id for m in auth_methods],
584+
)
585+
543586
# Create a new session
544587
response = await conn.new_session(cwd=working_dir)
545588
session_id = response.session_id

tests/sdk/agent/test_acp_agent.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ACPAgent,
1414
_OpenHandsACPBridge,
1515
_resolve_bypass_mode,
16+
_select_auth_method,
1617
)
1718
from openhands.sdk.agent.base import AgentBase
1819
from openhands.sdk.conversation.state import (
@@ -1301,6 +1302,61 @@ def test_empty_name_defaults_to_full_access(self):
13011302
# ---------------------------------------------------------------------------
13021303

13031304

1305+
# ---------------------------------------------------------------------------
1306+
# _select_auth_method
1307+
# ---------------------------------------------------------------------------
1308+
1309+
1310+
class TestSelectAuthMethod:
1311+
"""Test auto-detection of ACP auth method from env vars."""
1312+
1313+
@staticmethod
1314+
def _make_auth_method(method_id: str) -> MagicMock:
1315+
m = MagicMock()
1316+
m.id = method_id
1317+
return m
1318+
1319+
def test_openai_api_key(self):
1320+
methods = [
1321+
self._make_auth_method("chatgpt"),
1322+
self._make_auth_method("codex-api-key"),
1323+
self._make_auth_method("openai-api-key"),
1324+
]
1325+
env = {"OPENAI_API_KEY": "sk-test"}
1326+
assert _select_auth_method(methods, env) == "openai-api-key"
1327+
1328+
def test_codex_api_key_preferred(self):
1329+
"""CODEX_API_KEY is checked first (appears first in the map)."""
1330+
methods = [
1331+
self._make_auth_method("codex-api-key"),
1332+
self._make_auth_method("openai-api-key"),
1333+
]
1334+
env = {"CODEX_API_KEY": "key1", "OPENAI_API_KEY": "key2"}
1335+
assert _select_auth_method(methods, env) == "codex-api-key"
1336+
1337+
def test_no_matching_env_var(self):
1338+
methods = [
1339+
self._make_auth_method("chatgpt"),
1340+
self._make_auth_method("openai-api-key"),
1341+
]
1342+
env = {"UNRELATED": "value"}
1343+
assert _select_auth_method(methods, env) is None
1344+
1345+
def test_empty_auth_methods(self):
1346+
assert _select_auth_method([], {}) is None
1347+
1348+
def test_method_not_in_server_list(self):
1349+
"""Even if env var is set, method must be offered by server."""
1350+
methods = [self._make_auth_method("chatgpt")]
1351+
env = {"OPENAI_API_KEY": "sk-test"}
1352+
assert _select_auth_method(methods, env) is None
1353+
1354+
1355+
# ---------------------------------------------------------------------------
1356+
# acp_session_mode field
1357+
# ---------------------------------------------------------------------------
1358+
1359+
13041360
class TestACPSessionMode:
13051361
def test_default_is_none(self):
13061362
agent = _make_agent()

0 commit comments

Comments
 (0)