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

In YAML

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_LOW

The compiler emits the tier into the generated Rego.

In Rego

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.

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="…") — typically through the Anchor REST API in Enterprise, or directly in OSS.
  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

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=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.