Skip to content

fix(agent-server): resolve linked providers for plaintext profile reads - #4952

Open
enyst wants to merge 4 commits into
mainfrom
fix/plaintext-profile-provider-resolution
Open

enyst wants to merge 4 commits into
mainfrom
fix/plaintext-profile-provider-resolution

Conversation

@enyst

@enyst enyst commented Sep 11, 2026 •

Copy link
Copy Markdown
Member

HUMAN:
This PR proposes an addition to assist PR 547 on extensions/ repo to show the LLM profile of an Automation.


AGENT:
I am an AI agent (smolpaws) acting on behalf of Engel.

Why

OpenHands/extensions#547 selects a named LLM profile and passes its configuration to a new conversation, but plaintext profile reads currently omit a linked provider's API key and base URL.

Summary

  • Resolve linked providers for GET /api/profiles/{name} with X-Expose-Secrets: plaintext.
  • Preserve stored profiles, active defaults, and the existing unresolved behavior of ordinary and encrypted editor reads.
  • Cover encrypted provider storage and credential rotation, plus real HTTP selection through RemoteWorkspace.get_llm(profile_name=...).

REST API contract changes

Compared with base OpenAPI b66c72436157 for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -26,0 +27 @@
+operation GET /api/canvas-extensions/installed/{extension_name}/icon operationId=get_canvas_extension_icon_endpoint_api_canvas_extensions_installed__extension_name__icon_get
@@ -191,0 +193 @@
+parameter GET /api/canvas-extensions/installed/{extension_name}/icon path:extension_name required=true schema=type="string" minLength=1 maxLength=255 pattern="^[a-z0-9]+(?:-[a-z0-9]+)*$"
@@ -471,0 +474,3 @@
+response GET /api/canvas-extensions/installed/{extension_name}/icon 200 image/svg+xml schema=type="string" format="binary"
+response GET /api/canvas-extensions/installed/{extension_name}/icon 404 no-content
+response GET /api/canvas-extensions/installed/{extension_name}/icon 422 application/json schema=HTTPValidationError
@@ -1334,0 +1340,2 @@
+schema CanvasExtensionBackendArtifact property strip_components optional schema=type="integer" default=0 minimum=0.0 maximum=16.0
+schema CanvasExtensionBackendArtifact property url optional schema=anyOf=[type="string",type="null"]
@@ -1346,0 +1354 @@
+schema CanvasExtensionManifest property icon optional schema=anyOf=[type="string",type="null"]

Issue Number

Fixes #5514

Supports OpenHands/extensions#548 and OpenHands/automation#430.

How to Test

uv run pytest tests/agent_server/test_profiles_router.py -q
uv run pytest tests/cross/test_remote_conversation_live_server.py -k 'workspace_named_llm_resolves_current_provider_credentials or workspace_default_llm_resolves_active_profile_despite_settings_drift' -q
uv run pre-commit run --files openhands-agent-server/openhands/agent_server/profiles_router.py tests/agent_server/test_profiles_router.py tests/cross/test_remote_conversation_live_server.py

Results: 106 profile tests passed; both live-server tests passed; all applicable pre-commit hooks passed.
The live test starts Uvicorn, creates a provider and profile through HTTP, selects the profile through the SDK, rotates the provider key, and confirms the next selection uses that key while the active default is unchanged.
The existing shared-provider test failed on the original route because the plaintext API key was None, then passed with this fix.

Type

  • Bug fix

Notes

This dependency must reach the deployed Agent Server before the extensions#547 follow-up can run provider-linked profiles. No external LLM calls are used by these tests.


🐳 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:6b41f7a-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:6b41f7a-golang-amd64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-golang-amd64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-golang-amd64
ghcr.io/openhands/agent-server:6b41f7a-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:6b41f7a-golang-arm64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-golang-arm64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-golang-arm64
ghcr.io/openhands/agent-server:6b41f7a-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:6b41f7a-java-amd64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-java-amd64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-java-amd64
ghcr.io/openhands/agent-server:6b41f7a-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:6b41f7a-java-arm64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-java-arm64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-java-arm64
ghcr.io/openhands/agent-server:6b41f7a-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:6b41f7a-python-amd64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-amd64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-amd64
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-amd64
ghcr.io/openhands/agent-server:6b41f7a-python-arm64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-arm64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-arm64
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-arm64
ghcr.io/openhands/agent-server:6b41f7a-python-minimal-amd64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-minimal-amd64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-minimal-amd64
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:6b41f7a-python-minimal-arm64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-minimal-arm64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-minimal-arm64
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:6b41f7a-python-slim-amd64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-slim-amd64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-slim-amd64
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:6b41f7a-python-slim-arm64
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-slim-arm64
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-slim-arm64
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:6b41f7a-golang
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-golang
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-golang
ghcr.io/openhands/agent-server:6b41f7a-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:6b41f7a-java
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-java
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-java
ghcr.io/openhands/agent-server:6b41f7a-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:6b41f7a-python-minimal
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-minimal
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-minimal
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-minimal
ghcr.io/openhands/agent-server:6b41f7a-python-slim
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python-slim
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python-slim
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime-slim
ghcr.io/openhands/agent-server:6b41f7a-python
ghcr.io/openhands/agent-server:6b41f7ad1717a18eb11488601ff3e833f805c65b-python
ghcr.io/openhands/agent-server:fix-plaintext-profile-provider-resolution-python
ghcr.io/openhands/agent-server:6b41f7a-python-node-runtime

About Multi-Architecture Support

  • Each variant tag (e.g., 6b41f7a-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., 6b41f7a-python-amd64) are also available if needed

Jev-Fast-Audit

⚡ Jev fast audit · estimates · 0.74s · commit edc90a4
Strongest signal: Sensitive data disclosure · 25% estimated likelihood.
Evidence: F001H003 · openhands-agent-server/openhands/agent_server/profiles_router.py:181–191.
Coverage: complete supplied coverage; 8/8 hunks, 3/3 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 16.0% F001H003 · openhands-agent-server/openhands/agent_server/profiles_router.py:181–191
Contract regression 19.0% F001H003 · openhands-agent-server/openhands/agent_server/profiles_router.py:181–191
Data loss 5.0% No direct hunk selected
Sensitive data disclosure 25.0% F001H003 · openhands-agent-server/openhands/agent_server/profiles_router.py:181–191
Unexpected data transfer 4.0% No direct hunk selected
Credential misuse 12.0% No direct hunk selected
Untrusted instruction authority 3.0% No direct hunk selected
Package source redirection 3.0% No direct hunk selected
Unverified remote execution 2.0% No direct hunk selected
Privileged environment access 4.0% No direct hunk selected
Security assessment bypass 18.0% F001H003 · openhands-agent-server/openhands/agent_server/profiles_router.py:181–191
Prohibited workload 2.0% No direct hunk selected
Primary concern Sensitive data disclosure; confidence 80.0% F001H003 · openhands-agent-server/openhands/agent_server/profiles_router.py:181–191

Return current linked credentials to runtime clients without changing stored
profiles or default settings. Keep editor reads unresolved. Cover encrypted
storage, rotation, and real HTTP profile selection with RemoteWorkspace.

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

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@enyst

enyst commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

@OpenHands /codereview this pr please and post your feedback as a comment. look at the linked pr too. do we really need to add a plaintext secrets? other options? why does it seemingly needed?

@openhands-ai

openhands-ai Bot commented Sep 11, 2026

Copy link
Copy Markdown

I'm on it! enyst can track my progress at all-hands.dev

enyst commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

🟢 Good taste

I looked through this PR and the linked OpenHands/extensions#547 flow. I do not think this PR adds a new plaintext-secret capability: X-Expose-Secrets: plaintext already exists on settings and profile reads, RemoteWorkspace.get_llm(profile_name=...) already requests it, and an inline profile API key was already returned in plaintext under that mode. This patch makes a provider-linked profile obey the same runtime-read contract as an inline profile.

Why it is needed in the current design: a linked profile deliberately stores only provider_connection_id; the current provider key and base URL live in the provider store. The conversation-create API accepts a concrete agent/LLM, but it does not accept an LLM profile name to resolve server-side. Therefore extensions#547 must resolve the named profile before constructing the spawned conversation. Without this change, the existing plaintext profile read returns an unrunnable LLM (api_key=None, base_url=None) specifically for provider-linked profiles.

There are alternatives, but none is a smaller fix:

  1. Best long-term design: add an additive server-side LLM-profile selector to conversation creation, and return non-secret resolved model metadata separately for provenance. That would keep the API key inside the agent server. It also requires a new public REST/SDK/client contract and clear precedence against agent, agent_settings, and agent_profile_id.
  2. Use /switch_profile after creation: avoids returning plaintext, but requires creating an unstarted conversation, switching it, then sending/running. That is multi-request, non-atomic, and complicates failure cleanup.
  3. Activate the profile globally first: not acceptable; it mutates shared defaults and introduces races.
  4. Add a dedicated /resolve endpoint: clearer naming, but it still returns plaintext and duplicates the existing runtime contract without reducing exposure.

The relevant trust boundary remains unchanged: /api/* requires the session API key when configured, and the server explicitly treats authenticated clients as one trust domain. Deployments with no session keys already expose the existing plaintext mode and should not be considered secure production configurations.

The tests cover encrypted-at-rest provider credentials, key rotation, unchanged stored profiles/default settings, and a real HTTP path through RemoteWorkspace.get_llm(profile_name=...). I also reran the focused suites locally: 106 profile tests passed; 2 selected live-server tests passed.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM

This touches credential material, so the impact of a mistake is meaningful. The implementation itself is narrow, authenticated under the existing trust model, preserves redacted/encrypted editor reads, and adds no new secret-exposure mode. The two current failing checks appear unrelated: a timing-sensitive session-socket test and an expired deprecation elsewhere.

VERDICT:
✅ Worth merging: This is the correct minimal compatibility fix for the API that exists today. A server-side profile selector would be a worthwhile follow-up, not a prerequisite.

KEY INSIGHT:
Plaintext is not fundamentally required by the product goal; it is required by today’s client-resolved conversation API, and this PR makes linked profiles consistent with that already-established runtime contract.

This review comment was generated by an AI agent (OpenHands) on behalf of @enyst.

@openhands-ai

openhands-ai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Reviewed PR #4952 and linked OpenHands/extensions#547, then posted feedback here:

#4952 (comment)

Conclusion: the PR does not introduce plaintext exposure; it makes provider-linked profiles consistent with the existing authenticated X-Expose-Secrets: plaintext runtime contract. A server-side LLM-profile selector would avoid plaintext transit and is the cleaner long-term design, but it is a larger API change and should not block this focused fix.

Validation:

  • 106 profile-router tests passed
  • 2 selected live-server tests passed
  • Current CI failures appear unrelated
  • No workspace files were changed

GitHub formal approval was not possible because the available token belongs to the PR author, so I posted the requested review as a PR comment.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   profiles_router.py200697%504–509
TOTAL478431904760% 

@enyst
enyst marked this pull request as ready for review September 14, 2026 02:27
@all-hands-bot

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

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

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was posted by an AI agent (OpenHands).

Scope

This change belongs in this repository. openhands-agent-server/openhands/agent_server/profiles_router.py is the canonical Agent Server REST surface, which this repo owns; the extensions/automation follow-ups only consume it. No file move or maintainer architecture decision is needed.

What the change does

GET /api/profiles/{name} now passes resolve_provider=expose_mode == "plaintext" to LLMProfileStore.load. Previously the route always used resolve_provider=False. I traced the surrounding code:

  • The plaintext branch (the only one that resolves) then dumps with build_expose_context("plaintext", cipher), so the profile's own secret is exposed as before and now the linked provider's api_key / base_url are added.
  • The absent-header and encrypted branches keep resolve_provider=False, so editor/frontend reads still return the stored reference with a nulled api_key. This matches the PR's stated intent.
  • load does not write anything, so resolving a provider does not copy credentials into the stored profile. The new parametrized test asserts exactly that (store.load(..., resolve_provider=False) still has api_key is None).

The change is minimal, has no side effects on unrelated state, and is consistent with the existing plaintext exposure already available through GET /api/settings (which exposes the resolved provider key on the active profile). The routes are behind check_session_api_key, so plaintext reads are authenticated backend-only traffic. I found no new authorization or disclosure escalation.

One behavior nuance worth making explicit for reviewers (not a defect): for a plaintext read of a profile whose provider_connection_id dangles, load raises ProviderConnectionNotFound, which store_errors() maps to 422 — previously the plaintext read returned 200 with a nulled key. That is the right call (a dangling link cannot yield a runnable LLM config, and it matches /{name}/activate), and ordinary/encrypted reads keep the old unresolved behavior as the description says. The docstring enumerates the modes but does not mention this 422; a one-line note would be nice-to-have, not blocking.

Verification

  • Local: uv run pytest tests/agent_server/test_profiles_router.py -q → 106 passed.
  • CI: the exact head edc90a4 passed all required checks, and the cross-tests job log shows both new live-server tests running and passing (test_workspace_named_llm_resolves_current_provider_credentials PASSED, test_workspace_default_llm_resolves_active_profile_despite_settings_drift PASSED).
  • I could not run the two live-server tests locally: they abort at import in env_parser.merge (IndexError: list assignment index out of range) because this sandbox injects an OH_* env var that collides with config-file list merging. I reproduced the identical failure on a checkout of origin/main for a pre-existing test, so it is an environment artifact, not a regression from this PR.

Failing check on this head

The Validate PR description check is failing on edc90a4. The failure is not about code: the ## Issue Number section references OpenHands/extensions#548 and OpenHands/automation#430. The validator regex cannot match a repo#N reference, so it finds no linked issue and errors with "Link an issue in the ## Issue Number section". Both referenced issues are healthy — extensions#548 and automation#430 are open and carry ready-for-dev — and the same-numbered issues in this repo are unrelated, closed, and pre-rollout. Because the actual work is owned downstream, the PR structurally cannot satisfy this repo's in-repo linked-issue gate. This is a process/merge-readiness gap rather than a code defect, but it is red on the current head, so I am leaving this for a human maintainer to confirm the cross-repo linkage is acceptable (or to add an in-repo tracking issue) before merging. No version bumps or dependency changes are present.

🔄 CHANGES REQUESTED

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

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor
  ✅ **PR Artifacts Cleaned Up**

  The `.pr/` directory is no longer present.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was posted by an AI agent (OpenHands).

Scope

Belongs here. openhands-agent-server/openhands/agent_server/profiles_router.py is this repo's canonical Agent Server REST surface; the extensions/automation follow-ups only consume it. No file move or architecture decision needed.

Change

GET /api/profiles/{name} now passes resolve_provider=expose_mode == "plaintext" to LLMProfileStore.load instead of the hard-coded resolve_provider=False.

I traced both branches against the base behavior:

  • Plaintext (the only mode that resolves): load applies the linked connection's api_key/base_url, and the subsequent build_expose_context("plaintext", cipher) dump exposes them alongside the profile's own secret. This is exactly the credential set RemoteWorkspace.get_llm(profile_name=...) needs, and it matches the plaintext exposure already available on the active profile via GET /api/settings.
  • Absent-header and encrypted: still resolve_provider=False, so editor/frontend reads keep the stored reference and a nulled api_key. Matches the stated intent.
  • load performs no writes, so resolving does not copy provider credentials into the stored profile; the new parametrized test asserts precisely that with a resolve_provider=False re-read.

No new authorization or disclosure escalation: the routes sit behind check_session_api_key, so plaintext reads are authenticated backend traffic, and the same resolved key was already reachable through the settings endpoint. No version bumps or dependency changes.

One non-blocking nuance: a plaintext read of a profile whose provider_connection_id dangles now surfaces ProviderConnectionNotFound -> 422 via store_errors(), where it previously returned 200 with a nulled key. That is the correct trade (a dangling link cannot yield a runnable config, and it mirrors /{name}/activate); the mode docstring just doesn't call it out. Optional to note, not worth blocking.

Verification

  • Local: uv run pytest tests/agent_server/test_profiles_router.py -q -> 109 passed.
  • CI on exact head da27a9ec: every check green, including Validate PR description (the previously failing cross-repo linked-issue gate is resolved by the new Fixes #5514, which is open and carries bug, priority:medium, ready-for-dev). The cross-tests log confirms both new live-server tests ran and passed.
  • The two live-server tests cannot run in this sandbox: they abort at import in env_parser.merge (IndexError) because an injected OH_* var collides with config-file list merging. I reproduced the identical failure on a clean origin/main checkout for a pre-existing test, so it is an environment artifact, not a regression from this PR.

✅ APPROVED

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was posted by an AI agent (OpenHands).

Scope

Belongs here. openhands-agent-server/openhands/agent_server/profiles_router.py is this repo's canonical Agent Server REST surface; the extensions/automation follow-ups only consume it.

What changed since the last reviewed head

The previous head da27a9ec was approved. This head 6b41f7ad differs only by commit "chore: Remove PR-only artifacts [automated]", which deletes .pr/stop-bash-evidence.md and .pr/stop-bash-evidence.py — scratch evidence files left over from the unrelated BashCommand PR #5348, absent from both main and this head. The router diff and both test files are byte-identical to the approved head (git diff da27a9ec 6b41f7ad -- .../profiles_router.py is empty), so the substantive change is unchanged.

Change

GET /api/profiles/{name} now passes resolve_provider=expose_mode == "plaintext" to LLMProfileStore.load instead of the hard-coded resolve_provider=False.

  • Plaintext (the only resolving mode): load applies the linked connection's api_key/base_url, and the build_expose_context("plaintext", cipher) dump exposes them alongside the profile's own secret — the credential set RemoteWorkspace.get_llm(profile_name=...) needs.
  • Absent-header and encrypted: still resolve_provider=False, so editor/frontend reads keep the stored reference and a nulled api_key.
  • load performs no writes, so resolution does not copy provider credentials into the stored profile; the parametrized test asserts that with a resolve_provider=False re-read.

No new authorization or disclosure escalation: routes sit behind check_session_api_key, and the same resolved key was already reachable via GET /api/settings plaintext. No version bumps or dependency changes in this diff.

One non-blocking nuance carried over: a plaintext read of a profile with a dangling provider_connection_id now surfaces ProviderConnectionNotFound -> 422 via store_errors() rather than 200 with a nulled key. That is the correct trade and mirrors /{name}/activate.

Verification

  • Local: uv run pytest tests/agent_server/test_profiles_router.py -q -> 109 passed.
  • CI on exact head 6b41f7ad: all checks green, including Validate PR description, and the cross-tests log shows both new live-server tests passing. Linked issue #5514 is open with bug, priority:medium, ready-for-dev.
  • The two live-server tests cannot run in this sandbox: they abort at import in env_parser.merge (IndexError) because an injected OH_* var collides with config-file list merging. Reproduced identically on a clean origin/main checkout for a pre-existing test, so it is an environment artifact, not a regression.

✅ APPROVED

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.

[Bug]: Plaintext LLM profile reads omit linked provider credentials

2 participants