AIControl Docs

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

FieldTypeDescription
namestringUnique identifier, used in audit events and the dashboard
descriptionstringHuman-readable explanation
principal_type / principal_idstring or nullWhich agent (or agent group) this policy scopes to. null means "every agent"
action_toolstring or nullWhich tool this policy scopes to. null means "any tool"
resource_systemstring or nullWhich business system this policy scopes to. null means "any system"
conditionobjectThe match logic — see Condition keys below
effectdeny | reviewWhat happens when the policy matches
severitystringlow / medium / high / critical — informational, shown on the audit event
compliance_frameworksarray of stringsMetadata tags — see Compliance frameworks
activebooleanInactive policies are stored but never evaluated
priorityintegerStored 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 is supported by the engine (principal_type: "group") but requires agents to actually belong to a group, which most deployments don't configure yet. In practice, today, policies scope to one named agent or to every agent — plan accordingly if you're designing around groups.


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:

KeyChecks
tool_name_containsTool name matches a substring pattern
parameter_matchA call parameter matches a value or pattern
numeric_conditionsA numeric parameter crosses a threshold
rate_limitCall count within a window exceeds a threshold
token_budgetCumulative token or cost usage exceeds a threshold
time_conditionsDay-of-week or hour-of-day window
all_ofEvery nested condition must match (AND)
any_ofAny nested condition must match (OR)

A few older spellings (tool_name_in, blocked_tools, tool_aliases, tools, budget) still compile — they're accepted for backward compatibility with policies written against an earlier version of the engine — but the eight keys above are what you should write new policies against.

tool_name_contains — block or flag by tool name pattern

json
{
  "name": "block_dangerous_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

json
{
  "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:

json
{
  "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":

json
"parameter_match": { "filter": { "equals": "all" } }

numeric_conditions — threshold on a numeric parameter

json
{
  "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

json
{
  "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 one of session (resets per session_id), 5m, 60m, 24h, or 7d (rolling, across all sessions for the matching scope). 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

json
{
  "name": "deny_over_budget_calls",
  "principal_type": null,
  "action_tool": null,
  "resource_system": null,
  "effect": "deny",
  "condition": {
    "token_budget": { "max_cost_usd": 5.00 }
  },
  "severity": "high",
  "compliance_frameworks": ["SOC2"]
}

Accepts max_tokens, max_cost_usd, or both (either crossing its threshold fires the policy). Usage is accumulated server-side per the policy's scope; the engine only compares the running total it's handed.

time_conditions — day/hour window

json
{
  "name": "example_no_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": 17, "to": 9 }
    }
  },
  "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 hour of day (UTC). When both are given, they're ANDed — "weekends AND after-hours" — not ORed.

all_of / any_of — compound logic

json
{
  "name": "example_bulk_revenue_export_deny",
  "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 composes a policy list with two authoring paths, side by side: a structured editor (pick the scope fields and one condition directly) and natural-language authoring (below). A policy library of 18 pre-built templates, grouped by category, can be activated directly into an active policy without hand-writing JSON. After first setup, you're also prompted to activate a governance baseline: Standard (four dangerous-operation blocks) or Strict (adds four more, including data-protection patterns).

Via the API

bash
curl -X POST https://your-instance/policies \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "block_dangerous_tools",
    "description": "Block globally dangerous 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.


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:

json
{
  "name": "block_dangerous_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"]
}
json
{
  "name": "require_review_for_external_calls",
  "principal_type": null, "action_tool": null, "resource_system": null,
  "effect": "review",
  "condition": { "tool_name_contains": ["http", "api_call", "webhook"] },
  "compliance_frameworks": ["SOC2"]
}
json
{
  "name": "block_unapproved_outbound_http",
  "principal_type": null, "action_tool": null, "resource_system": null,
  "effect": "deny",
  "condition": { "tool_name_in": ["http_post", "webhook_call"] },
  "compliance_frameworks": ["SOC2"]
}

The first two ship active on purpose, not redundantly: require_review_for_external_calls flags anything matching an HTTP-ish name pattern for a human to see, and block_unapproved_outbound_http hard-blocks the two known HTTP tool names outright even if a reviewer never gets to the review queue.

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_* template policies also ship, each demonstrating one condition type end to end (numeric threshold, rate limit, compound all_of, time window, tool-name pattern) — none active by default, meant as a starting point to copy and adapt.

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 every audit event where that policy fires.

This enables compliance queries like: show me all SOC2-relevant deny events from the last 30 days, filtered directly in PostgreSQL or the dashboard.

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 the scope fields, condition, effect, severity, compliance_frameworks, or active — 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 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.