From d20d9635105f43c12e33c336f0a1f49d35496438 Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Wed, 12 Aug 2026 07:40:13 -0400 Subject: [PATCH 1/8] =?UTF-8?q?docs:=20wip=20=E2=80=94=20agentic=20runtime?= =?UTF-8?q?=20capability=20page=20(run=20identity=20+=20Agent=20Sessions?= =?UTF-8?q?=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/docs/capabilities/agentic-runtime.mdx | 229 ++++++++++++++++++ .../img/agentic-runtime/architecture.d2 | 130 ++++++++++ .../img/agentic-runtime/architecture.svg | 106 ++++++++ .../img/agentic-runtime/sandbox-pod.d2 | 100 ++++++++ .../img/agentic-runtime/sandbox-pod.svg | 105 ++++++++ plugins/llms-txt-plugin.js | 4 + sidebars.js | 1 + 7 files changed, 675 insertions(+) create mode 100644 content/docs/capabilities/agentic-runtime.mdx create mode 100644 content/docs/capabilities/img/agentic-runtime/architecture.d2 create mode 100644 content/docs/capabilities/img/agentic-runtime/architecture.svg create mode 100644 content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 create mode 100644 content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx new file mode 100644 index 000000000..41a0ef33e --- /dev/null +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -0,0 +1,229 @@ +--- +# cSpell:ignore runid Codex OpenCode kubelet serviceaccount autonumber refreshable Kata uncredentialed misroute + +title: 'Run AI Agents in Isolated Sandboxes' +sidebar_label: 'AI Agent Runtime' +lang: en-US +description: 'Run any agent harness in a sandbox that holds no secrets, with Pomerium binding every request to a human approval and an attested pod.' +keywords: + [ + AI agents, + agent sandbox, + agentic runtime, + agent sessions API, + run identity, + workload identity, + secret injection, + egress control, + prompt injection, + MCP, + LLM security, + Claude Code, + ] +--- + +# Run AI Agents in Isolated Sandboxes + +You want an agent to do real work: read tickets, query a database, call an internal API, open a pull request. Real work needs credentials. But an agent is a program that takes instructions from untrusted text. Give it an API key, and you have given the key to anyone who can influence its input — a hostile issue comment, a poisoned dependency README, a malicious tool description. + +The usual mitigations try to make the agent careful. Careful is not a security boundary. The alternative is an architecture in which the agent has nothing to leak: it runs in a sandbox with no secrets, and everything it is allowed to reach sits behind a gateway that decides — per request, deterministically — whether this agent, acting for this person, may reach this resource. + +Pomerium — an open-source identity-aware proxy — provides that architecture as one stack. You choose the harness (Claude Code, Codex, Gemini CLI, OpenCode, or your own program) and the model; Pomerium provides the sandbox lifecycle, the identity, the policy, and the egress. The result is not another chat assistant: a Slack bot ships with it, but only as a reference client of the same public API any client you build would use. The product is the layer underneath. + +:::info Availability + +The agentic runtime is in active development and available for early access. [Contact us](https://www.pomerium.com/contact-sales) to get started, or see [MCP support](/docs/capabilities/mcp) for the generally available building blocks it composes with. + +::: + +## Architecture + +![Agentic runtime architecture: clients, the Agent Sessions API, sandbox pods, the Pomerium gateway, and upstream services](./img/agentic-runtime/architecture.svg) + +The layers, from enforcement up to product: + +- **Pomerium** is the gateway. It authenticates every request against your identity provider, evaluates a deterministic policy, injects the credentials the upstream needs, and logs the result. In the reference configuration, every credentialed request a sandbox makes leaves through it. +- **The sandbox pod** runs your agent next to a sidecar. The agent container holds no credentials — no LLM key, no OAuth token, no Kubernetes token. The sidecar holds the pod's credentials and proxies the agent's traffic to Pomerium. +- **The Agent Sessions API** manages sessions: it claims sandbox pods, launches agents in them, streams their output as durable events, and suspends and revives them. +- **Your clients** are whatever your users already work in — a Slack bot, a Teams bot, a web app, a CI job. The bundled Slack bot is a client like any other, with no special access. + +One more party does not appear on the data path but controls all of it: **a person**. No agent acts until a specific person approves a specific run on a consent page, and the agent's credentials stop renewing once that person's identity provider session can no longer be refreshed. + +## The Foundation: Pomerium + +If you have not used Pomerium before, four properties of the underlying proxy make the rest of this page work. If you know Pomerium, skip ahead. + +**Bring your identity provider.** Pomerium [authenticates](/docs/capabilities/authentication) users against any OIDC or SAML identity provider and carries its data — subject, email, groups, custom claims — into every decision. There is no separate user store to manage. + +**Deterministic authorization.** Every request passes through [Pomerium Policy Language](/docs/internals/ppl) — declarative rules over identity, and for MCP servers down to the [individual tool call](/docs/capabilities/mcp/limit-mcp-tools). A policy is not a prompt. It does not get talked out of its answer. + +**Secrets stay at the gateway.** Routes inject credentials into upstream requests as they pass through: API keys referenced from your secret store with `set_request_headers` (`${secret.…}`), per-user OAuth tokens through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth). The client behind the route never holds them. + +**One layer for people and machines.** The same routes and the same policy language govern a person in a browser, a service account, and — with this capability — an agent acting for a person. One audit log covers all three. + +## Run Identity: Two Principals in One Request + +Pomerium historically had two kinds of principal, and an agent is neither. A **user session** carries a person's authority, but it is created by browser SSO, and an agent has no browser. A **service account** is a long-lived machine credential with authority of its own — no person approved any particular use of it. + +An agent needs a policy that can say: + +> Allow this request if Alice approved this run, and if it comes from that one specific pod. + +A **run** makes that sentence expressible. A run is a record that binds two principals: a person, identified by their IdP subject, who approved it; and an executor — one specific Kubernetes pod, identified by claims the cluster's API server attests (namespace, service account, pod name, pod UID). The pod never reports its own identity: those four values are read from the API server and stored on the run as its **seal** — the exact claim set a pod must later present — before anyone is asked to approve anything. + +Pomerium exchanges an approved run for an opaque **run token** (`pom_art_…`). Each token lives one hour; the sandbox renews it for as long as the run is alive, so a long run never means a long-lived credential. A request bearing it carries the approver's identity as ordinary claims and the sealed pod identity as `act.*` claims — actor claims, in the sense of OAuth token exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)) — so the policy above is written in plain PPL: + +```yaml +policy: + allow: + and: + - claim/email: alice@example.com # who approved + - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent # which pod acts +``` + +Three properties are worth pausing on. + +**A run is not a grant.** The run names no routes and confers no access. Each route decides for itself whether it accepts run tokens at all (`bearer_token_format: agentic_run_token`) and which runs it admits (its policy). Approving a run gives the agent exactly what your routes say it may have — nothing arrives bundled. + +**The pod cannot claim a run.** The token exchange takes no run ID as input. The sandbox presents only its own projected service-account token — a short-lived JWT the kubelet writes into the pod, verified against the cluster's own OIDC issuer. Pomerium finds the run whose seal matches the verified claims, byte for byte. A pod cannot ask for another pod's run, because it cannot present another pod's identity. + +**The person stays in control.** Approval happens on a Pomerium-served consent page, behind ordinary SSO, showing the run's prompt, the sealed pod identity, and the MCP servers the agent expects to use — with a Connect button for any that need the person's own OAuth consent. A run can name the one person allowed to approve it, and a revived session always pins approval to the person who approved it first. A run also cannot outlive the approver's identity: each token renewal — roughly every ten minutes — checks that the approver's IdP session is still alive and refreshable. End the person's session at the identity provider, and renewals start failing as soon as the approver's cached IdP token lapses — typically within the hour — and the sandbox winds down. + +The flow, end to end: + +```mermaid +sequenceDiagram + autonumber + participant M as Session manager + participant P as Pomerium + participant A as Alice + participant S as Sandbox sidecar + M->>P: POST /agentic/runs (sealed pod identity, expected approver) + P-->>M: run_id, approval_url + S->>P: POST /agentic/token (projected token) + P-->>S: 400 authorization_pending + A->>P: opens approval_url, signs in, approves + S->>P: POST /agentic/token (projected token) + P-->>S: 200 run token (pom_art_…) + S->>P: request to an upstream route (run token) + P-->>S: policy evaluated, credentials injected +``` + +Each hop carries a different credential — the manager's workload token, the person's SSO session, the pod's projected token, the run token — and none of them can stand in for another. + +Expiry is enforced on every request: Pomerium reads the run record on each one, without a cache, so a run past its end stops the very next request. The same uncached read checks a revocation flag; an operator API for revoking a run early is on the roadmap. + +## The Sandbox: An Agent With Nothing to Leak + +The unit of execution is a two-container pod, and the boundary between the containers is the boundary that matters. + +![Sandbox pod: a credential-free agent container and a sidecar whose Envoy adds the run token toward Pomerium](./img/agentic-runtime/sandbox-pod.svg) + +The **agent container** runs your harness — any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. It holds no credentials. Its Kubernetes service-account token is not mounted. Its LLM configuration points the provider's base URL at loopback — `http://127.0.0.1:9999` — and its API-key variable holds a placeholder that exists only because the harness refuses to start without one. + +The **sidecar container** holds the pod's credentials — the projected token the kubelet writes, and the run token it exchanges that for — and runs Envoy with one loopback listener per permitted upstream. Envoy overwrites the placeholder with the run token and refuses to forward any request when no token is present; fail closed, not open. Each listener forwards to one fixed Pomerium route, configured by the operator. The agent cannot add a destination, and there is no proxy admin port for it to read secrets from. + +In this configuration the real API key never enters the pod at all. It lives on the Pomerium route, resolved from your secret store as the request passes through: + +```yaml +routes: + - from: https://llm.example.com + to: https://api.provider.example + timeout: 0s # streaming responses + bearer_token_format: agentic_run_token + set_request_headers: + Authorization: 'Bearer ${secret.upstream-api-token}' + policy: + allow: + and: + - domain: example.com + - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent +``` + +A sandbox template can instead keep a provider key on the sidecar container, for upstreams you choose not to route through the gateway. Either way the invariant holds: no credential ever exists in the agent container. + +The same pattern covers every kind of upstream. MCP servers that require the person's own account — GitHub, Google, an internal tool — get the approver's OAuth token injected per request through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth); the person connects them once, on the consent page. Databases and internal APIs sit behind routes with their own policies. Git credentials are consumed by an init container that clones the workspace and exits before the agent starts. + +What the agent can reach with a credential is therefore a short, auditable list: the loopback ports its operator configured, each ending at a Pomerium route, each governed by policy, each logged. Everything else — the projected token, the run token, every real credential — lives on the other side of the container boundary. To make Pomerium the only network path as well, and not just the only credentialed one, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. + +Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources — the Kubernetes SIG project announced at KubeCon NA 2025 — so they run on any conforming cluster, managed cloud or on-premises, with warm pools for fast starts and persistent workspace volumes that survive suspension. Kernel-level isolation below the pod is the runtime's choice: agent-sandbox composes with runtime classes such as gVisor and Kata Containers. Support for additional sandbox technologies is planned. + +## The Agent Sessions API + +Run identity and sandboxes are the substrate. The **Agent Sessions API** is the layer that makes them usable from product code: a session manager that owns the pod lifecycle, the approval plumbing, and the conversation, so that a client team never touches claims, seals, or consent pages. + +The API is one Connect RPC service — fourteen verbs over gRPC or plain HTTP+JSON on a single Pomerium route. The model is small: + +- A **session** is one conversation: one client, one approver, one sandbox at a time, one workspace. +- A **turn** is one prompt and the work that follows it. +- An **event** is one fact about a session, on a durable ordered log: `approval_required`, `agent_message`, `tool_call`, `permission_request`, `turn_completed`, `session_ended`, and the rest. Clients subscribe and replay from any sequence number; delivery is at-least-once and order never resets. + +`CreateSession` claims a pod from the warm pool, reads its attested identity from the API server, creates the run sealed to it, and emits the approval link as an event. Your client delivers that link to the person it belongs to — a Slack DM, a PR comment, a page in your app. After approval, `Prompt` drives turns; a `permission_request` event becomes buttons in your UI and `RespondPermission` carries the answer back. Idle sessions suspend, keeping the workspace volume; a `Prompt` on a suspended session revives it on a fresh pod, with a fresh run and a fresh approval pinned to the same approver. + +Client identity works the same way agent identity does: a client is a workload, authenticated by its platform's token — a projected token in a cluster, an OIDC token in CI. There is no long-lived client secret to steal, because the platform issues and rotates the token. On the server side, a per-client registration names which agent templates a client may launch and caps its live sessions, pending approvals, and creation rate. + +```mermaid +sequenceDiagram + autonumber + participant C as Client (Slack bot, CI, your app) + participant H as Agent Sessions API + participant P as Pomerium + participant A as Approver + C->>H: CreateSession + H->>P: create a run sealed to a claimed sandbox pod + H-->>C: event approval_required (approval_url) + C->>A: deliver the link + A->>P: approve on the consent page + Note over H,P: the sandbox exchanges its token and attaches + H-->>C: event state_changed (running) + C->>H: Prompt + H-->>C: events agent_message, tool_call, turn_completed +``` + +## Building a Client + +Clients talk to one API, in whatever way fits their runtime: + +| Tier | You Write | Good For | +| --- | --- | --- | +| SDK (Go, TypeScript, Python) | typed calls; reconnects handled in the SDK | long-lived services | +| Companion sidecar | plain HTTP against a local socket | shell scripts, languages without an SDK | +| Polling | HTTP and a loop | CI jobs with short-lived tokens | + +The SDKs are thin, because the API carries the weight: + +```typescript +const session = await client.createSession({ + template: 'runid', + conversationRef: `build-4711`, + approvalPrompt: 'Deploy build 4711 to staging?', +}); + +const feed = await client.subscribe({sessionId: session.id}); +await client.prompt({sessionId: session.id}, 'Deploy it.'); + +for await (const event of feed) { + // approval_required → deliver the link; agent_message → render; permission_request → ask +} +``` + +Notice what the client code does not contain: no API keys, no OAuth flows, no identity assertions. A client cannot claim to be a person — the approver's identity enters the system only when the person authenticates on the consent page. A client cannot read another client's sessions. A client cannot mint a run token. The security-relevant decisions live in Pomerium, behind routes the client's identity cannot pass, which means a client team cannot compromise them by accident. + +The Slack bot that ships with the platform is built this way, as a reference client: it holds no Kubernetes access, no database, and no special API. If a UX cannot be built from the public event stream alone, that is treated as a missing API, not a reason to grant the bot a back door. + +## What Each Party Cannot Do + +Security arguments in this space tend to list features. The sharper question is what a compromised component can still do. Taking them in turn: + +1. **A compromised agent** holds no secrets to leak. Its credentialed reach is the loopback ports its operator configured, each terminating at a Pomerium route, subject to policy — down to the individual MCP tool — and logged. It can still repeat whatever it was allowed to read, so bound what it reads with route and tool policies, and deny it uncredentialed egress with a NetworkPolicy. +2. **A compromised client** can create sessions within its registration and quotas, and solicit approvals from people the consent page fully informs. It cannot assert a person's identity, mint a run token, or reach any upstream. +3. **A compromised pod identity is not enough.** A stolen projected token binds only to runs sealed to that exact pod, approved by a specific person, while that person's IdP session lives. +4. **A compromised sidecar** holds the projected token and the run token of one pod — both short-lived, both bound to one run and one approver, and useful only at routes whose policies admit that run — plus any provider key an operator chose to keep on the sidecar rather than at the gateway. The blast radius is one session. +5. **A compromised harness** can misroute conversations, and it can bind the wrong — but genuine — sandbox pod to a run; verifying the seal against the Kubernetes API at the authorization server is planned hardening. It holds no static credentials to disclose, and it cannot approve a run, mint a run token, or widen what a run token reaches. +6. **Nobody skips the person.** `act.*` claims exist if and only if a person approved the run. A workload acting for itself authenticates as itself, and policies tell the two apart. + +## Why One Stack + +Every piece of this exists somewhere on its own. Sandbox runtimes isolate a process, and leave credential handling to you. LLM gateways centralize model keys, and know nothing about which person approved which run, or about your database and your Git remotes. Identity vendors now issue agents identities, and do not sit on the data path to enforce them. The work is in the seams: making the sandbox's identity the thing the policy matches on, the approval the thing the token encodes, and the gateway the only credentialed way out. + +Pomerium's agentic runtime is that assembly, with no new trust anchor: your IdP, your cluster's kubelet and API server, and your routes' policies are the roots, and everything above them is derived. You keep the freedom of each piece — any IdP, any conforming Kubernetes cluster, any harness, any model — and the agent that results can do real work while holding nothing worth stealing. diff --git a/content/docs/capabilities/img/agentic-runtime/architecture.d2 b/content/docs/capabilities/img/agentic-runtime/architecture.d2 new file mode 100644 index 000000000..db6636925 --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/architecture.d2 @@ -0,0 +1,130 @@ +# Pomerium Agentic Runtime — the whole system, top to bottom +# +# Regenerate: d2 architecture.d2 architecture.svg + +direction: down + +vars: { + d2-config: { + pad: 40 + } +} + +classes: { + card: { + shape: rectangle + style: { + fill: "#FFFFFF" + stroke: "#6F43E7" + stroke-width: 2 + border-radius: 12 + font-size: 26 + bold: true + font-color: "#2E1065" + } + } + layer: { + style: { + fill: "#F3EEFF" + stroke: "#6F43E7" + stroke-width: 3 + border-radius: 18 + font-size: 32 + bold: true + font-color: "#2E1065" + } + } + plain: { + style: { + fill: "#FFFFFF" + stroke: "#94A3B8" + stroke-width: 2 + border-radius: 12 + font-size: 26 + font-color: "#0F172A" + } + } + actor: { + shape: person + style: { + fill: "#FFFFFF" + stroke: "#6F43E7" + stroke-width: 2 + font-size: 26 + bold: true + font-color: "#2E1065" + } + } + edge: { + style: { + stroke: "#475569" + stroke-width: 3 + font-size: 22 + font-color: "#334155" + } + } +} + +clients: "Your clients — any workload with an identity" { + class: layer + grid-rows: 1 + grid-gap: 24 + + slack: "Slack bot\n(reference client)" {class: card} + teams: "Teams bot" {class: card} + web: "Web app" {class: card} + ci: "CI job" {class: card} +} + +harness: "Agent Sessions API" { + class: layer + grid-rows: 1 + grid-gap: 24 + + sessions: "Session\nlifecycle" {class: card} + events: "Durable\nevent log" {class: card} + orch: "Sandbox\norchestration" {class: card} +} + +sandbox: "Sandbox pod — one per session" { + class: layer + grid-rows: 1 + grid-gap: 24 + + agent: "Agent container\nClaude Code, Codex,\nor your own program\nholds no credentials" {class: card} + sidecar: "Sidecar container\nEnvoy + run token" {class: card} +} + +pomerium: "Pomerium — the only credentialed way out" { + class: layer + grid-rows: 1 + grid-gap: 24 + + idp: "Any IdP\nSSO, groups" {class: card} + policy: "Policy on\nevery request" {class: card} + secrets: "Secret\ninjection" {class: card} + audit: "Audit\nlog" {class: card} +} + +upstreams: "" { + class: layer + style.fill: transparent + style.stroke: transparent + grid-rows: 1 + grid-gap: 24 + + llm: "LLM APIs" {class: plain} + mcp: "MCP servers" {class: plain} + db: "Databases" {class: plain} + git: "Git" {class: plain} +} + +person: "A person\napproves each run" { + class: actor +} + +clients -> harness: "create session, prompt,\nsubscribe to events" {class: edge} +harness -> sandbox: "claims a pod,\nspawns the agent" {class: edge} +sandbox -> pomerium: "egress with\nthe run token" {class: edge} +pomerium -> upstreams: "credentials injected\nat the gateway" {class: edge} +person -> pomerium: "consent page" {class: edge} diff --git a/content/docs/capabilities/img/agentic-runtime/architecture.svg b/content/docs/capabilities/img/agentic-runtime/architecture.svg new file mode 100644 index 000000000..8e7d300c4 --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/architecture.svg @@ -0,0 +1,106 @@ +Your clients — any workload with an identityAgent Sessions APISandbox pod — one per sessionPomerium — the only credentialed way outA personapproves each runSlack bot(reference client)Teams botWeb appCI jobSessionlifecycleDurableevent logSandboxorchestrationAgent containerClaude Code, Codex,or your own programholds no credentialsSidecar containerEnvoy + run tokenAny IdPSSO, groupsPolicy onevery requestSecretinjectionAuditlogLLM APIsMCP serversDatabasesGit create session, prompt,subscribe to eventsclaims a pod,spawns the agentegress withthe run tokencredentials injectedat the gatewayconsent page + + + + + + + diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 new file mode 100644 index 000000000..32ca656ad --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 @@ -0,0 +1,100 @@ +# The sandbox pod — the credential boundary between the two containers +# +# Regenerate: d2 sandbox-pod.d2 sandbox-pod.svg +# cSpell:ignore cardplain agentc sidecarc kubelet + +direction: down + +vars: { + d2-config: { + pad: 40 + } +} + +classes: { + card: { + shape: rectangle + style: { + fill: "#FFFFFF" + stroke: "#6F43E7" + stroke-width: 2 + border-radius: 12 + font-size: 26 + bold: true + font-color: "#2E1065" + } + } + cardplain: { + shape: rectangle + style: { + fill: "#FFFFFF" + stroke: "#94A3B8" + stroke-width: 2 + border-radius: 12 + font-size: 24 + font-color: "#0F172A" + } + } + container: { + style: { + fill: "#F3EEFF" + stroke: "#6F43E7" + stroke-width: 3 + border-radius: 18 + font-size: 30 + bold: true + font-color: "#2E1065" + } + } + outer: { + style: { + fill: "#FAF8FF" + stroke: "#6F43E7" + stroke-width: 4 + border-radius: 22 + font-size: 34 + bold: true + font-color: "#2E1065" + } + } + edge: { + style: { + stroke: "#475569" + stroke-width: 3 + font-size: 22 + font-color: "#334155" + } + } +} + +pod: "Sandbox pod — no inbound connections, no Kubernetes API token" { + class: outer + + agentc: "Agent container — no credentials" { + class: container + + agent: "Any agent harness\nClaude Code, Codex, or your own\nAPI base URL = 127.0.0.1:9999" {class: card} + } + + sidecarc: "Sidecar container — holds every credential" { + class: container + + token: "Projected token\nwritten by the kubelet" {class: cardplain} + sidecar: "Sidecar" {class: card} + envoy: "Envoy\none loopback port\nper upstream" {class: card} + + token -> sidecar {class: edge} + sidecar -> envoy: "run token" {class: edge} + } +} + +pomerium: "Pomerium" { + class: card + style.font-size: 30 + width: 260 + height: 110 +} + +pod.agentc.agent -> pod.sidecarc.envoy: "loopback, no headers" {class: edge} +pod.sidecarc.envoy -> pomerium: "TLS + run token" {class: edge} +pod.sidecarc.sidecar -> pomerium: "projected token in, run token out" {class: edge} diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg new file mode 100644 index 000000000..b3c291b27 --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg @@ -0,0 +1,105 @@ +Sandbox pod — no inbound connections, no Kubernetes API tokenPomeriumAgent container — no credentialsSidecar container — holds every credentialAny agent harnessClaude Code, Codex, or your ownAPI base URL = 127.0.0.1:9999Projected tokenwritten by the kubeletSidecarEnvoyone loopback portper upstream run tokenloopback, no headersTLS + run tokenprojected token in, run token out + + + + + + diff --git a/plugins/llms-txt-plugin.js b/plugins/llms-txt-plugin.js index ce52b4461..362fd214f 100644 --- a/plugins/llms-txt-plugin.js +++ b/plugins/llms-txt-plugin.js @@ -499,6 +499,8 @@ const CURATED_ROUTES = new Set([ '/docs/capabilities/mcp/mcp-upstream-oauth', '/docs/capabilities/mcp/tunnel-to-chatgpt', '/docs/capabilities/mcp/reference', + // Agentic runtime + '/docs/capabilities/agentic-runtime', // Internals '/docs/internals/ppl', '/docs/internals/configuration', @@ -550,6 +552,8 @@ const TIER1_ROUTES = new Set([ '/docs/capabilities/mcp/reference', '/docs/capabilities/mcp/develop-mcp-app', '/docs/capabilities/mcp/tunnel-to-chatgpt', + // Agentic runtime + '/docs/capabilities/agentic-runtime', // Internals '/docs/internals/ppl', '/docs/internals/architecture', diff --git a/sidebars.js b/sidebars.js index 7699fc91a..45dcdae35 100644 --- a/sidebars.js +++ b/sidebars.js @@ -53,6 +53,7 @@ const sidebars = { link: {type: 'doc', id: 'docs/capabilities/mcp'}, items: [{type: 'autogenerated', dirName: 'docs/capabilities/mcp'}], }, + 'docs/capabilities/agentic-runtime', // secondary capabilities 'docs/capabilities/audit-logs', 'docs/capabilities/kubernetes-access', From b765fbc50bed401b1182b1732719206041a1c5c6 Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Wed, 12 Aug 2026 07:44:16 -0400 Subject: [PATCH 2/8] docs: agent sessions api naming, secret-ref example, prose pass --- content/docs/capabilities/agentic-runtime.mdx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx index 41a0ef33e..7a4b78529 100644 --- a/content/docs/capabilities/agentic-runtime.mdx +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -28,7 +28,7 @@ You want an agent to do real work: read tickets, query a database, call an inter The usual mitigations try to make the agent careful. Careful is not a security boundary. The alternative is an architecture in which the agent has nothing to leak: it runs in a sandbox with no secrets, and everything it is allowed to reach sits behind a gateway that decides — per request, deterministically — whether this agent, acting for this person, may reach this resource. -Pomerium — an open-source identity-aware proxy — provides that architecture as one stack. You choose the harness (Claude Code, Codex, Gemini CLI, OpenCode, or your own program) and the model; Pomerium provides the sandbox lifecycle, the identity, the policy, and the egress. The result is not another chat assistant: a Slack bot ships with it, but only as a reference client of the same public API any client you build would use. The product is the layer underneath. +Pomerium — an open-source identity-aware proxy — provides that architecture as one stack. You choose the harness (Claude Code, Codex, Gemini CLI, OpenCode, or your own program) and the model; Pomerium provides the sandbox lifecycle, run identity, policy enforcement, and egress control. A Slack bot ships with it, but only as a reference client of the same public API any client you build would use; the API and the gateway are the product, and the bot is a demo of them. :::info Availability @@ -47,15 +47,15 @@ The layers, from enforcement up to product: - **The Agent Sessions API** manages sessions: it claims sandbox pods, launches agents in them, streams their output as durable events, and suspends and revives them. - **Your clients** are whatever your users already work in — a Slack bot, a Teams bot, a web app, a CI job. The bundled Slack bot is a client like any other, with no special access. -One more party does not appear on the data path but controls all of it: **a person**. No agent acts until a specific person approves a specific run on a consent page, and the agent's credentials stop renewing once that person's identity provider session can no longer be refreshed. +A **person** sits outside the data path but gates all of it: no agent acts until a specific person approves a specific run on a consent page, and the agent's credentials stop renewing once that person's identity provider session can no longer be refreshed. ## The Foundation: Pomerium -If you have not used Pomerium before, four properties of the underlying proxy make the rest of this page work. If you know Pomerium, skip ahead. +Four properties of the underlying proxy underpin everything below. **Bring your identity provider.** Pomerium [authenticates](/docs/capabilities/authentication) users against any OIDC or SAML identity provider and carries its data — subject, email, groups, custom claims — into every decision. There is no separate user store to manage. -**Deterministic authorization.** Every request passes through [Pomerium Policy Language](/docs/internals/ppl) — declarative rules over identity, and for MCP servers down to the [individual tool call](/docs/capabilities/mcp/limit-mcp-tools). A policy is not a prompt. It does not get talked out of its answer. +**Deterministic authorization.** Every request passes through [Pomerium Policy Language](/docs/internals/ppl) — declarative rules over identity, and for MCP servers down to the [individual tool call](/docs/capabilities/mcp/limit-mcp-tools). Policy evaluation is deterministic: the same request gets the same answer, no matter what is in the agent's context window. **Secrets stay at the gateway.** Routes inject credentials into upstream requests as they pass through: API keys referenced from your secret store with `set_request_headers` (`${secret.…}`), per-user OAuth tokens through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth). The client behind the route never holds them. @@ -81,13 +81,13 @@ policy: - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent # which pod acts ``` -Three properties are worth pausing on. +Three properties of runs: -**A run is not a grant.** The run names no routes and confers no access. Each route decides for itself whether it accepts run tokens at all (`bearer_token_format: agentic_run_token`) and which runs it admits (its policy). Approving a run gives the agent exactly what your routes say it may have — nothing arrives bundled. +**A run is not a grant.** The run names no routes and confers no access. Each route decides for itself whether it accepts run tokens at all (`bearer_token_format: agentic_run_token`) and which runs it admits (its policy). Approving a run gives the agent exactly what your routes say it may have. **The pod cannot claim a run.** The token exchange takes no run ID as input. The sandbox presents only its own projected service-account token — a short-lived JWT the kubelet writes into the pod, verified against the cluster's own OIDC issuer. Pomerium finds the run whose seal matches the verified claims, byte for byte. A pod cannot ask for another pod's run, because it cannot present another pod's identity. -**The person stays in control.** Approval happens on a Pomerium-served consent page, behind ordinary SSO, showing the run's prompt, the sealed pod identity, and the MCP servers the agent expects to use — with a Connect button for any that need the person's own OAuth consent. A run can name the one person allowed to approve it, and a revived session always pins approval to the person who approved it first. A run also cannot outlive the approver's identity: each token renewal — roughly every ten minutes — checks that the approver's IdP session is still alive and refreshable. End the person's session at the identity provider, and renewals start failing as soon as the approver's cached IdP token lapses — typically within the hour — and the sandbox winds down. +**The person stays in control.** Approval happens on a Pomerium-served consent page, behind ordinary SSO, showing the run's prompt, the sealed pod identity, and the MCP servers the agent expects to use — with a Connect button for any that need the person's own OAuth consent. A run can name the one person allowed to approve it, and a revived session always pins approval to the person who approved it first. A run also cannot outlive the approver's identity: each token renewal, roughly every ten minutes, checks that the approver's IdP session is still alive and refreshable. End the person's session at the identity provider, and renewals start failing once the approver's cached IdP token lapses, typically within the hour, and the sandbox winds down. The flow, end to end: @@ -113,9 +113,9 @@ Each hop carries a different credential — the manager's workload token, the pe Expiry is enforced on every request: Pomerium reads the run record on each one, without a cache, so a run past its end stops the very next request. The same uncached read checks a revocation flag; an operator API for revoking a run early is on the roadmap. -## The Sandbox: An Agent With Nothing to Leak +## The Sandbox -The unit of execution is a two-container pod, and the boundary between the containers is the boundary that matters. +The unit of execution is a two-container pod. The security boundary is between the two containers. ![Sandbox pod: a credential-free agent container and a sidecar whose Envoy adds the run token toward Pomerium](./img/agentic-runtime/sandbox-pod.svg) @@ -144,7 +144,7 @@ A sandbox template can instead keep a provider key on the sidecar container, for The same pattern covers every kind of upstream. MCP servers that require the person's own account — GitHub, Google, an internal tool — get the approver's OAuth token injected per request through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth); the person connects them once, on the consent page. Databases and internal APIs sit behind routes with their own policies. Git credentials are consumed by an init container that clones the workspace and exits before the agent starts. -What the agent can reach with a credential is therefore a short, auditable list: the loopback ports its operator configured, each ending at a Pomerium route, each governed by policy, each logged. Everything else — the projected token, the run token, every real credential — lives on the other side of the container boundary. To make Pomerium the only network path as well, and not just the only credentialed one, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. +What the agent can reach with a credential is therefore a short, auditable list: the loopback ports its operator configured. Each one ends at a Pomerium route with its own policy, and every request is logged. Everything else — the projected token, the run token, every real credential — lives on the other side of the container boundary. To make Pomerium the only network path as well, and not just the only credentialed one, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources — the Kubernetes SIG project announced at KubeCon NA 2025 — so they run on any conforming cluster, managed cloud or on-premises, with warm pools for fast starts and persistent workspace volumes that survive suspension. Kernel-level isolation below the pod is the runtime's choice: agent-sandbox composes with runtime classes such as gVisor and Kata Containers. Support for additional sandbox technologies is planned. @@ -190,7 +190,7 @@ Clients talk to one API, in whatever way fits their runtime: | Companion sidecar | plain HTTP against a local socket | shell scripts, languages without an SDK | | Polling | HTTP and a loop | CI jobs with short-lived tokens | -The SDKs are thin, because the API carries the weight: +The SDKs are thin wrappers; most of the behavior lives in the API: ```typescript const session = await client.createSession({ @@ -207,15 +207,15 @@ for await (const event of feed) { } ``` -Notice what the client code does not contain: no API keys, no OAuth flows, no identity assertions. A client cannot claim to be a person — the approver's identity enters the system only when the person authenticates on the consent page. A client cannot read another client's sessions. A client cannot mint a run token. The security-relevant decisions live in Pomerium, behind routes the client's identity cannot pass, which means a client team cannot compromise them by accident. +The client code contains no API keys, no OAuth flows, and no identity assertions. A client cannot claim to be a person — the approver's identity enters the system only when the person authenticates on the consent page. A client cannot read another client's sessions. A client cannot mint a run token. The security-relevant decisions live in Pomerium, behind routes the client's identity cannot pass, which means a client team cannot compromise them by accident. -The Slack bot that ships with the platform is built this way, as a reference client: it holds no Kubernetes access, no database, and no special API. If a UX cannot be built from the public event stream alone, that is treated as a missing API, not a reason to grant the bot a back door. +The Slack bot that ships with the platform is built this way, as a reference client: it has no Kubernetes access, no database of its own, and nothing the public API does not offer. If a UX cannot be built from the public event stream alone, that is treated as a missing API, not a reason to grant the bot a back door. ## What Each Party Cannot Do -Security arguments in this space tend to list features. The sharper question is what a compromised component can still do. Taking them in turn: +Assume each component is compromised and ask what it can still do: -1. **A compromised agent** holds no secrets to leak. Its credentialed reach is the loopback ports its operator configured, each terminating at a Pomerium route, subject to policy — down to the individual MCP tool — and logged. It can still repeat whatever it was allowed to read, so bound what it reads with route and tool policies, and deny it uncredentialed egress with a NetworkPolicy. +1. **A compromised agent** holds no secrets. Its credentialed reach is the loopback ports its operator configured, each terminating at a Pomerium route, subject to policy — down to the individual MCP tool — and logged. It can still repeat whatever it was allowed to read, so bound what it reads with route and tool policies, and deny it uncredentialed egress with a NetworkPolicy. 2. **A compromised client** can create sessions within its registration and quotas, and solicit approvals from people the consent page fully informs. It cannot assert a person's identity, mint a run token, or reach any upstream. 3. **A compromised pod identity is not enough.** A stolen projected token binds only to runs sealed to that exact pod, approved by a specific person, while that person's IdP session lives. 4. **A compromised sidecar** holds the projected token and the run token of one pod — both short-lived, both bound to one run and one approver, and useful only at routes whose policies admit that run — plus any provider key an operator chose to keep on the sidecar rather than at the gateway. The blast radius is one session. @@ -224,6 +224,6 @@ Security arguments in this space tend to list features. The sharper question is ## Why One Stack -Every piece of this exists somewhere on its own. Sandbox runtimes isolate a process, and leave credential handling to you. LLM gateways centralize model keys, and know nothing about which person approved which run, or about your database and your Git remotes. Identity vendors now issue agents identities, and do not sit on the data path to enforce them. The work is in the seams: making the sandbox's identity the thing the policy matches on, the approval the thing the token encodes, and the gateway the only credentialed way out. +Every piece of this exists somewhere on its own. Sandbox runtimes isolate a process, and leave credential handling to you. LLM gateways centralize model keys, and know nothing about which person approved which run, or about your database and your Git remotes. Identity vendors now issue agents identities, and do not sit on the data path to enforce them. The hard part is integration: the policy has to match on the sandbox's attested identity, the token has to encode the approval, and the gateway has to be the only credentialed path out. -Pomerium's agentic runtime is that assembly, with no new trust anchor: your IdP, your cluster's kubelet and API server, and your routes' policies are the roots, and everything above them is derived. You keep the freedom of each piece — any IdP, any conforming Kubernetes cluster, any harness, any model — and the agent that results can do real work while holding nothing worth stealing. +Pomerium's agentic runtime is that integration, with no new trust anchor: your IdP, your cluster's kubelet and API server, and your routes' policies are the roots, and everything above them is derived. You keep the freedom of each piece — any IdP, any conforming Kubernetes cluster, any harness, any model — and the agent can do real work without ever holding a credential. From ea40600ec0d40f561d0cc12295ff3aa6686ccede Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Wed, 12 Aug 2026 09:05:18 -0400 Subject: [PATCH 3/8] docs: remove ai-writing patterns per researched antipattern catalog --- content/docs/capabilities/agentic-runtime.mdx | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx index 7a4b78529..40f424f3f 100644 --- a/content/docs/capabilities/agentic-runtime.mdx +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -26,13 +26,13 @@ keywords: You want an agent to do real work: read tickets, query a database, call an internal API, open a pull request. Real work needs credentials. But an agent is a program that takes instructions from untrusted text. Give it an API key, and you have given the key to anyone who can influence its input — a hostile issue comment, a poisoned dependency README, a malicious tool description. -The usual mitigations try to make the agent careful. Careful is not a security boundary. The alternative is an architecture in which the agent has nothing to leak: it runs in a sandbox with no secrets, and everything it is allowed to reach sits behind a gateway that decides — per request, deterministically — whether this agent, acting for this person, may reach this resource. +The usual mitigations try to make the agent careful. Careful is not a security boundary. The alternative is an architecture in which the agent has nothing to leak: it runs in a sandbox with no secrets, and everything it is allowed to reach sits behind a gateway that decides, per request and deterministically, whether this agent, acting for this person, may reach this resource. -Pomerium — an open-source identity-aware proxy — provides that architecture as one stack. You choose the harness (Claude Code, Codex, Gemini CLI, OpenCode, or your own program) and the model; Pomerium provides the sandbox lifecycle, run identity, policy enforcement, and egress control. A Slack bot ships with it, but only as a reference client of the same public API any client you build would use; the API and the gateway are the product, and the bot is a demo of them. +Pomerium — an open-source identity-aware proxy — provides that architecture as one stack. You choose the harness (Claude Code, Codex, Gemini CLI, OpenCode, or your own program) and the model; Pomerium provides the sandbox lifecycle, run identity, policy enforcement, and egress control. A Slack bot ships with it as a reference client, built on the same public API as anything you build. :::info Availability -The agentic runtime is in active development and available for early access. [Contact us](https://www.pomerium.com/contact-sales) to get started, or see [MCP support](/docs/capabilities/mcp) for the generally available building blocks it composes with. +The agentic runtime is in active development and available for early access. [Contact us](https://www.pomerium.com/contact-sales) to get started, or see [MCP support](/docs/capabilities/mcp) for the generally available building blocks it is built on. ::: @@ -43,17 +43,17 @@ The agentic runtime is in active development and available for early access. [Co The layers, from enforcement up to product: - **Pomerium** is the gateway. It authenticates every request against your identity provider, evaluates a deterministic policy, injects the credentials the upstream needs, and logs the result. In the reference configuration, every credentialed request a sandbox makes leaves through it. -- **The sandbox pod** runs your agent next to a sidecar. The agent container holds no credentials — no LLM key, no OAuth token, no Kubernetes token. The sidecar holds the pod's credentials and proxies the agent's traffic to Pomerium. +- **The sandbox pod** runs your agent next to a sidecar. The agent container holds no credentials of any kind. The sidecar holds the pod's credentials and proxies the agent's traffic to Pomerium. - **The Agent Sessions API** manages sessions: it claims sandbox pods, launches agents in them, streams their output as durable events, and suspends and revives them. -- **Your clients** are whatever your users already work in — a Slack bot, a Teams bot, a web app, a CI job. The bundled Slack bot is a client like any other, with no special access. +- **Your clients** are whatever your users already work in — a Slack bot, a Teams bot, a web app, a CI job. The bundled Slack bot is one such client, with no special access. -A **person** sits outside the data path but gates all of it: no agent acts until a specific person approves a specific run on a consent page, and the agent's credentials stop renewing once that person's identity provider session can no longer be refreshed. +A **person** gates all of this from outside the data path: no agent acts until a specific person approves a specific run on a consent page, and the agent's credentials stop renewing once that person's identity provider session can no longer be refreshed. -## The Foundation: Pomerium +## What Pomerium Provides -Four properties of the underlying proxy underpin everything below. +This capability builds on four properties of the proxy itself. -**Bring your identity provider.** Pomerium [authenticates](/docs/capabilities/authentication) users against any OIDC or SAML identity provider and carries its data — subject, email, groups, custom claims — into every decision. There is no separate user store to manage. +**Bring your identity provider.** Pomerium [authenticates](/docs/capabilities/authentication) users against any OIDC or SAML identity provider and carries its data (subject, email, groups, custom claims) into every decision. There is no separate user store to manage. **Deterministic authorization.** Every request passes through [Pomerium Policy Language](/docs/internals/ppl) — declarative rules over identity, and for MCP servers down to the [individual tool call](/docs/capabilities/mcp/limit-mcp-tools). Policy evaluation is deterministic: the same request gets the same answer, no matter what is in the agent's context window. @@ -61,7 +61,7 @@ Four properties of the underlying proxy underpin everything below. **One layer for people and machines.** The same routes and the same policy language govern a person in a browser, a service account, and — with this capability — an agent acting for a person. One audit log covers all three. -## Run Identity: Two Principals in One Request +## Run Identity Pomerium historically had two kinds of principal, and an agent is neither. A **user session** carries a person's authority, but it is created by browser SSO, and an agent has no browser. A **service account** is a long-lived machine credential with authority of its own — no person approved any particular use of it. @@ -69,7 +69,7 @@ An agent needs a policy that can say: > Allow this request if Alice approved this run, and if it comes from that one specific pod. -A **run** makes that sentence expressible. A run is a record that binds two principals: a person, identified by their IdP subject, who approved it; and an executor — one specific Kubernetes pod, identified by claims the cluster's API server attests (namespace, service account, pod name, pod UID). The pod never reports its own identity: those four values are read from the API server and stored on the run as its **seal** — the exact claim set a pod must later present — before anyone is asked to approve anything. +A **run** makes that sentence expressible. A run is a record that binds two principals: a person, identified by their IdP subject, who approved it; and an executor — one specific Kubernetes pod, identified by claims the cluster's API server attests (namespace, service account, pod name, pod UID). The pod never reports its own identity: those four values are read from the API server and stored on the run as its **seal** (the exact claim set a pod must later present) before anyone is asked to approve anything. Pomerium exchanges an approved run for an opaque **run token** (`pom_art_…`). Each token lives one hour; the sandbox renews it for as long as the run is alive, so a long run never means a long-lived credential. A request bearing it carries the approver's identity as ordinary claims and the sealed pod identity as `act.*` claims — actor claims, in the sense of OAuth token exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)) — so the policy above is written in plain PPL: @@ -83,7 +83,7 @@ policy: Three properties of runs: -**A run is not a grant.** The run names no routes and confers no access. Each route decides for itself whether it accepts run tokens at all (`bearer_token_format: agentic_run_token`) and which runs it admits (its policy). Approving a run gives the agent exactly what your routes say it may have. +**A run is not a grant.** Approving one gives the agent exactly what your routes say it may have: each route opts into run tokens (`bearer_token_format: agentic_run_token`) and filters runs with its own policy. **The pod cannot claim a run.** The token exchange takes no run ID as input. The sandbox presents only its own projected service-account token — a short-lived JWT the kubelet writes into the pod, verified against the cluster's own OIDC issuer. Pomerium finds the run whose seal matches the verified claims, byte for byte. A pod cannot ask for another pod's run, because it cannot present another pod's identity. @@ -109,19 +109,19 @@ sequenceDiagram P-->>S: policy evaluated, credentials injected ``` -Each hop carries a different credential — the manager's workload token, the person's SSO session, the pod's projected token, the run token — and none of them can stand in for another. +Each hop carries a different credential: the manager's workload token, the person's SSO session, the pod's projected token, the run token. None of them can stand in for another. Expiry is enforced on every request: Pomerium reads the run record on each one, without a cache, so a run past its end stops the very next request. The same uncached read checks a revocation flag; an operator API for revoking a run early is on the roadmap. ## The Sandbox -The unit of execution is a two-container pod. The security boundary is between the two containers. +A sandbox is a two-container pod, and the security boundary runs between the two containers. ![Sandbox pod: a credential-free agent container and a sidecar whose Envoy adds the run token toward Pomerium](./img/agentic-runtime/sandbox-pod.svg) -The **agent container** runs your harness — any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. It holds no credentials. Its Kubernetes service-account token is not mounted. Its LLM configuration points the provider's base URL at loopback — `http://127.0.0.1:9999` — and its API-key variable holds a placeholder that exists only because the harness refuses to start without one. +The **agent container** runs your harness, any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. It holds no credentials. Its Kubernetes service-account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API-key variable holds a placeholder that exists only because the harness refuses to start without one. -The **sidecar container** holds the pod's credentials — the projected token the kubelet writes, and the run token it exchanges that for — and runs Envoy with one loopback listener per permitted upstream. Envoy overwrites the placeholder with the run token and refuses to forward any request when no token is present; fail closed, not open. Each listener forwards to one fixed Pomerium route, configured by the operator. The agent cannot add a destination, and there is no proxy admin port for it to read secrets from. +The **sidecar container** holds the pod's credentials — the projected token the kubelet writes, and the run token it exchanges that for — and runs Envoy with one loopback listener per permitted upstream. Envoy overwrites the placeholder with the run token and refuses to forward any request when no token is present. Each listener forwards to one fixed Pomerium route, configured by the operator. The agent cannot add a destination, and there is no proxy admin port for it to read secrets from. In this configuration the real API key never enters the pod at all. It lives on the Pomerium route, resolved from your secret store as the request passes through: @@ -140,25 +140,25 @@ routes: - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent ``` -A sandbox template can instead keep a provider key on the sidecar container, for upstreams you choose not to route through the gateway. Either way the invariant holds: no credential ever exists in the agent container. +A sandbox template can instead keep a provider key on the sidecar container, for upstreams you choose not to route through the gateway. Either way, no credential ever exists in the agent container. The same pattern covers every kind of upstream. MCP servers that require the person's own account — GitHub, Google, an internal tool — get the approver's OAuth token injected per request through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth); the person connects them once, on the consent page. Databases and internal APIs sit behind routes with their own policies. Git credentials are consumed by an init container that clones the workspace and exits before the agent starts. -What the agent can reach with a credential is therefore a short, auditable list: the loopback ports its operator configured. Each one ends at a Pomerium route with its own policy, and every request is logged. Everything else — the projected token, the run token, every real credential — lives on the other side of the container boundary. To make Pomerium the only network path as well, and not just the only credentialed one, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. +The agent's entire credentialed surface is a list you can read: the loopback ports its operator configured. Each one ends at a Pomerium route with its own policy, and every request is logged. Everything else, including the run token itself, lives on the other side of the container boundary. To make Pomerium the only network path as well, and not just the only credentialed one, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. -Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources — the Kubernetes SIG project announced at KubeCon NA 2025 — so they run on any conforming cluster, managed cloud or on-premises, with warm pools for fast starts and persistent workspace volumes that survive suspension. Kernel-level isolation below the pod is the runtime's choice: agent-sandbox composes with runtime classes such as gVisor and Kata Containers. Support for additional sandbox technologies is planned. +Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources (the Kubernetes SIG project announced at KubeCon NA 2025), so they run on any conforming cluster, managed cloud or on-premises, with warm pools for fast starts and persistent workspace volumes that survive suspension. Kernel-level isolation below the pod is the runtime's choice: agent-sandbox works with runtime classes such as gVisor and Kata Containers. Support for additional sandbox technologies is planned. ## The Agent Sessions API -Run identity and sandboxes are the substrate. The **Agent Sessions API** is the layer that makes them usable from product code: a session manager that owns the pod lifecycle, the approval plumbing, and the conversation, so that a client team never touches claims, seals, or consent pages. +The **Agent Sessions API** makes run identity and sandboxes usable from product code. It is a session manager: it owns the pod lifecycle and the approval flow, and it holds the conversation, so a client team never handles seals or consent pages. -The API is one Connect RPC service — fourteen verbs over gRPC or plain HTTP+JSON on a single Pomerium route. The model is small: +The API is one Connect RPC service — fourteen verbs over gRPC or plain HTTP+JSON on a single Pomerium route — built around three nouns: - A **session** is one conversation: one client, one approver, one sandbox at a time, one workspace. - A **turn** is one prompt and the work that follows it. - An **event** is one fact about a session, on a durable ordered log: `approval_required`, `agent_message`, `tool_call`, `permission_request`, `turn_completed`, `session_ended`, and the rest. Clients subscribe and replay from any sequence number; delivery is at-least-once and order never resets. -`CreateSession` claims a pod from the warm pool, reads its attested identity from the API server, creates the run sealed to it, and emits the approval link as an event. Your client delivers that link to the person it belongs to — a Slack DM, a PR comment, a page in your app. After approval, `Prompt` drives turns; a `permission_request` event becomes buttons in your UI and `RespondPermission` carries the answer back. Idle sessions suspend, keeping the workspace volume; a `Prompt` on a suspended session revives it on a fresh pod, with a fresh run and a fresh approval pinned to the same approver. +`CreateSession` claims a pod from the warm pool, reads its attested identity from the API server, creates the run sealed to it, and emits the approval link as an event. Your client delivers that link to the person it belongs to — a Slack DM, a PR comment, a page in your app. After approval, `Prompt` drives turns; a `permission_request` event becomes buttons in your UI and `RespondPermission` carries the answer back. Idle sessions suspend, keeping the workspace volume; a `Prompt` on a suspended session revives it on a new pod with a new run; the approval is requested again and pinned to the same approver. Client identity works the same way agent identity does: a client is a workload, authenticated by its platform's token — a projected token in a cluster, an OIDC token in CI. There is no long-lived client secret to steal, because the platform issues and rotates the token. On the server side, a per-client registration names which agent templates a client may launch and caps its live sessions, pending approvals, and creation rate. @@ -207,23 +207,23 @@ for await (const event of feed) { } ``` -The client code contains no API keys, no OAuth flows, and no identity assertions. A client cannot claim to be a person — the approver's identity enters the system only when the person authenticates on the consent page. A client cannot read another client's sessions. A client cannot mint a run token. The security-relevant decisions live in Pomerium, behind routes the client's identity cannot pass, which means a client team cannot compromise them by accident. +The client code contains no API keys and no OAuth flows. A client cannot claim to be a person — the approver's identity enters the system only when the person signs in on the consent page — and it cannot read another client's sessions or mint a run token. The security-relevant decisions live in Pomerium, behind routes the client's identity cannot pass, so a client team cannot compromise them by accident. -The Slack bot that ships with the platform is built this way, as a reference client: it has no Kubernetes access, no database of its own, and nothing the public API does not offer. If a UX cannot be built from the public event stream alone, that is treated as a missing API, not a reason to grant the bot a back door. +The Slack bot that ships with the platform is built this way, as a reference client: it has no Kubernetes access and nothing the public API does not offer. If a UX cannot be built from the public event stream alone, that is treated as a missing API, not a reason to grant the bot a back door. ## What Each Party Cannot Do Assume each component is compromised and ask what it can still do: 1. **A compromised agent** holds no secrets. Its credentialed reach is the loopback ports its operator configured, each terminating at a Pomerium route, subject to policy — down to the individual MCP tool — and logged. It can still repeat whatever it was allowed to read, so bound what it reads with route and tool policies, and deny it uncredentialed egress with a NetworkPolicy. -2. **A compromised client** can create sessions within its registration and quotas, and solicit approvals from people the consent page fully informs. It cannot assert a person's identity, mint a run token, or reach any upstream. +2. **A compromised client** can create sessions within its registration and quotas, and solicit approvals from people the consent page fully informs. It cannot assert a person's identity or mint a run token, and it reaches no upstream. 3. **A compromised pod identity is not enough.** A stolen projected token binds only to runs sealed to that exact pod, approved by a specific person, while that person's IdP session lives. 4. **A compromised sidecar** holds the projected token and the run token of one pod — both short-lived, both bound to one run and one approver, and useful only at routes whose policies admit that run — plus any provider key an operator chose to keep on the sidecar rather than at the gateway. The blast radius is one session. -5. **A compromised harness** can misroute conversations, and it can bind the wrong — but genuine — sandbox pod to a run; verifying the seal against the Kubernetes API at the authorization server is planned hardening. It holds no static credentials to disclose, and it cannot approve a run, mint a run token, or widen what a run token reaches. -6. **Nobody skips the person.** `act.*` claims exist if and only if a person approved the run. A workload acting for itself authenticates as itself, and policies tell the two apart. +5. **A compromised harness** can misroute conversations, and it can bind the wrong — but genuine — sandbox pod to a run; verifying the seal against the Kubernetes API at the authorization server is planned hardening. It holds no static credentials to disclose, and it cannot approve a run or mint a run token; what a run token reaches is decided by route policies it cannot touch. +6. **The approval step cannot be skipped.** `act.*` claims exist if and only if a person approved the run. A workload acting for itself authenticates as itself, and policies tell the two apart. ## Why One Stack -Every piece of this exists somewhere on its own. Sandbox runtimes isolate a process, and leave credential handling to you. LLM gateways centralize model keys, and know nothing about which person approved which run, or about your database and your Git remotes. Identity vendors now issue agents identities, and do not sit on the data path to enforce them. The hard part is integration: the policy has to match on the sandbox's attested identity, the token has to encode the approval, and the gateway has to be the only credentialed path out. +Every piece of this exists somewhere on its own. A sandbox runtime isolates the process but leaves credential handling to you. An LLM gateway centralizes model keys; it has no idea which person approved which run, and it never sees your database or your Git remotes. Identity vendors will issue an agent an identity, but none of them sits on the data path to enforce it. The hard part is wiring these together so they agree: the policy has to match on the sandbox's attested identity, the token has to encode the approval, and the gateway has to be the only credentialed path out. -Pomerium's agentic runtime is that integration, with no new trust anchor: your IdP, your cluster's kubelet and API server, and your routes' policies are the roots, and everything above them is derived. You keep the freedom of each piece — any IdP, any conforming Kubernetes cluster, any harness, any model — and the agent can do real work without ever holding a credential. +Pomerium's agentic runtime is that wiring, and it adds no new trust anchor: every check traces back to your IdP, your cluster's API server, or a route policy you wrote. You still pick the IdP, the cluster, the harness, and the model. The agent does real work without ever holding a credential. From 774981e6e08402fc0d75d0cf2ebfca1d1f852527 Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Wed, 12 Aug 2026 09:20:54 -0400 Subject: [PATCH 4/8] docs: hide envoy as implementation detail, call it the sidecar --- content/docs/capabilities/agentic-runtime.mdx | 4 +- .../img/agentic-runtime/architecture.d2 | 2 +- .../img/agentic-runtime/architecture.svg | 170 ++++++++--------- .../img/agentic-runtime/sandbox-pod.d2 | 8 +- .../img/agentic-runtime/sandbox-pod.svg | 171 +++++++++--------- 5 files changed, 176 insertions(+), 179 deletions(-) diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx index 40f424f3f..1cacb2424 100644 --- a/content/docs/capabilities/agentic-runtime.mdx +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -117,11 +117,11 @@ Expiry is enforced on every request: Pomerium reads the run record on each one, A sandbox is a two-container pod, and the security boundary runs between the two containers. -![Sandbox pod: a credential-free agent container and a sidecar whose Envoy adds the run token toward Pomerium](./img/agentic-runtime/sandbox-pod.svg) +![Sandbox pod: a credential-free agent container and a sidecar that adds the run token toward Pomerium](./img/agentic-runtime/sandbox-pod.svg) The **agent container** runs your harness, any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. It holds no credentials. Its Kubernetes service-account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API-key variable holds a placeholder that exists only because the harness refuses to start without one. -The **sidecar container** holds the pod's credentials — the projected token the kubelet writes, and the run token it exchanges that for — and runs Envoy with one loopback listener per permitted upstream. Envoy overwrites the placeholder with the run token and refuses to forward any request when no token is present. Each listener forwards to one fixed Pomerium route, configured by the operator. The agent cannot add a destination, and there is no proxy admin port for it to read secrets from. +The **sidecar container** holds the pod's credentials — the projected token the kubelet writes, and the run token it exchanges that for — and proxies each permitted upstream on its own loopback port. It overwrites the placeholder with the run token and refuses to forward any request when no token is present. Each port forwards to one fixed Pomerium route, configured by the operator. The agent cannot add a destination, and there is no proxy admin port for it to read secrets from. In this configuration the real API key never enters the pod at all. It lives on the Pomerium route, resolved from your secret store as the request passes through: diff --git a/content/docs/capabilities/img/agentic-runtime/architecture.d2 b/content/docs/capabilities/img/agentic-runtime/architecture.d2 index db6636925..12d0d4a13 100644 --- a/content/docs/capabilities/img/agentic-runtime/architecture.d2 +++ b/content/docs/capabilities/img/agentic-runtime/architecture.d2 @@ -92,7 +92,7 @@ sandbox: "Sandbox pod — one per session" { grid-gap: 24 agent: "Agent container\nClaude Code, Codex,\nor your own program\nholds no credentials" {class: card} - sidecar: "Sidecar container\nEnvoy + run token" {class: card} + sidecar: "Sidecar container\nholds the run token" {class: card} } pomerium: "Pomerium — the only credentialed way out" { diff --git a/content/docs/capabilities/img/agentic-runtime/architecture.svg b/content/docs/capabilities/img/agentic-runtime/architecture.svg index 8e7d300c4..42fe5032a 100644 --- a/content/docs/capabilities/img/agentic-runtime/architecture.svg +++ b/content/docs/capabilities/img/agentic-runtime/architecture.svg @@ -1,17 +1,17 @@ -Your clients — any workload with an identityAgent Sessions APISandbox pod — one per sessionPomerium — the only credentialed way outA personapproves each runSlack bot(reference client)Teams botWeb appCI jobSessionlifecycleDurableevent logSandboxorchestrationAgent containerClaude Code, Codex,or your own programholds no credentialsSidecar containerEnvoy + run tokenAny IdPSSO, groupsPolicy onevery requestSecretinjectionAuditlogLLM APIsMCP serversDatabasesGit create session, prompt,subscribe to eventsclaims a pod,spawns the agentegress withthe run tokencredentials injectedat the gatewayconsent page - + .d2-3574065157 .fill-N1{fill:#0A0F25;} + .d2-3574065157 .fill-N2{fill:#676C7E;} + .d2-3574065157 .fill-N3{fill:#9499AB;} + .d2-3574065157 .fill-N4{fill:#CFD2DD;} + .d2-3574065157 .fill-N5{fill:#DEE1EB;} + .d2-3574065157 .fill-N6{fill:#EEF1F8;} + .d2-3574065157 .fill-N7{fill:#FFFFFF;} + .d2-3574065157 .fill-B1{fill:#0D32B2;} + .d2-3574065157 .fill-B2{fill:#0D32B2;} + .d2-3574065157 .fill-B3{fill:#E3E9FD;} + .d2-3574065157 .fill-B4{fill:#E3E9FD;} + .d2-3574065157 .fill-B5{fill:#EDF0FD;} + .d2-3574065157 .fill-B6{fill:#F7F8FE;} + .d2-3574065157 .fill-AA2{fill:#4A6FF3;} + .d2-3574065157 .fill-AA4{fill:#EDF0FD;} + .d2-3574065157 .fill-AA5{fill:#F7F8FE;} + .d2-3574065157 .fill-AB4{fill:#EDF0FD;} + .d2-3574065157 .fill-AB5{fill:#F7F8FE;} + .d2-3574065157 .stroke-N1{stroke:#0A0F25;} + .d2-3574065157 .stroke-N2{stroke:#676C7E;} + .d2-3574065157 .stroke-N3{stroke:#9499AB;} + .d2-3574065157 .stroke-N4{stroke:#CFD2DD;} + .d2-3574065157 .stroke-N5{stroke:#DEE1EB;} + .d2-3574065157 .stroke-N6{stroke:#EEF1F8;} + .d2-3574065157 .stroke-N7{stroke:#FFFFFF;} + .d2-3574065157 .stroke-B1{stroke:#0D32B2;} + .d2-3574065157 .stroke-B2{stroke:#0D32B2;} + .d2-3574065157 .stroke-B3{stroke:#E3E9FD;} + .d2-3574065157 .stroke-B4{stroke:#E3E9FD;} + .d2-3574065157 .stroke-B5{stroke:#EDF0FD;} + .d2-3574065157 .stroke-B6{stroke:#F7F8FE;} + .d2-3574065157 .stroke-AA2{stroke:#4A6FF3;} + .d2-3574065157 .stroke-AA4{stroke:#EDF0FD;} + .d2-3574065157 .stroke-AA5{stroke:#F7F8FE;} + .d2-3574065157 .stroke-AB4{stroke:#EDF0FD;} + .d2-3574065157 .stroke-AB5{stroke:#F7F8FE;} + .d2-3574065157 .background-color-N1{background-color:#0A0F25;} + .d2-3574065157 .background-color-N2{background-color:#676C7E;} + .d2-3574065157 .background-color-N3{background-color:#9499AB;} + .d2-3574065157 .background-color-N4{background-color:#CFD2DD;} + .d2-3574065157 .background-color-N5{background-color:#DEE1EB;} + .d2-3574065157 .background-color-N6{background-color:#EEF1F8;} + .d2-3574065157 .background-color-N7{background-color:#FFFFFF;} + .d2-3574065157 .background-color-B1{background-color:#0D32B2;} + .d2-3574065157 .background-color-B2{background-color:#0D32B2;} + .d2-3574065157 .background-color-B3{background-color:#E3E9FD;} + .d2-3574065157 .background-color-B4{background-color:#E3E9FD;} + .d2-3574065157 .background-color-B5{background-color:#EDF0FD;} + .d2-3574065157 .background-color-B6{background-color:#F7F8FE;} + .d2-3574065157 .background-color-AA2{background-color:#4A6FF3;} + .d2-3574065157 .background-color-AA4{background-color:#EDF0FD;} + .d2-3574065157 .background-color-AA5{background-color:#F7F8FE;} + .d2-3574065157 .background-color-AB4{background-color:#EDF0FD;} + .d2-3574065157 .background-color-AB5{background-color:#F7F8FE;} + .d2-3574065157 .color-N1{color:#0A0F25;} + .d2-3574065157 .color-N2{color:#676C7E;} + .d2-3574065157 .color-N3{color:#9499AB;} + .d2-3574065157 .color-N4{color:#CFD2DD;} + .d2-3574065157 .color-N5{color:#DEE1EB;} + .d2-3574065157 .color-N6{color:#EEF1F8;} + .d2-3574065157 .color-N7{color:#FFFFFF;} + .d2-3574065157 .color-B1{color:#0D32B2;} + .d2-3574065157 .color-B2{color:#0D32B2;} + .d2-3574065157 .color-B3{color:#E3E9FD;} + .d2-3574065157 .color-B4{color:#E3E9FD;} + .d2-3574065157 .color-B5{color:#EDF0FD;} + .d2-3574065157 .color-B6{color:#F7F8FE;} + .d2-3574065157 .color-AA2{color:#4A6FF3;} + .d2-3574065157 .color-AA4{color:#EDF0FD;} + .d2-3574065157 .color-AA5{color:#F7F8FE;} + .d2-3574065157 .color-AB4{color:#EDF0FD;} + .d2-3574065157 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-3574065157);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-3574065157);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-3574065157);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-3574065157);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-3574065157);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-3574065157);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-3574065157);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-3574065157);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>Your clients — any workload with an identityAgent Sessions APISandbox pod — one per sessionPomerium — the only credentialed way outA personapproves each runSlack bot(reference client)Teams botWeb appCI jobSessionlifecycleDurableevent logSandboxorchestrationAgent containerClaude Code, Codex,or your own programholds no credentialsSidecar containerholds the run tokenAny IdPSSO, groupsPolicy onevery requestSecretinjectionAuditlogLLM APIsMCP serversDatabasesGit create session, prompt,subscribe to eventsclaims a pod,spawns the agentegress withthe run tokencredentials injectedat the gatewayconsent page + - - - + + + diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 index 32ca656ad..6bab2dc45 100644 --- a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 @@ -80,11 +80,9 @@ pod: "Sandbox pod — no inbound connections, no Kubernetes API token" { class: container token: "Projected token\nwritten by the kubelet" {class: cardplain} - sidecar: "Sidecar" {class: card} - envoy: "Envoy\none loopback port\nper upstream" {class: card} + sidecar: "Sidecar\none loopback port per upstream\nadds the run token" {class: card} token -> sidecar {class: edge} - sidecar -> envoy: "run token" {class: edge} } } @@ -95,6 +93,6 @@ pomerium: "Pomerium" { height: 110 } -pod.agentc.agent -> pod.sidecarc.envoy: "loopback, no headers" {class: edge} -pod.sidecarc.envoy -> pomerium: "TLS + run token" {class: edge} +pod.agentc.agent -> pod.sidecarc.sidecar: "loopback, no headers" {class: edge} +pod.sidecarc.sidecar -> pomerium: "TLS + run token" {class: edge} pod.sidecarc.sidecar -> pomerium: "projected token in, run token out" {class: edge} diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg index b3c291b27..7ab30108d 100644 --- a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg @@ -1,17 +1,17 @@ -Sandbox pod — no inbound connections, no Kubernetes API tokenPomeriumAgent container — no credentialsSidecar container — holds every credentialAny agent harnessClaude Code, Codex, or your ownAPI base URL = 127.0.0.1:9999Projected tokenwritten by the kubeletSidecarEnvoyone loopback portper upstream run tokenloopback, no headersTLS + run tokenprojected token in, run token out - - - - - + .d2-535764365 .fill-N1{fill:#0A0F25;} + .d2-535764365 .fill-N2{fill:#676C7E;} + .d2-535764365 .fill-N3{fill:#9499AB;} + .d2-535764365 .fill-N4{fill:#CFD2DD;} + .d2-535764365 .fill-N5{fill:#DEE1EB;} + .d2-535764365 .fill-N6{fill:#EEF1F8;} + .d2-535764365 .fill-N7{fill:#FFFFFF;} + .d2-535764365 .fill-B1{fill:#0D32B2;} + .d2-535764365 .fill-B2{fill:#0D32B2;} + .d2-535764365 .fill-B3{fill:#E3E9FD;} + .d2-535764365 .fill-B4{fill:#E3E9FD;} + .d2-535764365 .fill-B5{fill:#EDF0FD;} + .d2-535764365 .fill-B6{fill:#F7F8FE;} + .d2-535764365 .fill-AA2{fill:#4A6FF3;} + .d2-535764365 .fill-AA4{fill:#EDF0FD;} + .d2-535764365 .fill-AA5{fill:#F7F8FE;} + .d2-535764365 .fill-AB4{fill:#EDF0FD;} + .d2-535764365 .fill-AB5{fill:#F7F8FE;} + .d2-535764365 .stroke-N1{stroke:#0A0F25;} + .d2-535764365 .stroke-N2{stroke:#676C7E;} + .d2-535764365 .stroke-N3{stroke:#9499AB;} + .d2-535764365 .stroke-N4{stroke:#CFD2DD;} + .d2-535764365 .stroke-N5{stroke:#DEE1EB;} + .d2-535764365 .stroke-N6{stroke:#EEF1F8;} + .d2-535764365 .stroke-N7{stroke:#FFFFFF;} + .d2-535764365 .stroke-B1{stroke:#0D32B2;} + .d2-535764365 .stroke-B2{stroke:#0D32B2;} + .d2-535764365 .stroke-B3{stroke:#E3E9FD;} + .d2-535764365 .stroke-B4{stroke:#E3E9FD;} + .d2-535764365 .stroke-B5{stroke:#EDF0FD;} + .d2-535764365 .stroke-B6{stroke:#F7F8FE;} + .d2-535764365 .stroke-AA2{stroke:#4A6FF3;} + .d2-535764365 .stroke-AA4{stroke:#EDF0FD;} + .d2-535764365 .stroke-AA5{stroke:#F7F8FE;} + .d2-535764365 .stroke-AB4{stroke:#EDF0FD;} + .d2-535764365 .stroke-AB5{stroke:#F7F8FE;} + .d2-535764365 .background-color-N1{background-color:#0A0F25;} + .d2-535764365 .background-color-N2{background-color:#676C7E;} + .d2-535764365 .background-color-N3{background-color:#9499AB;} + .d2-535764365 .background-color-N4{background-color:#CFD2DD;} + .d2-535764365 .background-color-N5{background-color:#DEE1EB;} + .d2-535764365 .background-color-N6{background-color:#EEF1F8;} + .d2-535764365 .background-color-N7{background-color:#FFFFFF;} + .d2-535764365 .background-color-B1{background-color:#0D32B2;} + .d2-535764365 .background-color-B2{background-color:#0D32B2;} + .d2-535764365 .background-color-B3{background-color:#E3E9FD;} + .d2-535764365 .background-color-B4{background-color:#E3E9FD;} + .d2-535764365 .background-color-B5{background-color:#EDF0FD;} + .d2-535764365 .background-color-B6{background-color:#F7F8FE;} + .d2-535764365 .background-color-AA2{background-color:#4A6FF3;} + .d2-535764365 .background-color-AA4{background-color:#EDF0FD;} + .d2-535764365 .background-color-AA5{background-color:#F7F8FE;} + .d2-535764365 .background-color-AB4{background-color:#EDF0FD;} + .d2-535764365 .background-color-AB5{background-color:#F7F8FE;} + .d2-535764365 .color-N1{color:#0A0F25;} + .d2-535764365 .color-N2{color:#676C7E;} + .d2-535764365 .color-N3{color:#9499AB;} + .d2-535764365 .color-N4{color:#CFD2DD;} + .d2-535764365 .color-N5{color:#DEE1EB;} + .d2-535764365 .color-N6{color:#EEF1F8;} + .d2-535764365 .color-N7{color:#FFFFFF;} + .d2-535764365 .color-B1{color:#0D32B2;} + .d2-535764365 .color-B2{color:#0D32B2;} + .d2-535764365 .color-B3{color:#E3E9FD;} + .d2-535764365 .color-B4{color:#E3E9FD;} + .d2-535764365 .color-B5{color:#EDF0FD;} + .d2-535764365 .color-B6{color:#F7F8FE;} + .d2-535764365 .color-AA2{color:#4A6FF3;} + .d2-535764365 .color-AA4{color:#EDF0FD;} + .d2-535764365 .color-AA5{color:#F7F8FE;} + .d2-535764365 .color-AB4{color:#EDF0FD;} + .d2-535764365 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-535764365);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-535764365);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-535764365);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-535764365);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-535764365);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-535764365);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-535764365);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-535764365);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>Sandbox pod — no inbound connections, no Kubernetes API tokenPomeriumAgent container — no credentialsSidecar container — holds every credentialAny agent harnessClaude Code, Codex, or your ownAPI base URL = 127.0.0.1:9999Projected tokenwritten by the kubeletSidecarone loopback port per upstreamadds the run token loopback, no headersTLS + run tokenprojected token in, run token out + + + + From 484aa8062a451a923ffee55194e1f6ea7baff948 Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Wed, 12 Aug 2026 19:51:20 -0400 Subject: [PATCH 5/8] =?UTF-8?q?docs:=20theme-aware=20diagrams=20=E2=80=94?= =?UTF-8?q?=20dark=20variants=20switched=20with=20the=20color-mode=20toggl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/docs/capabilities/agentic-runtime.mdx | 16 ++ .../img/agentic-runtime/architecture-dark.d2 | 133 +++++++++++++++ .../img/agentic-runtime/architecture-dark.svg | 106 ++++++++++++ .../img/agentic-runtime/architecture.d2 | 2 +- .../img/agentic-runtime/architecture.svg | 158 ++++++++--------- .../img/agentic-runtime/sandbox-pod-dark.d2 | 102 +++++++++++ .../img/agentic-runtime/sandbox-pod-dark.svg | 104 ++++++++++++ .../img/agentic-runtime/sandbox-pod.d2 | 5 +- .../img/agentic-runtime/sandbox-pod.svg | 160 +++++++++--------- src/css/custom.css | 18 ++ 10 files changed, 642 insertions(+), 162 deletions(-) create mode 100644 content/docs/capabilities/img/agentic-runtime/architecture-dark.d2 create mode 100644 content/docs/capabilities/img/agentic-runtime/architecture-dark.svg create mode 100644 content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.d2 create mode 100644 content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.svg diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx index 1cacb2424..162ca869f 100644 --- a/content/docs/capabilities/agentic-runtime.mdx +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -38,8 +38,16 @@ The agentic runtime is in active development and available for early access. [Co ## Architecture + + ![Agentic runtime architecture: clients, the Agent Sessions API, sandbox pods, the Pomerium gateway, and upstream services](./img/agentic-runtime/architecture.svg) + + +![Agentic runtime architecture: clients, the Agent Sessions API, sandbox pods, the Pomerium gateway, and upstream services](./img/agentic-runtime/architecture-dark.svg) + + + The layers, from enforcement up to product: - **Pomerium** is the gateway. It authenticates every request against your identity provider, evaluates a deterministic policy, injects the credentials the upstream needs, and logs the result. In the reference configuration, every credentialed request a sandbox makes leaves through it. @@ -117,8 +125,16 @@ Expiry is enforced on every request: Pomerium reads the run record on each one, A sandbox is a two-container pod, and the security boundary runs between the two containers. + + ![Sandbox pod: a credential-free agent container and a sidecar that adds the run token toward Pomerium](./img/agentic-runtime/sandbox-pod.svg) + + +![Sandbox pod: a credential-free agent container and a sidecar that adds the run token toward Pomerium](./img/agentic-runtime/sandbox-pod-dark.svg) + + + The **agent container** runs your harness, any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. It holds no credentials. Its Kubernetes service-account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API-key variable holds a placeholder that exists only because the harness refuses to start without one. The **sidecar container** holds the pod's credentials — the projected token the kubelet writes, and the run token it exchanges that for — and proxies each permitted upstream on its own loopback port. It overwrites the placeholder with the run token and refuses to forward any request when no token is present. Each port forwards to one fixed Pomerium route, configured by the operator. The agent cannot add a destination, and there is no proxy admin port for it to read secrets from. diff --git a/content/docs/capabilities/img/agentic-runtime/architecture-dark.d2 b/content/docs/capabilities/img/agentic-runtime/architecture-dark.d2 new file mode 100644 index 000000000..e9b911fd6 --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/architecture-dark.d2 @@ -0,0 +1,133 @@ +# Pomerium Agentic Runtime — the whole system, top to bottom (dark theme) +# +# Regenerate: d2 architecture-dark.d2 architecture-dark.svg + +direction: down + +vars: { + d2-config: { + pad: 40 + theme-overrides: { + N7: "#1b1b1d" + } + } +} + +classes: { + card: { + shape: rectangle + style: { + fill: "#1E1E24" + stroke: "#9F7AEA" + stroke-width: 2 + border-radius: 12 + font-size: 26 + bold: true + font-color: "#EDE9FE" + } + } + layer: { + style: { + fill: "#2A2140" + stroke: "#9F7AEA" + stroke-width: 3 + border-radius: 18 + font-size: 32 + bold: true + font-color: "#EDE9FE" + } + } + plain: { + style: { + fill: "#1E1E24" + stroke: "#64748B" + stroke-width: 2 + border-radius: 12 + font-size: 26 + font-color: "#E2E8F0" + } + } + actor: { + shape: person + style: { + fill: "#2A2140" + stroke: "#9F7AEA" + stroke-width: 2 + font-size: 26 + bold: true + font-color: "#EDE9FE" + } + } + edge: { + style: { + stroke: "#94A3B8" + stroke-width: 3 + font-size: 22 + font-color: "#CBD5E1" + } + } +} + +clients: "Your clients — any workload with an identity" { + class: layer + grid-rows: 1 + grid-gap: 24 + + slack: "Slack bot\n(reference client)" {class: card} + teams: "Teams bot" {class: card} + web: "Web app" {class: card} + ci: "CI job" {class: card} +} + +harness: "Agent Sessions API" { + class: layer + grid-rows: 1 + grid-gap: 24 + + sessions: "Session\nlifecycle" {class: card} + events: "Durable\nevent log" {class: card} + orch: "Sandbox\norchestration" {class: card} +} + +sandbox: "Sandbox pod — one per session" { + class: layer + grid-rows: 1 + grid-gap: 24 + + agent: "Agent container\nClaude Code, Codex,\nor your own program\nholds no credentials" {class: card} + sidecar: "Sidecar container\nholds the run token" {class: card} +} + +pomerium: "Pomerium — the only credentialed way out" { + class: layer + grid-rows: 1 + grid-gap: 24 + + idp: "Any IdP\nSSO, groups" {class: card} + policy: "Policy on\nevery request" {class: card} + secrets: "Secret\ninjection" {class: card} + audit: "Audit\nlog" {class: card} +} + +upstreams: "" { + class: layer + style.fill: transparent + style.stroke: transparent + grid-rows: 1 + grid-gap: 24 + + llm: "LLM APIs" {class: plain} + mcp: "MCP servers" {class: plain} + db: "Databases" {class: plain} + git: "Git" {class: plain} +} + +person: "A person\napproves each run" { + class: actor +} + +clients -> harness: "create session, prompt,\nsubscribe to events" {class: edge} +harness -> sandbox: "claims a pod,\nspawns the agent" {class: edge} +sandbox -> pomerium: "egress with\nthe run token" {class: edge} +pomerium -> upstreams: "credentials injected\nat the gateway" {class: edge} +person -> pomerium: "consent page" {class: edge} diff --git a/content/docs/capabilities/img/agentic-runtime/architecture-dark.svg b/content/docs/capabilities/img/agentic-runtime/architecture-dark.svg new file mode 100644 index 000000000..b4a48c6b8 --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/architecture-dark.svg @@ -0,0 +1,106 @@ +Your clients — any workload with an identityAgent Sessions APISandbox pod — one per sessionPomerium — the only credentialed way outA personapproves each runSlack bot(reference client)Teams botWeb appCI jobSessionlifecycleDurableevent logSandboxorchestrationAgent containerClaude Code, Codex,or your own programholds no credentialsSidecar containerholds the run tokenAny IdPSSO, groupsPolicy onevery requestSecretinjectionAuditlogLLM APIsMCP serversDatabasesGit create session, prompt,subscribe to eventsclaims a pod,spawns the agentegress withthe run tokencredentials injectedat the gatewayconsent page + + + + + + + diff --git a/content/docs/capabilities/img/agentic-runtime/architecture.d2 b/content/docs/capabilities/img/agentic-runtime/architecture.d2 index 12d0d4a13..64712bed5 100644 --- a/content/docs/capabilities/img/agentic-runtime/architecture.d2 +++ b/content/docs/capabilities/img/agentic-runtime/architecture.d2 @@ -47,7 +47,7 @@ classes: { actor: { shape: person style: { - fill: "#FFFFFF" + fill: "#F3EEFF" stroke: "#6F43E7" stroke-width: 2 font-size: 26 diff --git a/content/docs/capabilities/img/agentic-runtime/architecture.svg b/content/docs/capabilities/img/agentic-runtime/architecture.svg index 42fe5032a..c4fdf82d9 100644 --- a/content/docs/capabilities/img/agentic-runtime/architecture.svg +++ b/content/docs/capabilities/img/agentic-runtime/architecture.svg @@ -1,16 +1,16 @@ -Your clients — any workload with an identityAgent Sessions APISandbox pod — one per sessionPomerium — the only credentialed way outA personapproves each runSlack bot(reference client)Teams botWeb appCI jobSessionlifecycleDurableevent logSandboxorchestrationAgent containerClaude Code, Codex,or your own programholds no credentialsSidecar containerholds the run tokenAny IdPSSO, groupsPolicy onevery requestSecretinjectionAuditlogLLM APIsMCP serversDatabasesGit create session, prompt,subscribe to eventsclaims a pod,spawns the agentegress withthe run tokencredentials injectedat the gatewayconsent page + .d2-682026588 .fill-N1{fill:#0A0F25;} + .d2-682026588 .fill-N2{fill:#676C7E;} + .d2-682026588 .fill-N3{fill:#9499AB;} + .d2-682026588 .fill-N4{fill:#CFD2DD;} + .d2-682026588 .fill-N5{fill:#DEE1EB;} + .d2-682026588 .fill-N6{fill:#EEF1F8;} + .d2-682026588 .fill-N7{fill:#FFFFFF;} + .d2-682026588 .fill-B1{fill:#0D32B2;} + .d2-682026588 .fill-B2{fill:#0D32B2;} + .d2-682026588 .fill-B3{fill:#E3E9FD;} + .d2-682026588 .fill-B4{fill:#E3E9FD;} + .d2-682026588 .fill-B5{fill:#EDF0FD;} + .d2-682026588 .fill-B6{fill:#F7F8FE;} + .d2-682026588 .fill-AA2{fill:#4A6FF3;} + .d2-682026588 .fill-AA4{fill:#EDF0FD;} + .d2-682026588 .fill-AA5{fill:#F7F8FE;} + .d2-682026588 .fill-AB4{fill:#EDF0FD;} + .d2-682026588 .fill-AB5{fill:#F7F8FE;} + .d2-682026588 .stroke-N1{stroke:#0A0F25;} + .d2-682026588 .stroke-N2{stroke:#676C7E;} + .d2-682026588 .stroke-N3{stroke:#9499AB;} + .d2-682026588 .stroke-N4{stroke:#CFD2DD;} + .d2-682026588 .stroke-N5{stroke:#DEE1EB;} + .d2-682026588 .stroke-N6{stroke:#EEF1F8;} + .d2-682026588 .stroke-N7{stroke:#FFFFFF;} + .d2-682026588 .stroke-B1{stroke:#0D32B2;} + .d2-682026588 .stroke-B2{stroke:#0D32B2;} + .d2-682026588 .stroke-B3{stroke:#E3E9FD;} + .d2-682026588 .stroke-B4{stroke:#E3E9FD;} + .d2-682026588 .stroke-B5{stroke:#EDF0FD;} + .d2-682026588 .stroke-B6{stroke:#F7F8FE;} + .d2-682026588 .stroke-AA2{stroke:#4A6FF3;} + .d2-682026588 .stroke-AA4{stroke:#EDF0FD;} + .d2-682026588 .stroke-AA5{stroke:#F7F8FE;} + .d2-682026588 .stroke-AB4{stroke:#EDF0FD;} + .d2-682026588 .stroke-AB5{stroke:#F7F8FE;} + .d2-682026588 .background-color-N1{background-color:#0A0F25;} + .d2-682026588 .background-color-N2{background-color:#676C7E;} + .d2-682026588 .background-color-N3{background-color:#9499AB;} + .d2-682026588 .background-color-N4{background-color:#CFD2DD;} + .d2-682026588 .background-color-N5{background-color:#DEE1EB;} + .d2-682026588 .background-color-N6{background-color:#EEF1F8;} + .d2-682026588 .background-color-N7{background-color:#FFFFFF;} + .d2-682026588 .background-color-B1{background-color:#0D32B2;} + .d2-682026588 .background-color-B2{background-color:#0D32B2;} + .d2-682026588 .background-color-B3{background-color:#E3E9FD;} + .d2-682026588 .background-color-B4{background-color:#E3E9FD;} + .d2-682026588 .background-color-B5{background-color:#EDF0FD;} + .d2-682026588 .background-color-B6{background-color:#F7F8FE;} + .d2-682026588 .background-color-AA2{background-color:#4A6FF3;} + .d2-682026588 .background-color-AA4{background-color:#EDF0FD;} + .d2-682026588 .background-color-AA5{background-color:#F7F8FE;} + .d2-682026588 .background-color-AB4{background-color:#EDF0FD;} + .d2-682026588 .background-color-AB5{background-color:#F7F8FE;} + .d2-682026588 .color-N1{color:#0A0F25;} + .d2-682026588 .color-N2{color:#676C7E;} + .d2-682026588 .color-N3{color:#9499AB;} + .d2-682026588 .color-N4{color:#CFD2DD;} + .d2-682026588 .color-N5{color:#DEE1EB;} + .d2-682026588 .color-N6{color:#EEF1F8;} + .d2-682026588 .color-N7{color:#FFFFFF;} + .d2-682026588 .color-B1{color:#0D32B2;} + .d2-682026588 .color-B2{color:#0D32B2;} + .d2-682026588 .color-B3{color:#E3E9FD;} + .d2-682026588 .color-B4{color:#E3E9FD;} + .d2-682026588 .color-B5{color:#EDF0FD;} + .d2-682026588 .color-B6{color:#F7F8FE;} + .d2-682026588 .color-AA2{color:#4A6FF3;} + .d2-682026588 .color-AA4{color:#EDF0FD;} + .d2-682026588 .color-AA5{color:#F7F8FE;} + .d2-682026588 .color-AB4{color:#EDF0FD;} + .d2-682026588 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-682026588);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-682026588);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-682026588);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-682026588);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-682026588);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-682026588);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-682026588);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-682026588);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>Your clients — any workload with an identityAgent Sessions APISandbox pod — one per sessionPomerium — the only credentialed way outA personapproves each runSlack bot(reference client)Teams botWeb appCI jobSessionlifecycleDurableevent logSandboxorchestrationAgent containerClaude Code, Codex,or your own programholds no credentialsSidecar containerholds the run tokenAny IdPSSO, groupsPolicy onevery requestSecretinjectionAuditlogLLM APIsMCP serversDatabasesGit create session, prompt,subscribe to eventsclaims a pod,spawns the agentegress withthe run tokencredentials injectedat the gatewayconsent page diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.d2 b/content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.d2 new file mode 100644 index 000000000..f6f4faa02 --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.d2 @@ -0,0 +1,102 @@ +# The sandbox pod — the credential boundary between the two containers (dark theme) +# +# Regenerate: d2 sandbox-pod-dark.d2 sandbox-pod-dark.svg +# cSpell:ignore cardplain agentc sidecarc kubelet + +direction: down + +vars: { + d2-config: { + pad: 40 + theme-overrides: { + N7: "#1b1b1d" + } + } +} + +classes: { + card: { + shape: rectangle + style: { + fill: "#1E1E24" + stroke: "#9F7AEA" + stroke-width: 2 + border-radius: 12 + font-size: 26 + bold: true + font-color: "#EDE9FE" + } + } + cardplain: { + shape: rectangle + style: { + fill: "#1E1E24" + stroke: "#64748B" + stroke-width: 2 + border-radius: 12 + font-size: 24 + font-color: "#E2E8F0" + } + } + container: { + style: { + fill: "#2A2140" + stroke: "#9F7AEA" + stroke-width: 3 + border-radius: 18 + font-size: 30 + bold: true + font-color: "#EDE9FE" + } + } + outer: { + style: { + fill: "#251C3B" + stroke: "#9F7AEA" + stroke-width: 4 + border-radius: 22 + font-size: 34 + bold: true + font-color: "#EDE9FE" + } + } + edge: { + style: { + stroke: "#94A3B8" + stroke-width: 3 + font-size: 22 + font-color: "#CBD5E1" + } + } +} + +pod: "Sandbox pod — no inbound connections, no Kubernetes API token" { + class: outer + + agentc: "Agent container\nno credentials" { + class: container + + agent: "Any agent harness\nClaude Code, Codex, or your own\nAPI base URL = 127.0.0.1:9999" {class: card} + } + + sidecarc: "Sidecar container\nholds every credential" { + class: container + + token: "Projected token\nwritten by the kubelet" {class: cardplain} + sidecar: "Sidecar\none loopback port per upstream\nadds the run token" {class: card} + + token -> sidecar {class: edge} + } +} + +pomerium: "Pomerium" { + class: card + style.fill: "#2A2140" + style.font-size: 30 + width: 260 + height: 110 +} + +pod.agentc.agent -> pod.sidecarc.sidecar: "loopback, no headers" {class: edge} +pod.sidecarc.sidecar -> pomerium: "TLS + run token" {class: edge} +pod.sidecarc.sidecar -> pomerium: "projected token in, run token out" {class: edge} diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.svg b/content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.svg new file mode 100644 index 000000000..a38c7fbba --- /dev/null +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod-dark.svg @@ -0,0 +1,104 @@ +Sandbox pod — no inbound connections, no Kubernetes API tokenPomeriumAgent containerno credentialsSidecar containerholds every credentialAny agent harnessClaude Code, Codex, or your ownAPI base URL = 127.0.0.1:9999Projected tokenwritten by the kubeletSidecarone loopback port per upstreamadds the run token loopback, no headersTLS + run tokenprojected token in, run token out + + + + + diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 index 6bab2dc45..f13a02919 100644 --- a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.d2 @@ -70,13 +70,13 @@ classes: { pod: "Sandbox pod — no inbound connections, no Kubernetes API token" { class: outer - agentc: "Agent container — no credentials" { + agentc: "Agent container\nno credentials" { class: container agent: "Any agent harness\nClaude Code, Codex, or your own\nAPI base URL = 127.0.0.1:9999" {class: card} } - sidecarc: "Sidecar container — holds every credential" { + sidecarc: "Sidecar container\nholds every credential" { class: container token: "Projected token\nwritten by the kubelet" {class: cardplain} @@ -88,6 +88,7 @@ pod: "Sandbox pod — no inbound connections, no Kubernetes API token" { pomerium: "Pomerium" { class: card + style.fill: "#F3EEFF" style.font-size: 30 width: 260 height: 110 diff --git a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg index 7ab30108d..5815daa37 100644 --- a/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg +++ b/content/docs/capabilities/img/agentic-runtime/sandbox-pod.svg @@ -1,16 +1,16 @@ -Sandbox pod — no inbound connections, no Kubernetes API tokenPomeriumAgent container — no credentialsSidecar container — holds every credentialAny agent harnessClaude Code, Codex, or your ownAPI base URL = 127.0.0.1:9999Projected tokenwritten by the kubeletSidecarone loopback port per upstreamadds the run token loopback, no headersTLS + run tokenprojected token in, run token out - + .d2-1501482626 .fill-N1{fill:#0A0F25;} + .d2-1501482626 .fill-N2{fill:#676C7E;} + .d2-1501482626 .fill-N3{fill:#9499AB;} + .d2-1501482626 .fill-N4{fill:#CFD2DD;} + .d2-1501482626 .fill-N5{fill:#DEE1EB;} + .d2-1501482626 .fill-N6{fill:#EEF1F8;} + .d2-1501482626 .fill-N7{fill:#FFFFFF;} + .d2-1501482626 .fill-B1{fill:#0D32B2;} + .d2-1501482626 .fill-B2{fill:#0D32B2;} + .d2-1501482626 .fill-B3{fill:#E3E9FD;} + .d2-1501482626 .fill-B4{fill:#E3E9FD;} + .d2-1501482626 .fill-B5{fill:#EDF0FD;} + .d2-1501482626 .fill-B6{fill:#F7F8FE;} + .d2-1501482626 .fill-AA2{fill:#4A6FF3;} + .d2-1501482626 .fill-AA4{fill:#EDF0FD;} + .d2-1501482626 .fill-AA5{fill:#F7F8FE;} + .d2-1501482626 .fill-AB4{fill:#EDF0FD;} + .d2-1501482626 .fill-AB5{fill:#F7F8FE;} + .d2-1501482626 .stroke-N1{stroke:#0A0F25;} + .d2-1501482626 .stroke-N2{stroke:#676C7E;} + .d2-1501482626 .stroke-N3{stroke:#9499AB;} + .d2-1501482626 .stroke-N4{stroke:#CFD2DD;} + .d2-1501482626 .stroke-N5{stroke:#DEE1EB;} + .d2-1501482626 .stroke-N6{stroke:#EEF1F8;} + .d2-1501482626 .stroke-N7{stroke:#FFFFFF;} + .d2-1501482626 .stroke-B1{stroke:#0D32B2;} + .d2-1501482626 .stroke-B2{stroke:#0D32B2;} + .d2-1501482626 .stroke-B3{stroke:#E3E9FD;} + .d2-1501482626 .stroke-B4{stroke:#E3E9FD;} + .d2-1501482626 .stroke-B5{stroke:#EDF0FD;} + .d2-1501482626 .stroke-B6{stroke:#F7F8FE;} + .d2-1501482626 .stroke-AA2{stroke:#4A6FF3;} + .d2-1501482626 .stroke-AA4{stroke:#EDF0FD;} + .d2-1501482626 .stroke-AA5{stroke:#F7F8FE;} + .d2-1501482626 .stroke-AB4{stroke:#EDF0FD;} + .d2-1501482626 .stroke-AB5{stroke:#F7F8FE;} + .d2-1501482626 .background-color-N1{background-color:#0A0F25;} + .d2-1501482626 .background-color-N2{background-color:#676C7E;} + .d2-1501482626 .background-color-N3{background-color:#9499AB;} + .d2-1501482626 .background-color-N4{background-color:#CFD2DD;} + .d2-1501482626 .background-color-N5{background-color:#DEE1EB;} + .d2-1501482626 .background-color-N6{background-color:#EEF1F8;} + .d2-1501482626 .background-color-N7{background-color:#FFFFFF;} + .d2-1501482626 .background-color-B1{background-color:#0D32B2;} + .d2-1501482626 .background-color-B2{background-color:#0D32B2;} + .d2-1501482626 .background-color-B3{background-color:#E3E9FD;} + .d2-1501482626 .background-color-B4{background-color:#E3E9FD;} + .d2-1501482626 .background-color-B5{background-color:#EDF0FD;} + .d2-1501482626 .background-color-B6{background-color:#F7F8FE;} + .d2-1501482626 .background-color-AA2{background-color:#4A6FF3;} + .d2-1501482626 .background-color-AA4{background-color:#EDF0FD;} + .d2-1501482626 .background-color-AA5{background-color:#F7F8FE;} + .d2-1501482626 .background-color-AB4{background-color:#EDF0FD;} + .d2-1501482626 .background-color-AB5{background-color:#F7F8FE;} + .d2-1501482626 .color-N1{color:#0A0F25;} + .d2-1501482626 .color-N2{color:#676C7E;} + .d2-1501482626 .color-N3{color:#9499AB;} + .d2-1501482626 .color-N4{color:#CFD2DD;} + .d2-1501482626 .color-N5{color:#DEE1EB;} + .d2-1501482626 .color-N6{color:#EEF1F8;} + .d2-1501482626 .color-N7{color:#FFFFFF;} + .d2-1501482626 .color-B1{color:#0D32B2;} + .d2-1501482626 .color-B2{color:#0D32B2;} + .d2-1501482626 .color-B3{color:#E3E9FD;} + .d2-1501482626 .color-B4{color:#E3E9FD;} + .d2-1501482626 .color-B5{color:#EDF0FD;} + .d2-1501482626 .color-B6{color:#F7F8FE;} + .d2-1501482626 .color-AA2{color:#4A6FF3;} + .d2-1501482626 .color-AA4{color:#EDF0FD;} + .d2-1501482626 .color-AA5{color:#F7F8FE;} + .d2-1501482626 .color-AB4{color:#EDF0FD;} + .d2-1501482626 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-1501482626);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-1501482626);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-1501482626);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-1501482626);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-1501482626);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-1501482626);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-1501482626);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-1501482626);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>Sandbox pod — no inbound connections, no Kubernetes API tokenPomeriumAgent containerno credentialsSidecar containerholds every credentialAny agent harnessClaude Code, Codex, or your ownAPI base URL = 127.0.0.1:9999Projected tokenwritten by the kubeletSidecarone loopback port per upstreamadds the run token loopback, no headersTLS + run tokenprojected token in, run token out + diff --git a/src/css/custom.css b/src/css/custom.css index e48351f33..d4e92e371 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -17,6 +17,24 @@ --docusaurus-highlighted-code-line-bg: #a387ef3d; } +/* + * Theme-aware diagram swapping (e.g. D2-generated SVGs with light/dark + * variants). Mirrors the pattern used by Docusaurus's own ThemedImage + * component: both variants render in markup, and [data-theme] (set by the + * color-mode toggle, not prefers-color-scheme) controls which is visible. + */ +.themedDiagram { + display: none; +} + +[data-theme='light'] .themedDiagram--light { + display: initial; +} + +[data-theme='dark'] .themedDiagram--dark { + display: initial; +} + /* adjust headers because they are huge */ .markdown h1:first-child { --ifm-h1-font-size: 2.2rem; From 63b4110247feab73b3191649df2a98fcebb05838 Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Thu, 3 Sep 2026 10:49:22 -0400 Subject: [PATCH 6/8] docs: restructure agentic runtime page as a reference page Replace the essay-shaped page with a docs-shaped one matching the sibling capability pages: noun headings, a one-paragraph opener, a security-model table, no positioning prose, no bold-label paragraph stacks, no em dashes. Remove all mentions of unshipped features. --- content/docs/capabilities/agentic-runtime.mdx | 136 +++++++----------- 1 file changed, 54 insertions(+), 82 deletions(-) diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx index 162ca869f..134a25117 100644 --- a/content/docs/capabilities/agentic-runtime.mdx +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -1,10 +1,10 @@ --- -# cSpell:ignore runid Codex OpenCode kubelet serviceaccount autonumber refreshable Kata uncredentialed misroute +# cSpell:ignore runid Codex OpenCode kubelet serviceaccount autonumber Kata -title: 'Run AI Agents in Isolated Sandboxes' +title: 'AI Agent Runtime' sidebar_label: 'AI Agent Runtime' lang: en-US -description: 'Run any agent harness in a sandbox that holds no secrets, with Pomerium binding every request to a human approval and an attested pod.' +description: 'Run Claude Code, Codex, or your own agent in a Kubernetes sandbox that holds no credentials. Pomerium ties each request to an approved run and injects secrets at the gateway.' keywords: [ AI agents, @@ -22,17 +22,13 @@ keywords: ] --- -# Run AI Agents in Isolated Sandboxes +# AI Agent Runtime -You want an agent to do real work: read tickets, query a database, call an internal API, open a pull request. Real work needs credentials. But an agent is a program that takes instructions from untrusted text. Give it an API key, and you have given the key to anyone who can influence its input — a hostile issue comment, a poisoned dependency README, a malicious tool description. - -The usual mitigations try to make the agent careful. Careful is not a security boundary. The alternative is an architecture in which the agent has nothing to leak: it runs in a sandbox with no secrets, and everything it is allowed to reach sits behind a gateway that decides, per request and deterministically, whether this agent, acting for this person, may reach this resource. - -Pomerium — an open-source identity-aware proxy — provides that architecture as one stack. You choose the harness (Claude Code, Codex, Gemini CLI, OpenCode, or your own program) and the model; Pomerium provides the sandbox lifecycle, run identity, policy enforcement, and egress control. A Slack bot ships with it as a reference client, built on the same public API as anything you build. +The agentic runtime runs AI agents in Kubernetes sandboxes that hold no credentials. The agent harness (Claude Code, Codex, Gemini CLI, OpenCode, or a program of your own) runs in a pod next to a sidecar. The sidecar forwards the agent's traffic to Pomerium, which checks each request against a run that a person approved, evaluates the route's policy, and adds the credential the upstream needs. This covers LLM APIs, MCP servers, databases, and Git remotes. The Agent Sessions API creates and manages the sandboxes from your own product code; a Slack bot ships with it as a reference client. :::info Availability -The agentic runtime is in active development and available for early access. [Contact us](https://www.pomerium.com/contact-sales) to get started, or see [MCP support](/docs/capabilities/mcp) for the generally available building blocks it is built on. +The agentic runtime is available for early access. [Contact us](https://www.pomerium.com/contact-sales) to get started, or see [MCP support](/docs/capabilities/mcp) for the generally available features it builds on. ::: @@ -48,38 +44,31 @@ The agentic runtime is in active development and available for early access. [Co -The layers, from enforcement up to product: - -- **Pomerium** is the gateway. It authenticates every request against your identity provider, evaluates a deterministic policy, injects the credentials the upstream needs, and logs the result. In the reference configuration, every credentialed request a sandbox makes leaves through it. -- **The sandbox pod** runs your agent next to a sidecar. The agent container holds no credentials of any kind. The sidecar holds the pod's credentials and proxies the agent's traffic to Pomerium. -- **The Agent Sessions API** manages sessions: it claims sandbox pods, launches agents in them, streams their output as durable events, and suspends and revives them. -- **Your clients** are whatever your users already work in — a Slack bot, a Teams bot, a web app, a CI job. The bundled Slack bot is one such client, with no special access. - -A **person** gates all of this from outside the data path: no agent acts until a specific person approves a specific run on a consent page, and the agent's credentials stop renewing once that person's identity provider session can no longer be refreshed. +Pomerium is the gateway. It authenticates every request against your identity provider, evaluates a [Pomerium Policy Language](/docs/internals/ppl) rule, injects credentials into the upstream request, and writes an audit log entry. Policy evaluation is deterministic: the same request gets the same answer regardless of what is in the agent's context window. In the reference configuration, every credentialed request a sandbox makes goes through Pomerium. -## What Pomerium Provides +The sandbox pod holds the agent container and a sidecar. The agent container has no credentials. The sidecar holds the pod's credentials and proxies the agent's traffic to Pomerium. -This capability builds on four properties of the proxy itself. +The Agent Sessions API claims sandbox pods, starts agents in them, streams their output as a durable event log, and suspends and revives them. -**Bring your identity provider.** Pomerium [authenticates](/docs/capabilities/authentication) users against any OIDC or SAML identity provider and carries its data (subject, email, groups, custom claims) into every decision. There is no separate user store to manage. +Clients are the applications your users already work in: a Slack bot, a Teams bot, a web app, or a CI job. Each one talks to the Agent Sessions API with its own workload identity. -**Deterministic authorization.** Every request passes through [Pomerium Policy Language](/docs/internals/ppl) — declarative rules over identity, and for MCP servers down to the [individual tool call](/docs/capabilities/mcp/limit-mcp-tools). Policy evaluation is deterministic: the same request gets the same answer, no matter what is in the agent's context window. +A person approves each run on a consent page served by Pomerium, behind ordinary SSO. The agent's credentials stop renewing when that person's identity provider session can no longer be refreshed. -**Secrets stay at the gateway.** Routes inject credentials into upstream requests as they pass through: API keys referenced from your secret store with `set_request_headers` (`${secret.…}`), per-user OAuth tokens through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth). The client behind the route never holds them. +## How a run works -**One layer for people and machines.** The same routes and the same policy language govern a person in a browser, a service account, and — with this capability — an agent acting for a person. One audit log covers all three. +Pomerium has two kinds of principal for ordinary traffic. A user session carries a person's authority and is created by browser SSO. A service account is a long-lived machine credential with authority of its own. An agent fits neither: it acts on a person's behalf, but it has no browser, and no person approved any particular use of a service account. -## Run Identity +A run is a record that binds the two. The approver is identified by their IdP subject. The executor is one Kubernetes pod, identified by four claims that the cluster's API server attests: namespace, service account name, pod name, and pod UID. The Agent Sessions API reads these values from the API server, not from the pod, and stores them on the run as its seal before anyone is asked to approve. -Pomerium historically had two kinds of principal, and an agent is neither. A **user session** carries a person's authority, but it is created by browser SSO, and an agent has no browser. A **service account** is a long-lived machine credential with authority of its own — no person approved any particular use of it. +### Token exchange -An agent needs a policy that can say: +The sidecar obtains a run token by calling Pomerium with the pod's projected service account token, a short-lived JWT the kubelet writes into the pod and Pomerium verifies against the cluster's OIDC issuer. The request carries no run ID. Pomerium looks up the run whose seal matches the verified claims byte for byte, and returns a token only if that run has been approved. A pod cannot obtain another pod's run, because it cannot present another pod's identity. -> Allow this request if Alice approved this run, and if it comes from that one specific pod. +The run token (`pom_art_…`) is opaque and valid for one hour. The sidecar renews it about every ten minutes for as long as the run is alive. Each renewal checks that the approver's identity provider session is still alive and can be refreshed. If the person signs out at the identity provider, renewals fail once their cached IdP token lapses, typically within the hour, and the sandbox winds down. Pomerium reads the run record on every request without caching, so a run past its end stops the next request. -A **run** makes that sentence expressible. A run is a record that binds two principals: a person, identified by their IdP subject, who approved it; and an executor — one specific Kubernetes pod, identified by claims the cluster's API server attests (namespace, service account, pod name, pod UID). The pod never reports its own identity: those four values are read from the API server and stored on the run as its **seal** (the exact claim set a pod must later present) before anyone is asked to approve anything. +### Policy -Pomerium exchanges an approved run for an opaque **run token** (`pom_art_…`). Each token lives one hour; the sandbox renews it for as long as the run is alive, so a long run never means a long-lived credential. A request bearing it carries the approver's identity as ordinary claims and the sealed pod identity as `act.*` claims — actor claims, in the sense of OAuth token exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)) — so the policy above is written in plain PPL: +A request bearing a run token carries the approver's identity as ordinary claims and the sealed pod identity as `act.*` claims, in the sense of the actor claim in OAuth token exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)). A policy can therefore name both the person and the pod: ```yaml policy: @@ -89,20 +78,16 @@ policy: - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent # which pod acts ``` -Three properties of runs: +Routes accept run tokens only when they opt in with `bearer_token_format: agentic_run_token`. Approving a run grants the agent nothing on its own; each route decides with its own policy which runs it admits. The `act.*` claims exist only when a person approved the run, so a policy can tell an agent acting for a person apart from a workload acting for itself. -**A run is not a grant.** Approving one gives the agent exactly what your routes say it may have: each route opts into run tokens (`bearer_token_format: agentic_run_token`) and filters runs with its own policy. +### Approval -**The pod cannot claim a run.** The token exchange takes no run ID as input. The sandbox presents only its own projected service-account token — a short-lived JWT the kubelet writes into the pod, verified against the cluster's own OIDC issuer. Pomerium finds the run whose seal matches the verified claims, byte for byte. A pod cannot ask for another pod's run, because it cannot present another pod's identity. - -**The person stays in control.** Approval happens on a Pomerium-served consent page, behind ordinary SSO, showing the run's prompt, the sealed pod identity, and the MCP servers the agent expects to use — with a Connect button for any that need the person's own OAuth consent. A run can name the one person allowed to approve it, and a revived session always pins approval to the person who approved it first. A run also cannot outlive the approver's identity: each token renewal, roughly every ten minutes, checks that the approver's IdP session is still alive and refreshable. End the person's session at the identity provider, and renewals start failing once the approver's cached IdP token lapses, typically within the hour, and the sandbox winds down. - -The flow, end to end: +The consent page shows the run's prompt, the sealed pod identity, and the MCP servers the agent expects to use, with a Connect button for any that need the person's own OAuth consent. A run can name the one person allowed to approve it. When a suspended session is revived on a new pod, a new run is created and approval is requested again from the same person. ```mermaid sequenceDiagram autonumber - participant M as Session manager + participant M as Agent Sessions API participant P as Pomerium participant A as Alice participant S as Sandbox sidecar @@ -117,13 +102,9 @@ sequenceDiagram P-->>S: policy evaluated, credentials injected ``` -Each hop carries a different credential: the manager's workload token, the person's SSO session, the pod's projected token, the run token. None of them can stand in for another. - -Expiry is enforced on every request: Pomerium reads the run record on each one, without a cache, so a run past its end stops the very next request. The same uncached read checks a revocation flag; an operator API for revoking a run early is on the roadmap. +Each hop uses a different credential: the Agent Sessions API's workload token, the person's SSO session, the pod's projected token, and the run token. -## The Sandbox - -A sandbox is a two-container pod, and the security boundary runs between the two containers. +## The sandbox pod @@ -135,11 +116,11 @@ A sandbox is a two-container pod, and the security boundary runs between the two -The **agent container** runs your harness, any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. It holds no credentials. Its Kubernetes service-account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API-key variable holds a placeholder that exists only because the harness refuses to start without one. +The agent container runs the harness, which can be any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. Its Kubernetes service account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API key variable holds a placeholder, because most harnesses refuse to start without one. -The **sidecar container** holds the pod's credentials — the projected token the kubelet writes, and the run token it exchanges that for — and proxies each permitted upstream on its own loopback port. It overwrites the placeholder with the run token and refuses to forward any request when no token is present. Each port forwards to one fixed Pomerium route, configured by the operator. The agent cannot add a destination, and there is no proxy admin port for it to read secrets from. +The sidecar container holds the projected token and the run token it exchanges that for. It listens on one loopback port per permitted upstream, and each port forwards to one Pomerium route fixed by the operator. It replaces the placeholder with the run token and refuses to forward a request when it has no token. The agent cannot add a destination, and the sidecar has no admin port the agent can reach. -In this configuration the real API key never enters the pod at all. It lives on the Pomerium route, resolved from your secret store as the request passes through: +The upstream's real credential lives on the Pomerium route and is resolved from your secret store as the request passes through: ```yaml routes: @@ -156,27 +137,23 @@ routes: - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent ``` -A sandbox template can instead keep a provider key on the sidecar container, for upstreams you choose not to route through the gateway. Either way, no credential ever exists in the agent container. - -The same pattern covers every kind of upstream. MCP servers that require the person's own account — GitHub, Google, an internal tool — get the approver's OAuth token injected per request through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth); the person connects them once, on the consent page. Databases and internal APIs sit behind routes with their own policies. Git credentials are consumed by an init container that clones the workspace and exits before the agent starts. +A sandbox template can instead keep a provider key on the sidecar container for upstreams you choose not to route through the gateway. In both cases the agent container has no credential. -The agent's entire credentialed surface is a list you can read: the loopback ports its operator configured. Each one ends at a Pomerium route with its own policy, and every request is logged. Everything else, including the run token itself, lives on the other side of the container boundary. To make Pomerium the only network path as well, and not just the only credentialed one, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. +Other upstreams follow the same pattern. MCP servers that require the person's own account, such as GitHub or Google, receive the approver's OAuth token per request through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth); the person connects them once on the consent page. Databases and internal APIs sit behind routes with their own policies. Git credentials are used by an init container that clones the workspace and exits before the agent starts. -Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources (the Kubernetes SIG project announced at KubeCon NA 2025), so they run on any conforming cluster, managed cloud or on-premises, with warm pools for fast starts and persistent workspace volumes that survive suspension. Kernel-level isolation below the pod is the runtime's choice: agent-sandbox works with runtime classes such as gVisor and Kata Containers. Support for additional sandbox technologies is planned. +The sidecar limits the agent's credentialed traffic to the configured ports. To limit its network traffic as well, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. -## The Agent Sessions API +Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources, so they run on any conforming cluster, managed or on-premises, with warm pools of pre-started pods and persistent workspace volumes that survive suspension. Isolation below the pod is set by the runtime class; agent-sandbox works with gVisor and Kata Containers. -The **Agent Sessions API** makes run identity and sandboxes usable from product code. It is a session manager: it owns the pod lifecycle and the approval flow, and it holds the conversation, so a client team never handles seals or consent pages. +## Agent Sessions API -The API is one Connect RPC service — fourteen verbs over gRPC or plain HTTP+JSON on a single Pomerium route — built around three nouns: +One Connect RPC service, served over gRPC or HTTP+JSON on a single Pomerium route, owns the pod lifecycle and the approval flow and stores the conversation. A client never handles seals or consent pages directly. The API works with three objects: -- A **session** is one conversation: one client, one approver, one sandbox at a time, one workspace. -- A **turn** is one prompt and the work that follows it. -- An **event** is one fact about a session, on a durable ordered log: `approval_required`, `agent_message`, `tool_call`, `permission_request`, `turn_completed`, `session_ended`, and the rest. Clients subscribe and replay from any sequence number; delivery is at-least-once and order never resets. +- A session is one conversation: one client, one approver, one sandbox at a time, one workspace. +- Each prompt, together with the work that follows it, is a turn. +- Events record what happened in a session on a durable, ordered log, such as `approval_required`, `agent_message`, `tool_call`, `permission_request`, `turn_completed`, or `session_ended`. Clients subscribe and can replay from any sequence number. Delivery is at least once and sequence numbers never reset. -`CreateSession` claims a pod from the warm pool, reads its attested identity from the API server, creates the run sealed to it, and emits the approval link as an event. Your client delivers that link to the person it belongs to — a Slack DM, a PR comment, a page in your app. After approval, `Prompt` drives turns; a `permission_request` event becomes buttons in your UI and `RespondPermission` carries the answer back. Idle sessions suspend, keeping the workspace volume; a `Prompt` on a suspended session revives it on a new pod with a new run; the approval is requested again and pinned to the same approver. - -Client identity works the same way agent identity does: a client is a workload, authenticated by its platform's token — a projected token in a cluster, an OIDC token in CI. There is no long-lived client secret to steal, because the platform issues and rotates the token. On the server side, a per-client registration names which agent templates a client may launch and caps its live sessions, pending approvals, and creation rate. +`CreateSession` claims a pod from the warm pool, reads its identity from the API server, creates the run sealed to it, and emits the approval link as an event. The client delivers that link to the approver in whatever form fits: a Slack DM, a pull request comment, a page in the app. After approval, `Prompt` starts turns. A `permission_request` event can be rendered as buttons in the client UI, and `RespondPermission` returns the answer. Idle sessions suspend and keep their workspace volume. A `Prompt` on a suspended session revives it on a new pod with a new run, and the same approver is asked again. ```mermaid sequenceDiagram @@ -196,17 +173,17 @@ sequenceDiagram H-->>C: events agent_message, tool_call, turn_completed ``` -## Building a Client +Clients authenticate as workloads, with a projected token in a cluster or an OIDC token in CI. There is no long-lived client secret; the platform issues and rotates the token. A per-client registration on the server names the agent templates the client may launch and caps its live sessions, pending approvals, and session creation rate. -Clients talk to one API, in whatever way fits their runtime: +## Clients -| Tier | You Write | Good For | +| Tier | You write | Suited to | | --- | --- | --- | -| SDK (Go, TypeScript, Python) | typed calls; reconnects handled in the SDK | long-lived services | +| SDK (Go, TypeScript, Python) | typed calls; the SDK handles reconnects | long-lived services | | Companion sidecar | plain HTTP against a local socket | shell scripts, languages without an SDK | | Polling | HTTP and a loop | CI jobs with short-lived tokens | -The SDKs are thin wrappers; most of the behavior lives in the API: +The SDKs are thin wrappers over the API: ```typescript const session = await client.createSession({ @@ -223,23 +200,18 @@ for await (const event of feed) { } ``` -The client code contains no API keys and no OAuth flows. A client cannot claim to be a person — the approver's identity enters the system only when the person signs in on the consent page — and it cannot read another client's sessions or mint a run token. The security-relevant decisions live in Pomerium, behind routes the client's identity cannot pass, so a client team cannot compromise them by accident. - -The Slack bot that ships with the platform is built this way, as a reference client: it has no Kubernetes access and nothing the public API does not offer. If a UX cannot be built from the public event stream alone, that is treated as a missing API, not a reason to grant the bot a back door. +Client code holds only its own workload token. The approver's identity enters the system when the person signs in on the consent page, so a client cannot assert it, and a client cannot read another client's sessions or obtain a run token. -## What Each Party Cannot Do +The bundled Slack bot is written this way. It has no Kubernetes access and uses only the public API. -Assume each component is compromised and ask what it can still do: +## Security model -1. **A compromised agent** holds no secrets. Its credentialed reach is the loopback ports its operator configured, each terminating at a Pomerium route, subject to policy — down to the individual MCP tool — and logged. It can still repeat whatever it was allowed to read, so bound what it reads with route and tool policies, and deny it uncredentialed egress with a NetworkPolicy. -2. **A compromised client** can create sessions within its registration and quotas, and solicit approvals from people the consent page fully informs. It cannot assert a person's identity or mint a run token, and it reaches no upstream. -3. **A compromised pod identity is not enough.** A stolen projected token binds only to runs sealed to that exact pod, approved by a specific person, while that person's IdP session lives. -4. **A compromised sidecar** holds the projected token and the run token of one pod — both short-lived, both bound to one run and one approver, and useful only at routes whose policies admit that run — plus any provider key an operator chose to keep on the sidecar rather than at the gateway. The blast radius is one session. -5. **A compromised harness** can misroute conversations, and it can bind the wrong — but genuine — sandbox pod to a run; verifying the seal against the Kubernetes API at the authorization server is planned hardening. It holds no static credentials to disclose, and it cannot approve a run or mint a run token; what a run token reaches is decided by route policies it cannot touch. -6. **The approval step cannot be skipped.** `act.*` claims exist if and only if a person approved the run. A workload acting for itself authenticates as itself, and policies tell the two apart. - -## Why One Stack - -Every piece of this exists somewhere on its own. A sandbox runtime isolates the process but leaves credential handling to you. An LLM gateway centralizes model keys; it has no idea which person approved which run, and it never sees your database or your Git remotes. Identity vendors will issue an agent an identity, but none of them sits on the data path to enforce it. The hard part is wiring these together so they agree: the policy has to match on the sandbox's attested identity, the token has to encode the approval, and the gateway has to be the only credentialed path out. +| Component | Credentials it holds | What it can reach | +| --- | --- | --- | +| Agent container | None | The sidecar's loopback ports | +| Sidecar | The pod's projected token and its run token; optionally a provider key kept on the sidecar by template | The Pomerium routes configured for its ports, subject to each route's policy | +| Agent Sessions API | Its own workload token | Pomerium's run endpoints and the Kubernetes API for pods. It cannot approve a run or obtain a run token. | +| Client | Its platform-issued workload token | The Agent Sessions API, within its registration and quotas. It cannot assert a person's identity, obtain a run token, or reach an upstream. | +| Approver | Their SSO session | The consent page | -Pomerium's agentic runtime is that wiring, and it adds no new trust anchor: every check traces back to your IdP, your cluster's API server, or a route policy you wrote. You still pick the IdP, the cluster, the harness, and the model. The agent does real work without ever holding a credential. +A stolen projected token is useful only for runs sealed to that exact pod and approved by a specific person, and only while that person's identity provider session is alive. A run token that leaks is bound to the same run and approver, expires within the hour, and works only at routes whose policies admit that run. An agent under prompt injection can repeat whatever it was allowed to read; route and tool policies, including [per-tool MCP policy](/docs/capabilities/mcp/limit-mcp-tools), bound what that is. From cba2767a7d94efe4f1dfb783971357b8c7dc5648 Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Fri, 4 Sep 2026 13:48:09 -0400 Subject: [PATCH 7/8] wip: manual edits to agentic runtime page --- content/docs/capabilities/agentic-runtime.mdx | 140 ++++++++++++++---- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx index 134a25117..a66579abc 100644 --- a/content/docs/capabilities/agentic-runtime.mdx +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -44,7 +44,7 @@ The agentic runtime is available for early access. [Contact us](https://www.pome -Pomerium is the gateway. It authenticates every request against your identity provider, evaluates a [Pomerium Policy Language](/docs/internals/ppl) rule, injects credentials into the upstream request, and writes an audit log entry. Policy evaluation is deterministic: the same request gets the same answer regardless of what is in the agent's context window. In the reference configuration, every credentialed request a sandbox makes goes through Pomerium. +Pomerium is the single egress point. It authenticates every request against your identity provider, evaluates a [Pomerium Policy Language](/docs/internals/ppl) rule, injects credentials into the upstream request, and writes an audit log entry. The sandbox pod holds the agent container and a sidecar. The agent container has no credentials. The sidecar holds the pod's credentials and proxies the agent's traffic to Pomerium. @@ -54,35 +54,33 @@ Clients are the applications your users already work in: a Slack bot, a Teams bo A person approves each run on a consent page served by Pomerium, behind ordinary SSO. The agent's credentials stop renewing when that person's identity provider session can no longer be refreshed. -## How a run works +## Agentic Run -Pomerium has two kinds of principal for ordinary traffic. A user session carries a person's authority and is created by browser SSO. A service account is a long-lived machine credential with authority of its own. An agent fits neither: it acts on a person's behalf, but it has no browser, and no person approved any particular use of a service account. +An agentic run is one execution of an agent. The work runs in a Kubernetes Sandbox Pod whose identity is the cluster’s attested claims. User has to explicitly authorize each agentic run, which allows Pomerium to stamp that execution with the requesting user’s identity, so every request is both this pod and this person. -A run is a record that binds the two. The approver is identified by their IdP subject. The executor is one Kubernetes pod, identified by four claims that the cluster's API server attests: namespace, service account name, pod name, and pod UID. The Agent Sessions API reads these values from the API server, not from the pod, and stores them on the run as its seal before anyone is asked to approve. +The user explicitly approves each agentic run before it could be executed. -### Token exchange - -The sidecar obtains a run token by calling Pomerium with the pod's projected service account token, a short-lived JWT the kubelet writes into the pod and Pomerium verifies against the cluster's OIDC issuer. The request carries no run ID. Pomerium looks up the run whose seal matches the verified claims byte for byte, and returns a token only if that run has been approved. A pod cannot obtain another pod's run, because it cannot present another pod's identity. - -The run token (`pom_art_…`) is opaque and valid for one hour. The sidecar renews it about every ten minutes for as long as the run is alive. Each renewal checks that the approver's identity provider session is still alive and can be refreshed. If the person signs out at the identity provider, renewals fail once their cached IdP token lapses, typically within the hour, and the sandbox winds down. Pomerium reads the run record on every request without caching, so a run past its end stops the next request. +Once approved, the sandbox pod's sidecar is able to exchange its Kubernetes projected service account token into an agentic run session token to access upstreams behind Pomerium. ### Policy -A request bearing a run token carries the approver's identity as ordinary claims and the sealed pod identity as `act.*` claims, in the sense of the actor claim in OAuth token exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)). A policy can therefore name both the person and the pod: +A request originating from an agentic run workload carries the approver's identity as ordinary claims and the sealed pod identity as `act.*` claims, in the sense of the actor claim in OAuth token exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)). A policy can therefore name both the person and the pod: ```yaml policy: allow: and: - - claim/email: alice@example.com # who approved - - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent # which pod acts + - email: alice@example.com # who approved + - claim/act.kubernetes.io.namespace: prod-troubleshooting # + - claim/act.kubernetes.io.serviceaccount.name: agent # pod's claims + - claim/act.kubernetes.io.serviceaccount.uid: 3a34c5b3-1d18-45d1-a4e8-657034456c1a # ``` -Routes accept run tokens only when they opt in with `bearer_token_format: agentic_run_token`. Approving a run grants the agent nothing on its own; each route decides with its own policy which runs it admits. The `act.*` claims exist only when a person approved the run, so a policy can tell an agent acting for a person apart from a workload acting for itself. +Some routes (i.e. MCP servers) may be excluded from being accessible from agentic run workloads. ### Approval -The consent page shows the run's prompt, the sealed pod identity, and the MCP servers the agent expects to use, with a Connect button for any that need the person's own OAuth consent. A run can name the one person allowed to approve it. When a suspended session is revived on a new pod, a new run is created and approval is requested again from the same person. +The consent page shows the run's prompt, the sealed pod identity, and the MCP servers the agent expects to use. A run can name the one person allowed to approve it. When a suspended session is revived on a new pod, a new run is created and approval is requested again from the same person. ```mermaid sequenceDiagram @@ -104,6 +102,95 @@ sequenceDiagram Each hop uses a different credential: the Agent Sessions API's workload token, the person's SSO session, the pod's projected token, and the run token. +## Agents + +Agents are defined via the Kubernetes CRD and define MCP servers that should be provided to the agent, the Kubernetes Sandbox pool and pod template. + +```yaml +apiVersion: agents.pomerium.com/v1alpha1 +kind: AgentTemplate +metadata: + name: claude + namespace: agentops +spec: + requiredMCPServers: + - name: mcp-1 + url: https://mcp-1.localhost.pomerium.io + - name: mcp-2 + url: https://mcp-2.localhost.pomerium.io + systemPrompt: | + You are a helpful agent. + warmPoolRef: + name: claude-code-runid +``` + +Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources, so they run on any conforming cluster, managed or on-premises, with warm pools of pre-started pods and persistent workspace volumes that survive suspension. + +```yaml +apiVersion: extensions.agents.x-k8s.io/v1beta1 +kind: SandboxWarmPool +metadata: + name: claude-code + namespace: agentops +spec: + replicas: 5 + sandboxTemplateRef: + name: claude-code +``` + +```yaml +apiVersion: extensions.agents.x-k8s.io/v1beta1 +kind: SandboxTemplate +metadata: + name: claude-code + namespace: agentops +spec: + envVarsInjectionPolicy: Allowed + networkPolicyManagement: Managed + podTemplate: + metadata: {} + spec: + automountServiceAccountToken: false + containers: + - name: agent + image: claude-code:dev + volumeMounts: + - mountPath: /workspace + name: workspace + - name: sidecar + command: + - /usr/local/bin/sidecar + - serve + image: agentops-sidecar:dev + volumeMounts: + - mountPath: /var/run/agentic + name: agentic-token + readOnly: true + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + serviceAccountName: sandbox-agent + volumes: + - name: agentic-token + projected: + sources: + - serviceAccountToken: + audience: pomerium-agentic-as + expirationSeconds: 600 + path: token + volumeClaimTemplates: + - metadata: + name: workspace + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +``` + ## The sandbox pod @@ -116,9 +203,9 @@ Each hop uses a different credential: the Agent Sessions API's workload token, t -The agent container runs the harness, which can be any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. Its Kubernetes service account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API key variable holds a placeholder, because most harnesses refuse to start without one. +The agent container runs the harness, which can be any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. Most popular harnesses (Claude, Codex, etc) have ACP support. Its Kubernetes service account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API key variable holds a placeholder, because most harnesses refuse to start without one. The MCP servers are dynamically injected via ACP protocol and similarly point to specific loopback addreses, served by the sidecar. -The sidecar container holds the projected token and the run token it exchanges that for. It listens on one loopback port per permitted upstream, and each port forwards to one Pomerium route fixed by the operator. It replaces the placeholder with the run token and refuses to forward a request when it has no token. The agent cannot add a destination, and the sidecar has no admin port the agent can reach. +The sidecar container holds the Kubernetes service account projected token that it exchanges to agentic run Bearer token. It listens on one loopback port per permitted upstream, and each port forwards to one Pomerium route fixed by the operator. It replaces the placeholder with the run token and refuses to forward a request when it has no token. The agent cannot add a destination, and the sidecar has no admin port the agent can reach. The upstream's real credential lives on the Pomerium route and is resolved from your secret store as the request passes through: @@ -137,23 +224,21 @@ routes: - claim/act.kubernetes.io.serviceaccount.name: sandbox-agent ``` -A sandbox template can instead keep a provider key on the sidecar container for upstreams you choose not to route through the gateway. In both cases the agent container has no credential. - Other upstreams follow the same pattern. MCP servers that require the person's own account, such as GitHub or Google, receive the approver's OAuth token per request through [MCP upstream OAuth](/docs/capabilities/mcp/mcp-upstream-oauth); the person connects them once on the consent page. Databases and internal APIs sit behind routes with their own policies. Git credentials are used by an init container that clones the workspace and exits before the agent starts. The sidecar limits the agent's credentialed traffic to the configured ports. To limit its network traffic as well, add a NetworkPolicy that denies the sandbox all egress except Pomerium and cluster DNS. -Sandboxes are [Kubernetes agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) resources, so they run on any conforming cluster, managed or on-premises, with warm pools of pre-started pods and persistent workspace volumes that survive suspension. Isolation below the pod is set by the runtime class; agent-sandbox works with gVisor and Kata Containers. - ## Agent Sessions API -One Connect RPC service, served over gRPC or HTTP+JSON on a single Pomerium route, owns the pod lifecycle and the approval flow and stores the conversation. A client never handles seals or consent pages directly. The API works with three objects: +API that your applications use to initiate the agentic sessions and stream events back to the UI (i.e. Slack or web). The API interacts internally with Kubernetes Sandbox and Pomerium to abstract your application so that your application only handles with very high level user-facing events. + +Served over gRPC or HTTP+JSON, it owns the pod lifecycle and the approval flow and stores the conversation. A client never handles identity seals or consent pages directly. The API works with three objects: -- A session is one conversation: one client, one approver, one sandbox at a time, one workspace. -- Each prompt, together with the work that follows it, is a turn. -- Events record what happened in a session on a durable, ordered log, such as `approval_required`, `agent_message`, `tool_call`, `permission_request`, `turn_completed`, or `session_ended`. Clients subscribe and can replay from any sequence number. Delivery is at least once and sequence numbers never reset. +- A session that represents one agentic multi-turn conversation. +- Each prompt in a session, together with the work that follows it, is a turn. +- Events record what happened in a session on a durable, ordered log, such as `approval_required`, `agent_message`, `tool_call`, `permission_request`, `turn_completed`, or `session_ended`. Clients subscribe and can replay from any sequence number. -`CreateSession` claims a pod from the warm pool, reads its identity from the API server, creates the run sealed to it, and emits the approval link as an event. The client delivers that link to the approver in whatever form fits: a Slack DM, a pull request comment, a page in the app. After approval, `Prompt` starts turns. A `permission_request` event can be rendered as buttons in the client UI, and `RespondPermission` returns the answer. Idle sessions suspend and keep their workspace volume. A `Prompt` on a suspended session revives it on a new pod with a new run, and the same approver is asked again. +Under the hood, `CreateSession` claims a pod from the warm pool, reads its identity from the API server, creates the agentic run with Pomerium, and emits the approval link as an event. The UI client delivers that link to the human approver in whatever form fits: a Slack DM, a pull request comment, a page in the app. After approval, `Prompt` starts turns. A `permission_request` event can be rendered as buttons in the client UI, and `RespondPermission` returns the answer. Idle sessions suspend and keep their workspace volume. A `Prompt` on a suspended session revives it on a new pod with a new run, and the same approver is asked again. ```mermaid sequenceDiagram @@ -173,7 +258,7 @@ sequenceDiagram H-->>C: events agent_message, tool_call, turn_completed ``` -Clients authenticate as workloads, with a projected token in a cluster or an OIDC token in CI. There is no long-lived client secret; the platform issues and rotates the token. A per-client registration on the server names the agent templates the client may launch and caps its live sessions, pending approvals, and session creation rate. +Clients authenticate as workloads, with a Kubernetes projected token in a cluster or an OIDC token in CI. ## Clients @@ -200,7 +285,7 @@ for await (const event of feed) { } ``` -Client code holds only its own workload token. The approver's identity enters the system when the person signs in on the consent page, so a client cannot assert it, and a client cannot read another client's sessions or obtain a run token. +Client code holds only its own workload token. The approver's identity enters the system when the person signs in on the consent page, so a client cannot assert it, and a client cannot read another client's sessions or obtain a run token to interact as the workload itself. The bundled Slack bot is written this way. It has no Kubernetes access and uses only the public API. @@ -214,4 +299,3 @@ The bundled Slack bot is written this way. It has no Kubernetes access and uses | Client | Its platform-issued workload token | The Agent Sessions API, within its registration and quotas. It cannot assert a person's identity, obtain a run token, or reach an upstream. | | Approver | Their SSO session | The consent page | -A stolen projected token is useful only for runs sealed to that exact pod and approved by a specific person, and only while that person's identity provider session is alive. A run token that leaks is bound to the same run and approver, expires within the hour, and works only at routes whose policies admit that run. An agent under prompt injection can repeat whatever it was allowed to read; route and tool policies, including [per-tool MCP policy](/docs/capabilities/mcp/limit-mcp-tools), bound what that is. From ae8d8e1fb8955dec1de037ef4b44b52db9e3b70d Mon Sep 17 00:00:00 2001 From: Denis Mishin Date: Fri, 4 Sep 2026 13:51:57 -0400 Subject: [PATCH 8/8] grammar --- content/docs/capabilities/agentic-runtime.mdx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/content/docs/capabilities/agentic-runtime.mdx b/content/docs/capabilities/agentic-runtime.mdx index a66579abc..2fde947bf 100644 --- a/content/docs/capabilities/agentic-runtime.mdx +++ b/content/docs/capabilities/agentic-runtime.mdx @@ -56,11 +56,11 @@ A person approves each run on a consent page served by Pomerium, behind ordinary ## Agentic Run -An agentic run is one execution of an agent. The work runs in a Kubernetes Sandbox Pod whose identity is the cluster’s attested claims. User has to explicitly authorize each agentic run, which allows Pomerium to stamp that execution with the requesting user’s identity, so every request is both this pod and this person. +An agentic run is one execution of an agent. The work runs in a Kubernetes Sandbox Pod whose identity is the cluster’s attested claims. The user has to explicitly authorize each agentic run, which allows Pomerium to stamp that execution with the requesting user’s identity, so every request is both this pod and this person. -The user explicitly approves each agentic run before it could be executed. +The user explicitly approves each agentic run before it can be executed. -Once approved, the sandbox pod's sidecar is able to exchange its Kubernetes projected service account token into an agentic run session token to access upstreams behind Pomerium. +Once approved, the sandbox pod's sidecar is able to exchange its Kubernetes projected service account token for an agentic run session token to access upstreams behind Pomerium. ### Policy @@ -76,7 +76,7 @@ policy: - claim/act.kubernetes.io.serviceaccount.uid: 3a34c5b3-1d18-45d1-a4e8-657034456c1a # ``` -Some routes (i.e. MCP servers) may be excluded from being accessible from agentic run workloads. +Some routes (for example, MCP servers) may be excluded from being accessible from agentic run workloads. ### Approval @@ -203,9 +203,9 @@ spec: -The agent container runs the harness, which can be any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. Most popular harnesses (Claude, Codex, etc) have ACP support. Its Kubernetes service account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API key variable holds a placeholder, because most harnesses refuse to start without one. The MCP servers are dynamically injected via ACP protocol and similarly point to specific loopback addreses, served by the sidecar. +The agent container runs the harness, which can be any program that speaks the [Agent Client Protocol](https://agentclientprotocol.com/) on stdio. Most popular harnesses (Claude, Codex, etc.) have ACP support. Its Kubernetes service account token is not mounted. Its LLM configuration points the provider's base URL at loopback (`http://127.0.0.1:9999`), and its API key variable holds a placeholder, because most harnesses refuse to start without one. The MCP servers are dynamically injected via the ACP protocol and similarly point to specific loopback addresses, served by the sidecar. -The sidecar container holds the Kubernetes service account projected token that it exchanges to agentic run Bearer token. It listens on one loopback port per permitted upstream, and each port forwards to one Pomerium route fixed by the operator. It replaces the placeholder with the run token and refuses to forward a request when it has no token. The agent cannot add a destination, and the sidecar has no admin port the agent can reach. +The sidecar container holds the Kubernetes service account projected token that it exchanges for an agentic run Bearer token. It listens on one loopback port per permitted upstream, and each port forwards to one Pomerium route fixed by the operator. It replaces the placeholder with the run token and refuses to forward a request when it has no token. The agent cannot add a destination, and the sidecar has no admin port the agent can reach. The upstream's real credential lives on the Pomerium route and is resolved from your secret store as the request passes through: @@ -230,7 +230,7 @@ The sidecar limits the agent's credentialed traffic to the configured ports. To ## Agent Sessions API -API that your applications use to initiate the agentic sessions and stream events back to the UI (i.e. Slack or web). The API interacts internally with Kubernetes Sandbox and Pomerium to abstract your application so that your application only handles with very high level user-facing events. +API that your applications use to initiate the agentic sessions and stream events back to the UI (for example, Slack or web). The API interacts internally with Kubernetes Sandbox and Pomerium to abstract your application so that your application only handles very high-level user-facing events. Served over gRPC or HTTP+JSON, it owns the pod lifecycle and the approval flow and stores the conversation. A client never handles identity seals or consent pages directly. The API works with three objects: