Policy cookbook
Ready-to-adapt NrvqPolicy recipes for the situations teams actually hit, from “I just deployed an
agent and want visibility” to “this one tool needs human sign-off”. Each recipe tells you when to
use it, what to fill in, and what decision it produces. The mechanics behind them (the policy
contract, packages, overlays, validation) are in Writing policies; a
full end-to-end walkthrough is the worked example.
Before you write anything: five questions
Section titled “Before you write anything: five questions”Every policy is the answer to these five questions — collect them first and the YAML writes itself:
- Where does the agent run? The Kubernetes namespace. That’s the policy’s
metadata.namespace, and (for injection) the namespace that carries thenorviq-injection=enabledlabel. - Who is the policy for? The
spec.target— one of:agentClass: <class>— every agent whose pod is labelednorviq.io/agent-class: <class>(register the class with anNrvqClassfirst — see the worked example),namespace: <ns>— everything in a namespace (a baseline/floor), orkind: Deployment+name: <workload>— one specific workload (kindacceptsDeployment/StatefulSet/DaemonSet/ReplicaSet).
- Which tools does it call, and what do the params look like? Don’t guess — read the agent’s
real traffic first: the console’s live audit stream, or
norviq audit list -n <ns>/norviq audit top-blockedfrom the CLI. Your Rego matches oninput.tool_nameandinput.tool_params.<field>, so the exact names matter. - What should a violation do?
block(deny outright),audit(log what would have happened, let it proceed), orescalate(hold for human review — the SDK adapters treatescalateas not allowed, so the call does not run while it awaits review; it fails closed, it doesn’t fail open). - Where does it sit in the stack?
priority(0–499 for namespace users; see choosing a priority). Highest priority wins; ties break toward the most restrictive decision.
With those answered, pick the closest recipe below.
Recipe 1 — Observe first: an audit-only namespace baseline
Section titled “Recipe 1 — Observe first: an audit-only namespace baseline”Use when you’ve just onboarded a namespace and want visibility before you block anything. This
is the shipped crds/examples/policy-namespace-baseline.yaml: a whole-namespace floor in audit
mode at low priority, so anything more specific you add later automatically outranks it.
apiVersion: norviq.io/v1alpha1kind: NrvqPolicymetadata: name: prod-baseline namespace: chatbot-prod # ← your namespacespec: target: namespace: chatbot-prod # ← same namespace: applies to every agent in it enforcementMode: audit preset: permissive priority: 50 # low on purpose — class policies outrank itFill in: the namespace (twice). What it does: the permissive preset default-allows and
only escalates a call from an agent whose trust score has dropped below 0.4 — everything else
flows, and every call lands in the audit log so you can see the real tool names and params before
writing anything stricter.
Recipe 2 — The standard production guard: strict preset on an agent class
Section titled “Recipe 2 — The standard production guard: strict preset on an agent class”Use when an agent class handles untrusted input (customer chat, inbound email) and you want the
full security ruleset enforcing. This is crds/examples/policy-strict-chatbot.yaml:
apiVersion: norviq.io/v1alpha1kind: NrvqPolicymetadata: name: chatbot-strict namespace: chatbot-prod # ← your namespacespec: target: agentClass: customer-support # ← your NrvqClass name / pod label value enforcementMode: block preset: strict priority: 200Fill in: namespace + agent class. What it does: the strict preset is the full
comprehensive.rego baseline — it blocks high-risk tools outright (execute_sql, anything named
delete_*/drop_*/truncate_*/destroy_*) plus prompt-injection, SQL/shell-injection, PII/PCI,
and data-exfiltration patterns in the params. Check first: if your agent legitimately calls a
tool the strict preset blocks (e.g. a genuine SQL tool), don’t fight the preset — use the allowlist
recipe (4) or a custom policy instead, and dry-run either way.
Recipe 3 — Middle ground: escalate the risky, block the destructive
Section titled “Recipe 3 — Middle ground: escalate the risky, block the destructive”Use when an agent needs a powerful tool sometimes, and you want a human in the loop rather than
a hard no. This is crds/examples/policy-moderate-analyst.yaml:
apiVersion: norviq.io/v1alpha1kind: NrvqPolicymetadata: name: analyst-moderate namespace: analytics # ← your namespacespec: target: agentClass: data-analyst # ← your agent class enforcementMode: audit # start observing; flip to block/escalate when confident preset: moderate # Informational: canonical rule_ids the bundled comprehensive policy enforces. rules: - llm01_prompt_injection - llm06_excessive_agency - deny_sql_injection priority: 150Fill in: namespace + agent class. What it does: the moderate preset escalates any
execute_sql call (held for review, not run) and hard-blocks a query containing drop. The
optional rules: list is informational — a place to record which canonical rule IDs you care
about; the preset’s Rego is what actually decides.
Recipe 4 — Positive security: allowlist the tools, deny everything else
Section titled “Recipe 4 — Positive security: allowlist the tools, deny everything else”Use when you know exactly what an agent is supposed to do — the strongest posture, because a brand-new attack tool is blocked by default instead of needing a matching deny rule. A support agent that should only ever search the KB, look up orders, and open tickets:
apiVersion: norviq.io/v1alpha1kind: NrvqPolicymetadata: name: support-allowlist namespace: chatbot-prod # ← your namespacespec: target: agentClass: customer-support # ← your agent class enforcementMode: block rego: | package norviq.custom.support_allowlist
allowed_tools = {"search_kb", "get_order", "create_ticket"} # ← your agent's real tools
allowed { allowed_tools[input.tool_name] }
default decision = "block" decision = "allow" { allowed } decision = "block" { not allowed }
rule_id = "support_allowlist_deny" { decision == "block" } reason = "Tool is not in the customer-support allowlist" { decision == "block" } rule_id = "support_allowlist_allow" { decision == "allow" } reason = "Allowlisted tool" { decision == "allow" } priority: 250Fill in: namespace, agent class, and the allowed_tools set — take the names from the audit
log (question 3 above), not from memory; one typo means a legitimate tool gets denied. The explicit
decision = "block" { not allowed } rule looks redundant next to the default, but it isn’t
optional: the validator requires a complete block/escalate rule and rejects a module that leans
on the default alone.
Recipe 5 — Block one dangerous pattern on one workload
Section titled “Recipe 5 — Block one dangerous pattern on one workload”Use when a single deployment needs one targeted rule and you don’t want to touch its class
policy. This is crds/examples/policy-custom-rego.yaml — block execute_sql only when the query
contains DROP, on one named Deployment:
apiVersion: norviq.io/v1alpha1kind: NrvqPolicymetadata: name: custom-sql-guard namespace: chatbot-prod # ← your namespacespec: target: kind: Deployment name: smartsales-agent # ← the workload's name enforcementMode: block rego: | package norviq.custom.sql_guard default decision = "allow" violation { input.tool_name == "execute_sql" contains(lower(input.tool_params.query), "drop") } decision = "block" { violation } decision = "allow" { not violation } rule_id = "custom_sql_guard" { decision == "block" } reason = "DROP statement blocked by custom policy" { decision == "block" } rule_id = "default_allow" { decision == "allow" } reason = "Allowed" { decision == "allow" } priority: 300Fill in: namespace, workload name, and the tool/param condition. Note the shape: a violation
helper, both decision branches, and a rule_id/reason pair for each — the validator rejects a
module that could leave decision undefined (see
the policy contract).
Recipe 6 — Require human approval for one tool
Section titled “Recipe 6 — Require human approval for one tool”Use when a tool is legitimate but consequential — refunds, wire transfers, account deletion —
and policy shouldn’t auto-decide either way. escalate holds the call (the SDK adapters do not
run an escalated call) and surfaces it for review:
apiVersion: norviq.io/v1alpha1kind: NrvqPolicymetadata: name: refund-approval namespace: chatbot-prod # ← your namespacespec: target: agentClass: customer-support # ← your agent class enforcementMode: escalate rego: | package norviq.custom.refund_approval
default decision = "allow" decision = "escalate" { input.tool_name == "issue_refund" } # ← your sensitive tool
rule_id = "refund_needs_approval" { decision == "escalate" } reason = "Refunds require human approval" { decision == "escalate" } rule_id = "default_allow" { decision == "allow" } reason = "Allowed" { decision == "allow" } priority: 260Fill in: namespace, agent class, tool name. You can tighten the condition to params too — e.g. only escalate large refunds — but remember an undefined param makes the rule silently not fire, so prefer escalating the whole tool unless you’ve verified the param is always present in real traffic.
Recipe 7 — Namespace-wide guardrails and sector packs (no YAML)
Section titled “Recipe 7 — Namespace-wide guardrails and sector packs (no YAML)”Two policy layers are materialized through the API rather than authored as NrvqPolicy objects,
and both are tighten-only overlays — they can add restrictions on top of the recipes above, never
relax them (see Writing policies §3):
-
Sector packs — curated rulesets for finance, healthcare, government, energy, telecom, ecommerce, ERP/CRM, and media (
policies/sector/). Enable one per namespace:Terminal window curl -s -X POST "$NRVQ_API_URL/api/v1/policy-packs/<pack-id>/enable?namespace=chatbot-prod" \-H "Authorization: Bearer $NRVQ_API_TOKEN" -
Guardrail overlay (
__guardrail__) — an opt-in per-namespace tool allowlist that sits on top of every class policy in the namespace, created via the normalPOST /api/v1/policiesendpoint against the__guardrail__agent-class key.
Compliance-gap remediation overlays (<class>__remediation__) are generated from the console’s
compliance dashboard, not written by hand.
Rego building blocks
Section titled “Rego building blocks”Complete, drop-in Rego modules for the conditions you’ll actually write. Every one below compiles
under the engine’s OPA mode and satisfies the write-time validator — paste one into spec.rego
(recipes 4–6 show the surrounding YAML) and adapt the names.
What your Rego can see
Section titled “What your Rego can see”The engine hands every policy this input (built in norviq/engine/evaluator.py::_build_input) —
these are the only fields to match on; there is no input.action or input.resource:
| Field | What it is |
|---|---|
input.tool_name |
the tool being called, exactly as the agent sent it |
input.tool_name_normalized |
confusable-skeleton of the name — homoglyph/zero-width evasion collapsed; match against this for evasion-resistant name checks |
input.tool_params |
the call’s parameters, as a (possibly nested) object |
input.tool_params_normalized |
the params, normalized the same way for matching |
input.agent.spiffe_id / .namespace / .agent_class |
the caller’s workload identity |
input.trust_score / input.trust_category |
the caller’s current trust score (0–1) and band |
input.session_id / input.call_depth |
session correlation + agent-to-agent chain depth |
Several rules in one policy — the partial-set skeleton
Section titled “Several rules in one policy — the partial-set skeleton”The shape comprehensive.rego and every sector pack use, minimized. Multiple rules can fire on one
call without a compile-time conflict; precedence is block > escalate > audit > allow, and the fired
rule’s own rule_id/reason come through (ties broken by sorted id). Start here whenever a
policy needs more than one rule — add a blocks[...]/escalates[...]/audits[...] line plus a
reasons entry per rule, and leave the resolver tail untouched:
package norviq.custom.support_guard
default decision = "allow"default rule_id = "default_allow"default reason = "Allowed"
blocks["no_raw_sql"] { input.tool_name == "execute_sql" }blocks["no_secret_params"] { walk(input.tool_params, [path, _]) lower(path[count(path) - 1]) == "api_key"}escalates["large_refund"] { input.tool_name == "issue_refund" to_number(input.tool_params.amount) > 500}audits["watch_exports"] { input.tool_name == "export_data" }
reasons = { "no_raw_sql": "Raw SQL is not available to this agent", "no_secret_params": "api_key parameters are never accepted", "large_refund": "Refunds over 500 require human approval", "watch_exports": "Exports are allowed but audited", "default_allow": "Allowed",}
# ── canonical resolver: keep as-is ─────────────────────────────block_fired { blocks[_] }escalate_fired { escalates[_] }audit_fired { audits[_] }
decision = "block" { block_fired }decision = "escalate" { escalate_fired; not block_fired }decision = "audit" { audit_fired; not block_fired; not escalate_fired }
rule_id = sort([id | blocks[id]])[0] { block_fired }rule_id = sort([id | escalates[id]])[0] { escalate_fired; not block_fired }rule_id = sort([id | audits[id]])[0] { audit_fired; not block_fired; not escalate_fired }
reason = reasons[rule_id]This is also the only admissible way to write an audit (log-only) rule: the validator rejects a
module whose only complete rule is decision = "audit" — it demands a block or escalate rule,
which the resolver tail provides. One compile gotcha: the resolver references all three sets, so if
your policy has (say) only audits[...] rules, seed the unused sets with a never-fires rule or OPA
rejects the module with var blocks is unsafe:
blocks["reserved"] { false } # defined but never firesescalates["reserved"] { false }Block a family of tools by name
Section titled “Block a family of tools by name”Catch every delete_*/drop_*-style tool, including ones that don’t exist yet:
package norviq.custom.no_destructive_tools
destructive_prefixes = ["delete_", "drop_", "truncate_", "destroy_"]
default decision = "allow"violation { startswith(lower(input.tool_name), destructive_prefixes[_]) }decision = "block" { violation }rule_id = "no_destructive_tools" { decision == "block" }reason = "Destructive tool names are blocked for this agent" { decision == "block" }rule_id = "default_allow" { decision == "allow" }reason = "Allowed" { decision == "allow" }For evasion resistance (a homoglyph delete_uѕer slipping past), match input.tool_name_normalized
instead of (or in addition to) input.tool_name.
Condition on a parameter value — and fail closed when it’s missing
Section titled “Condition on a parameter value — and fail closed when it’s missing”The single most common Rego mistake: a condition on input.tool_params.amount silently doesn’t
fire when the param is absent, so a call with no amount sails through. If the check matters,
pair it with an explicit missing-param rule:
package norviq.custom.refund_cap
default decision = "allow"
refund { input.tool_name == "issue_refund" }over_cap { refund; to_number(input.tool_params.amount) > 500 }no_amount { refund; not input.tool_params.amount }
decision = "escalate" { over_cap }decision = "escalate" { no_amount }
rule_id = "refund_needs_approval" { decision == "escalate" }reason = "Refunds over 500, or with no stated amount, require approval" { decision == "escalate" }rule_id = "default_allow" { decision == "allow" }reason = "Allowed" { decision == "allow" }Scan every parameter, however deeply nested
Section titled “Scan every parameter, however deeply nested”walk() recurses the whole tool_params object — use it whenever the value you’re looking for
could hide inside a nested object or array instead of a top-level field:
package norviq.custom.no_internal_hosts
default decision = "allow"
internal_ref { walk(input.tool_params, [_, val]) is_string(val) contains(lower(val), "internal.corp")}decision = "block" { internal_ref }rule_id = "no_internal_hosts" { decision == "block" }reason = "References to internal.corp hosts are blocked" { decision == "block" }rule_id = "default_allow" { decision == "allow" }reason = "Allowed" { decision == "allow" }The skeleton’s no_secret_params rule shows the sibling trick: walk the key path instead of the
value (path[count(path) - 1]) to catch a sensitive key name at any depth.
Choosing a priority
Section titled “Choosing a priority”Highest priority wins; on a tie, the most restrictive decision (block > escalate > audit >
allow) does. The shipped conventions:
| Range | Who | Use for |
|---|---|---|
| ~50 | you | namespace baselines / floors (Recipe 1) |
| 100 | — | the CRD default if you omit priority |
| 150–300 | you | agent-class policies and targeted custom rules (Recipes 2–6) |
| up to 499 | you | the ceiling for namespace-scoped policies — the API rejects higher |
| 500–1000 | admin | clusterPriority — cluster baselines and control-plane policy only |
Give a more specific policy a higher number than the floors beneath it — specificity alone
doesn’t win, the number does. GET /api/v1/policies/effective?namespace=<ns>&agent_class=<class>
shows the exact ordered stack the evaluator would resolve right now, which layer is winning, and why.
Validate before you enforce
Section titled “Validate before you enforce”The same loop for every recipe — details in Writing policies §4–5:
# 1. Dry-run: replay the draft against real recent traffic — how many currently-allowed# calls would it NEWLY block?norviq policy dry-run -f policy.rego -n chatbot-prod -c customer-support
# 2. Apply the CRD and confirm the controller synced it (PHASE goes Active)kubectl apply -f my-policy.yamlkubectl get nrvqpolicy -n chatbot-prod
# 3. Prove it blocks: run the adversarial suite against the classnorviq redteam run --namespace chatbot-prod --agent customer-supportIf you want a whole namespace observe-only while a batch of changes settles, flip its posture to
audit (PUT /api/v1/settings?namespace=<ns> with {"enforcement_mode": "audit"}), watch for
monitor_would_block:* audit entries, then flip back.