Configuration, conversations, profiles, events, and APIs should hold stable IDs—not values, ciphertext, lookup headers, or masked sentinels.
References are necessary. Boundaries do the protecting.
Durable state should contain credential references only. But a reference does not answer the harder question: which protection domain is allowed to see the value? Broker known protocols; disclose the risk when arbitrary runtime code must receive plaintext.
01The short answer
When OpenHands knows the target and protocol, a trusted adapter performs the narrow operation without handing the reusable provider credential to the runtime.
Some CLIs, files, environment consumers, and co-located stdio servers require plaintext. Admit that boundary honestly; do not claim secrecy from arbitrary code in the same runtime.
Direct response to Engel’s question: no, MCP servers cannot always be brokered merely because MCP has a clear tool interface. Remote HTTP MCP can be brokered at its transport boundary. A stdio MCP server is a subprocess and the MCP authorization specification says it should obtain credentials from the environment. To keep those credentials out of the agent runtime, move that server into a trusted broker domain or build a typed adapter for its upstream protocol.
That distinction agrees with the central direction of issue #15298, but narrows two overclaims: runtime-visible plaintext cannot exist “only in the store,” and a generic egress proxy is not a protocol-independent security boundary.
02Do not collapse two different risks
Credential theft
The runtime reads a reusable value and can copy it somewhere else. A typed broker prevents this because the value never crosses the seam.
Authorized capability abuse
The runtime cannot see the credential, but it may misuse the permitted operation. Solve this with narrow operations, fixed targets, quotas, policy, confirmation, and audit—not by pretending the risk vanished.
03Choose by consumer boundary, not by secret name
The same credential may use different delivery modes for different operations. Select the deployment and consumer below; the answer changes when the consumer moves across the trust seam.
A fixed adapter can inject the credential for one target and operation without exposing it to the shell.
| Consumer | Preferred mode | Why |
|---|---|---|
| Known provider API | brokered | Protocol and destination are fixed; the adapter can perform authentication. |
| Remote HTTP MCP | brokered | Transport-level OAuth/header delivery can remain outside the runtime. |
| stdio MCP in trusted sidecar/service | brokered | The credential enters the MCP process, but that process is across the protection seam. |
| stdio MCP co-located with arbitrary agent code | runtime visible | The process needs the credential and shares the runtime’s readable environment/process domain. |
| Generic CLI, environment, or credential file | runtime visible | No generic adapter knows the downstream protocol. Label and scope the exposure. |
What does not work: scanning shell syntax and allowing “safe-looking” uses. A Turing-complete shell can delegate to Python, read process state, encode output, or choose a new network path. Syntax judgment is useful friction against accidents, not a confidentiality boundary.
04The MCP question, precisely
Remote MCP is naturally brokerable
The MCP authorization specification defines transport-level authorization for HTTP. OpenHands can own OAuth state or headers in the trusted control plane and expose only the MCP tool surface to the runtime.
stdio is a subprocess boundary, not an auth broker
The client launches the server as a subprocess. The same specification says stdio implementations should retrieve credentials from the environment. If that subprocess lives beside arbitrary agent code, the reusable value is in the same trust domain.
How to make stdio brokered
- Run the MCP server under a separate identity or outside the sandbox.
- Give it the credential there, and expose only MCP messages across the seam.
- Or replace its direct upstream authentication with a typed service connector.
A generic egress proxy helps only when all egress is enforced through it and it understands enough of the protocol to inject authentication safely. CyberArk Secretless demonstrates the pattern—and its requirement: each target protocol needs a Service Connector that replaces the authentication handshake. That is evidence for typed adapters, not for magical command inspection.
05Why the current code is coupled
| Current seam | Observed behavior | Design consequence |
|---|---|---|
| SDK secret sources | StaticSecret serializes a value; LookupSecret serializes a URL and auth headers, then performs a broad HTTP GET. | Secret-bearing configuration is portable and participates in durable model validation. source |
| Conversation catalog | StoredConversation extends the complete start request, and catalog startup validates meta.json before registering the conversation. | An optional integration’s malformed credential form can hide the entire conversation. model · load path |
| ACP launch | The ACP process receives the whole non-file secret registry. File credentials live under the durable per-conversation persistence directory. | No least-privilege binding; “runtime-visible” is implicit and durable working copies survive resume. file directory · registry injection |
| MCP configuration | MCPServer stores secret-bearing env, headers, and auth fields directly in the model. | The transport specification and credential material are one serializable object. source |
| Agent Canvas editing | The editor fetches encrypted settings, matches stdio servers by position/name, and substitutes encrypted values for redacted sentinels before saving. | Ciphertext behaves like a browser-visible configuration token; stable identity and atomic per-server updates are missing. recovery path · update path |
| OHE custom secrets | Rows are selected and replaced by both keycloak_user_id and effective org_id. | This partitions a user’s secrets by org context; it is not an organization-owned credential resource. source |
06A small external seam, a deep credential module
Callers should learn one durable abstraction: a typed binding to a stable credential ID. Encryption, ownership, rotation, authorization, grants, adapters, and audit remain behind that seam.
Storage invariant: outside the credential service, durable OpenHands state contains references only. Runtime-visible delivery may create a bounded plaintext working copy in memory, environment, or non-snapshotted runtime storage; that copy is never authoritative.
Ownership and delivery are separate axes
| Ownership | Default policy | Possible exception |
|---|---|---|
| Platform | Broker only | None for user-controlled runtimes. |
| Organization | Broker only | Explicit admin-approved shared runtime secret, with recoverability disclosed. |
| User | Prefer broker or token exchange | Runtime-visible for CLIs/files/env consumers the user chooses. |
Delivery belongs on the binding. A user-owned Codex credential can be materialized for auth.json while refresh is a brokered operation. Rotation updates material/version; the stable reference does not change.
07Worked trace: five consumers, five honest outcomes
platform-owned
user-owned
org-owned
user-owned
looks constrained
The negative row matters: output masking and command name-scanning remain useful defenses against accidental disclosure, but they cannot provide confidentiality from arbitrary code that already received the value.
08Migration order: reliability before elegance
- Split catalog metadata from launch/integration state.Register conversations and load history before credential hydration; one broken integration becomes an integration error, not a missing conversation.
- Create the credential service.Stable IDs, metadata/material separation, dedicated encryption key ring, versions, revocation, authorization, and audit.
- Introduce references and typed bindings.Dual-read legacy
StaticSecret/LookupSecret; all new durable writes use IDs only. - Add narrow runtime grants and adapters.Bind actor, tenant, conversation, runtime, credential, operation, target/config hash, expiry, and replay ID.
- Change Agent Canvas.Metadata-only listing, write-only rotate/replace, stable MCP IDs, versioned atomic updates, no ciphertext or sentinel round-trips.
- Add organization resources and workload identity.Separate management/use permissions; keep automations creator-bound until a real organization workload principal exists.
- Prove both delivery paths.Tavily as brokered; Codex as runtime-visible plus brokered refresh; then expand adapter by adapter.
- Migrate and remove legacy paths.Deterministic mappings,
needs_reconnectfor unrecoverable material, then remove product use of broad lookup and session-key cipher fallback.
09What must be proven
Persistence and failure isolation
- Canary scans across metadata, base state, events, exports, browser traffic, logs, and traces.
- Broker outage leaves catalog and history readable.
- One malformed MCP binding does not disable other integrations.
- Legacy cold resume migrates references without requiring unrelated credentials.
Authorization and capabilities
- Cross-tenant and cross-org IDOR tests.
- Wrong runtime, conversation, target, binding, or config hash is rejected.
- Expiry, replay, membership removal, revocation, and redirect tests.
- Brokered values never enter sandbox-facing env, files, APIs, logs, or traces.
Honest audit language: a broker can record “last used.” For runtime-visible material, OpenHands can only prove “last delivered.” Revocation prevents future resolution; it cannot recall a value already copied by arbitrary code.
10Source map
- OpenHands issue #15298Original reference-only proposal, OHE amendments, and implementation plan.
- Engel’s verification requestThe stdio MCP and generic egress-proxy questions answered by this page.
- MCP authorization specification (2025-06-18)HTTP authorization scope; stdio implementations should retrieve credentials from the environment.
- MCP transport specification (2025-06-18)stdio client launches the MCP server as a subprocess.
- CyberArk Secretless BrokerProtocol-aware Service Connector precedent for injecting authentication outside the application.
- software-agent-sdk snapshotSecret models, conversation persistence, ACP delivery, and MCP configuration.
- agent-canvas snapshotEncrypted settings/sentinel recovery and MCP update behavior.
- OpenHands/OHE snapshotUser/org scoping, encrypted MCP settings, and current control-plane seams.
- openhands-agent OS keyring exampleA useful local secret-store adapter, but not isolation from arbitrary code running as the same OS identity.
Recovery note
The original local Agent Canvas conversation completed the repository/specification research and architecture comparison, then failed three times at the LLM transport boundary with Unexpected completed event: ResponseFailedEvent before writing or publishing the artifact. Its recovered decision record was used as the spine of this page; code and public sources were rechecked before publication.