Policies
Access, data and attacks, approvals, detections, and policy as code.
Configure

/dashboard/policies has Overview, Access, Data & attacks, Approvals, Detections and Policy as code. Rules layer organization → agent → exception, and an agent can tighten but never loosen what the organization sets. Packs are inputs, not readiness or compliance proof.
The enforcer reads what a call does from its name and arguments (git force pushes, SQL deletes, shell pipes, payments, outbound messages) and gives it a consequence tier. An agent's role sets, per tier, whether calls run, ask in production, always ask, or are blocked. New runtime agents start in a learning period: consequential calls not granted yet ask, and nothing gets looser until a person reviews the proposal and locks it in. Organization guardrails add asks or data rules for every agent, with named exceptions that expire.
Policy as code keeps two files in git. The organization file is our rules: format (id, toolPattern, action allow, deny or require_approval) or a Preloop v1.0 policy document (imported tighten-only, with a warning for anything it can't map). A rule for one tool changes that tool only; only a whole-server rule (mcp:<server>/*) sets the server in the MCP registry. Optional sections set guardrails:, protected_branches:, approval_rules: (organization approval rules, where conditions such as "refunds over $1,000 ask a person" live and are evaluated), data:, mcp_registry: (mode and default action), tools: (the organization's default tool lists) and preapprovals: (always shown as loosening), policy_pack: (the organization's pack, or none; its tool lists and settings come with it as on the pack page, and a less strict pack or none is loosening), runtime_checks: (the built-in checks that wait for a person, on or off; switching one off is loosening and needs tool governance: manage, and a check that blocks or records always runs, so off is refused), roles: (each agent's role by id or name, as the role card saves it; a higher money limit is loosening, so from an API key it needs a person's approval of the plan) and guardrail_exceptions: (named, agent-scoped exceptions that expire, 7 days unless said and at most 90; always shown as loosening); a section left out stays as it is, and a file of rules: [] owns nothing (applying it removes what earlier revisions set). Validate refuses, with its line: invalid YAML, an unknown field or action, a condition on a tool rule such as cel: (a tool rule applies to every call of its tool, so write conditions under approval_rules: or in an agent's file, where they are evaluated) and a credential. Validate and diff also run apply's own checks against the organization (two approvers need two owners or admins, the protected-branch limit, approval rules and pre-approvals as their pages check them, agents that exist), so validate fails exactly when apply would refuse the file itself (SECTION_INVALID, with the section's line); apply can still refuse for who applies it, a base that moved since, or a loosening nobody acknowledged. It warns, and doesn't refuse, a deny on a catch-all such as * (it blocks every tool; apply then needs allow_catch_all) and an allow on a single tool (the organization file only tightens, so it does nothing). Applying from the dashboard needs the rules operate role, and tool governance manage when the file has approval rules or pre-approvals.
The server decides what loosens, never the client: a plan that loosens anything (a removed or weaker rule, a server unblocked, a section value relaxed, a pre-approval, an empty file removing what an earlier revision set) is refused with 409 LOOSENING_NOT_ACKNOWLEDGED, listing each loosening, unless the apply says it knows (--allow-loosening; in the dashboard, its confirm). The plan hash covers those facts, so a plan reviewed as tightening can't be applied as a loosening one. When an org API key applies a loosening plan, a person must approve that exact plan first: the apply answers 409 PLAN_NEEDS_APPROVAL with a link, the plan waits on Policies → Policy as code with its diff and every loosening, and a person with the rules operate role (plus tool governance manage for approval rules or pre-approvals) approves it. The approval is for that plan hash only, lasts 24 hours and is used once; the person who made the key can't approve it while someone else in the organization can (keys record who made them; a key made before that was recorded has no maker to check), and when they are the only approver, only if the organization allows its only approver to approve their own plans (off by default: add a second approver, or turn it on in the settings on Policies → Policy as code; refused with 403 SELF_APPROVAL_NOT_ALLOWED otherwise). A person's own apply in the dashboard is that person, so it needs no second approval, unless the organization asks a second person for loosening applied in the dashboard (the same settings, off by default, tool governance manage to change): then the plan waits for another person exactly as a key's does, and nobody approves a plan they asked for. While that rule is on, weakening it (turning it off, or letting the only approver approve their own plans) is itself a loosening: the save waits for another person's approval of exactly that change, and the person who asked can never approve it, so one admin can't disarm the rule alone (the file's policy_approval: section is gated the same way). No organization setting turns off the approval a key's loosening needs. One exception: a key may roll back its own latest apply within the hour without a person, when every loosening line puts back exactly what that apply changed and none of it is a role, a guardrail exception, a pre-approval, a built-in check switched off or a weaker rule for who approves (those always need a person). Each waiting plan shows who registered it and when it expires. A plan that only changes what the file owns (values already in effect) is not a change: apply records nothing and says ownership_only, unless asked to record it (--record-ownership; the GitHub Action's record-ownership: true for the onboarding merge). Pages that edit a value the applied file owns say so before you save, and their saves are checked against what the page read (tool lists, the pack and the built-in check switches refuse with 409 when the file or someone else changed them since). An agent's own pack is set through its role (role.attachPack); an agent's own copies of the built-in checks are not in the agent file yet. The offline check and the server are compared by a generated corpus (npm run policy:parity-fuzz, 20 seeds of about 1,500 variants each, run in CI over 5 seeds) as well as the hand-written fixture.
Before it applies: itr policy validate (--offline checks YAML and the schema only, and says so), itr policy test policy.yaml cases.yaml (each listed call decided against the organization's whole policy after the file, optionally with an agent's own layer; the answer lists what the dashboard can't decide, such as content checks), itr policy diff --detailed-exitcode --out plan.json (each rule, the MCP registry and every section before and after, anything that loosens; exit 0 no change, 2 changes, 1 error) and itr policy replay (how the file would have decided the last 30 days of recorded tool calls, and which sections it couldn't replay). itr policy apply --plan plan.json applies only the reviewed plan against the base it was made from: it refuses with 409 if another revision was applied first or the plan would now come out different, and it writes the revision, the registry and every section in one transaction, so a failure changes nothing. Each revision records the key or person that applied it. itr policy history lists revisions, itr policy drift names every value the file declares that is different now, itr policy export --current writes a file from the organization's live settings to start from (diff it right after and nothing changes: it exits 0 and lists, under ownership, the values the file starts to own), and itr policy rollback <n> previews, then applies revision n's file again and puts back what the later revisions set. Keys can be read-only (policy:read): enough to validate, test, diff and replay in CI, refused on apply.
The agent file (itr policy agent export|diff|test|replay|apply) is one agent's whole policy: purpose, tools, tool-call conditions (enforced on every plane, for one agent), role, data rules, limits, its own approval rules and its connections. A section left out of the file is left as it is. Data and limits only tighten the organization's; a role's money limit is set on the role card, never in the file; a new budget is created by a person in the dashboard. Before it applies, POST /api/control-plane/agent-policy/:id/test decides listed calls under the agent as the file would leave it (by the dashboard's parity-tested twin of the tool lists, connections, approval rules and the file's conditions; by the gateway's dry run once the file is in effect and the agent is connected; each row names which) and …/replay decides the agent's last 30 days of recorded tool calls under it, listing the sections records can't replay (argument conditions, role, data, limits). The file carries metadata.specVersion, a hash of each section this agent owns, and apply holds the file to exactly that base: it refuses with 409 when a section the file changes was changed since by anything (the agent page, or another file), while an organization change never makes the file stale and re-applying a file already in effect changes nothing. After an apply, take the spec_version it returns (or export again) as the file's next base. One file applies to an agent at a time, and its plan (what loosens, the base) is made under the same lock as its writes. Only a file with no specVersion applies after another and wins (and where it puts back what the first one changed, that is a loosening, gated as one); in CI, apply with --plan, which binds the base: the second file from the same base is then refused with 409. An agent's role is owned by one file: an agent file role: for an agent the organization file's roles: names, or the other way round, is refused (SECTION_INVALID, naming the other file). A loosening agent file needs --allow-loosening and, applied with an org API key, a person's approval of its plan, exactly as for the organization file. Apply checks every section before writing any, then writes them one after another through the agent page's own write paths, not as one database transaction: each settings key is written on its own (compare-and-set), so a dashboard save of another key at the same moment stays; if a later write fails it puts back what it wrote, key by key, never over a value someone saved since (that key is named as not put back), and if putting one back fails too it says so (rolled_back: "partial") and names it. It is recorded in the change log (Work → Operator) with an exact undo. Reading, checking and testing either file works with a policy read-only org key; applying and rolling back need a full org API key (Settings → API keys, 403 KEY_SCOPE otherwise); a runtime key or the key itr login saves is refused. In CI, the composite action in .github/actions/itr-policy runs validate, test and diff on every pull request. Control plane.