Skip to content

Policy semantics

The policy engine is intentionally boring. It walks a list of rules top-to-bottom and applies the first one that matches. If nothing matches, the policy's default_action is used. That's the entire algorithm.

This document is for the moments when "boring" isn't enough — when you need to know exactly how arg_regex interacts with contains_pii, what happens when two rules could both match, or how the engine treats a missing field.

The walk

For each incoming request, the engine evaluates rules in the order they appear in policies.yaml:

for rule in policy.rules:
    if rule.match.matches(request):
        return rule.apply(request)        # first-match-wins
return policy.default_action(request)

No backtracking, no scoring, no "best match". If rule 1 matches, rule 2 is never consulted, even if rule 2 is "more specific". Order is the priority. (Rules can be disabled — a disabled rule is skipped in the walk.)

How match works

A match block is a conjunction — every listed condition must be true. An empty match: {} matches everything (useful for a bottom-of-list catch-all).

FieldTypeTrue when
agentstr or [str]The request's agent equals the string, is in the list, or matches a * glob.
actiontool_call / http_request / llm_promptThe action kind matches exactly (or *).
destination{host / tool / model: str-or-glob}Each listed key is present on the request and matches. *.example.com matches api.example.com and chat.example.com, but not example.com.
arg_regex{field: regex}Each listed field in request.args matches its regex (Python re.search, partial match).
contains_piiboolThe PII scanner flagged the body.
contains_secretboolThe secrets scanner flagged the body.

There are more predicates than these — user / user_groups, workload / workload_labels, daily_cost_over, calls_per_minute_over, time_window, source_ip, destination_is_ai_provider / destination_is_unknown_ai, and the MCP fields. See Managing policy for the full set. Conditions you don't list are not checked.

What destination matching means

destination is a dict, matched per key — each key you list must be present on the request and match its value (string, list, or * glob). Keys you don't list aren't checked.

  • destination: { host: "*.openai.com" } matches an HTTP/LLM request to api.openai.com — but not a tool_call, which carries no host.
  • destination: { tool: "shell.exec" } matches a tool call to shell.exec regardless of host.

The Live Traffic view flattens the destination to a single display string for readability, but the engine matches on the structured keys, not the flattened string.

arg_regex — partial vs anchored

arg_regex uses re.search, not re.match. So:

yaml
arg_regex: { cmd: "rm\\s+-rf" }

matches any cmd containing rm -rf, regardless of what's around it (echo hi && rm -rf /tmp, sudo rm -rf foo). To anchor, use ^…$:

yaml
arg_regex: { method: "^(POST|PUT|DELETE)$" }   # exact match

Most rule authors get this wrong the first time — the rule fires more than they expected. If a rule is matching things you didn't mean to, anchor it.

PII and secret scanners

contains_pii and contains_secret are not regexes you control. They're the engine's built-in scanners (agentfw/matchers.py) running over request.args during evaluation:

  • PII — SSN (reserved ranges filtered), credit card (Luhn-validated), email, US phone.
  • Secret — AWS access key, AWS secret key, GitHub token, OpenAI key, Slack token, Bearer token, private-key blocks.

Scan-size note: secret patterns scan generously so a pasted script with an embedded key is still caught; very large PII fields may be sampled rather than scanned in full, since free-text blobs that size are noise-prone.

A redact action gated on contains_pii/contains_secret masks every sensitive leaf it finds — both PII and secret fields — not just the family that triggered the rule. So a payload with an SSN in one field and an AWS key in another has both masked, regardless of which rule fired.

Default action

Every policy has a default_action: allow | deny | log at the top of policies.yaml. It applies when no rule matched.

The shipping default for new tenants is default_action: allow with explicit rules that redact secrets/PII, hold sensitive writes for approval, and log AI chat — visibility-first, so nothing legitimate breaks on day one while the risky patterns are still caught. If you prefer default-deny ("know exactly what's allowed, reject everything else"), set default_action: deny and add explicit allow rules for the traffic you expect; use the Simulator to confirm you haven't blocked something legitimate before you ship it.

Ordering matters — pathological example

yaml
- name: deny_external_writes
  match: { action: http_request, arg_regex: { method: "POST|PUT|DELETE" } }
  action: deny

- name: approve_external_writes
  match: { action: http_request, arg_regex: { method: "POST|PUT|DELETE" } }
  action: require_approval

The require_approval rule never fires — the deny rule above it always wins. Reorder to make require_approval come first.

The Simulator page can tell you this before you save: it replays the last 24h of traffic through your draft policy and shows which rules actually fire. If a rule has zero matches in the simulation, it's almost certainly being shadowed by an earlier one.

Order is just a list — reorder freely

There's no rule-id-based dependency tree. Rules are just an ordered list. Reorder them on the Policy Rules page; the engine re-reads the new order on the next request — no restart.

What the engine returns

engine.evaluate(request) returns a Decision:

python
@dataclass
class Decision:
    action:        str               # "allow" | "deny" | "redact" | "require_approval" | "log"
    reason:        str               # the rule's reason, or a system-generated one
    matched_rule:  str               # the rule's name, or "<default>" if no rule matched
    redacted_args: dict | None       # for action=="redact", the masked args to forward
    matched_paths: list[str]         # the field paths that triggered the match

That's what the collector then ships to the cloud (with the request metadata) as a single event.

→ See Decisions & verdicts for what each action actually does to the request, and Mid-session policy changes for when an edit takes effect on conversations already in flight (and why a new rule can match content from hours ago).

Documentation for kilasec — the AI Agent Firewall.