Skip to content

ADR 0011 — Connection credential isolation across workflows and runs

Status: Accepted (Phase 1 + Phase 2)
Date: 2026-07-07
Related: ADR 0003, Connections reference, engine/services/orchestrator/app/oauth_token_manager.py, engine/services/orchestrator/app/connections.py


Context

Operators asked whether an HTTP connection authenticated as user A in workflow A can leak into workflow B where user B should be used.

MeshFlows separates two concerns:

Layer What is shared Credential risk
HTTP transport httpx connection pool (orchestrator → egress pods) None — auth is per-request headers, not socket state
Credential / token cache Process-wide OAuth and provider token caches Yes — if cache keys are too coarse

Before this ADR:

  • OAuth tokens were cached keyed on connection name only, so two workflows embedding the same name with different client_id / client_secret could reuse the wrong token once OAuth was wired to the execution path.
  • OAuthTokenManager existed but was not used during service_call execution; raw oauth blocks were merged into provider payloads.
  • workflow_call passed the parent merged connection dict to child workflows instead of re-merging registry + child embedded connections.
  • X-Root-Run-ID was sent to providers for future per-run isolation but not consumed by egress caches.

Decision

1. Cache key = connection name + OAuth identity digest

OAuth client-credentials tokens are cached under:

{connection_name}|{sha256(oauth_identity)[:16]}

where oauth_identity is the stable JSON of token_url, client_id, client_secret, scope, and audience after $secret: resolution.

Rationale: Client-credentials tokens are application-level credentials. Runs that share the same resolved OAuth config should share a token. Runs with different configs must not collide even when the connection name is the same (e.g. embedded overrides per workflow).

For delegated OAuth (authorization_methodclient_credentials), the cache key also includes |run:{X-Root-Run-ID} when the orchestrator passes a root run id (Phase 2).

2. Execution path: resolve OAuth before egress

On service_call steps with a connection: that defines oauth:

  1. Resolve $secret: references in the OAuth block.
  2. Obtain a bearer token via OAuthTokenManager.
  3. Strip oauth from the outbound provider payload.
  4. Inject Authorization: Bearer … into default_headers (merged with any existing headers).

Raw client secrets must never be forwarded to egress microservices.

3. Effective connections per workflow document

At each top-level invocation:

effective = registry_connections ∪ embedded_workflow_connections   # embedded wins on name clash

workflow_call must recompute effective_connections_for_workflow(child_doc) from the registry snapshot, not inherit the parent's embedded overrides.

4. Transport pooling unchanged

The shared orchestrator httpx.AsyncClient remains process-wide. This ADR does not introduce per-run HTTP clients; transport reuse is safe because headers are set per request.

5. Phase 2 — per-run isolation for delegated credentials

Mechanism Behaviour
meshflows_egress.isolation Shared helpers: is_delegated_oauth(), egress_cache_key_suffix()
Orchestrator OAuthTokenManager Appends \|run:{root_run_id} when OAuth is delegated and X-Root-Run-ID is set
egress-google-workspace HTTP middleware captures X-Root-Run-ID; token cache includes run suffix when impersonate_user is set
Client credentials Unchanged — tokens remain shared across runs with the same OAuth identity

workflow_runner passes effective_root_run_id into build_connection_fields_for_service_calloauth_token_getter.

6. Future work (Phase 3)

Item Intent
Parallel scope isolation ADR 0003 / 0004 — separate connection views per lane (if ever required)
Additional egress providers Reuse meshflows_egress.isolation wherever provider-side token caches exist

Consequences

  • Workflows A and B with different connection names or different embedded OAuth configs under the same name are isolated for token reuse.
  • Workflows that intentionally share a global registry connection continue to share credentials (by design).
  • Inbound gateway user identity (JWT) is not automatically mapped to outbound connection credentials; use separate connections or explicit impersonate_user (Google Workspace).
  • DELETE /internal/oauth/tokens/{connection_name} clears all cache entries whose key starts with {connection_name}| (credential rotation).

Mapping to code

Mechanism Location
OAuth cache key oauth_token_manager.oauth_token_cache_key()
Strip oauth + inject bearer connections.build_connection_fields_for_service_call()
service_call wiring workflow_runner.run_workflow()
Registry + embedded merge connections.effective_connections_for_workflow()
Child workflow connections workflow_call branch in workflow_runner
Egress run isolation helpers engine/meshflows_egress/isolation.py
Google Workspace run-scoped cache engine/services/google-workspace/app/main.py middleware + _token_cache_key