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