Policies
A policy is the rule AIControl evaluates against an agent tool call. Every policy declares who it applies to, what it applies to, and what to do when it matches — nothing else. Policy changes take effect on the very next call_tool request through the gateway: there's no restart, no redeployment, and no separate build/push step. Writing or editing a policy recompiles it to Cedar source immediately and invalidates the cached policy set.
The policy model
| Field | Type | Description |
|---|---|---|
name | string | Unique identifier, used in audit events and the dashboard |
description | string | Human-readable explanation |
principal_type / principal_id | string or null | Which agent this policy scopes to: principal_type: "agent" with principal_id set to the agent's name (not its id; the API returns 422 if no agent has that name). null means "every agent" |
action_tool | string or null | Which tool this policy scopes to. null means "any tool" |
resource_system | string or null | Which business system this policy scopes to. null means "any system" |
condition | object | The match logic — see Condition keys below |
effect | deny | review | What happens when the policy matches |
severity | string | low / medium / high / critical — informational, stored on the policy; defaults to medium |
compliance_frameworks | array of strings | Metadata tags — see Compliance frameworks |
active | boolean | Inactive policies are stored but never evaluated |
priority | integer | Stored but not read by the engine — see Precedence below |
There is no allow effect. A policy either denies or flags a call for review; if nothing matches, the call is allowed by a built-in fallback the engine supplies itself, not by a policy row you create or manage.
Scoping to an agent group (principal_type: "group") compiles, but the agent record has no group membership today, so a group-scoped policy never matches any call. Scope to one named agent or to every agent.
Precedence
If more than one policy matches a call, any deny wins. If none of the matches are deny, the call is routed to review. If nothing matches at all, the call is allowed. This is structural, not configured — the priority field exists on the schema but has no effect on which policy wins; write policies as if only match/no-match and effect matter, because that's all that does.
Condition keys
A condition is a JSON object. The key(s) present determine what the policy checks — there's no separate type selector. Combine multiple keys inside all_of / any_of for compound logic. These eight keys are the full supported vocabulary:
| Key | Checks |
|---|---|
tool_name_contains | Tool name matches a substring pattern |
parameter_match | A call parameter matches a value or pattern |
numeric_conditions | A numeric parameter crosses a threshold |
rate_limit | Call count within a window reaches a threshold |
token_budget | Cumulative token or cost usage exceeds a threshold |
time_conditions | Day-of-week or hour-of-day window |
all_of | Every nested condition must match (AND) |
any_of | Any nested condition must match (OR) |
Other keys still compile and are accepted by the API: tool_name_in (an exact-match tool list, used by the shipped default policies), the older spellings blocked_tools, tool_aliases and tools (merged into the same exact-match list), and budget (an agent-wide or org-wide spend cap with scope, max_tokens, max_cost_usd). The eight keys above are the vocabulary natural-language authoring drafts against.
tool_name_contains — block or flag by tool name pattern
{
"name": "block_code_execution_tools",
"principal_type": null,
"action_tool": null,
"resource_system": null,
"effect": "deny",
"condition": {
"tool_name_contains": ["execute_code", "shell_exec", "rm_rf"]
},
"compliance_frameworks": ["SOC2"]
}Matches if the tool name contains any of the listed substrings — use this for a pattern (*_export), and scope with action_tool directly instead if you mean one exact tool name.
parameter_match — inspect a call parameter
{
"name": "block_untrusted_domain_calls",
"principal_type": null,
"action_tool": null,
"resource_system": null,
"effect": "deny",
"condition": {
"parameter_match": {
"url": { "contains_any": ["evil.com", "attacker.io", "exfil.net"] }
}
},
"severity": "critical",
"compliance_frameworks": ["SOC2", "ISO27001"]
}Use the key "*" in place of a parameter name to scan every parameter value on the call, regardless of which parameter carries it — useful for cross-cutting checks like prompt-injection phrases or credential patterns that could show up under any field name:
{
"name": "block_prompt_injection",
"principal_type": null,
"action_tool": null,
"resource_system": null,
"effect": "deny",
"condition": {
"parameter_match": {
"*": { "contains_any": ["ignore previous instructions", "disregard all prior", "you are now", "jailbreak"] }
}
},
"severity": "critical",
"compliance_frameworks": ["SOC2", "NIST_CSF"]
}An exact match (rather than substring) uses "equals" instead of "contains_any":
"parameter_match": { "filter": { "equals": "all" } }A plain value is also accepted: it means exact equality, or a glob when the string contains * or ? (for example "patient_id": "PT-2024-09*"). A bare * or ? on its own is compared as a literal.
numeric_conditions — threshold on a numeric parameter
{
"name": "review_high_value_transactions",
"principal_type": null,
"action_tool": null,
"resource_system": null,
"effect": "review",
"condition": {
"numeric_conditions": { "amount": { "gt": 50000 } }
},
"severity": "critical",
"compliance_frameworks": ["SOC2", "OCC"]
}Supported operators: gt, gte, lt, lte, eq. Multiple fields inside numeric_conditions are ANDed; use any_of if you want an OR across several numeric checks.
rate_limit — call-count threshold within a window
{
"name": "deny_credit_bureau_rate_limit",
"principal_type": null,
"action_tool": "query_credit_bureau",
"resource_system": null,
"effect": "deny",
"condition": {
"rate_limit": { "max_calls": 3, "window": "session" }
},
"severity": "high",
"compliance_frameworks": ["GLBA", "FCRA", "SOC2"]
}window is required and is one of session (resets per session_id), 5m, 60m, 24h, or 7d (rolling, across all sessions for the matching scope). The policy matches once the number of prior calls to the tool reaches max_calls, so the example above denies the fourth call. A rate_limit or token_budget policy must bind a tool, through action_tool (as above) or a tool_name_in list, or the API returns 422. Call counting happens server-side, ahead of the engine — Cedar only compares the count it's handed against your threshold.
token_budget — cumulative usage threshold
{
"name": "deny_over_budget_calls",
"principal_type": null,
"action_tool": "call_llm_tool",
"resource_system": null,
"effect": "deny",
"condition": {
"token_budget": { "max_cost_usd": 5.00, "window": "session" }
},
"severity": "high",
"compliance_frameworks": ["SOC2"]
}Accepts max_tokens, max_cost_usd, or both (either crossing its threshold fires the policy). window is required and takes the same values as rate_limit. Usage is the sum of the input_tokens + output_tokens and cost_usd your agent reported on earlier calls to the bound tool; the engine only compares the running total it's handed.
time_conditions — day/hour window
{
"name": "review_weekend_production_changes",
"principal_type": null,
"action_tool": "deploy_to_production",
"resource_system": null,
"effect": "review",
"condition": {
"time_conditions": {
"deny_days": [5, 6],
"deny_hours": { "from": 9, "to": 17 }
}
},
"severity": "medium",
"compliance_frameworks": ["SOC2"]
}deny_days uses Python's weekday() numbering (0=Monday … 6=Sunday). deny_hours is a {from, to} window on the UTC hour of day, matching when from <= hour < to; a window that runs past midnight (such as from: 17, to: 9) never matches, so express it as two windows inside any_of. When both deny_days and deny_hours are given, they're ANDed (the example matches weekends between 09:00 and 17:00 UTC), not ORed. No other keys are accepted inside time_conditions.
all_of / any_of — compound logic
{
"name": "deny_bulk_csv_revenue_export",
"principal_type": null,
"action_tool": "export_revenue_report",
"resource_system": null,
"effect": "deny",
"condition": {
"all_of": [
{ "numeric_conditions": { "row_count": { "gt": 1000 } } },
{ "parameter_match": { "format": { "equals": "csv" } } }
]
},
"severity": "high",
"compliance_frameworks": ["SOC2"]
}Nest any of the other seven keys, including another all_of/any_of, to any depth.
Creating a policy
Via the dashboard
The Policies page has an Active tab and a Library tab. New policy opens a modal with two authoring paths: natural-language authoring (below, Enterprise, the default when licensed) and a structured editor (scope fields plus one numeric threshold condition), each with a live preview. Other condition types are created through the API. The Library tab lists inactive templates: 18 pre-built ones, grouped by category, plus the shipped example_* reference policies. Open one and click Activate to turn it on without hand-writing JSON. After first setup, the Overview page also prompts you to activate a governance baseline: Standard (four dangerous-operation blocks) or Strict (adds four more, including data-protection patterns).
Via the API
curl -X POST https://your-instance/policies \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "block_code_execution_tools",
"description": "Block code-execution tools regardless of parameters",
"principal_type": null,
"action_tool": null,
"resource_system": null,
"effect": "deny",
"condition": { "tool_name_contains": ["execute_code", "shell_exec", "rm_rf"] },
"compliance_frameworks": ["SOC2"]
}'The response echoes the scope fields with camelCase aliases (principalType, principalId, actionTool, resourceSystem) alongside the compiled cedar_text, so you can see exactly what the policy evaluates to.
A policy with an empty condition must still constrain at least one of principal_id, action_tool, or resource_system — an unconstrained, unconditional policy would deny every call from every agent, and the API rejects that at creation time. A policy created through the API is active immediately.
Natural-language policy authoring
Enterprise feature.
Describe the policy you want in plain English on the Policies page. AIControl drafts a scoped policy — the same principal_type/action_tool/resource_system/condition/effect shape you'd write by hand — and checks server-side that it actually compiles before ever showing it to you; a draft that doesn't compile is flagged for manual authoring instead of being presented as if it were ready.
From there, you can simulate the draft against your own historical audit traffic — a read-only replay against real past calls, showing how many would have been denied or flagged under the new policy — before you decide whether to activate it. Nothing is written to your policy set, and no audit event is created, until you explicitly activate. This is the same describe → draft → simulate → activate loop end to end; there's no path that reaches your active policies without a human clicking Activate.
Default policies
Three policies ship active by default, deliberately generic and free of any industry-specific tool names:
{
"name": "block_code_execution_tools",
"principal_type": null, "action_tool": null, "resource_system": null,
"effect": "deny",
"condition": { "tool_name_in": ["execute_code", "delete_database", "drop_table", "shell_exec", "rm_rf"] },
"compliance_frameworks": ["SOC2"]
}{
"name": "require_review_for_external_calls",
"principal_type": null, "action_tool": null, "resource_system": null,
"effect": "review",
"condition": { "tool_name_contains": ["http_request", "webhook", "external_api"] },
"compliance_frameworks": ["SOC2"]
}{
"name": "block_unapproved_outbound_http",
"principal_type": null, "action_tool": null, "resource_system": null,
"effect": "deny",
"condition": { "tool_name_in": ["http_post", "http_request"] },
"compliance_frameworks": ["SOC2"]
}require_review_for_external_calls routes any tool whose name contains http_request, webhook or external_api to a human reviewer, and block_unapproved_outbound_http hard-denies the exact tool names http_post and http_request. A call to http_request matches both, and a deny wins over a review, so it is denied outright. On Community, where a review match is collapsed to deny, all three defaults deny.
There's no fourth "default allow" policy row to configure — the catch-all fallback that lets ungoverned calls through is built into the engine itself, not something you activate or edit.
Five inactive example_* reference policies also ship (example_high_value_loan_review, example_bulk_revenue_export_deny, example_no_weekend_production_changes, example_phi_access_all_variants, example_rate_limit_sensitive_reads), covering a numeric threshold, a compound all_of, a time window, exact tool-name aliases with a glob parameter match, and a rate limit. None is active by default; copy and adapt them.
Industry-scenario policies are demo fixtures, not defaults. AIControl's own demo environment additionally seeds policies like deny_bulk_credit_query (lending), deny_cross_encounter_phi_access (healthcare), or review_high_value_claim_payment (insurance) — built around each demo's fictional tool names. These live under policies/demo_seeds/ in the source repo and are loaded only by the demo data-seeding script, never by the API's own startup path. A real deployment never receives them unless you write and activate your own equivalent policy for your actual tools — use the examples throughout this page as a starting point for that.
Compliance frameworks
The compliance_frameworks field is metadata — it does not affect evaluation. It is stored on the policy, and every audit event where that policy fires carries its policy_id.
This enables compliance queries like: show me all SOC2-relevant deny events from the last 30 days, by joining audit_events to policies in PostgreSQL (see the query in Audit Log).
Values are free-form strings. Common values: SOC2, GDPR, HIPAA, GLBA, FCRA, ISO27001, CCPA, NIST_CSF, OCC, EU_AI_Act.
Updating and deleting a policy
PUT /policies/{policy_id} accepts a partial body — any of description, the scope fields, condition, effect, severity, compliance_frameworks, active, priority, library or category — and recompiles cedar_text on write, same as creation. Changes are live on the very next call_tool request through the gateway.
DELETE /policies/{policy_id} removes the policy outright. Both operations invalidate the cached policy set, so there's no propagation delay to reason about.
Policy activity
GET /policies/{policy_id}/activity?window=7d (the window is a number plus d or h, such as 24h or 30d; anything else falls back to 7 days) returns how often a policy has actually fired recently — {window, fired, calls_evaluated} — so you can tell a policy that's silently doing nothing from one that's actively shaping traffic. calls_evaluated is scoped by agent and tool only (the business system a call resolves to isn't stored on the audit event itself), so for a system-scoped policy this number is an upper bound on calls actually in its scope, not an exact count.