Skip to content

Decisions & verdicts

Every AI request that flows through a kilasec collector produces exactly one decision. A decision is an immutable audit record consisting of: who made the request, what they tried to do, where to, which policy rule matched, and what verdict was applied.

Anatomy of a decision

The wire format that the collector ships to the cloud:

json
{
  "id":  "ev_W9zQoQ3bk",
  "ts":  1715520000.12,           // unix seconds, float
  "status": "redact",             // verdict, lowercased
  "request": {
    "id": "req_k753ucn5",
    "agent": "support-bot-v2",
    "action": "http_request",     // tool_call | http_request | llm_prompt
    "destination": {
      "host":  "api.anthropic.com",
      "ip":    "160.79.104.10",
      "port":  443
    },
    "args": {
      "method": "POST",
      "path":   "/v1/messages"
    },
    "metadata": {
      "url":         "https://api.anthropic.com/v1/messages",
      "method":      "POST",
      "source_ip":   "10.0.3.42",
      "source_port": 51234,
      "user_agent":  "anthropic-sdk-python/0.30",
      "identity": {
        "name":             "support-bot-v2",
        "hostname":         "ops-mbp",
        "mac":              "3c:22:fb:a1:b2:c3",
        "principal_type":   "agent",
        "display_name":     "support-bot-v2",
        "labels":           { "hostname_source": "reverse_dns" }
      }
    }
  },
  "decision": {
    "action":           "redact",
    "matched_rule":     "redact_pii_on_egress",
    "reason":           "PII detected in egress payload",
    "redacted_fields":  ["body.card_number"],
    "duration_ms":      1.83
  }
}

Three things to notice:

  1. request.metadata.identity is rich. Source IP, MAC, hostname, principal_type, plus provenance for where the hostname came from. This is what the Agents page renders.
  2. decision.redacted_fields lists field names, not values. The values are scrubbed at the collector — cloud only knows that the field was redacted.
  3. No Authorization header. Scrubbed in _classify_request before the event ever ships. The collector keeps the original; the cloud doesn't see it.

The five verdicts

VerdictWireWhat it meansWhere you see it
allowallowRequest passes through unchangedmost rows on Live Traffic
loglogPasses through, but a row is written for auditlow-noise observability rules
redactredactRequest body gets rewritten with masks before egressLive Traffic with amber pill
require_approvalrequire_approvalRequest hangs until a human resolves on the Approvals pageApprovals page
denydenyRequest is blocked; agent gets the reason stringIncidents page (red)

Verdicts are exclusive — exactly one applies per decision. They're decided by the first matching rule in policies.yaml, or by the policy's default_action if nothing matches.

What "redact" actually does

When a rule with action: redact matches, the collector:

  1. Walks the request body.
  2. For each redact_fields path, replaces the value with a [REDACTED:<type>] token. Example: body.card_number: "4242424242424242" becomes body.card_number: "[REDACTED:credit_card]".
  3. Forwards the rewritten body to the destination.
  4. Records the original field path (not the value) in decision.redacted_fields and ships the decision to the cloud.

This is the only verdict that mutates the request. Allow / log / deny / require_approval all leave the body alone.

What "require_approval" actually does

The collector parks the request and waits, with a configurable timeout (default 60s). During the wait:

  • The agent's HTTP call is literally hanging — the connection is held open.
  • The decision shows up on the Approvals page.
  • A human clicks Allow → the verdict flips to allow and the collector finishes forwarding the request.
  • A human clicks Deny, or the timeout expires → the verdict flips to deny and the collector returns the failure to the agent.

This is the only verdict that blocks the agent until a human acts. Use sparingly — a backlog on the Approvals page means agents are sitting idle.

How fast a decision is

The decision.duration_ms field is the wall-clock time from "request entered the engine" to "verdict ready" — typically single-digit milliseconds because the policy is just a list of compiled predicates evaluated in order. Network round-trip to the destination is not included (the verdict happens before the request leaves the collector).

For require_approval, duration_ms includes the human-wait time. So a 47-second approval shows duration_ms: 47000. The Live Traffic page also exposes this as resolution_latency_ms in the right drawer.

What rule matched

Every audit row carries decision.matched_rule. Two things to know:

  • The rule name is durable. Even if you rename a rule on the editor, old audit rows keep the original name — you can find them by grepping the rule history.
  • <default> means the policy's default_action. If no listed rule matched, the engine falls back to the policy default. Most policies set this to deny (default-deny is the security posture); some test policies set it to allow.

Decisions vs. events vs. requests

Three terms that get used interchangeably in the codebase:

  • Request — what the agent tried to do (the input to /evaluate).
  • Decision — the verdict for that request, including the matched rule.
  • Event — the cloud-side audit row that combines request + decision into one JSON blob. Plural shows up at /v1/ingest/events.

When you read the code: "event" is the wire format, "decision" is the in-memory record on the collector, "request" is what the agent sent.

→ Up next: Policy semantics covers exactly how the engine picks a rule.

Documentation for kilasec — the AI Agent Firewall.