MAQPNADocs

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 &gt; require_approval &gt; 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):

  1. A policy applies to a call when its namespace matches the caller's namespace and one of its agents globs matches the agent.
  2. 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.
  3. When no rule matches, the policy's defaultAction applies (deny when unset), with reason default_action.
  4. When several policies apply, the most restrictive decision wins: deny, then require_approval, then allow. Between two require_approval decisions, the one asking for more approvers wins.
  5. When no policy applies, the call is denied with reason no_applicable_policy.
  6. 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.