Developers / Cookbook

Rules for the actions that actually cost something.

Policy is where governance stops being architecture and starts being a decision someone wrote down. These are reference rules for the consequential cases — each one narrow enough to review, and mapped to the seam it is enforced at.

Deterministic — no model in the path Reviewed as a file, not a setting Every rule states its seam

DESTRUCTIVE ACTIONS

Changes to a system of record.

The boundary worth defending is the point where intent becomes an entry someone else depends on.

Deny deletion outright

Intent. No agent deletes a record, under any condition. The simplest rule in the book, and the one most deployments start with.

Seam. Any — argument-aware seams inspect the operation  ·  Verdict. deny

# policy.yaml — the tool lists are evaluated BEFORE any rule, and an explicit # deny wins outright, so no later rule can override it. tools: denied: - "*.delete" - "database.drop" - "database.truncate" # Or as a named rule, when you want a rule_id on the decision record. rules: - id: deny-all-deletes priority: 1 match: tool: "*.delete" action: deny
# policy.rego — contributes to the decision object the gateway reads at # data.governor.decision.decision. tool_matches/2 is defined by the base module. package governor.decision import future.keywords.if deny if { tool_matches(input.action.tool, "*.delete") } deny_reason := sprintf("Tool %s deletes a record and is denied", [input.action.tool]) if { tool_matches(input.action.tool, "*.delete") } rule_id := "deny-all-deletes" if { tool_matches(input.action.tool, "*.delete") }

Start here. A rule you can explain in one sentence is a rule an approver will actually sign off.

Require approval above a threshold

Intent. Writes proceed unattended below a stated value and pause for a human above it. The threshold is the policy; the agent does not choose it.

Seam. Argument-aware seams only — the value must be readable off the call  ·  Verdict. pause

# policy.yaml — a condition's field path resolves against the execute # request's `args` object; the `args.` prefix is stripped before lookup. rules: - id: high_value_payment priority: 1 match: tool: "erp.process_payment" condition: field: "args.amount" operator: ">" value: 5000 action: require_approval # Operators: == != > < >= <= in contains # Actions: allow deny require_approval transform
package governor.decision import future.keywords.if require_approval if { input.action.tool == "erp.process_payment" to_number(input.action.args.amount) > 5000 } approval_reason := sprintf("Payment of $%.2f requires approval", [to_number(input.action.args.amount)]) if { input.action.tool == "erp.process_payment" to_number(input.action.args.amount) > 5000 } rule_id := "high_value_payment" if { input.action.tool == "erp.process_payment" to_number(input.action.args.amount) > 5000 }

The pause verdict is expressed differently per seam: 202 on a gateway, an approval response on the MCP proxy. State which you are targeting.

Scope by environment

Intent. The same agent is permitted in staging and denied in production. One rule, evaluated against deployment context rather than agent identity.

Seam. Any  ·  Verdict. allow / deny

# A structured condition resolves ONLY against `args.*` and `inspection.*`, # so environment is not a rule condition. It is policy-level scope: the engine # selects a policy by the request's agent_id and namespace, so the same rules # ship twice under two namespaces. # staging.yaml id: payments-staging namespace: staging environment: staging governance_mode: shadow fail_closed: false rules: - id: writes priority: 1 match: tool: "erp.*" action: allow # production.yaml id: payments-production namespace: production environment: production governance_mode: enforcement fail_closed: true rules: - id: writes priority: 1 match: tool: "erp.*" action: deny
# In Rego it IS a condition: `namespace` is a canonical execute-request field # and arrives as input.action.namespace. (input.policy carries no environment.) package governor.decision import future.keywords.if deny if { input.action.namespace == "production" tool_matches(input.action.tool, "erp.*") } deny_reason := sprintf("%s is not permitted in production", [input.action.tool]) if { input.action.namespace == "production" tool_matches(input.action.tool, "erp.*") } rule_id := "writes" if { input.action.namespace == "production" tool_matches(input.action.tool, "erp.*") }

Cheap to add, and it removes the most common objection in an architecture review.

DATA EGRESS

What leaves, in prompts, completions and tool calls.

Redact before the call proceeds

Intent. The action is allowed, but a field is rewritten on the way through. This is the verdict that needs the most care: it is the one seams differ on.

Seam. Envoy ext_proc (body mutation) and inbound seams. Forward-auth cannot do this — it cannot rewrite a body, by protocol  ·  Verdict. redact

How it is configured. Redaction is not a rule action. RuleAction admits four values — allow, deny, require_approval and transform — and a rule carries no field naming what to rewrite. A rule routes an action into shaping; the detector decides which leaf fields are shaped.

# policy.yaml — the rule selects the transform path. It matches the tool and, # optionally, one structured condition. It does NOT name the field to redact: # pkg/types Rule has id, priority, match and action, and nothing else. rules: - id: shape-support-payloads priority: 20 match: tool: "crm.case_update" action: transform

What actually selects the field. Inline inspection walks the leaf fields of args on the canonical execute request — session_id, agent_id, namespace, tool, args. A detector fires against those fields, and a per-account override maps that detector, on a named seam, to an outcome.

# outcome override — detector_kind x seam -> decision. # detector_kind: secret | sensitive_data | regulated_identifier | prompt_injection # | encoded_payload | risky_tool_intent | unsafe_destination # seam: gateway_execute | mcp_invocation | provider_bedrock | provider_azure # | provider_gcp | sdk_wrapper | saas_native detector_kind: regulated_identifier seam: gateway_execute decision: redact
Why this section does not show a Rego redact rule

The Rego contract returns {"decision", "reason"}, and the decision is the verdict, not a set of rewritten fields. Nothing in the shipped Rego corpus returns redact, and a module cannot name the field to shape either. Writing one here would be inventing a form the policy engine does not read. Where a seam cannot shape at all, the capability is declared rather than assumed: redaction_supported is a per-seam flag, and forward-auth sets a shaping-required header instead of rewriting, because it cannot rewrite a proxied body by protocol.

Shaped arguments returned end-to-end are materialized for the AWS Bedrock inbound seam today. On other adapters, plan around allow, deny and pause.

Deny egress to an unapproved destination

Intent. The tool call is permitted in principle but its destination is not on the approved list.

Seam. Any  ·  Verdict. deny

# policy.yaml — there is no `not in` operator, so a structured rule denies the # attribute you can name rather than absence from a list. rules: - id: deny-external-transfers priority: 1 match: tool: "payments.wire_transfer" condition: field: "args.destination_type" operator: "==" value: "external" action: deny - id: deny-pii-export priority: 2 match: tool: "api.get" condition: field: "args.endpoint" operator: "contains" value: "/pii" action: deny
# The approved-list form the YAML cannot express — Rego can negate a set. package governor.decision import future.keywords.if import future.keywords.in approved_destinations := {"erp.internal", "warehouse.internal", "s3://acme-reports"} deny if { input.action.args.destination not input.action.args.destination in approved_destinations } deny_reason := sprintf("Destination %s is not on the approved list", [input.action.args.destination]) if { input.action.args.destination not input.action.args.destination in approved_destinations } rule_id := "destination-not-approved" if { input.action.args.destination not input.action.args.destination in approved_destinations }

Pairs with discovery: the approved list is only meaningful if you know what is actually running.

UNREGISTERED ACTIVITY

Tools and agents nobody declared.

Deny unregistered tools by default

Intent. Anything not in the manifest is denied rather than logged. This converts discovery from a report into a control.

Seam. Any  ·  Verdict. deny

# policy.yaml — `tools.allowed` IS the manifest. A tool that matches nothing in # it is denied at evaluation step 4, before any rule runs; `fail_closed: true` # makes the default decision a deny for anything that reaches it. governance_mode: enforcement fail_closed: true tools: allowed: - "crm.read_case" - "crm.create_case" - "erp.*" # Ship it as `governance_mode: shadow` first: the policy is evaluated and the # would-be verdict recorded, but nothing is enforced. The other non-enforcing # modes are audit_only and learning.
package governor.decision import future.keywords.if import future.keywords.in registered_tools := {"crm.read_case", "crm.create_case", "erp.read_invoice"} deny if { not input.action.tool in registered_tools } deny_reason := sprintf("Tool %s is not in the manifest", [input.action.tool]) if { not input.action.tool in registered_tools } rule_id := "deny-unregistered" if { not input.action.tool in registered_tools }

A deny-by-default rule is only deployable once discovery is complete. Run it in report mode first, or it will stop work on day one.

Honesty note

One body above is still marked: the redact rule. The engine accepts transform as a rule action, but a structured rule carries no place to say what to rewrite, and the evaluator treats every non-allow action as a block — so a transform rule written today would stop the call, not shape it. Shaped arguments are produced by the inline-inspection path and materialized end-to-end for the AWS Bedrock inbound seam only. Writing a redact rule that looks runnable and is not would cost a reader more than an empty block, and this cookbook is aimed at exactly the reader who pastes it straight in. Every other rule here uses real field names, real operators and real actions, matched against the canonical execute request.

Continue