Skip to content
Browse documentation

Policy schema

The rule format, the effects, the condition operators, and the context a rule can read.

A policy is an ordered array of JSON rules. There is no separate policy language, no compiler, and no expression evaluator — a rule is data, and every policy example in these docs is written against the schema on this page.

A rule

cirvix.policy.jsonJSON
{  "rules": [    {      "name": "deny-dotenv-read",      "effect": "forbid",      "actions": ["fs.read", "fs.*"],      "resources": ["**/.env", "**/.env.*"],      "reason": "Reading .env files is denied outside an approved secrets flow.",      "remediation": "Request the value as a handle: secrets.get(\"STRIPE_KEY\")"    }  ]}
FieldTypeRequiredMeaning
namestringyesUnique. Appears in every decision, alert, and audit record
effectpermit \| forbid \| holdyesWhat happens on a match
agentsstring \| string[]noGlob(s) against the agent name. Omitted means any
actionsstring \| string[]noGlob(s) against the action
resourcesstring \| string[]noGlob(s) against the canonical resource
whencondition[]noEvery condition must hold
reasonstringnoShown to the agent and recorded
remediationstringnoThe legitimate path — what lets an agent re-plan
approversstring[]hold onlyWho may release the call

Three properties that are load-bearing

  • **Forbid always wins.** A matching forbid cannot be overridden by any permit, regardless of order or specificity, and it short-circuits. This is what makes a rule set safe to extend — adding a permissive rule can never silently punch a hole through an existing prohibition.
  • **Default deny.** A request matching no rule is denied, with rule: null. Fail-open is how a control plane becomes decorative the first time a rule file fails to parse.
  • **Resources are canonicalized before matching.** ./x/../.env, .env, and an absolute path to the same file are one resource. Rules that match raw strings are bypassed by the first traversal, and by the first agent that uses a relative path.

A fourth follows from the first two: a hold outranks a permit. If any rule says a human must see this call, no other rule quietly skips them.

Effects and verdicts

Rule effectVerdictMeaning
permitpermitThe call proceeds
forbiddenyThe call is refused. Re-planning is the only recovery
holdholdSuspended for a named human. This exact call may still happen

Glob matching

TokenMatches
*Any run of characters within one /-separated segment
**Any run of characters across segments
?Exactly one character that is not /
anything elseItself, literally

Matching is case-insensitive, and every other character is a literal — a . means a dot, not "any character", or **/.env would also match xenv. The matcher is a two-pointer wildcard match rather than a compiled regular expression, because the pattern comes from a rule and the input comes from whatever resource an agent named: a caller on the far side of the enforcement boundary picks the input, and a backtracking matcher has pathological ones.

Conditions

A hold with a conditionJSON
{  "name": "require-approval-destructive",  "effect": "hold",  "actions": ["fs.delete", "db.write", "db.migrate", "k8s.apply", "shell.exec"],  "resources": ["*"],  "when": [{ "path": "environment", "op": "in", "value": ["production", "prod"] }],  "approvers": ["platform-oncall"],  "reason": "Destructive action in production. Held for a named human."}

Conditions are plain data, never expressions. eval and new Function are deliberately absent: a policy file is exactly the kind of thing that gets templated by a script, and turning it into an execution surface would make the security product the vulnerability.

OperatorHolds when
eq / neThe value is / is not equal
in / ninvalue is an array containing / not containing it
gt / gte / lt / lteNumeric comparison
matchesThe value is a string matching the glob in value
existstrue → the path is set; false → it is unset
containsThe value is an array containing value
supersetOfThe value is an array containing every element of value

An unknown operator **fails closed** — the condition does not match, so a typo cannot accidentally widen a rule.

The context a rule can read

PathTypeSet from
agentstringThe request's agent name
actionstringThe request's action
environmentstring--env, or the Guard's environment
path.insideWorkspacebooleanWhether the canonical resource is inside the workspace root
egress.externalbooleanWhether the resource is an http(s) URL to a non-local host
egress.allowlistedbooleanWhether the destination is allowlisted
session.touchedSecretbooleanWhether this session already read secret material
mcp.server / mcp.toolstringSet when the call came through the gateway

session.touchedSecret is the one to understand. It is set once a session reads secret-shaped material and never resets — it is what makes "read a credential, then post it somewhere" fail even when both calls are individually allowed. A brokered secret handle deliberately does not set it: the agent never held the material, which is the entire point of a handle.

Actions

Policy is written against a normalized action vocabulary rather than raw tool names, so one rule covers every tool that does the same thing.

Tool name containsAction
read, get, cat, fetchfs.read
write, create, put, save, editfs.write
delete, remove, rm, unlink, dropfs.delete
list, ls, search, find, query, grepfs.list
exec, run, shell, command, spawnshell.exec
request, http, curl, browse, scrapehttp.request
apply, deploy, rolloutk8s.apply
*no match, via the gateway*mcp.<server>.<tool>
*no match, via wrap*tool.<name>

Matching is on whole words delimited by start/end, ., _, or - — so read_file maps to fs.read but spreadsheet does not. Because unmatched tools fall through to mcp.* and tool.*, a rule can always name one specific tool.

Validating a rule set

TerminalShell
$ cirvix policy --policy cirvix.policy.json $ curl -X POST $CIRVIX_API/v1/policy/validate \    -H "authorization: Bearer cvx_..." \    -H "content-type: application/json" \    -d @cirvix.policy.json
Edit this page on GitHub
Was this page helpful?