Privilege Rings: Scoping What an Agent Can Do Without Asking
Privilege rings are a tiered permission model for agent tools: low-risk actions run freely, mid-risk actions need soft gates, and high-risk actions require explicit human approval.
Search across all documentation pages
Privilege rings are a tiered permission model for agent tools: low-risk actions run freely, mid-risk actions need soft gates, and high-risk actions require explicit human approval.
The model is inspired by OS protection rings and capability security, but applied to what the agent may invoke, not only who the human is.
An agent without privilege rings typically has one of two defaults:
Privilege rings sit between those extremes. They answer a design question for each capability: Can this action auto-run, or must a human see it first?
A practical four-ring model for product agents:
| Ring | Typical actions | Default policy |
|---|---|---|
| R0 - Observe | Read docs, search, list tickets, fetch public data | Auto-run |
| R1 - Draft | Write drafts, propose plans, create private notes | Auto-run or soft notify |
| R2 - External / mutate | Send email, update CRM, open PRs, post to chat | Approval gate |
| R3 - Irreversible / high cost | Refunds, deletes, deploys, payments, secret access | Dual control or senior approval |
Rings are host policy, not model manners. The LLM may want to send mail; the runtime must refuse or pause until the policy is satisfied.
Rings compose with identity. The same tool can be R1 for an internal admin agent and R3 for a customer-facing bot.
Prefer rings on capabilities (for example email.send, crm.update, payments.refund) rather than on raw HTTP verbs alone.
A tool that can both draft and send should expose two modes or two tools so draft stays R1 and send is R2.
from enum import IntEnum
from dataclasses import dataclass
class Ring(IntEnum):
OBSERVE = 0
DRAFT = 1
MUTATE = 2
IRREVERSIBLE = 3
@dataclass(frozen=True)
class Capability:
name: str
ring: Ring
description: str
CAPS = {
"search_docs": Capability("search_docs", Ring.OBSERVE, "Read-only knowledge search"),
"draft_reply": Capability("draft_reply", Ring.DRAFT, "Write a reply draft"),
"send_email": Capability("send_email", Ring.MUTATE, "Send external email"),
"issue_refund": Capability("issue_refund", Ring.IRREVERSIBLE, "Post a refund"),
}Each run, role, or environment sets max_auto_ring. Anything above that ring enters an approval queue (or is denied).
def authorize(cap_name: str, max_auto_ring: Ring) -> str:
cap = CAPS[cap_name]
if cap.ring <= max_auto_ring:
return "allow"
if cap.ring == Ring.IRREVERSIBLE:
return "require_senior_approval"
return "require_approval"Typical environments:
Rings do not replace least privilege. An agent that never needs refunds should not load issue_refund into its tool schema at all.
Prompt text like "be careful with refunds" is not a ring. Enforcement lives in the tool dispatcher before side effects run.
When authorize returns require_approval, the host:
See Building an Approval Gate Before Irreversible Actions.
A kill switch is orthogonal: it stops any ring mid-run when something is wrong.
Audit logs should record ring, capability, decision, and actor so post-incident review is possible without replaying model vibes.
Some systems temporarily raise a ring after a successful verification step (for example user confirmed an amount in chat).
Treat elevation as short-lived and scoped: elevate send_email for one recipient and one body hash, not "all mutate tools for 24 hours."
High-blast actions often need two humans or human-plus-policy (for example amount over a threshold plus manager approval).
Dual control is still a privilege ring pattern; the gate just requires two distinct decision records.
| Approach | Strength | Weakness |
|---|---|---|
| Single allowlist, all auto | Fast demos | Incidents at first real side effect |
| Gate every tool | Feels safe | Operator fatigue, rubber stamps |
| 3-4 fixed rings + env policy | Clear, reviewable | Needs ownership of mappings |
| Per-arg dynamic risk scores | Nuanced | Hard to explain and test |
Prefer fixed rings with a few arg-based overrides (amount > $X, production env, external domain) rather than opaque ML risk scores as the only gate.
Specialists inherit rings from their tool sets. A research agent stays R0-R1. An executor agent may hold R2 tools but still hit approval for R2+.
Do not let handoff expand privileges silently. The handoff packet should not re-register admin tools on a low-privilege worker.
Three or four. Observe, draft, mutate, irreversible is enough for most products. Split only when ops cannot write clear policy without another level.
The feature owner proposes a ring; security or platform reviews it. Record the decision next to the tool registration, not only in chat.
No. Prompts help the model prefer good tools. The host must still refuse or gate out-of-policy calls.
RBAC answers "what may this human do in the product." Rings answer "what may this agent run without pausing for a human." Both layers usually apply.
Usually auto-run, unless the read itself is sensitive (PII export, secret store list, competitor scrape under legal limits). Then treat as a higher ring or a separate allowlist.
Keep R0-R1 wide enough that only meaningful risk hits the queue. Batch low-value R2 items carefully, or auto-approve only with strict arg templates.
Either keep max auto-ring at R1, queue R2+ for business hours, or require a pre-approved plan with a fixed arg envelope.
They implement the pause. You still design which tools enter those nodes. LangGraph interrupt nodes, AI SDK tool-approval states, and custom queues are mechanisms, not the policy model.
Ship a capability table in the repo: tool name, ring, default env policy, and whether credentials are loaded. Reviewers should see this before merge.
Gate external side effects (send, pay, delete, deploy) as a single high ring, and keep reads auto-run. Expand to finer rings once volume justifies it.
Yes for low-stakes personal agents. For enterprise risk, self-approval of R3 by the requesting user is often insufficient; use separation of duties.
Treat each MCP tool (or server) as a capability with an assigned ring before it is exposed to the model. Do not auto-import a whole server as R0.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026