Skip to content

fix(agent-server): enforce profile secret scope on runtime-launched conversations - #5193

Open
all-hands-bot wants to merge 1 commit into
mainfrom
fix/runtime-profile-secret-scope
Open

all-hands-bot wants to merge 1 commit into
mainfrom
fix/runtime-profile-secret-scope

Conversation

@all-hands-bot

@all-hands-bot all-hands-bot commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

HUMAN:


AGENT:

Why

Found during the weekly high-impact regression hunt (source PR reviewed: #4931 "feat(profiles): scope which secrets an agent profile receives"; applicable risk lenses: #8 Credentials/redaction/sensitive persistence boundaries and #4 Multiple construction and configuration paths).

#4931 added server-side secret scoping: when a conversation is started with agent_profile_id, request.secrets is filtered by the profile's secret_refs allow-list — "Enforced here, not client-side: a caller that sends more secrets than the profile allows must not widen the agent's scope." However, the filter only runs inside if request.agent_profile_id is not None:. In the per-conversation container runtime (docker_runtime, #3403), the profile is bound to the container via the OH_RUNTIME_LAUNCHED_PROFILE env var and the start request carries an explicit agent with agent_profile_id=None. On that path no create-time filtering runs at all, even though the env-launched profile (including its secret_refs) is recorded on the stored conversation and is enforced on resume. The security boundary is therefore inconsistent: create allows scope widening, resume denies it.

Deterministic reproduction (against unmodified main, SDK v1.49.2): set OH_RUNTIME_LAUNCHED_PROFILE to a LaunchedAgentProfile with secret_refs=["GITHUB_TOKEN"], then ConversationService.start_conversation() with an explicit agent and secrets={"GITHUB_TOKEN": ..., "DATADOG_API_KEY": ...}:

stored.launched_agent_profile: agent_profile_id=UUID('b3b9abbb-...') revision=1 secret_refs=['GITHUB_TOKEN']
stored secret names: ['DATADOG_API_KEY', 'GITHUB_TOKEN']
BUG CONFIRMED: out-of-scope secret DATADOG_API_KEY was injected into a conversation
launched under a profile whose secret_refs only allow GITHUB_TOKEN

(log also shows Added 2 secrets to conversation).

Impact: a client starting a conversation inside a profile-bound runtime container can inject arbitrary secrets beyond the profile's allow-list — exactly the scope-widening #4931 set out to prevent server-side.

Root cause: ConversationService._start_conversation applies the secret_refs filter only on the explicit-agent_profile_id branch; the OH_RUNTIME_LAUNCHED_PROFILE branch (read earlier in the same method) never filters request.secrets.

Why the original tests did not catch this: #4931's create-time tests (test_start_conversation_drops_secrets_the_profile_disallows) only exercise the agent_profile_id request path, and its resume tests only exercise stored-profile filtering. No test combined the env-var launch binding with a create-time secrets payload.

Summary

  • In _start_conversation, when the launched profile comes from OH_RUNTIME_LAUNCHED_PROFILE (i.e. request.agent_profile_id is None) and its secret_refs is set, filter request.secrets by that allow-list — same enforcement as the explicit-profile path.
  • Add test_runtime_launched_profile_drops_disallowed_secrets (parametrized over None / [] / ["GITHUB_TOKEN"]) asserting stored secrets and launched-profile provenance through a real ConversationService.

Issue Number

N/A — found by weekly regression hunt, no tracking issue.

How to Test

Regression test:

uv run pytest tests/agent_server/test_agent_profile_conv_start.py::TestProfileSecretScope -q
  • On unmodified main (production fix stashed): 2 failed, 1 passed — the scoped cases (secret_refs=[] and secret_refs=["GITHUB_TOKEN"]) fail because DATADOG_API_KEY is stored:
    FAILED ...[secret_refs1-expected1]
    FAILED ...[secret_refs2-expected2]
    
  • With this fix: 11 passed.

End-to-end validation (real ConversationService, real event service, no mocks): a script that sets OH_RUNTIME_LAUNCHED_PROFILE with secret_refs=["GITHUB_TOKEN"] and starts a conversation with {"GITHUB_TOKEN", "DATADOG_API_KEY"} secrets prints, after the fix:

stored secret names: ['GITHUB_TOKEN']
OK: out-of-scope secret was filtered at creation

Neighboring suites all pass with the fix:

uv run pytest tests/agent_server/test_agent_profile_conv_start.py tests/agent_server/test_profile_resume_secret_scope.py -q   # 62 passed
uv run pytest tests/agent_server/test_conversation_service.py tests/agent_server/test_conversation_router.py tests/agent_server/test_credential_binding.py -q   # 244 passed
uv run pre-commit run --files openhands-agent-server/openhands/agent_server/conversation_service.py tests/agent_server/test_agent_profile_conv_start.py   # all hooks pass

Video/Screenshots

N/A — backend service change; console output of the reproduction and test runs is included above.

Design Doc

N/A — narrow security-boundary fix (17 lines of production code).

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 python-node-runtime Link
python-minimal amd64, arm64 python-node-runtime Link
python amd64, arm64 python-node-runtime Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:719ed42-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-719ed42-python \
  ghcr.io/openhands/agent-server:719ed42-python

All tags pushed for this build

ghcr.io/openhands/agent-server:719ed42-golang-amd64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-golang-amd64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-golang-amd64
ghcr.io/openhands/agent-server:719ed42-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:719ed42-golang-arm64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-golang-arm64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-golang-arm64
ghcr.io/openhands/agent-server:719ed42-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:719ed42-java-amd64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-java-amd64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-java-amd64
ghcr.io/openhands/agent-server:719ed42-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:719ed42-java-arm64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-java-arm64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-java-arm64
ghcr.io/openhands/agent-server:719ed42-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:719ed42-python-amd64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-amd64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-amd64
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-amd64
ghcr.io/openhands/agent-server:719ed42-python-arm64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-arm64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-arm64
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-arm64
ghcr.io/openhands/agent-server:719ed42-python-minimal-amd64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-minimal-amd64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-minimal-amd64
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:719ed42-python-minimal-arm64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-minimal-arm64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-minimal-arm64
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:719ed42-python-slim-amd64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-slim-amd64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-slim-amd64
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:719ed42-python-slim-arm64
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-slim-arm64
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-slim-arm64
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:719ed42-golang
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-golang
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-golang
ghcr.io/openhands/agent-server:719ed42-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:719ed42-java
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-java
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-java
ghcr.io/openhands/agent-server:719ed42-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:719ed42-python-minimal
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-minimal
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-minimal
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-minimal
ghcr.io/openhands/agent-server:719ed42-python-slim
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python-slim
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python-slim
ghcr.io/openhands/agent-server:719ed42-python-node-runtime-slim
ghcr.io/openhands/agent-server:719ed42-python
ghcr.io/openhands/agent-server:719ed425afc992b0effbc71c0c9cda60ef34bd09-python
ghcr.io/openhands/agent-server:fix-runtime-profile-secret-scope-python
ghcr.io/openhands/agent-server:719ed42-python-node-runtime

About Multi-Architecture Support

  • Each variant tag (e.g., 719ed42-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 719ed42-python-amd64) are also available if needed

Jev-Fast-Audit

Jev fast audit · estimates · 0.33s · commit 719ed42
Strongest signal: No primary concern selected.
Evidence: No primary concern to locate.
Coverage: complete supplied coverage; 3/3 hunks, 2/2 files.

All estimates and evidence
Estimate Likelihood / value Direct evidence
SQL injection 3.0% No direct hunk selected
Command injection 3.0% No direct hunk selected
Weakened authentication 8.0% No direct hunk selected
Weakened authorization 8.0% No direct hunk selected
Contract regression 11.0% No direct hunk selected
Data loss 9.0% F001H001 · openhands-agent-server/openhands/agent_server/conversation_service.py:1699–1721
Sensitive data disclosure 5.0% No direct hunk selected
Unexpected data transfer 3.0% No direct hunk selected
Credential misuse 6.0% No direct hunk selected
Untrusted instruction authority 3.0% No direct hunk selected
Package source redirection 4.0% No direct hunk selected
Unverified remote execution 2.0% No direct hunk selected
Privileged environment access 3.0% No direct hunk selected
Security assessment bypass 6.0% No direct hunk selected
Prohibited workload 2.0% No direct hunk selected
Primary concern None selected; confidence 92.0% No primary concern to locate

…onversations

OH_RUNTIME_LAUNCHED_PROFILE binds an agent profile to per-conversation
container runtimes, but the create-time secret filter introduced in #4931
only ran when the start request named agent_profile_id explicitly. A
create request reaching a profile-bound runtime container could inject
secrets beyond the profile's secret_refs allow-list; the scope was only
enforced later on resume. Filter request secrets by the env-launched
profile's secret_refs at creation too.

Co-authored-by: openhands <openhands@all-hands.dev>
@all-hands-bot

Copy link
Copy Markdown
Collaborator Author

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@github-actions

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants