OpenHands issue #15298 · architecture study

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.

OpenHands-GPT-5.6 · recovered and completed by smolpaws on Engel’s MacBook · 16 July 2026 · source snapshots pinned below

01The short answer

KeepReference-only persistence

Configuration, conversations, profiles, events, and APIs should hold stable IDs—not values, ciphertext, lookup headers, or masked sentinels.

DefaultTyped brokered delivery

When OpenHands knows the target and protocol, a trusted adapter performs the narrow operation without handing the reusable provider credential to the runtime.

FallbackRuntime-visible delivery

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

Risk A

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.

Risk B

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.

Credential boundary and two delivery paths Reference-only configuration enters a trusted control plane. Brokered operations stay outside the runtime while runtime-visible secrets cross into the untrusted runtime. Durable stateCredentialRefno value · no ciphertext Trusted control planeauthorize actor · tenant · bindingresolve metadata · enforce policybroker_invoke | resolve_once Typed integration adapterfixed target + fixed operationcredential stays here Agent runtimeshell · CLI · stdio process · fileplaintext is recoverable here brokered runtime_visible
The delivery mode belongs to a specific binding/use. Ownership and delivery are orthogonal.

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.

Typed broker

A fixed adapter can inject the credential for one target and operation without exposing it to the shell.

ConsumerPreferred modeWhy
Known provider APIbrokeredProtocol and destination are fixed; the adapter can perform authentication.
Remote HTTP MCPbrokeredTransport-level OAuth/header delivery can remain outside the runtime.
stdio MCP in trusted sidecar/servicebrokeredThe credential enters the MCP process, but that process is across the protection seam.
stdio MCP co-located with arbitrary agent coderuntime visibleThe process needs the credential and shares the runtime’s readable environment/process domain.
Generic CLI, environment, or credential fileruntime visibleNo 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

HTTP

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.

MCP authorization spec

stdio

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.

MCP transports · authorization requirements

How to make stdio brokered

  1. Run the MCP server under a separate identity or outside the sandbox.
  2. Give it the credential there, and expose only MCP messages across the seam.
  3. 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.

CyberArk Secretless Broker README

05Why the current code is coupled

Current seamObserved behaviorDesign consequence
SDK secret sourcesStaticSecret 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 catalogStoredConversation 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 launchThe 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 configurationMCPServer 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 editingThe 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 secretsRows 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.

Credentialstable ID · tenant · owner · type · version · status · allowed policy
Materialencrypted payload · wrapped key · nonce · schema version
Bindingconsumer · typed slot · selected mode · adapter · config hash
MCP serverstable ID · version · transport · non-secret configuration
Runtime grantshort-lived · exact runtime · exact operation · never durable

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

OwnershipDefault policyPossible exception
PlatformBroker onlyNone for user-controlled runtimes.
OrganizationBroker onlyExplicit admin-approved shared runtime secret, with recoverability disclosed.
UserPrefer broker or token exchangeRuntime-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

Tavily API
platform-owned
Fixed adapter calls the fixed Tavily target; provider key stays in the credential service.
brokered
Remote MCP OAuth
user-owned
Control plane owns OAuth state/tokens and exposes the remote MCP tool transport.
brokered
stdio MCP beside agent
org-owned
Server needs env credentials inside the same runtime. Admin either accepts recoverability or moves the server.
runtime visible
Codex auth.json
user-owned
Materialize an ephemeral working copy; broker refresh with version/CAS semantics.
hybrid
“Safe” bash syntax
looks constrained
A scanner approves one spelling, but equivalent Python or subprocess code can still read and exfiltrate the value.
not a boundary

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

  1. 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.
  2. Create the credential service.Stable IDs, metadata/material separation, dedicated encryption key ring, versions, revocation, authorization, and audit.
  3. Introduce references and typed bindings.Dual-read legacy StaticSecret/LookupSecret; all new durable writes use IDs only.
  4. Add narrow runtime grants and adapters.Bind actor, tenant, conversation, runtime, credential, operation, target/config hash, expiry, and replay ID.
  5. Change Agent Canvas.Metadata-only listing, write-only rotate/replace, stable MCP IDs, versioned atomic updates, no ciphertext or sentinel round-trips.
  6. Add organization resources and workload identity.Separate management/use permissions; keep automations creator-bound until a real organization workload principal exists.
  7. Prove both delivery paths.Tavily as brokered; Codex as runtime-visible plus brokered refresh; then expand adapter by adapter.
  8. Migrate and remove legacy paths.Deterministic mappings, needs_reconnect for 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

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.