Skip to content

Risk tiers & HITL

Every policy decision carries a risk_tier label. The tier doesn't drive enforcement directly — allow / deny / requires_hitl do that — but it is the canonical signal for audit logs, dashboards, and reviewer routing.

The six tiers

Defined as RiskTier in kitelogik.tether.models:

TierTypical useDefault convention
INFORMATIONALRead-only lookups, memory queriesAuto-allow
OPERATIONALWrite/update operations within scopeAllow if scope check passes
TRANSACTIONAL_LOWLow-value financial / state changesAllow if scope check passes
TRANSACTIONAL_HIGHHigh-value financial operationsOften requires_hitl := true
DESTRUCTIVEDelete operations, bulk modificationsPolicy-defined — allow with caution or escalate
SECURITY_CRITICALShell access, credential ops, path traversalHard block by default

The README's marketing-tier table compresses these to five — the code ships six (the scaffolded policies/policy.yaml from kitelogik init uses TRANSACTIONAL_LOW for the small-refund rule). Treat the code as authoritative.

Assigning a tier

Risk tiers are assigned by main.rego, which inspects the action and its arguments and picks a tier deterministically. The shipped rules classify a security or delegation hard-deny as SECURITY_CRITICAL, approve_refund with amount > 100 as TRANSACTIONAL_HIGH, and so on, falling back to OPERATIONAL.

YAML risk_tier: is not yet honored

The YAML compiler accepts a risk_tier: field and emits it into the kitelogik.userpolicy package, but main.rego does not aggregate it, so it has no effect on the decision today. Aggregating user-authored tiers — with a defined precedence against the built-in rules — is a planned enhancement. To customise tiers now, edit the risk_tier rules in main.rego directly:

rego
# kitelogik/policies/main.rego
risk_tier := "TRANSACTIONAL_HIGH" if {
    not security.deny
    not delegation.deny
    input.action == "approve_refund"
    is_number(input.args.amount)
    input.args.amount > 1000
}

How HITL escalation actually fires

HITL is only triggered when an OPA policy explicitly sets requires_hitl := true. There is no automatic escalation by tier — even SECURITY_CRITICAL decisions don't escalate unless a rule says so. Most governance decisions resolve instantly with no human delay.

rego
default requires_hitl := false

requires_hitl := true if {
    input.action == "approve_refund"
    input.args.amount > 1000
}

When the gate returns requires_hitl=True, the runtime:

  1. Builds a PendingAction with the tool name, args, and risk_tier
  2. Inserts it into HITLQueue (SQLite-backed by default)
  3. The agent's await_decision() blocks on the queue's event for that action ID
  4. A reviewer calls queue.approve(action_id, decided_by="…") or queue.deny(action_id, decided_by="…", denial_reason="…") — directly from your operator UI, Slack handler, or any other surface you wire to the queue API.
  5. The agent receives the decision and resumes
  6. If no decision arrives within the configured timeout (default 300 seconds), the action transitions to TIMED_OUT and the agent raises GovernanceError

The four ActionStatus values

Defined in kitelogik.anchor.models:

python
class ActionStatus(StrEnum):
    PENDING   = "PENDING"
    APPROVED  = "APPROVED"
    DENIED    = "DENIED"
    TIMED_OUT = "TIMED_OUT"

Every status transition is recorded with decided_at and decided_by for the audit log.

Storage

HITLQueue uses SQLite by default — single-file persistence, fine for single-process and small teams. The queue API is a small surface (enqueue, approve, deny, wait_for_decision, get_pending), so swapping in a Postgres backend or a custom store for higher-throughput deployments is a subclass-and-register exercise.

Patterns

  • HITL as the high-stakes default for TRANSACTIONAL_HIGH — common for refunds above a threshold, wire transfers, large grants.
  • Hard deny for SECURITY_CRITICAL — shell, credential extraction, arbitrary file read. There is no escalation path; a human cannot approve out of these. To run code, the session must be launched inside a verified sandbox (sandbox_verified=True on SessionContext).
  • Tier as a trace dimension — record decision.risk_tier on every audit row so you can query "how often does TRANSACTIONAL_HIGH escalate?" without joining tables.

Released under the Apache 2.0 License.