Policies
Policies are CEL-based rules that decide what happens to every AI request — allow, deny, audit, mutate, or route for human approval. They run in real time before the request reaches the model.
Policy anatomy
Policies are Kyverno CEL YAML documents. Each policy has optional match conditions and one or more validation expressions:
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: block-prod-kubectl
annotations:
proxy.nirmata.io/enforcement-mode: Approve
spec:
matchConditions:
- expression: 'object.mcp.tool == "kubectl"'
- expression: 'object.agent.namespace == "production"'
validations:
- expression: |
now.getHours() >= 8 && now.getHours() < 18 &&
now.getDayOfWeek() in [1, 2, 3, 4, 5]
message: "Production kubectl outside business hours requires approval"
validationActions: [Deny]
Use MutatingPolicy to rewrite arguments before the request is forwarded (for example, redirecting a disallowed model to an approved one).
Defaults mutations
Before policies run, config resolution computes the effective per-request defaults for the caller — budget limits, egress allowlists — and exposes them to every CEL expression as object.defaults.*. A MutatingPolicy mutation with patchType: Defaults adjusts these resolved values for situations the scoped configuration can't express — dynamic conditions like time of day or current budget pressure:
apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
name: tighten-budget-after-hours
spec:
matchConditions:
- expression: 'now.getHours() >= 18 || now.getHours() < 8'
mutations:
- patchType: Defaults
applyConfiguration:
expression: '{"budget": {"limits": object.defaults.budget.limits.map(l, {"tier": l.tier, "scope": l.scope, "modelPattern": l.modelPattern, "limitUSD": l.limitUSD / 2.0, "window": l.window, "mode": l.mode, "warnAt": l.warnAt})}}'
Two rules govern Defaults mutations:
- Narrow only — the widening clamp. A Defaults patch can tighten resolved values (lower a budget limit, drop an egress domain) but never loosen them. Any attempt to widen — raising a limit, deleting a limit entry, downgrading
denytowarn, adding an egress domain outside the resolved allowlist — is clamped back to the resolved value and recorded, while the rest of the patch still applies. - Identity conditions belong in scoped configuration, not policies. A Defaults policy whose match conditions only test who is calling (team, user, agent) is rejected at save time with a pointer to the matching scope tier in Settings — the scoped-rules hierarchy already expresses per-identity defaults declaratively. Combine identity with a dynamic signal (
now,object.session.percentUsed) and the policy is accepted.
routing is the one namespace this doesn't apply to: upstream routing has no "resolved ceiling" to narrow against — it's a single selected upstream, not a limit — so a Defaults patch setting routing.upstreamName is a full override, not a narrowing. For example, redirecting a request that a content-safety scan flagged as containing PII to an upstream provider with a stronger data-retention guarantee:
apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
name: pii-detected-route-to-zdr-upstream
spec:
matchConditions:
- expression: 'object.request.hasPII'
mutations:
- patchType: Defaults
applyConfiguration:
expression: '{"routing": {"upstreamName": "azure-zdr"}}'
The request is forwarded to the named upstream (azure-zdr here) instead of whatever routing would otherwise have selected. If the name doesn't match any configured upstream — a typo, or an upstream that was since removed — the override is silently ignored and the request proceeds to the originally resolved upstream; a misconfigured routing override never blocks a request.
Defaults patches never touch the request itself — arguments, headers, and message content are unaffected; only the object.defaults.* values later policies (and, for routing, the forwarding logic) read are changed.
Defaults patches and object.defaults.* reads are live end-to-end: egress enforcement consumes the resolved object.defaults.egress value on MCP tool calls and both LLM paths (see Restrict Network Egress), budget enforcement consumes the resolved object.defaults.budget.limits value on the same paths (see Set a Budget), and routing — LLM-only, since MCP tool calls have no upstream to route — consumes the resolved (or overridden) object.defaults.routing.upstreamName value on both LLM paths to pick which upstream actually receives the request. The widening clamp actively restores any widened budget/egress field before enforcement runs; routing has no clamp to apply.
Enforcement modes
Set via validationActions or the annotation proxy.nirmata.io/enforcement-mode. The annotation takes priority.
| Mode | Blocks? | Effect |
|---|---|---|
Deny | Yes | Request blocked immediately; developer sees the denial message. |
Approve | Yes (held) | Request is queued for human review. Session resumes on approval or times out on denial. |
Ask | No (asks) | Request is paused; the user confirms inline and the request proceeds. Supports cooldown-period-minutes. |
Warn | No | Request proceeds; a warning is appended to the response. |
Audit | No | Request proceeds; violation is logged. Use to test a policy before enforcing it. |
The annotation value is case-insensitive. Approve and Ask are shorthand for the longer-form warn-approve and warn-ask, which remain valid too.
Legacy annotation values require-approval and warn-confirm are still accepted and behave identically to Approve (warn-approve) and Ask (warn-ask) respectively — existing policies don't need to be updated.
Add a cooldown period to suppress repeated alerts from the same session:
annotations:
proxy.nirmata.io/cooldown-period-minutes: "15"
Changing modes from the UI
Every policy row on the Policies page has an inline mode selector — switch a policy between Audit, Warn, Enforce, and Approval without editing YAML. A mode set from the UI is authoritative: it overrides the spec annotation and is preserved even when built-in policies are refreshed on upgrade. Select multiple policies with the row checkboxes to set the mode, or enable/disable, in bulk.
Replay — preview before enforcing
Before flipping a policy from Audit to Enforce, click Replay to dry-run it against your recent traffic. Replay evaluates the policy over the last 1,000 audit events without affecting anything live, and reports how many events it would have acted on, the decision breakdown, and samples of the matching calls. Zero matches on real traffic is the signal that enforcing won't disrupt your team.
Matching: tools vs LLM calls
Every request is either a tool call (MCP) or an LLM call. Use these expressions to target the right type:
| What to match | Expression |
|---|---|
| Any MCP tool call | object.mcp.tool != "" |
| A specific tool | object.mcp.tool == "bash" |
| Any LLM call | object.llm.model != "" |
| A specific model | object.llm.model == "claude-opus-4-8" |
Content safety policies
AIControls scans every LLM prompt and tool call inline before forwarding it. Content safety enforcement is done through policies — you choose whether to block, warn, or audit depending on your team's risk tolerance.
Two built-in policies carry the scanner's default disposition, enabled in every workspace:
content-safety-credentials(Enforce) — blocks any request whose scanned content contains a credential (private key, cloud/API token, connection string) or an unsafe-content finding.content-safety-pii(Warn Approve) — routes requests containing PII to the approval queue.
Both read the scanner's findings via the object.request.* context fields — the same fields your own policies can use. Because the disposition is a policy, you control it like any other policy: switch content-safety-pii to Warn or Audit to loosen PII handling, tune its expression with a confidence threshold (e.g. !(object.request.hasPII && object.request.piiConfidence >= 0.5)), or grant a scoped exception. Disabling content-safety-credentials disables credential blocking on the request path entirely — findings are still scanned and visible to other policies and the audit log, but nothing blocks by default.
The Warn enforcement mode is particularly useful for content safety: the request is allowed through, but a warning is appended to the model's response. The AI tool (e.g. Claude Code) sees the warning in its next context window and surfaces it to the developer, who can then decide whether to continue. This gives you a soft interrupt without hard-blocking legitimate workflows.
Start with Audit mode for all content safety policies to measure false-positive rates against your team's real workload. Graduate to Warn, then Deny, once you have confidence in the patterns.
For worked content-safety examples (PII warnings, prompt injection blocking, credential-file access warnings, secret exfiltration blocking), see Write a Policy.
Built-in policy library
AIControls ships with a curated library of policies covering common governance scenarios, grouped into five categories — security, cost, compliance, operations, and autonomy governance. Security policies cover things like blocking credential patterns in prompts and denying requests with PII in tool arguments. Cost policies enforce model tiers by developer group and can block premium models for non-engineering roles. Compliance policies require audit mode for regulated data namespaces and can block data export tools. Operations policies rate-limit bash calls per session and require HITL approval for infrastructure mutations. Autonomy governance policies scale enforcement to each agent's autonomy tier — from logging every action at T1 to requiring approval for privileged operations at T4.
Enable library policies from Policies → Library — browse as cards or a sortable table. For the full enumerated list of built-in policies, see Policy Context.
Default policy set
New workspaces start with an audit-first baseline: an audit-everything policy plus detections for secret reads, shell injection, privilege escalation, malicious file writes, sensitive file access, and command-and-control traffic — all in Audit mode, alongside non-blocking context-efficiency warnings. The one enforcing default is PII scanning in prompts. The onboarding wizard lets you review and customize this set; use Replay to build confidence before graduating any policy to Enforce.
Human-in-the-loop
Set proxy.nirmata.io/enforcement-mode: Approve (warn-approve) on any policy to route matching requests to the approval queue instead of blocking them outright. The developer's session is held; they see a pending state in their AI tool. Admins approve or deny in Security → Approvals — the session resumes immediately on approval or receives an error on denial.
Approval requests expire after a configurable timeout (default: 30 minutes). If no admin acts within the window, the request is auto-denied.
On MCP tool calls the request is held while approval is pending. On LLM proxy requests (chat completions, Anthropic messages, token counting) there is no inline hold: an Approve (warn-approve) policy blocks the request with the policy's message. A granted exception or switching the policy's mode lifts the block. This applies to every Approve policy that matches LLM calls, including the built-in content-safety-pii.
Configure who receives approval notifications in Settings → Notifications.