fix(subagent): decrypt encrypted model profiles - #4413
Conversation
|
🚦 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 This is an automated check - no AI was used to generate this comment. |
enyst
left a comment
There was a problem hiding this comment.
Thank you for the PR! 🤔 TBH seeing how we need to keep treading it around only for the profile store (which is the only place who needs it) makes me think that maybe we could have set it in the profile store in the first place...
This is not a suggestion for change right now, though, because we also have a better secrets design issue, and those changes will clean up this... I think. @OpenHands can you find and link that issue? Then tell us WDYT.
|
I am OpenHands (OpenHands-Deepseek-Flash), an AI agent acting on behalf of @enyst. (Note: this is posted through enyst's GitHub token, so it appears under their name — the words here are from the AI agent, not enyst.) Regarding the "better secrets design issue" you asked about — it's #4288 (Design: reference-only credentials and safe runtime delivery): #4288 That's the consolidated design doc enyst contributed to (stdio-MCP, egress-proxy, OS-keyring, iron-proxy), and it's the one whose §2/§9 covers exactly this coupling: the cipher/ On the WDYT: I partly agree the threading is a smell — So: agree it's temporary, agree it's the profile store's job to own at-rest handling, and #4288 is the right place to track killing the whole pattern — link added above. WDYT, good to proceed with this as-is? |
|
Thanks for the review! I agree that threading the cipher through solely for the profile store is not ideal. Given #4288 and the bot’s recommendation, I’m happy to keep this PR focused as-is. Please let me know if you’d like me to make any changes before approval. |
|
🚦 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 This is an automated check - no AI was used to generate this comment. |
|
Hello, just found this bug as I'm also experiencing it when using subagents:
Is the fix getting merged? Thanks! |
55385ce to
5103299
Compare
Co-authored-by: openhands <openhands@all-hands.dev>
5103299 to
d87a6e8
Compare
|
Thanks @CarlosLanderas for confirming this in real use. @enyst I rebased the PR onto the latest |
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Overall verdict: No material findings. Approve from a review standpoint.
The change correctly threads the conversation cipher through the sub-agent registration path so that encrypted LLM profiles are decrypted before reaching the LLM provider. All four modified production call sites are covered:
agent_definition_to_factorynow acceptscipherand forwards it tostore.load(profile_name, cipher=cipher)— the only place where the cipher is actually needed.register_file_agents/register_plugin_agentsforwardcipherto the factory.LocalConversationpassesself._cipherat all three registration call sites (2 plugin-agent, 1 file-agent).ConversationService._register_agent_definitionsreceives and forwardsself.cipherat both the new-conversation and resume-conversation call sites.
Design note (not blocking): The per-load cipher approach (passing it to store.load rather than setting it on the LLMProfileStore instance) is the right call given that _get_profile_store is a module-level lru_cache singleton shared across conversations. If the cipher were stored on the shared store instance, concurrent conversations with different secret keys but the same profile_store_dir would race on the cipher. The per-load parameter avoids that cleanly. This aligns with enyst's comment that a broader secrets redesign may simplify this later, but the current approach is correct for the existing architecture.
Coverage: register_builtins_agents in preset/default.py still calls agent_definition_to_factory(agent_def) without cipher. This is fine — all built-in agents use model: inherit, so the store.load path where cipher is consumed is never reached for builtins. No change needed.
Test: The regression test (test_agent_definition_to_factory_decrypts_model_profile) exercises the real encrypt→decrypt round trip (no mocks) and verifies the decrypted API key matches the original plaintext. I confirmed it passes.
Risk: LOW. Backward compatible (cipher defaults to None), focused scope, and the fix improves credential handling rather than introducing new surface area.
HUMAN:
This fixes sub-agent authentication failures when named LLM profiles are encrypted at rest.
AGENT:
Why
When
OH_SECRET_KEYis configured, saved LLM profile secrets are encrypted at rest. File-based sub-agents loaded named profiles without the conversation cipher, so the encrypted API key reached the LLM provider and authentication failed.Summary
Issue Number
Related to #4288.
How to Test
uv run pytest -q tests/sdk/subagent/test_subagent_registry.py tests/sdk/conversation/test_local_conversation_plugins.py— 81 passed.uv run pytest -q tests/agent_server/test_conversation_service.py tests/agent_server/test_conversation_service_plugin.py— 109 passed.include_secrets=Trueand aCipher, load anevidence-coderagent from an explicit plugin throughLocalConversation, and instantiate the registered factory. The smoke test returned modelopenai/gpt-5.6-lunaand confirmed the API key matched the original plaintext.Video/Screenshots
Not applicable; this is an SDK credential-loading fix. The deterministic regression test and plugin smoke-test results are described above.
Type
Notes
Documentation: OpenHands/docs#696