Chapter 4.7 — Policy-as-code¶
🧠 Concept¶
Critical policies are codified, testable and versioned. Examples:
- "An agent cannot send external email without dual approval."
- "A CRM tool only accesses fields permitted by role."
- "Confidential documents do not go to an external model without a contract."
🧰 Common engines¶
- Open Policy Agent (OPA) / Rego — a general-purpose declarative engine, used in Kubernetes, microservices, gateways and agents.
- AWS Cedar — a policy language focused on authorization, with "policy + entity store" semantics.
- AuthZed SpiceDB — distributed ReBAC, a model inspired by Zanzibar (Google).
🧪 Practical example — authorizing an agent tool in Rego¶
Scenario: a harness calls OPA passing the input below before executing any tool_call. The policy must:
- deny a tool with status
deprecatedorremoved; - deny cross-tenant (
user.tenant_id != resource.tenant_id); - require human approval for
risk_level == "high"; - allow only tools in the role's allowlist.
Typical input sent by the executor:
{
"user": {
"id": "u-1042",
"tenant_id": "tenant-abc",
"roles": ["support-agent"]
},
"agent": {
"id": "support-agent@2.1.0",
"tenant_id": "tenant-abc"
},
"tool": {
"name": "crm.update_contact",
"version": "1.4.0",
"status": "active",
"risk_level": "high"
},
"action": "execute",
"resource": {
"type": "contact",
"tenant_id": "tenant-abc"
},
"approval": {
"required": true,
"granted_by": "manager-7",
"granted_at": "2026-05-10T18:22:00Z"
}
}
Rego policy (educational example):
package agent.tools.authz
default allow := false
# Allowlist of tools per role.
allowed_tools := {
"support-agent": {
"crm.search_contact",
"crm.update_contact",
"kb.search",
},
"billing-agent": {
"crm.search_contact",
"billing.create_invoice",
},
}
# Tools with a forbidden status can never run.
blocked_status := {"deprecated", "removed", "blocked"}
# Helper: is the tool in the allowlist of the user's role?
tool_in_allowlist {
some role
role := input.user.roles[_]
allowed_tools[role][input.tool.name]
}
# Helper: user tenant == agent tenant == resource tenant.
same_tenant {
input.user.tenant_id == input.agent.tenant_id
input.user.tenant_id == input.resource.tenant_id
}
# Helper: valid human approval for high risk.
approval_ok {
input.tool.risk_level != "high"
}
approval_ok {
input.tool.risk_level == "high"
input.approval.required
input.approval.granted_by != ""
}
# Main rule.
allow {
not blocked_status[input.tool.status]
tool_in_allowlist
same_tenant
approval_ok
}
# Denial reasons (useful for the audit log).
deny_reason["tool_status_blocked"] {
blocked_status[input.tool.status]
}
deny_reason["tool_not_in_allowlist"] {
not tool_in_allowlist
}
deny_reason["cross_tenant"] {
not same_tenant
}
deny_reason["approval_missing"] {
input.tool.risk_level == "high"
not input.approval.granted_by
}
What this policy guarantees:
- Blocks a deprecated/removed tool regardless of what the model proposes.
- Blocks a cross-tenant call even if the prompt said otherwise.
- Blocks a tool outside the role's allowlist.
- Requires recorded human approval for
risk_level == "high". - Returns structured reasons for the audit log.
What this policy does NOT guarantee:
- That the content of the arguments is valid (needs schema/Pydantic).
- That the human approval was conscious (depends on UX and training).
- That the tool's backend applies internal RBAC (the called service also needs authorization — defense in depth).
- That the
inputsent to OPA is honest (depends on the harness; whoever controls the input controls the decision).
🧪 Policy tests¶
Every policy needs positive and negative tests versioned in the same repo. In OPA, this can be done with opa test and *_test.rego files:
package agent.tools.authz_test
import data.agent.tools.authz
test_allow_simple_case {
authz.allow with input as {
"user": {"id": "u1", "tenant_id": "t1", "roles": ["support-agent"]},
"agent": {"id": "a@1", "tenant_id": "t1"},
"tool": {"name": "kb.search", "status": "active", "risk_level": "low"},
"action": "execute",
"resource": {"type": "doc", "tenant_id": "t1"},
"approval": {"required": false, "granted_by": "", "granted_at": ""}
}
}
test_deny_cross_tenant {
not authz.allow with input as {
"user": {"id": "u1", "tenant_id": "t1", "roles": ["support-agent"]},
"agent": {"id": "a@1", "tenant_id": "t1"},
"tool": {"name": "kb.search", "status": "active", "risk_level": "low"},
"action": "execute",
"resource": {"type": "doc", "tenant_id": "t2"},
"approval": {"required": false, "granted_by": "", "granted_at": ""}
}
}
test_deny_deprecated_tool {
not authz.allow with input as {
"user": {"id": "u1", "tenant_id": "t1", "roles": ["support-agent"]},
"agent": {"id": "a@1", "tenant_id": "t1"},
"tool": {"name": "crm.update_contact", "status": "deprecated", "risk_level": "high"},
"action": "execute",
"resource": {"type": "contact", "tenant_id": "t1"},
"approval": {"required": true, "granted_by": "m7"}
}
}
🧰 Related practical examples (planned)¶
EX-AGT-06— an agent with a Rego policy sidecar blocking tool calls.EX-SEC-03— tool misuse mitigated by a policy.
📌 Checklist¶
- [ ] Are critical policies in code and versioned?
- [ ] Are there policy tests (positive and negative)?
- [ ] Is there observability of policy decisions (decision + reason)?
- [ ] Are decisions logged with the policy bundle version?
- [ ] Does the backend called by the tools apply independent authorization (defense in depth)?
📚 References¶
- Open Policy Agent — Policy Language (Rego): https://www.openpolicyagent.org/docs/policy-language
- Open Policy Agent — Policy Testing: https://www.openpolicyagent.org/docs/policy-testing
- AWS Cedar — Policy Language Reference: https://docs.cedarpolicy.com/policies/syntax-policy.html
- AuthZed SpiceDB — Concepts: https://authzed.com/docs/spicedb/concepts/zedtokens
- Google Zanzibar (paper): https://research.google/pubs/pub48190/