Security

Four layers, stacked: who can talk to the plane at all, who can run which agent, which connectors an agent may call, and a durable record of every decision. This page explains each layer and, importantly, where the edges of what the plane can see and enforce actually are.

Layer 1 — can you talk to the plane at all?

Every request needs a credential (an API key or JWT) unless you've deliberately left auth off for local development. Each credential carries one or more permissions — read, write, or admin — and every route requires a specific one. A client key with only read cannot create a run; a key without admin cannot open the Admin dashboard, full stop, regardless of how much it can otherwise do. See Admin login for the exact config shape.

Layer 2 — can this credential run this agent?

By default, a valid client credential with write can create a run against any agent. If you need finer control than that — "this key may only run the support-bot agent, not the finance one" — add an agents:<id>:run grant to that key's permissions. This only applies to the run-create routes, not cancel/delete/store access, which still fall back to the plain read/write check.

Layer 3 — can this agent call this external service?

This is the connector policy layer, covered in full on Connectors & HITL. Short version: a connector call is denied unless a matching grant exists (static in config, or created live in Admin), and you can force specific tools to require human approval regardless of the grant. Every decision — allow, deny, or pending — gets written to a durable audit trail on SQL backends, searchable from Admin → Audit.

Fail-closed by default

If a security-relevant check can't be evaluated cleanly — a database write for an audit record fails, a required config value is missing, Redis is unreachable when a shared session lookup needs it — the plane's default behavior is to deny, not to quietly let the request through. In production mode, serve refuses to even start with an insecure combination (e.g. runner tokens configured but no matching tenant allow-list) rather than booting into a state that looks secure but isn't. See Why fail-closed for the reasoning.

Tenancy — what "multi-tenant" means here today

Every resource (agent, thread, run) carries a flat tenant_id — this is isolation by a tag on each row, not a full organization/team/workspace hierarchy with its own nested permissions. Checkpoint state is isolated the same way, by a key prefix rather than a separate database column. If you're using runner tokens (multiple runner fleets sharing one control plane), you must also configure RUNNER_TENANTS_<kind> per runner kind — see Multi-tenant keys for the full setup and what happens if you skip it (the plane refuses to boot rather than silently trusting an unbound tenant claim).

What the plane genuinely cannot see or control

Being precise about this matters more than sounding impressive. The plane does not see raw LLM prompts or completions by default — it sees run-level metadata and whatever your agent code explicitly streams back as events. If you want full prompt/completion capture, you add your own OTel/observability proxy; the plane doesn't do this for you automatically, on purpose (that data is often sensitive and shouldn't flow through infrastructure that didn't ask for it). Likewise, connector policy only governs calls that actually go through a configured connector — if your agent code has its own hardcoded credential to some other service and calls it directly, nothing here can see or stop that. Governing access you didn't hand out yourself isn't a gap in this feature; it's outside what any proxy-based system can do.

Reference: docs/trust-governance.md · docs/auth.md · Why fail-closed · Connectors & HITL · Multi-tenant keys