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:
| Tier | Typical use | Default convention |
|---|---|---|
INFORMATIONAL | Read-only lookups, memory queries | Auto-allow |
OPERATIONAL | Write/update operations within scope | Allow if scope check passes |
TRANSACTIONAL_LOW | Low-value financial / state changes | Allow if scope check passes |
TRANSACTIONAL_HIGH | High-value financial operations | Often requires_hitl := true |
DESTRUCTIVE | Delete operations, bulk modifications | Policy-defined — allow with caution or escalate |
SECURITY_CRITICAL | Shell access, credential ops, path traversal | Hard 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
In YAML
- name: allow_small_refund
when:
action: approve_refund
role: [support_agent, manager]
scope: approve_refund
args:
amount: { lte: 200 }
then: allow
risk_tier: TRANSACTIONAL_LOWThe compiler emits the tier into the generated Rego.
In Rego
package kitelogik.financial
import future.keywords.if
import future.keywords.in
default allow := false
default risk_tier := "OPERATIONAL"
risk_tier := "TRANSACTIONAL_HIGH" if {
input.action == "approve_refund"
input.args.amount > 1000
}
allow if {
input.action == "approve_refund"
input.args.amount <= 1000
"approve_refund" in input.context.session_scopes
}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.
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:
- Builds a
PendingActionwith the tool name, args, andrisk_tier - Inserts it into
HITLQueue(SQLite-backed by default) - The agent's
await_decision()blocks on the queue's event for that action ID - A reviewer calls
queue.approve(action_id, decided_by="…")orqueue.deny(action_id, decided_by="…", denial_reason="…")— typically through the Anchor REST API in Enterprise, or directly in OSS. - The agent receives the decision and resumes
- If no decision arrives within the configured timeout (default 300 seconds), the action transitions to
TIMED_OUTand the agent raisesGovernanceError
The four ActionStatus values
Defined in kitelogik.anchor.models:
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
OSS uses SQLite for HITLQueue — single-file persistence, fine for single-process and small teams. Enterprise swaps the backend for Postgres with HA + connection pooling, plus a REST API and dashboard for the approve/deny flow. Same Rego semantics in both editions.
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=TrueonSessionContext). - Tier as a trace dimension — record
decision.risk_tieron every audit row so you can query "how often doesTRANSACTIONAL_HIGHescalate?" without joining tables.