Skip to main content

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 action parameter is matched against the rule's action field.
  • The SDK's resource parameter is matched against the rule's resource field.
  • The SDK's context dictionary is matched against the rule's conditions object (if present).
  • If all fields match, the rule's effect determines 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:

ActionWhat It MeansWhen to Use
llm.generateCall an LLM for text generationBefore any chat.completions.create() or messages.create() call
llm.embedGenerate embeddingsBefore any embeddings.create() call
tool.callInvoke a tool or functionBefore executing a tool the LLM requested
mcp.tool.callInvoke an MCP toolBefore calling a tool via MCP protocol
mcp.resource.readRead an MCP resourceBefore reading data via MCP
data.readRead from a data sourceBefore querying a database or vector store
data.writeWrite to a data sourceBefore inserting or updating data
file.readRead a fileBefore accessing a file on disk
file.writeWrite a fileBefore writing or modifying a file
api.requestMake an outbound HTTP requestBefore calling an external API
*Any actionCatch-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 PatternWhat It TargetsExample SDK Call
model/gpt-4A specific LLM modelenforce(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_webA specific toolenforce(action="tool.call", resource="tool/search_web")
mcp://filesystem/read_fileA specific MCP toolenforce(action="mcp.tool.call", resource="mcp://filesystem/read_file")
mcp://filesystem/*All tools on an MCP serverMatches any tool on the filesystem server
vectorstore/documentsA data collectionenforce(action="data.read", resource="vectorstore/documents")
https://api.example.com/*An API endpointenforce(action="api.request", resource="https://api.example.com/v1/users")
*Any resourceCatch-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

FieldTypeRequiredDescription
effect"allow" or "deny"YesWhether the rule permits or blocks the action
actionstringYesThe action to match. Supports wildcards (*)
resourcestringYesThe resource to match. Supports wildcards (*)
conditionsobjectNoKey-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": "*"}
PatternMatchesDoes Not Match
model/gpt-4model/gpt-4 onlymodel/gpt-4-turbo
model/gpt-4*model/gpt-4, model/gpt-4-turbo, model/gpt-4omodel/gpt-3.5-turbo
model/*Any modeltool/search_web
mcp://filesystem/*mcp://filesystem/read_file, mcp://filesystem/write_filemcp://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:

  1. Explicit deny always wins. If any deny rule matches, the action is blocked -- even if an allow rule also matches.
  2. More specific rules take precedence over broader ones. A rule for model/gpt-4 beats a rule for model/*.
  3. 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 CallRule 1 Match?Rule 2 Match?Result
enforce("llm.generate", "model/gpt-4")Yes (allow)NoALLOWED
enforce("llm.generate", "model/gpt-3.5-turbo")Yes (allow)NoALLOWED
enforce("llm.generate", "model/gpt-4-turbo")Yes (allow)Yes (deny)DENIED (deny wins)
enforce("tool.call", "tool/search")NoNoDENIED (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

LayerProtection
Encryption at restAES-256-GCM with project-specific key
Encryption in transitTLS 1.3
Payload encryptionAES-256-GCM, key derived from project API key via HKDF-SHA256
Integrity verificationEd25519 signature checked before loading
VersioningSDKs 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.