feat(acp): materialise reserved file-content secrets onto disk#3269
feat(acp): materialise reserved file-content secrets onto disk#3269simonrosenberg wants to merge 1 commit into
Conversation
Some ACP servers authenticate via a JSON credential file rather than an env var (Codex's ChatGPT subscription via ``$CODEX_HOME/auth.json``, Gemini's Vertex AI service-account / ADC via ``GOOGLE_APPLICATION_CREDENTIALS``). Previously the only path for these deployments was to pass the credentials in the host filesystem out of band, which doesn't work when the SDK runs in a container without access to the user's home directory. This commit adds a small registry-driven mechanism in ``ACPAgent`` for reserved secret names that should be **materialised as files** before the subprocess spawns, rather than exported as env vars: - ``CODEX_AUTH_JSON`` → written to ``<tempdir>/codex_auth_json/auth.json``; ``CODEX_HOME`` is set to that directory. Codex writes back to the file on token refresh, so a real writable file (not an env var) is required. - ``GOOGLE_APPLICATION_CREDENTIALS_JSON`` → written to ``<tempdir>/google_application_credentials_json/gcloud-credentials.json``; ``GOOGLE_APPLICATION_CREDENTIALS`` is set to that file path. This covers Vertex AI service-account keys and ADC. Personal-account "Sign in with Google" subscriptions are intentionally not covered: gemini-cli encrypts those credentials with a key derived from ``hostname + username``, making the cached file machine-bound. Mechanism: 1. ``_FILE_SECRETS`` is a module-level registry mapping a reserved secret name to a ``_FileSecretSpec(filename, env_var, env_points_to)``. ``env_points_to`` is ``"dir"`` when the binary expects an env var naming a directory (Codex) and ``"file"`` when it expects an absolute file path (Google). 2. ``_materialise_file_secrets`` walks ``agent_context.secrets``, writes each matching payload into a per-agent ``TemporaryDirectory`` with 0o600 perms, sets the controlling env var, and returns the subset of secrets that still belong on the env-var injection path. 3. The existing env-var loop in ``_start_acp_server`` now iterates the filtered remainder, so a file-secret's (potentially multi-KB) JSON payload is **not** also leaked into ``os.environ``. 4. ``_cleanup`` removes the per-agent tempdir after the subprocess and executor are torn down, so we don't yank credential files out from under a still-running child. Adding a new file-based auth mechanism for a future ACP server is one ``_FILE_SECRETS`` entry. Consumers (e.g. OpenHands) get subscription auth by writing a plain secret with the reserved name — no provider- specific backend code on the host side. Tested with seven new cases in ``TestACPFileSecretMaterialisation`` covering both reserved names, file permissions, coexistence of file- and plain-secrets, empty-payload skip, shared-tempdir layout, and cleanup on ``close()``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟡 Acceptable - Solid implementation solving real problems, but one test needs fixing.
Overview: This PR elegantly solves the file-based authentication problem for Codex and Gemini with a simple registry pattern. The data structure is clean, the implementation is straightforward, and the security considerations are mostly sound. One test has an environment collision issue that needs fixing.
| # File-secret was materialised | ||
| assert env["CODEX_HOME"] | ||
| # Plain secret was injected as env var | ||
| assert env.get("GITHUB_TOKEN") == "ghp_xyz" |
There was a problem hiding this comment.
🔴 Critical - Test Failure: This test fails because GITHUB_TOKEN from os.environ takes precedence over the agent's secret (line 1131 in acp_agent.py: if name not in env).
The test environment already has GITHUB_TOKEN set, so the agent's ghp_xyz value is never injected. Use a non-colliding secret name instead:
| assert env.get("GITHUB_TOKEN") == "ghp_xyz" | |
| secrets={ | |
| "CODEX_AUTH_JSON": StaticSecret(value=SecretStr("{}")), | |
| "MY_API_TOKEN": StaticSecret(value=SecretStr("test_token_xyz")), | |
| } |
Then update the assertion:
assert env.get("MY_API_TOKEN") == "test_token_xyz"| target_path.write_text(value) | ||
| target_path.chmod(0o600) |
There was a problem hiding this comment.
🟡 Suggestion - Security: There's a brief window between write_text() and chmod(0o600) where the file might have default permissions.
This is mitigated by the parent directory already being 0o700 (line 1093), which prevents other users from accessing the file even during this window. However, for defense-in-depth, consider using os.open() with atomic mode setting:
fd = os.open(target_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, 'w') as f:
f.write(value)Not blocking since directory permissions provide adequate protection.
Risk Assessment[RISK ASSESSMENT]
This PR adds optional file-based secret materialization for specific ACP providers. The risk is low because:
[IMPROVEMENT OPPORTUNITIES]
VERDICT: KEY INSIGHT:
|
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
File-secret materialization works correctly for both Codex and Gemini CLI authentication, with proper security isolation and cleanup.
Does this PR achieve its stated goal?
Yes. This PR set out to add a mechanism for ACP servers that authenticate via JSON credential files on disk (specifically Codex ChatGPT subscription and Gemini Vertex AI service accounts). I verified that:
- CODEX_AUTH_JSON is correctly materialized to
auth.jsonwithCODEX_HOMEpointing to the containing directory - GOOGLE_APPLICATION_CREDENTIALS_JSON is correctly materialized to
gcloud-credentials.jsonwithGOOGLE_APPLICATION_CREDENTIALSpointing to the file path - Files are written with secure
0o600permissions (owner read/write only) - File-secrets are excluded from env-var injection (preventing multi-KB JSON payloads in the environment)
- Plain secrets continue to work normally alongside file-secrets
- Cleanup correctly removes the tempdir after agent close
The implementation delivers exactly what the PR description promises: a registry-driven mechanism that lets consumers (OpenHands or SDK embedders) provide file-based credentials without writing provider-specific file-injection code.
| Phase | Result |
|---|---|
| Environment Setup | ✅ Dependencies installed, project built successfully |
| CI Status | ✅ All critical checks passing (sdk-tests, tools-tests, pre-commit, API checks) |
| Functional Verification | ✅ All functionality verified end-to-end |
Functional Verification
Test 1: CODEX_AUTH_JSON Materialization
Setup: Created ACPAgent with CODEX_AUTH_JSON secret containing test JSON payload.
Execution:
from pydantic import SecretStr
from openhands.sdk.secret import StaticSecret
CODEX_PAYLOAD = '{"auth_mode":"chatgpt","tokens":{"id_token":"test_token_123"}}'
agent = ACPAgent(
llm=LLM(model="openai/gpt-4o", api_key="dummy"),
acp_command=["echo", "test"],
agent_context=AgentContext(
secrets={"CODEX_AUTH_JSON": StaticSecret(value=SecretStr(CODEX_PAYLOAD))}
),
)
env = {}
remaining = agent._materialise_file_secrets(env)Result:
- ✅
CODEX_HOMEenv var set:/tmp/acp-file-secrets-_1wzte74/codex_auth_json - ✅
auth.jsonfile created at$CODEX_HOME/auth.json - ✅ File content matches payload exactly
- ✅ File permissions:
0o600(verified withstat.S_IMODE()) - ✅
CODEX_AUTH_JSONremoved from regular secrets dict - ✅ Tempdir cleaned up after
agent.close()
Test 2: GOOGLE_APPLICATION_CREDENTIALS_JSON Materialization
Setup: Created ACPAgent with GOOGLE_APPLICATION_CREDENTIALS_JSON secret.
Result:
- ✅
GOOGLE_APPLICATION_CREDENTIALSenv var set to file path (not directory) - ✅
gcloud-credentials.jsoncreated at the specified path - ✅ File content matches payload
- ✅ File permissions:
0o600 - ✅ Secret removed from regular env-var injection
Test 3: Integration with Subprocess Environment
Setup: Created a shell script that prints environment variables and reads the materialized files. Simulated the full _start_acp_server() flow.
Result:
CODEX_HOME=/tmp/acp-file-secrets-ai1he84j/codex_auth_json
GOOGLE_APPLICATION_CREDENTIALS=/tmp/.../gcloud-credentials.json
CODEX_AUTH_FILE_EXISTS=yes
CODEX_AUTH_CONTENT={"test":"codex_auth"}
GAC_FILE_EXISTS=yes
GAC_CONTENT={"test":"gac_auth"}- ✅ Subprocess received correct environment variables
- ✅ Subprocess could read materialized files
- ✅ File-secret payloads NOT present as raw env vars
- ✅ Plain secrets (
GITHUB_TOKEN) still injected normally
Test 4: Security Isolation
Setup: Created agent with both file-secrets to verify isolation.
Result:
- ✅ Each file-secret in separate subdirectory:
codex_auth_json/auth.jsongoogle_application_credentials_json/gcloud-credentials.json
- ✅
CODEX_HOMEdirectory contains ONLYauth.json(no GAC file) - ✅ GAC directory contains ONLY
gcloud-credentials.json(no Codex file) - ✅ File permissions:
0o600(owner read/write only) - ✅ Directory permissions:
0o700(owner access only)
Test 5: Edge Cases
Empty secret value:
- ✅ Warning logged:
Reserved file-secret 'CODEX_AUTH_JSON' has an empty value; skipping materialisation - ✅ No tempdir created
- ✅ No
CODEX_HOMEenv var set
Mixed file-secrets and plain secrets:
- ✅ Both file-secrets materialized correctly
- ✅ Plain secrets (
GITHUB_TOKEN) retained in secrets dict - ✅ All secrets accessible to subprocess as expected
Issues Found
None.
Replaces the host-side codex auth.json materialisation introduced in d019357 (and the Claude-OAuth credential textarea introduced in e74389a) with the SDK's new reserved-secret materialisation in ``ACPAgent`` (OpenHands/software-agent-sdk#3269, commit 6a379c03f). Authentication for ACP subscriptions now lives entirely in the standard Settings → Secrets surface, with provider-specific behaviour pushed down to the SDK: - Claude Code · Max subscription → secret named ``CLAUDE_CODE_OAUTH_TOKEN``, value = the OAuth access token. SDK exports it as an env var on the spawned subprocess (claude reads it directly). - Codex · ChatGPT subscription → secret named ``CODEX_AUTH_JSON``, value = the contents of ``~/.codex/auth.json``. SDK writes it to a per-agent temp file and sets ``CODEX_HOME``. Codex's refresh-token state is preserved across turns because the file is real and writable. API-key paths (``ANTHROPIC_API_KEY``, ``OPENAI_API_KEY``) continue to work via the existing ``_acp_provider_env`` translation of the LLM profile's API key into the provider-native env var. What this PR drops from OpenHands: - ``_inject_codex_auth``, ``_cleanup_acp_temp_dir``, ``_acp_file_secrets_base_dir``, the ``CODEX_AUTH_JSON_SECRET_NAME`` constant, and their call sites in ``live_status_app_conversation_service.py``. - ``OH_ACP_FILE_SECRETS_DIR`` env var and the auto bind-mount in ``DockerSandboxService`` config — the SDK now writes the file next to the subprocess inside the agent-server, so the backend doesn't need filesystem reach into the agent's container. - ``/{secret_id:path}`` → ``/{secret_id}`` reverted in ``secrets_router`` (the slashed ``FILE:~/.codex/auth.json`` name is gone). - ``encodeURIComponent`` on PUT/DELETE in ``secrets-service.ts`` reverted for the same reason. - ``SecretsService.upsertSecret`` and the ``useUpsertSecret`` hook — no longer needed; users create secrets through the regular Secrets UI. - The two credential textareas under the Claude Code / Codex presets in ``agent-settings.tsx``, the ``extractClaudeOauthToken`` and ``isLikelyCodexAuthJson`` helpers, and the save-credentials flow. What replaces it: - A small ``SubscriptionAuthInfo`` panel per preset (Claude Code, Codex) telling the user which magic secret name to create, surfacing a "Secret found" / "Secret not set" indicator (via the existing ``useSearchSecrets`` hook), and showing copy-pasteable extraction commands for macOS / Linux. - Five new i18n keys × 15 languages (``SETTINGS$AGENT_AUTH_*``, ``SETTINGS$AGENT_CLAUDE_AUTH_INSTRUCTIONS``, ``SETTINGS$AGENT_CODEX_AUTH_INSTRUCTIONS``). Old credential- textarea keys are still in ``translation.json`` for now; they're no longer referenced from code and can be swept in a follow-up. Net diff: −596 / +300 lines on this PR vs the previous tip. The SDK pin is temporarily a git source pointing at the SDK PR's commit (6a379c03f). It must be replaced with a regular pinned release version (>= whichever SDK release first includes that commit) before this PR can land on main. Frontend tests updated: ``TestACPSecretsEnvInjection`` and codex credential textarea tests deleted; five new cases under "subscription auth info panels" covering visibility per preset, secret-name surfaced in the panel text, and detected / not-detected indicators. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
all-hands-bot
left a comment
There was a problem hiding this comment.
🟡 Acceptable - Solid implementation solving real problems, but critical issues need fixing.
Assessment
This PR elegantly solves file-based authentication for Codex and Gemini using a clean registry pattern. The data structure (_FileSecretSpec) is well-designed, the implementation is straightforward, and the test coverage is comprehensive. However, two critical issues must be addressed:
- Security race condition in file permission setting (minor risk, easy fix)
- Failing tests due to incorrect secret precedence logic (critical, pre-existing bug exposed by new tests)
[CRITICAL ISSUES]
Secret Precedence Bug (Line 1131)
Problem: Agent secrets are silently ignored when the same key exists in os.environ. Both test_static_secret_injected_into_subprocess_env (existing) and test_plain_secrets_still_exported_as_env_vars (new) fail.
Root cause: The condition if name not in env checks the merged environment dict that already includes os.environ, giving inherited environment variables precedence over user-provided agent secrets.
Expected precedence:
acp_env(explicit agent configuration)agent_context.secrets(user-provided runtime secrets)os.environ(inherited environment)
Fix: Change line 1131 from if name not in env to if name not in self.acp_env
Note: This is a pre-existing bug from PR #2984, but must be fixed here since the new tests expose it. Without this fix, users cannot override environment variables with agent secrets, which breaks the expected behavior.
File Permission Race Condition (Lines 1095-1096)
Problem: There's a brief window between write_text() and chmod(0o600) where files have default permissions.
Current mitigation: Parent directory is 0o700, preventing access from other users.
Better solution: Use atomic permission setting:
target_path.write_text(
value,
opener=lambda path, flags: os.open(path, flags, 0o600)
)This eliminates the race window entirely and doesn't rely on parent directory protection.
[TESTING GAPS]
The PR includes comprehensive tests (7 test cases), but two tests are currently failing:
test_static_secret_injected_into_subprocess_env(pre-existing)test_plain_secrets_still_exported_as_env_vars(new)
Both fail with:
AssertionError: assert '<secret-hidden>' == 'ghp_xyz'
This PR should not be merged with failing tests. The fix is straightforward (see line 1131 comment above).
[RISK ASSESSMENT]
Factors:
- Touches security-sensitive credential handling (file-based secrets)
- Has failing tests that must pass before merge
- Pre-existing bug affects all agent secrets, not just file-secrets
- Changes are additive (only affects new reserved secret names)
- Well-isolated with clear separation of concerns
Recommendation: Fix the two critical issues above before merging. The core file-secret materialization mechanism is sound and well-tested. Once the secret precedence bug is fixed and tests pass, this is ready to merge.
VERDICT
❌ Needs fixes: Core logic is excellent, but failing tests and the secret precedence bug must be addressed. Both fixes are straightforward.
KEY INSIGHT
The file-secret registry pattern is elegant and extensible — adding support for future ACP providers with file-based auth is a one-line registry entry. However, the pre-existing secret precedence bug undermines the entire secret injection mechanism by preventing users from overriding environment variables with agent secrets.
| target_path.write_text(value) | ||
| target_path.chmod(0o600) |
There was a problem hiding this comment.
🟠 Important - Security: There's a brief window between write_text() (line 1095) and chmod(0o600) (line 1096) where the file has default permissions.
While this is partially mitigated by the parent directory being 0o700 (line 1093), there's a cleaner atomic solution:
| target_path.write_text(value) | |
| target_path.chmod(0o600) | |
| target_path.write_text( | |
| value, | |
| opener=lambda path, flags: os.open(path, flags, 0o600) | |
| ) |
This sets permissions atomically during file creation, eliminating the race window entirely. You'll need to add import os at the top if not already present (it's already imported on line 21).
| # calling it here is safe because _start_acp_server is a regular | ||
| # (non-async) method. | ||
| for name, secret in regular_secrets.items(): | ||
| if name not in env: |
There was a problem hiding this comment.
🔴 Critical - Logic Error: This condition causes agent secrets to be silently ignored when the same key exists in os.environ.
Current behavior (incorrect):
acp_envhas highest precedence ✓os.environhas second precedence ✗agent_context.secretshas lowest precedence ✗
Expected behavior:
acp_env(explicit agent config)agent_context.secrets(user-provided runtime secrets)os.environ(inherited environment)
Impact: Both test_static_secret_injected_into_subprocess_env and test_plain_secrets_still_exported_as_env_vars fail because GITHUB_TOKEN from the test environment takes precedence over the agent's secret value.
Fix:
| if name not in env: | |
| if name not in self.acp_env: |
This ensures only explicit acp_env entries override agent secrets, while agent secrets can override inherited os.environ values.
Note: This appears to be a pre-existing bug from PR #2984 that introduced secret injection. However, it must be fixed before merging this PR since the new tests expose the issue.
| # File-secret was materialised | ||
| assert env["CODEX_HOME"] | ||
| # Plain secret was injected as env var | ||
| assert env.get("GITHUB_TOKEN") == "ghp_xyz" |
There was a problem hiding this comment.
This test currently fails due to the secret precedence bug at line 1131 in acp_agent.py. Once that's fixed, this test should pass.
Test output:
AssertionError: assert '<secret-hidden>' == 'ghp_xyz'
The test environment's GITHUB_TOKEN takes precedence over the agent's ghp_xyz value.
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
File-secret materialization works as described; verified CODEX_AUTH_JSON and GOOGLE_APPLICATION_CREDENTIALS_JSON materialize to temp files with correct permissions, env vars, and cleanup.
Does this PR achieve its stated goal?
Yes. The PR successfully enables ACP agents to authenticate via file-based credentials without requiring consumers to implement provider-specific file injection. Verified end-to-end:
- Reserved secrets (
CODEX_AUTH_JSON,GOOGLE_APPLICATION_CREDENTIALS_JSON) are written to temp files with 0o600 permissions - Appropriate env vars (
CODEX_HOME,GOOGLE_APPLICATION_CREDENTIALS) are set automatically - File secrets are filtered from regular env var injection (not leaked)
- Plain secrets continue to work normally alongside file secrets
- Temp files are cleaned up on
agent.close() - Consumer usage pattern works transparently as documented
| Phase | Result |
|---|---|
| Environment Setup | ✅ Dependencies synced, environment ready |
| CI Status | ✅ All checks passing (SUCCESS) |
| Functional Verification | ✅ 12 tests passed across unit + integration suites |
Functional Verification
Test Suite 1: Unit Tests
Created /tmp/test_file_secrets.py to verify the materialization mechanism:
Test 1 — CODEX_AUTH_JSON Materialization:
agent = ACPAgent(
llm=LLM(model="anthropic/claude-3-5-sonnet-20241022", api_key="fake"),
acp_command=["echo", "fake"],
agent_context=AgentContext(
secrets={"CODEX_AUTH_JSON": StaticSecret(value=SecretStr(codex_payload))}
)
)
test_env = {}
remaining = agent._materialise_file_secrets(test_env)Result:
✓ Materialization method returned remaining secrets: []
✓ CODEX_HOME set: /tmp/acp-file-secrets-djx050v9/codex_auth_json
✓ File exists: /tmp/acp-file-secrets-djx050v9/codex_auth_json/auth.json
✓ File content matches payload
✓ File permissions: 0o600
✓ CODEX_AUTH_JSON not in remaining env vars
✓ Agent closed successfully
✓ Temp directory cleaned up
This confirms CODEX_AUTH_JSON is written to auth.json with CODEX_HOME pointing to the containing directory, exactly as required by the Codex binary.
Test 2 — GOOGLE_APPLICATION_CREDENTIALS_JSON Materialization:
agent_context=AgentContext(
secrets={"GOOGLE_APPLICATION_CREDENTIALS_JSON": StaticSecret(value=SecretStr(gac_payload))}
)Result:
✓ GOOGLE_APPLICATION_CREDENTIALS set: /tmp/acp-file-secrets-mdh7zvaa/google_application_credentials_json/gcloud-credentials.json
✓ File exists at path
✓ File content matches payload
✓ File permissions: 0o600
✓ GOOGLE_APPLICATION_CREDENTIALS_JSON not in remaining env vars
✓ Cleanup verified
This confirms the Gemini Vertex AI credential is written with GOOGLE_APPLICATION_CREDENTIALS pointing to the file path (not dir), as required by Google's SDK.
Test 3 — Plain Secrets Still Work:
secrets={
"CODEX_AUTH_JSON": StaticSecret(value=SecretStr("{}")),
"GITHUB_TOKEN": StaticSecret(value=SecretStr("ghp_fake_token")),
"CUSTOM_SECRET": StaticSecret(value=SecretStr("custom_value")),
}Result:
✓ Remaining secrets: ['GITHUB_TOKEN', 'CUSTOM_SECRET']
✓ CODEX_AUTH_JSON filtered out (file secret)
✓ GITHUB_TOKEN and CUSTOM_SECRET in remaining (plain secrets)
This confirms the change doesn't break existing secret injection — file secrets are filtered, plain secrets pass through.
Tests 4-5: Both file secrets coexist with shared base tempdir but isolated subdirs ✅, empty payloads skipped gracefully ✅
Test Suite 2: Integration Tests
Created /tmp/test_integration.py to verify consumer usage pattern:
Test: Consumer Usage Pattern (as documented in PR description):
codex_auth_content = json.dumps({
"auth_mode": "chatgpt",
"tokens": {"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."},
})
agent = ACPAgent(
llm=LLM(model="anthropic/claude-3-5-sonnet-20241022", api_key="test"),
acp_command=["codex", "serve"],
agent_context=AgentContext(
secrets={"CODEX_AUTH_JSON": StaticSecret(value=SecretStr(codex_auth_content))}
)
)Result:
✓ Agent created successfully
✓ CODEX_HOME set: /tmp/acp-file-secrets-zooasij9/codex_auth_json
✓ auth.json materialized automatically at /tmp/acp-file-secrets-zooasij9/codex_auth_json/auth.json
✓ Secret not leaked as env var
✓ Agent closed and cleaned up
This confirms the exact consumer pattern described in the PR works: create agent with reserved secret name, materialization happens transparently, no provider-specific code needed.
All 12 tests passed across both suites.
Issues Found
None.
Why
Two ACP servers we already support authenticate via a JSON file on disk rather than an env var:
$CODEX_HOME/auth.jsonand writes back to that file on refresh. There is no env-var alternative — codex's auth state is inherently file-backed.GOOGLE_APPLICATION_CREDENTIALS=/abs/path/to/key.jsonpointing at a JSON file.Today the SDK has no way to surface these to the spawned subprocess. Consumers (e.g. OpenHands) end up reinventing per-conversation file injection, bind-mounts, cleanup, and path-routing — all of which is provider-specific knowledge that more properly belongs in the SDK alongside the rest of the per-provider config (
acp_command,acp_model,_acp_provider_env, etc.).This PR adds a small registry-driven mechanism: a reserved secret name → write its value to a per-agent tempdir → set the controlling env var on the subprocess → strip the secret from the regular env-var injection path so the (potentially multi-KB) JSON payload isn't also exported.
What changes
openhands-sdk/openhands/sdk/agent/acp_agent.py:ACPAgent._materialise_file_secrets(env)walksagent_context.secrets, materialises matches intoself._file_secrets_tempdir(atempfile.TemporaryDirectorycreated lazily), sets the controlling env vars, and returns the remaining (plain) secrets that should flow through the existing env-var injection loop in_start_acp_server. Files are written with0o600perms; each file-secret lives in its own subdirectory soenv_points_to="dir"handlers don't expose siblings. The per-agent tempdir is cleaned up in_cleanupafter the subprocess and executor are torn down.Adding a new file-based auth mechanism for a future ACP server is one
_FILE_SECRETSentry.Why these two reserved names
auth.jsonon token refresh. An env var would lose refresh state between turns.GOOGLE_APPLICATION_CREDENTIALSis contractually a path to a JSON file. There is no env-var-with-content form of this credential.Intentionally not covered: Gemini personal-account "Sign in with Google" subscription.
gemini-clicaches those credentials in~/.gemini/gemini-credentials.jsonencrypted with a key derived fromos.hostname() + os.userInfo().username(see@google/gemini-cli-core/dist/src/services/fileKeychain.js,deriveEncryptionKey()salted with${hostname}-${username}-gemini-clithen scrypt'd). That file is machine-bound by design and cannot be transported to a cloud host. Google's own headless docs confirm: subscription users running headless must fall back to API key or Vertex AI.Why not just inject as env vars
The SDK already exports
AgentContext.secretsas env vars on the spawned subprocess. We could put the JSON payload in an env var named (say)CODEX_AUTH_JSONand tell the binary to read it from there. But:GOOGLE_APPLICATION_CREDENTIALSis contractually a file path, not file content. There's no way to make Google's SDKs read a JSON blob out of an env var.os.environis visible to anything that can read/proc/<pid>/environ. Materialising the file with0o600and dropping the secret from the env-var path is the safer default.How consumers use this
A consumer (OpenHands or anyone embedding the SDK) doesn't need provider-specific code. To authenticate Codex via ChatGPT subscription, they write a secret named
CODEX_AUTH_JSONwith the contents of the user's~/.codex/auth.json:The same flow with
GOOGLE_APPLICATION_CREDENTIALS_JSONactivates Gemini Vertex AI auth.Tests
Seven new cases in
TestACPFileSecretMaterialisation:auth.json+CODEX_HOMEpoints at containing dirGOOGLE_APPLICATION_CREDENTIALSpoints at the file path0o600ACPAgent.close()removes the materialised tempdirAll 180 tests in
test_acp_agent.pypass.Related
This unblocks an architectural simplification on the OpenHands side: OpenHands/OpenHands#14425 currently carries ~500 lines of provider-specific file-injection machinery (
_inject_codex_auth,_cleanup_acp_temp_dir,OH_ACP_FILE_SECRETS_DIR, Docker bind-mount auto-config,secrets_router:pathhack, special credential textareas per preset). Once this PR is released and pinned, that all collapses to a small informational panel telling users which secret name to create — provider-specific knowledge stays here in the SDK where it already lives next to_acp_provider_env.Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:6a379c0-pythonRun
All tags pushed for this build
About Multi-Architecture Support
6a379c0-python) is a multi-arch manifest supporting both amd64 and arm646a379c0-python-amd64) are also available if needed