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 carries what the collector could resolve — source IP, agent name and its provenance, and (when the eBPF host sensor is running) the owning workload. This is what the dashboard renders.
  2. decision.matched_paths lists the field paths that triggered the match, not values. The values are masked at the collector for a redact — cloud only knows that a field was redacted.
  3. No Authorization header. Credential-shaped keys are scrubbed by the collector before the event ships. The raw key stays on the collector; 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, or by the policy's default_action if nothing matches.

What "redact" actually does

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

  1. Walks the request body and identifies the sensitive leaves (the PII/secret matches, or the rule's explicit redact_fields).
  2. Replaces each matched value with ***REDACTED***. Example: body.card_number: "4242424242424242" becomes body.card_number: "***REDACTED***".
  3. The collector forwards the rewritten body to the destination.
  4. Records the masked paths (not the values) in the decision and ships it to the cloud.

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

(The response-side scanner uses a typed [REDACTED:<type>] marker instead — that's a separate path that masks provider responses, not the request body described here.)

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.