Policies
Policies are the rules that govern what your AI agents can and cannot do. This page covers everything: how to construct them, how they connect to your SDK calls, evaluation order, wildcards, conditions, encryption, and caching.
The Core Concept
A policy is a named collection of rules. Each rule says: "When an agent tries to do action X on resource Y, the answer is allow or deny."
{
"name": "my-policy",
"rules": [
{
"effect": "allow",
"action": "llm.generate",
"resource": "model/gpt-4"
}
]
}
When your SDK code calls cz.enforce(action="llm.generate", resource="model/gpt-4"), the SDK finds this rule, sees the effect is allow, and lets the action proceed.
How SDK Calls Map to Policy Rules
This is the most important thing to understand. Here is the exact mapping:
SDK call: Policy rule:
cz.enforce( {
action="llm.generate", --> "action": "llm.generate",
resource="model/gpt-4", --> "resource": "model/gpt-4",
context={ --> "conditions": {
"agent_id": "agent-001", --> "agent_id": "agent-*"
}, },
) "effect": "allow"
}
- The SDK's
actionparameter is matched against the rule'sactionfield. - The SDK's
resourceparameter is matched against the rule'sresourcefield. - The SDK's
contextdictionary is matched against the rule'sconditionsobject (if present). - If all fields match, the rule's
effectdetermines the outcome.
Constructing a Policy: Step by Step
Step 1: Choose Your Actions
Actions describe what the agent is trying to do. You define the naming convention. Here are common patterns:
| Action | What It Means | When to Use |
|---|---|---|
llm.generate | Call an LLM for text generation | Before any chat.completions.create() or messages.create() call |
llm.embed | Generate embeddings | Before any embeddings.create() call |
tool.call | Invoke a tool or function | Before executing a tool the LLM requested |
mcp.tool.call | Invoke an MCP tool | Before calling a tool via MCP protocol |
mcp.resource.read | Read an MCP resource | Before reading data via MCP |
data.read | Read from a data source | Before querying a database or vector store |
data.write | Write to a data source | Before inserting or updating data |
file.read | Read a file | Before accessing a file on disk |
file.write | Write a file | Before writing or modifying a file |
api.request | Make an outbound HTTP request | Before calling an external API |
* | Any action | Catch-all rule |
You can invent your own action names too. They are just strings. The SDK and policy rules use the same strings -- that is how they connect.
Step 2: Choose Your Resources
Resources describe the target of the action. You define the naming convention:
| Resource Pattern | What It Targets | Example SDK Call |
|---|---|---|
model/gpt-4 | A specific LLM model | enforce(action="llm.generate", resource="model/gpt-4") |
model/claude-* | All Claude models (wildcard) | enforce(action="llm.generate", resource="model/claude-sonnet-4-20250514") |
tool/search_web | A specific tool | enforce(action="tool.call", resource="tool/search_web") |
mcp://filesystem/read_file | A specific MCP tool | enforce(action="mcp.tool.call", resource="mcp://filesystem/read_file") |
mcp://filesystem/* | All tools on an MCP server | Matches any tool on the filesystem server |
vectorstore/documents | A data collection | enforce(action="data.read", resource="vectorstore/documents") |
https://api.example.com/* | An API endpoint | enforce(action="api.request", resource="https://api.example.com/v1/users") |
* | Any resource | Catch-all |
Step 3: Set the Effect
Each rule has an effect: either allow or deny.
{"effect": "allow", "action": "llm.generate", "resource": "model/gpt-4"}
{"effect": "deny", "action": "llm.generate", "resource": "model/gpt-4-turbo*"}
Step 4: Add Conditions (Optional)
Conditions let you restrict rules based on context values passed from the SDK:
{
"effect": "allow",
"action": "llm.generate",
"resource": "model/gpt-4",
"conditions": {
"agent_id": "support-*",
"environment": "production"
}
}
This rule only matches when the SDK call includes context={"agent_id": "support-...", "environment": "production"}. All conditions must match (AND logic). Condition values support glob wildcards (*).
In your SDK code:
cz.enforce(
action="llm.generate",
resource="model/gpt-4",
context={
"agent_id": "support-agent-1", # matches "support-*"
"environment": "production", # matches "production"
},
)
Rule Fields Reference
| Field | Type | Required | Description |
|---|---|---|---|
effect | "allow" or "deny" | Yes | Whether the rule permits or blocks the action |
action | string | Yes | The action to match. Supports wildcards (*) |
resource | string | Yes | The resource to match. Supports wildcards (*) |
conditions | object | No | Key-value pairs that must all match the request context. Values support glob patterns |
Wildcards
Both action and resource fields support glob-style wildcards:
{"action": "llm.*", "resource": "*"}
{"action": "mcp.tool.call", "resource": "mcp://filesystem/*"}
{"action": "*", "resource": "*"}
| Pattern | Matches | Does Not Match |
|---|---|---|
model/gpt-4 | model/gpt-4 only | model/gpt-4-turbo |
model/gpt-4* | model/gpt-4, model/gpt-4-turbo, model/gpt-4o | model/gpt-3.5-turbo |
model/* | Any model | tool/search_web |
mcp://filesystem/* | mcp://filesystem/read_file, mcp://filesystem/write_file | mcp://github/create_issue |
* | Everything | (matches all) |
Policy Evaluation Order
When multiple rules could match the same enforce() call, Control Zero applies this precedence:
Three rules:
- Explicit deny always wins. If any deny rule matches, the action is blocked -- even if an allow rule also matches.
- More specific rules take precedence over broader ones. A rule for
model/gpt-4beats a rule formodel/*. - No match = deny. If no rule matches the action and resource, the action is denied. This is the secure-by-default posture.
Example: Evaluation in Practice
Given this policy:
{
"rules": [
{ "effect": "allow", "action": "llm.generate", "resource": "model/*" },
{ "effect": "deny", "action": "llm.generate", "resource": "model/gpt-4-turbo*" }
]
}
| SDK Call | Rule 1 Match? | Rule 2 Match? | Result |
|---|---|---|---|
enforce("llm.generate", "model/gpt-4") | Yes (allow) | No | ALLOWED |
enforce("llm.generate", "model/gpt-3.5-turbo") | Yes (allow) | No | ALLOWED |
enforce("llm.generate", "model/gpt-4-turbo") | Yes (allow) | Yes (deny) | DENIED (deny wins) |
enforce("tool.call", "tool/search") | No | No | DENIED (no match = deny) |
Complete Policy Examples
Example 1: Model Governance
Allow specific models, deny everything else:
{
"name": "model-governance",
"description": "Only approved models can be used",
"rules": [
{
"effect": "allow",
"action": "llm.generate",
"resource": "model/gpt-4"
},
{
"effect": "allow",
"action": "llm.generate",
"resource": "model/claude-sonnet-4-20250514"
},
{
"effect": "deny",
"action": "llm.generate",
"resource": "model/*"
}
]
}
The last rule acts as a catch-all: any model not explicitly allowed is denied.
In your code:
cz.enforce(action="llm.generate", resource="model/gpt-4") # ALLOWED
cz.enforce(action="llm.generate", resource="model/claude-sonnet-4-20250514") # ALLOWED
cz.enforce(action="llm.generate", resource="model/gpt-4o") # DENIED (catch-all)
Example 2: MCP Tool Control
Control which MCP tools an agent can invoke:
{
"name": "mcp-tool-control",
"description": "Restrict MCP tool access per agent",
"rules": [
{
"effect": "allow",
"action": "mcp.tool.call",
"resource": "mcp://filesystem/read_file",
"conditions": { "agent_id": "analyst-*" }
},
{
"effect": "deny",
"action": "mcp.tool.call",
"resource": "mcp://filesystem/write_file"
},
{
"effect": "allow",
"action": "mcp.tool.call",
"resource": "mcp://github/*",
"conditions": { "agent_id": "dev-agent" }
},
{
"effect": "deny",
"action": "mcp.tool.call",
"resource": "mcp://*"
}
]
}
In your code:
# Analyst agent reading a file -- ALLOWED (rule 1 matches)
cz.enforce(
action="mcp.tool.call",
resource="mcp://filesystem/read_file",
context={"agent_id": "analyst-42"},
)
# Any agent writing a file -- DENIED (rule 2 matches)
cz.enforce(
action="mcp.tool.call",
resource="mcp://filesystem/write_file",
context={"agent_id": "analyst-42"},
)
# Dev agent using GitHub -- ALLOWED (rule 3 matches)
cz.enforce(
action="mcp.tool.call",
resource="mcp://github/create_issue",
context={"agent_id": "dev-agent"},
)
# Unknown MCP tool -- DENIED (rule 4 catch-all)
cz.enforce(
action="mcp.tool.call",
resource="mcp://slack/send_message",
context={"agent_id": "analyst-42"},
)
Example 3: Multi-Layer Production Policy
A comprehensive policy combining model governance, tool control, data access, and API restrictions:
{
"name": "production-agent-policy",
"description": "Full governance for production AI agents",
"rules": [
{
"effect": "allow",
"action": "llm.generate",
"resource": "model/gpt-4",
"conditions": { "environment": "production" }
},
{
"effect": "allow",
"action": "tool.call",
"resource": "tool/search_web"
},
{
"effect": "deny",
"action": "tool.call",
"resource": "tool/execute_code"
},
{
"effect": "allow",
"action": "data.read",
"resource": "vectorstore/public-docs"
},
{
"effect": "deny",
"action": "data.read",
"resource": "vectorstore/internal-*"
},
{
"effect": "allow",
"action": "api.request",
"resource": "https://api.internal.example.com/*"
},
{
"effect": "deny",
"action": "api.request",
"resource": "https://*.external.example.com/*"
},
{
"effect": "deny",
"action": "*",
"resource": "*"
}
]
}
The last rule is a global catch-all: anything not explicitly allowed is denied.
Policy Bundles
Policies are not sent to SDKs individually. Instead, all active policies for a project are compiled into a single policy bundle.
How Bundles Work
Bundle Security
| Layer | Protection |
|---|---|
| Encryption at rest | AES-256-GCM with project-specific key |
| Encryption in transit | TLS 1.3 |
| Payload encryption | AES-256-GCM, key derived from project API key via HKDF-SHA256 |
| Integrity verification | Ed25519 signature checked before loading |
| Versioning | SDKs only apply bundles newer than the current one |
If signature verification fails, the SDK rejects the bundle and continues using the previous valid policy.
Local Caching
The SDK caches the current bundle on disk. This provides:
- Offline enforcement -- If the SDK cannot reach the server, it enforces the last known good policy.
- Zero-latency evaluation -- Every
enforce()call evaluates against local memory. No network round-trip. - Resilience -- Network outages or server maintenance do not interrupt policy enforcement.
Default cache location:
- Python:
~/.controlzero/cache/ - Go:
~/.controlzero/cache/ - Node.js:
~/.controlzero/cache/
Refreshing Policies
The SDK polls for new bundles every 60 seconds by default. You can also force a refresh:
# Python
cz.refresh_policies()
// Go
err := client.RefreshPolicies(ctx)
// Node.js
await cz.refreshPolicies();
Next Steps
- Quick Start -- Build a working agent with policy enforcement.
- Projects -- Organize agents into projects with separate policies.
- MCP Tool Control Guide -- Deep dive into controlling MCP tools.
- Integrations -- Auto-enforce on OpenAI, Anthropic, LangChain, and more.