Skip to content

Governance events

Every action the agent attempts is converted into a GovernanceEvent, evaluated by the policy gate, and either allowed, denied, escalated, or budget-blocked. There are five event types, distinguished by the event_type field on GovernanceEvent:

Event typeFired when…Common policy questions
tool_callThe agent invokes a tool"Can this agent call this tool with these args?"
agent.spawnThe agent creates a sub-agent"How deep can the delegation tree go?"
agent.delegateThe agent hands a task to another agent"Are the child's scopes a subset of the parent's?"
agent.planThe agent emits a multi-step plan before executing"Does this plan exceed the step limit or contain blocked tools?"
agent.budgetThe agent is about to exceed a configured budget"Has this session spent its token / API-call / cost ceiling?"

A sixth concern — data classification flow — is not a separate event type. It rides on every event as the data_classification field (e.g. "PII", "CONFIDENTIAL") so the same policy can gate "tool X with PII data" rather than "any tool that touches PII".

The shared event shape

Every event carries the same envelope, defined in kitelogik.tether.models:

python
class GovernanceEvent(BaseModel):
    event_type: Literal["tool_call", "agent.spawn", "agent.delegate",
                        "agent.plan", "agent.budget"]
    session_id: str
    action: str                                  # The policy-layer name
    tool_name: str | None = None                 # tool_call only
    args: dict = {}                              # tool_call only
    resource_path: str | None = None             # tool_call only (file/URL paths)
    context: SessionContext                      # role, scopes, delegation depth, budgets
    requested_capabilities: list[str] = []       # spawn / delegate only
    delegation_target: str | None = None         # delegate only
    steps: list[dict] = []                       # agent.plan only
    data_classification: str | None = None       # cross-cutting

The policy receives this payload as the input document. input.context carries the per-session identity and budget envelope — see the SessionContext reference below.

Per-event policy patterns

tool_call

The most common event. Every tool invocation goes through it.

rego
package kitelogik.financial

import future.keywords.if
import future.keywords.in

default allow := false

allow if {
    input.action == "approve_refund"
    input.context.user_role in {"support_agent", "manager"}
    "approve_refund_under_100" in input.context.session_scopes
    input.args.amount <= 100
}

agent.spawn

Fires when the agent tries to create a sub-agent. The policy enforces spawn caps and capability bounds.

rego
package kitelogik.agent_lifecycle

import future.keywords.if
import future.keywords.in
import future.keywords.every

default allow := false
default deny := false

allow if {
    input.event_type == "agent.spawn"
    input.context.delegation_depth <= 2
    every cap in input.requested_capabilities {
        cap in input.context.session_scopes
    }
}

deny if {
    input.event_type == "agent.spawn"
    input.context.delegation_depth > 2
}

agent.delegate

Hand-off between agents. Policy enforces scope-narrowing — a child can inherit at most what the parent had.

agent.plan

Emitted when an agent produces an explicit plan and asks permission to execute. input.steps is the proposed sequence; the policy can reject the whole plan rather than catching it mid-execution.

agent.budget

Fires when a budget threshold is about to be crossed. Read the budget state from input.context.budget_used_* and input.context.budget_total_*.

SessionContext

Attached to every event as input.context. The same shape regardless of event type, defined in kitelogik.tether.models:

python
class SessionContext(BaseModel):
    session_id: str
    user_role: str
    session_scopes: list[str]
    sandbox_verified: bool = False
    token_id: str = ""
    delegation_depth: int = 0
    parent_token_id: str = ""
    parent_session_id: str = ""
    tenant_id: str | None = None
    # Budget envelope — None means "no budget tracking"
    budget_total_tokens: int | None = None
    budget_used_tokens: int | None = None
    budget_total_api_calls: int | None = None
    budget_used_api_calls: int | None = None
    budget_total_cost_cents: int | None = None
    budget_used_cost_cents: int | None = None

Treat SessionContext as immutable for the duration of a session. Use model_copy(update=...) if you need a derived context (e.g. when a delegated token is attached).

How a decision comes back

Every evaluation returns a PolicyDecision:

python
class PolicyDecision(BaseModel):
    allow: bool
    deny: bool
    risk_tier: RiskTier
    requires_hitl: bool
    reason: str
    rule_matched: str | None = None
    resolution_trace: list[ResolutionStep] = []

Four meaningful flag combinations:

allowdenyrequires_hitlOutcome
TrueFalseProceed
FalseTrueHard block (model cannot override)
FalseFalseTruePause for HITL — see risk tiers & HITL
FalseFalseFalseSoft deny

rule_matched is the canonical anchor for audit logs and traces.

Released under the Apache 2.0 License.