Policies
Authorization is an ordered set of JSON rules read by a deterministic evaluator. One evaluator decides in development, in CI, and at runtime.
Permissions for agents tend to get written as if-statements scattered through application code, at which point nobody can answer "what is this agent allowed to do" without reading every call site — and no auditor accepts that as an answer. A rule set is a thing you can point at, diff, and reason about.
Anatomy of a policy
{ "rules": [ { "name": "deny-exfiltration-after-secret", "effect": "forbid", "actions": ["http.request", "net.*"], "resources": ["*"], "when": [ { "path": "egress.external", "op": "eq", "value": true }, { "path": "session.touchedSecret", "op": "eq", "value": true } ], "reason": "This session read secret material, so external egress is blocked for the rest of it." }, { "name": "hold-production-writes", "effect": "hold", "agents": ["deploy-*"], "actions": ["db.write", "db.migrate", "k8s.apply"], "resources": ["*"], "when": [{ "path": "environment", "op": "in", "value": ["production", "prod"] }], "approvers": ["data-oncall"], "reason": "Held for a named human. The call waits rather than failing." }, { "name": "allow-analytics-queries", "effect": "permit", "actions": ["db.query", "*.read", "*.list"], "resources": ["**/analytics/**"], "reason": "Read-only access to the analytics dataset." } ]}name is what gives a rule the short identifier you see in traces, CLI output, alerts, and audit exports — deny-dotenv-read, hold-production-writes. It is required, and a duplicate name across a rule set is rejected rather than silently shadowing.
permit vs. forbid
The default is deny: if no permit matches, the call is refused. A matching forbid overrides any permit, unconditionally and regardless of rule order — you cannot write a permit that wins against a forbid that also matches. This is what makes a guardrail rule like deny-dotenv-read reliable: it does not matter how many other rules get added later, or by whom.
What a policy can read
| Field or path | Example | Source |
|---|---|---|
agents | ["pr-triage"], ["deploy-*"] | Glob(s) against the calling agent's name |
actions | ["fs.read"], ["k8s.apply"] | Glob(s) against the normalized action |
resources | ["**/.env"], ["postgres://prod*"] | Glob(s) against the canonical resource |
environment | "local", "staging", "production" | Where the agent is running |
path.insideWorkspace | true / false | Whether the resolved path is inside the workspace root |
egress.external, egress.allowlisted | true / false | Where an outbound request is headed |
session.touchedSecret | true / false | Whether this session already read secret material |
mcp.server, mcp.tool | "github", "repo.read" | Set when the call came through the gateway |
The first three are rule fields, matched by glob. The rest are context paths, read by a when condition through a closed set of operators — conditions are plain data, never expressions, because a policy file is exactly the kind of thing that gets templated by a script.
Approval as an outcome
A policy resolving to a named human sign-off, rather than only permit or forbid, is what makes graduated autonomy expressible at all — see Approvals.
Change control
Every rule-set change is a reviewable diff, and replay re-decides a recorded run against a candidate so the blast radius is known before merge rather than discovered after. Nothing is re-executed — replay re-decides only.
$ cirvix replay run_01JQ8F2K7M --policy candidate.json --diff run_01JQ8F2K7M agent pr-triage 38 calls changed decisions 2 of 36 00.412 fs.read /workspace/.env.production was PERMIT allow-workspace-read now DENY deny-dotenv-read No side effects were executed.Exit code 1 when anything changed, which is what makes this a CI gate. For a diff with no recorded run to replay against, expectNoLoosening from the SDK's testing entry point asserts the same property over a list of calls you name.
Limits
- The engine decides authorization; it does not inspect payload semantics. A permitted query that returns more rows than intended is a query design problem, not a policy outcome.
- Policy quality is your responsibility. Cirvix ships a nine-rule starter set, but a permissive rule you wrote will be honoured exactly as written.
- Replay re-decides historical calls against a candidate rule set — it predicts behaviour on traffic you have already seen, not on traffic you have not.
