Policies, rules and decisions#
A policy (kind ToolPolicy, short name tp) is a named set of rules for which agents may call which tools, and how. The operator renders every valid ToolPolicy in the cluster into one document, policies.json in the ConfigMap maqpna-policies, which the gateway hot-reloads (every policyReloadSeconds, default 2 seconds) without dropping connections.
| Term | Meaning |
|---|---|
| policy | ToolPolicy: agents[] globs it applies to, a defaultAction (default deny) and an ordered list of rules[] |
| rule | One entry: it matches calls by server, tool, arguments, user, groups, scopes, schedule, taint and sinks, and gives an action |
| action | What a rule says: allow, deny or require_approval |
| decision | What the gateway did with one call: allowed, denied or held for approval, written to the ledger as allow, deny or require_approval with the policy and rule |
| default deny | No applicable policy means the call is denied (no_applicable_policy) |
How a decision is made#
flowchart TD
R["Request: namespace, agent, session, user, groups,<br/>scopes, server, tool, args, session taints, sinks"] --> P{"Any policy applies?<br/>(namespace + agents globs)"}
P -- no --> D0["deny · no_applicable_policy"]
P -- yes --> E["For each applicable policy:<br/>rules in order, first match wins"]
E --> M{"A rule matched?"}
M -- yes --> RA["rule action<br/>(Cedar text in the same policy also votes,<br/>strictest wins)"]
M -- no --> DA["policy defaultAction<br/>(deny when unset) · default_action"]
RA --> C["Combine all policies:<br/>deny > require_approval > allow<br/>(ties: larger minApprovers)"]
DA --> C
C --> X{"Engine-wide external evaluator?<br/>(OPA sidecar)"}
X -- yes --> C2["Stricter result wins"]
X -- no --> RL
C2 --> RL{"Result not deny and a matched rule<br/>has maxCallsPerMinute?"}
RL -- "over limit" --> D1["deny · rate_limited"]
RL -- "within limit" --> F["Final decision"]
The rules, step by step (pkg/policy):
- A policy applies to a call when its namespace matches the caller's namespace and one of its
agentsglobs matches the agent. - Within one policy, rules are evaluated in order; the first matching rule wins. A rule matches only when every matcher it sets passes, in this order:
servers,tools,argMatch,principals,scopes,sessionTaints,sinks,schedule,when. - When no rule matches, the policy's
defaultActionapplies (denywhen unset), with reasondefault_action. - When several policies apply, the most restrictive decision wins:
deny, thenrequire_approval, thenallow. Between tworequire_approvaldecisions, the one asking for more approvers wins. - When no policy applies, the call is denied with reason
no_applicable_policy. - Rate limits (
maxCallsPerMinute, a 60-second sliding window per session, policy and rule) are counted only when the combined result would let the call through. A call that is denied anyway never uses up the limit.
Rule matchers#
| Matcher | Matches on |
|---|---|
servers, tools |
path.Match globs over the MCP server name and tool name (delete_*) |
argMatch |
Globs over top-level or dotted argument paths (metadata.namespace: prod*) |
principals |
users / notUsers globs over the person the agent acts for; groups / notGroups over their verified groups |
scopes |
Every listed scope must be granted by the session token |
when[] |
Typed conditions over arguments: eq, neq, glob, regex, in, nin, lt, lte, gt, gte, exists, absent, cidr, len_gt, len_lt (all must hold) |
schedule |
Time windows in an IANA time zone (Mon-Fri 08:00-18:00), with exceptWindows |
sessionTaints |
any / all of the session's taint labels |
sinks |
The tool is labelled as an exfiltration sink (external) |
A condition that cannot be evaluated safely (for example a regex over a value longer than 64 KiB) fails closed: deny and require_approval rules match, allow rules do not.
Dry run#
A policy with enforcement: dryRun is evaluated normally, but only contributes its dryRunFallback (allow, or its defaultAction) to the enforced decision. The gateway writes an extra ledger record with ext.dryRun=true and the decision the policy would have made, so you can roll out a stricter policy and watch what it would do.
Cedar and Rego#
A ToolPolicy may carry spec.cedar text. Cedar policies in the same policy vote alongside its native rules, strictest wins. Cedar runs only in binaries built with -tags cedar; in a binary without it, a policy with Cedar text denies every call it applies to (cedar_unavailable). An optional OPA sidecar (Rego) can be registered engine-wide; it can only make a decision stricter. Errors and timeouts fail closed (cedar_error, rego_unavailable, external_policy_error). See policy evaluation.
Decisions in the ledger#
Every governed call produces exactly one decision in the audit ledger, including denials:
Ledger decision |
Shown as | Written when |
|---|---|---|
allow |
Allowed | The call was forwarded (also after an approval, with approver set) |
deny |
Denied | A policy, the kill switch, DLP, a budget, a pin, a scope or an approval decision refused the call |
require_approval |
Held for approval | An asynchronous call was held; the record carries approval_pending:<id> |
observe |
— | A governance fact that is not a call: taint added or cleared, a pin first seen, a fork link, a session result |
admin |
— | An administrative action, such as creating a break-glass revocation |
usage |
— | Sandbox time reported by the operator for metering |
The rule field is written as <policy>/<rule>. For decisions not made by a policy, it names the source: revocation/<id>, budget/<scope>-<window>, pin/<server>/<tool>, dlp (with the profile in ext).
What you see#
Test a call against a policy file offline, in CI:
$ maqpna policy test --policy prod-guard.yaml --ns team-a --agent coder \
--server kubernetes --tool delete_pod --expect require_approval
maqpna policy test prints the request and the decision as JSON, and exits 3 when the decision differs from --expect:
{
"decision": {
"action": "require_approval",
"policy": "prod-guard",
"rule": "k8s-delete",
"reason": "destructive"
},
"request": {
"namespace": "team-a",
"agent": "coder",
"server": "kubernetes",
"tool": "delete_pod"
}
}
Add --explain to print the trace: every policy, whether it applied, and for each rule whether it matched and why not. --gateway URL evaluates against the policies a running gateway has loaded (POST /v1/policies:evaluate), with no side effects. See maqpna policy test, maqpna replay and maqpna eval.