Skip to content

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.

Every policy is the answer to these five questions — collect them first and the YAML writes itself:

  1. Where does the agent run? The Kubernetes namespace. That’s the policy’s metadata.namespace, and (for injection) the namespace that carries the norviq-injection=enabled label.
  2. Who is the policy for? The spec.target — one of:
    • agentClass: <class> — every agent whose pod is labeled norviq.io/agent-class: <class> (register the class with an NrvqClass first — see the worked example),
    • namespace: <ns> — everything in a namespace (a baseline/floor), or
    • kind: Deployment + name: <workload> — one specific workload (kind accepts Deployment / StatefulSet / DaemonSet / ReplicaSet).
  3. 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-blocked from the CLI. Your Rego matches on input.tool_name and input.tool_params.<field>, so the exact names matter.
  4. What should a violation do? block (deny outright), audit (log what would have happened, let it proceed), or escalate (hold for human review — the SDK adapters treat escalate as not allowed, so the call does not run while it awaits review; it fails closed, it doesn’t fail open).
  5. 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/v1alpha1
kind: NrvqPolicy
metadata:
name: prod-baseline
namespace: chatbot-prod # ← your namespace
spec:
target:
namespace: chatbot-prod # ← same namespace: applies to every agent in it
enforcementMode: audit
preset: permissive
priority: 50 # low on purpose — class policies outrank it

Fill 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/v1alpha1
kind: NrvqPolicy
metadata:
name: chatbot-strict
namespace: chatbot-prod # ← your namespace
spec:
target:
agentClass: customer-support # ← your NrvqClass name / pod label value
enforcementMode: block
preset: strict
priority: 200

Fill 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/v1alpha1
kind: NrvqPolicy
metadata:
name: analyst-moderate
namespace: analytics # ← your namespace
spec:
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: 150

Fill 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/v1alpha1
kind: NrvqPolicy
metadata:
name: support-allowlist
namespace: chatbot-prod # ← your namespace
spec:
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: 250

Fill 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/v1alpha1
kind: NrvqPolicy
metadata:
name: custom-sql-guard
namespace: chatbot-prod # ← your namespace
spec:
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: 300

Fill 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/v1alpha1
kind: NrvqPolicy
metadata:
name: refund-approval
namespace: chatbot-prod # ← your namespace
spec:
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: 260

Fill 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 normal POST /api/v1/policies endpoint against the __guardrail__ agent-class key.

Compliance-gap remediation overlays (<class>__remediation__) are generated from the console’s compliance dashboard, not written by hand.

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.

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 fires
escalates["reserved"] { false }

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.

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.

The same loop for every recipe — details in Writing policies §4–5:

Terminal window
# 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.yaml
kubectl get nrvqpolicy -n chatbot-prod
# 3. Prove it blocks: run the adversarial suite against the class
norviq redteam run --namespace chatbot-prod --agent customer-support

If 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.