Skip to content
Browse documentation

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

cirvix.policy.jsonJSON
{  "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 pathExampleSource
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.insideWorkspacetrue / falseWhether the resolved path is inside the workspace root
egress.external, egress.allowlistedtrue / falseWhere an outbound request is headed
session.touchedSecrettrue / falseWhether 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.

TerminalShell
$ 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.
Edit this page on GitHub
Was this page helpful?