Skip to main content

Policies

Supported modes: Hosted Hybrid Local Available in: Free Solo Teams (retention varies by tier -- see Feature Availability)

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.guard("llm", method="generate", args={"model": "gpt-5.4"}), the SDK finds this rule, sees the effect is allow, and lets the action proceed.

Action string format: Action strings in policy rules use colon-separator format (tool:method). SDK calls pass tool and method as separate parameters, and they are matched as tool:method. Examples: llm:generate, tool:call, data:read, api:request.

How SDK Calls Map to Policy Rules

This is the most important thing to understand. The guard() call maps the tool name and method to a policy action, and the args to policy conditions:

SDK call:                                       Policy rule:
cz.guard( {
"llm", "action": "llm:generate",
method="generate", --> "resource": "model/gpt-5.4",
args={ --> "conditions": {
"model": "gpt-5.4", "agent_id": "agent-*"
"agent_id": "agent-001", },
}, "effect": "allow"
) }
  • The tool name and method form the policy action string (e.g., "llm" + "generate" = "llm:generate").
  • The args 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-5.4A specific LLM modelguard("llm", method="generate", context={"resource": "model/gpt-5.4"})
model/claude-*All Claude models (wildcard)guard("llm", method="generate", context={"resource": "model/claude-sonnet-4-6"})
tool/search_webA specific toolguard("tool", method="call", context={"resource": "tool/search_web"})
mcp://filesystem/read_fileA specific MCP toolguard("mcp.tool", method="call", context={"resource": "mcp://filesystem/read_file"})
mcp://filesystem/*All tools on an MCP serverMatches any tool on the filesystem server
vectorstore/documentsA data collectionguard("data", method="read", context={"resource": "vectorstore/documents"})
https://api.example.com/*An API endpointguard("api", method="request", context={"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-5.4" },
{ "effect": "deny", "action": "llm:generate", "resource": "model/gpt-4*" }
]

Step 4: Add Conditions (Optional)

Conditions let you restrict a rule based on runtime values passed from the SDK. They are implemented in the local policy enforcer (Python, Node, and Go), so they work in both hosted and local-only modes.

{
"effect": "allow",
"action": "llm:generate",
"resource": "model/gpt-4",
"conditions": {
"agent_id": "support-*",
"environment": "production"
}
}

See the dedicated Conditions section below for the full matching semantics.

Validation

When you publish or save a policy version, the platform validates every actions[*] entry in every rule against the canonical action set and the alias table (see Canonical tool names). A rule that targets an unknown action (typo, made-up name) is rejected with a 422 validation_failed response carrying a did_you_mean suggestion list. This catches the silent "rule lands but never fires" class of bug at authoring time rather than after the rule has been shipped to production.

Example response when a rule targets database:queryy (typo):

{
"error": "validation_failed",
"unknown_actions": ["database:queryy"],
"suggestions": {
"database:queryy": ["database:query (legacy)"]
}
}

The dashboard rule editor renders the offending rule with a red border and a one-click "use database:query" button per suggestion. Suggestions tagged (legacy) come from the alias shim for pre-canonical rules; suggestions without a tag are the modern canonical class (prefer those for new rules).

The SDK runs the same validator as a warning at policy-load time so local-policy-mode users (no platform backend) still see the typo. The SDK warning is non-blocking -- the policy still loads -- so customers using a custom tool the SDK does not yet know about can keep going while still seeing the did-you-mean for genuine typos.

The validator's known-action set is the union of canonical SDK extractor tools, host-tool aliases (e.g. Read resolves to file_read), the four canonical SQL semantic classes (database:read|write|admin|exec), every legacy SQL alias (database:query, database:SELECT, database:DROP, ...), and wildcards (*, tool:*, *:method). Updating the SDK alias table automatically widens what the validator accepts.

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
clientslist of stringsNoRestrict the rule to specific AI clients (cursor, claude-code, gemini-cli, codex-cli, windsurf, python-sdk, etc.). Empty/absent = matches every client. Supports glob patterns. Added in #175.
projectslist of stringsNoRestrict the rule to specific project IDs. Empty/absent = matches every project. Supports glob patterns. Added in #175.

Selector semantics (clients / projects)

A rule with a non-empty clients list is skipped unless the request's detected client name matches one of the entries. Same for projects. Both selectors must positively pick the request: an empty client_name does NOT match clients: ["cursor"]. First-match-wins still applies — there is no implicit "most specific wins" re-ordering. Express priority by authoring the more specific rule before the more general one:

rules:
- allow: delete_* # cursor-only override fires first
clients: ['cursor']
- deny: delete_* # global default applies to everything else

Glob syntax note: clients / projects glob support matches the rule for action / resource*, prefix*, *suffix in Rust + Go, plus ? and [seq] in Python (via fnmatch.fnmatchcase). This is a pre-existing cross-SDK behavior class that also applies to actions and resources; we recommend sticking to the lowest-common-denominator syntax (exact match, prefix*, *suffix) for portability.

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-5.4model/gpt-5.4 onlymodel/gpt-5.4-mini
model/gpt-5.4*model/gpt-5.4, model/gpt-5.4-minimodel/gpt-4-turbo
model/*Any modeltool/search_web
mcp://filesystem/*mcp://filesystem/read_file, mcp://filesystem/write_filemcp://github/create_issue
*Everything(matches all)

Conditions

Conditions refine a rule with key-value glob patterns that must all match at runtime. They are evaluated by the local enforcer in every SDK (Python, Node, Go), both in hosted and local-only modes — there is no server round trip.

What each condition matches against

When a rule has conditions: { key: pattern, ... }, the enforcer builds a merged view of the guard call and tests each key against that view. The merge rules are:

  1. Start with the call's args dict.
  2. Layer context on top (context wins on key collisions).
  3. If context["tags"] is a mapping, flatten it into the top level at the same precedence as context.
  4. Each condition value is a glob (* wildcard). If the value at key matches the glob, the condition passes. All conditions must pass.

Practical consequence: you can drive a condition from any of args, top-level context, or nested context["tags"]. Pick whichever is most natural for the caller.

Example: provider tag (simplified wrapper pattern)

- effect: allow
action: 'llm:generate'
conditions:
provider: 'openai'

This rule matches if any of these calls is made:

# via top-level context (direct guard)
cz.guard("llm", method="generate", context={"provider": "openai"})

# via nested tags (simplified wrapper populates tags)
cz.guard("llm", method="generate", context={"tags": {"provider": "openai"}})

# via args (some integrations put it there)
cz.guard("llm", method="generate", args={"provider": "openai"})

Example: agent + environment

{
"effect": "allow",
"action": "llm:generate",
"conditions": {
"agent_id": "support-*",
"environment": "production"
}
}
cz.guard(
"llm",
method="generate",
context={
"agent_id": "support-agent-1", # matches "support-*"
"environment": "production", # matches "production"
},
)

Example: nested tags

When a wrapper classifies a call with tags (provider, model family, cost tier), conditions can match those tags as if they were top-level:

{ "effect": "deny", "action": "llm:generate", "conditions": { "cost_tier": "premium" } }
cz.guard(
"llm",
method="generate",
context={"tags": {"cost_tier": "premium", "provider": "openai"}},
)
# Denied: conditions.cost_tier matches tags.cost_tier after flattening.

Policy Evaluation Order

When multiple rules could match the same guard() 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*" }
]
}
SDK CallRule 1 Match?Rule 2 Match?Result
guard("llm:generate", "model/gpt-4")Yes (allow)NoALLOWED
guard("llm:generate", "model/gpt-3.5-turbo")Yes (allow)NoALLOWED
guard("llm:generate", "model/gpt-4-turbo")Yes (allow)Yes (deny)DENIED (deny wins)
guard("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-5.4"
},
{
"effect": "allow",
"action": "llm:generate",
"resource": "model/claude-sonnet-4-6"
},
{
"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.guard("llm", method="generate", context={"resource": "model/gpt-5.4"})         # ALLOWED
cz.guard("llm", method="generate", context={"resource": "model/claude-sonnet-4-6"}) # ALLOWED
cz.guard("llm", method="generate", context={"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.guard(
"mcp.tool",
method="call",
context={"resource": "mcp://filesystem/read_file", "agent_id": "analyst-42"},
)

# Any agent writing a file: DENIED (rule 2 matches)
cz.guard(
"mcp.tool",
method="call",
context={"resource": "mcp://filesystem/write_file", "agent_id": "analyst-42"},
)

# Dev agent using GitHub: ALLOWED (rule 3 matches)
cz.guard(
"mcp.tool",
method="call",
context={"resource": "mcp://github/create_issue", "agent_id": "dev-agent"},
)

# Unknown MCP tool: DENIED (rule 4 catch-all)
cz.guard(
"mcp.tool",
method="call",
context={"resource": "mcp://slack/send_message", "agent_id": "analyst-42"},
)

Example 3: Multi-Layer Production Policy

A 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

When you publish policies in the dashboard, Control Zero delivers them to your SDK as a single bundle. New policies become active in your agent within about a minute, or immediately with a manual refresh.

Bundle Security

Bundles are delivered securely and tamper-protected end-to-end. If a bundle fails verification for any reason, the SDK ignores it and keeps enforcing the last known good policy, so you never silently lose enforcement.

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 guard() 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: policies refresh automatically; close the client when done
client.close()
// Go
err := client.RefreshPolicies(ctx)
// Node.js
await cz.refreshPolicies();
Audit log retention

Free: 7 days. Solo: 90 days. Teams: 365 days. Longer retention and compliance exports are available in Solo and Teams. View pricing

Governing MCP tool calls

Model Context Protocol (MCP) tools give AI agents broad capabilities: file system access, database queries, shell execution, API calls. Control Zero governs each MCP tool call as an (action, resource) pair where the action is mcp.tool:call and the resource follows the mcp://{server}/{tool} convention.

This section is for MCP-native clients (Claude Code, Cline, Cursor) and custom MCP gateways that want to enforce policies on every tool invocation.

Common patterns

Read-only agent -- allow reads, deny writes and shell:

{
"name": "read-only-agent",
"rules": [
{ "effect": "allow", "action": "mcp.tool:call", "resource": "mcp://filesystem/read_file" },
{ "effect": "allow", "action": "mcp.tool:call", "resource": "mcp://filesystem/list_directory" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://filesystem/write_file" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://filesystem/delete_file" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://shell/execute" }
]
}

Database reader -- allow SELECT, deny writes:

{
"name": "db-reader-agent",
"rules": [
{ "effect": "allow", "action": "mcp.tool:call", "resource": "mcp://database/read_query" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://database/write_query" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://database/execute_query" }
]
}

API-only agent -- allow outbound HTTP, deny local access:

{
"name": "api-only-agent",
"rules": [
{ "effect": "allow", "action": "mcp.tool:call", "resource": "mcp://http/request" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://filesystem/*" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://shell/*" },
{ "effect": "deny", "action": "mcp.tool:call", "resource": "mcp://database/*" }
]
}

MCP policy gateway pattern

For MCP-native clients, wrap tool invocations in a small gateway that calls guard() before forwarding to the real MCP server:

from controlzero import Client
from typing import Any

cz = Client(api_key="cz_live_your_api_key_here")


class PolicyGateway:
"""MCP gateway that enforces Control Zero policies on tool calls."""

def __init__(self, agent_id: str):
self.agent_id = agent_id

def call_tool(self, server: str, tool: str, arguments: dict) -> Any:
cz.guard(
"mcp.tool",
method="call",
context={"resource": f"mcp://{server}/{tool}", "agent_id": self.agent_id},
args=arguments,
)
return self._forward_to_server(server, tool, arguments)

def check_tool(self, server: str, tool: str) -> bool:
"""Check if a tool call would be allowed (without enforcing)."""
decision = cz.guard(
"mcp.tool",
method="call",
context={"resource": f"mcp://{server}/{tool}", "agent_id": self.agent_id},
)
return decision.effect == "allow"

def _forward_to_server(self, server, tool, arguments):
# Implementation depends on your MCP client library.
pass

Filtering the tool list at presentation time

Before presenting tools to an agent, filter the catalog by policy so the model never sees tools it is not allowed to call:

available_tools = [
("filesystem", "read_file"),
("filesystem", "write_file"),
("database", "read_query"),
("database", "write_query"),
("shell", "execute"),
("http", "request"),
]

allowed_tools = []
for server, tool in available_tools:
decision = cz.guard(
"mcp.tool",
method="call",
context={"resource": f"mcp://{server}/{tool}", "agent_id": "my-agent"},
)
if decision.effect == "allow":
allowed_tools.append((server, tool))

This keeps the model from even attempting denied tools, reducing wasted tokens and noisy audit events.

Next Steps

  • Quick Start: Build a working agent with policy enforcement.
  • Projects: Organize agents into projects with separate policies.
  • Integrations: Auto-enforce on OpenAI, Anthropic, LangChain, and more.