Skip to content

Commit b324ac1

Browse files
Simon Rosenbergclaude
andcommitted
chore: split ACP dry-run credential channels by role (#3717)
Address PR review: the dry-run lumped api_key_env_var, base_url_env_var, and file_secrets[].secret_name into one "required" list, but they are not jointly required — the API key and file-content creds are alternative auth paths (one suffices) and the base URL is optional proxy routing. Replace required_acp_secret_names with acp_api_key_secret_name / acp_base_url_secret_name / acp_file_secret_names so the editor/materialize can report set/missing honestly instead of flagging a working api-key-only setup as incomplete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 110749a commit b324ac1

2 files changed

Lines changed: 35 additions & 26 deletions

File tree

openhands-sdk/openhands/sdk/profiles/resolver.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,14 @@ class AgentProfileDiagnostics(BaseModel):
9494
resolved_mcp_servers: list[str] = Field(default_factory=list)
9595
dangling_mcp_server_refs: list[str] = Field(default_factory=list)
9696

97-
# ACP provider credential names the editor/materialize checks (ACP only).
98-
required_acp_secret_names: list[str] = Field(default_factory=list)
97+
# ACP provider credential channels the editor/materialize checks (ACP only).
98+
# These are NOT jointly required: authentication needs the API key *or* one
99+
# of the file-content credentials, and the base URL is optional proxy
100+
# routing. Keeping them in separate fields lets the editor mark set/missing
101+
# honestly instead of treating a working api-key-only setup as incomplete.
102+
acp_api_key_secret_name: str | None = None
103+
acp_base_url_secret_name: str | None = None
104+
acp_file_secret_names: list[str] = Field(default_factory=list)
99105

100106
# Redacted resolved settings, present iff ``valid``.
101107
resolved_settings: dict[str, Any] | None = None
@@ -177,22 +183,22 @@ def _api_key_set(llm: LLM) -> bool:
177183
return bool(value.strip()) and value != REDACTED_SECRET_VALUE
178184

179185

180-
def _required_acp_secret_names(acp_server: str) -> list[str]:
181-
"""Provider secret names derived from ``acp_server`` via ``ACP_PROVIDERS``.
186+
def _acp_credential_channels(
187+
acp_server: str,
188+
) -> tuple[str | None, str | None, list[str]]:
189+
"""Provider credential channels for ``acp_server`` via ``ACP_PROVIDERS``.
182190
183-
The env-var key for the API credential and (optional) base URL, plus any
184-
file-content credential secret names. Empty for ``'custom'`` servers.
191+
Returns ``(api_key_env_var, base_url_env_var, file_secret_names)`` kept
192+
separate by role: the API-key env var and the file-content credentials are
193+
*alternative* auth mechanisms (one suffices), and the base URL is optional
194+
proxy routing — not jointly required. All empty/``None`` for ``'custom'``
195+
servers, whose creds the user manages directly.
185196
"""
186197
info = get_acp_provider(acp_server)
187198
if info is None:
188-
return []
189-
names: list[str] = []
190-
if info.api_key_env_var:
191-
names.append(info.api_key_env_var)
192-
if info.base_url_env_var:
193-
names.append(info.base_url_env_var)
194-
names.extend(spec.secret_name for spec in info.file_secrets)
195-
return names
199+
return None, None, []
200+
file_names = [spec.secret_name for spec in info.file_secrets]
201+
return info.api_key_env_var, info.base_url_env_var, file_names
196202

197203

198204
def _build_openhands_settings(
@@ -318,9 +324,11 @@ def resolve_agent_profile_dry_run(
318324
f"LLM profile {profile.llm_profile_ref!r} not found"
319325
)
320326
else:
321-
diagnostics.required_acp_secret_names = _required_acp_secret_names(
322-
profile.acp_server
323-
)
327+
(
328+
diagnostics.acp_api_key_secret_name,
329+
diagnostics.acp_base_url_secret_name,
330+
diagnostics.acp_file_secret_names,
331+
) = _acp_credential_channels(profile.acp_server)
324332

325333
diagnostics.valid = not diagnostics.errors
326334
if diagnostics.valid:

tests/sdk/profiles/test_resolver.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ def test_dry_run_verdict_matches_real_resolve(
362362
)
363363

364364

365-
def test_dry_run_acp_reports_required_secret_names(
365+
def test_dry_run_acp_reports_credential_channels_by_role(
366366
llm_store: LLMProfileStore,
367367
) -> None:
368368
profile = ACPAgentProfile(name="acp", acp_server="codex")
@@ -371,16 +371,15 @@ def test_dry_run_acp_reports_required_secret_names(
371371
)
372372
assert diag.agent_kind == "acp"
373373
assert diag.valid is True
374-
# api_key + base_url env vars and the file-content secret name, from the registry.
375-
assert diag.required_acp_secret_names == [
376-
"OPENAI_API_KEY",
377-
"OPENAI_BASE_URL",
378-
"CODEX_AUTH_JSON",
379-
]
374+
# The API key and the file-content credential are alternative auth paths;
375+
# the base URL is optional proxy routing — each surfaced in its own field.
376+
assert diag.acp_api_key_secret_name == "OPENAI_API_KEY"
377+
assert diag.acp_base_url_secret_name == "OPENAI_BASE_URL"
378+
assert diag.acp_file_secret_names == ["CODEX_AUTH_JSON"]
380379
assert diag.resolved_settings is not None
381380

382381

383-
def test_dry_run_acp_custom_server_has_no_required_secrets(
382+
def test_dry_run_acp_custom_server_has_no_credential_channels(
384383
llm_store: LLMProfileStore,
385384
) -> None:
386385
profile = ACPAgentProfile(
@@ -389,7 +388,9 @@ def test_dry_run_acp_custom_server_has_no_required_secrets(
389388
diag = resolve_agent_profile_dry_run(
390389
profile, llm_store=llm_store, mcp_config=None, cipher=None
391390
)
392-
assert diag.required_acp_secret_names == []
391+
assert diag.acp_api_key_secret_name is None
392+
assert diag.acp_base_url_secret_name is None
393+
assert diag.acp_file_secret_names == []
393394

394395

395396
def test_dry_run_normalizes_settings_build_failure(

0 commit comments

Comments
 (0)