Docs Start here

How the guard works

Every framework page on this site follows the same spine: install → observe → activate → enforce. This page explains the model behind that spine once, so the per-framework pages can stay short.

One guard, two modes

The guard is a thin wrapper you attach to your agent’s tool calls. It has exactly two modes.

observe runs every tool, records a receipt, and learns your action catalog. It never calls KIFF and never blocks a tool. No client, no tenant, no API key, it’s the zero-config way to connect an agent you already run: see exactly what it does and derive a starter domain from real traffic instead of a blank file. (Every call is also recorded, so you get an audit trail for free, but the point is understanding the agent well enough to govern it.)

enforce asks KIFF to decide before each tool runs, and acts on the answer. There are three outcomes:

  • allowed: the tool runs.
  • approval_required: the call is held for a human.
  • blocked / invalid: the call is refused; the tool never runs.

Enforce needs a client (an API key) and an active domain for your tenant. You mint the key and activate the domain in the dashboard.

The one-receipt rule

Every tool call produces exactly one receipt, in both modes and across every adapter:

  • observe → one observed receipt; the tool always ran.
  • enforce, allowed → one governed receipt; the tool ran.
  • enforce, withheld → one governed receipt; the tool did not run.

This invariant is part of the guard’s conformance suite, a contract every adapter must pass. It’s what makes the audit trail trustworthy: one tool call, one record, never zero and never two.

Two adapter shapes

Frameworks expose their pre-tool-execution seam in one of two shapes. This is the only thing that differs between adapters; the guard logic lives in the core, once.

Middleware shape: the framework hands the guard a continuation that runs the tool, so the guard runs it itself:

guard.evaluate(tool, args, run=lambda: handler())

The guard runs the tool on allowed, and raises kiff_guard.Hold (skipping the tool) on a withheld decision. Agno and LangGraph are this shape.

Inverted-control / vote shape: the framework runs the tool after the hook returns; the hook only votes. The guard never gets a run callback:

if guard.mode == "observe":
    guard.observe(tool, args)            # record, never block
else:
    decision = guard.decide_only(tool, args)
    if decision.withheld:
        ...                              # tell the framework to skip the tool

Hermes and the OpenAI Agents SDK are this shape. The adapter records the receipt explicitly (record_executed / record_withheld) to keep the one-receipt rule.

Which shape a framework uses is a property of that framework’s API, not a choice, each framework page states its shape and why.

The trust boundary

The guard never asserts roles or authority. An API key’s roles govern what it’s allowed to do server-side; the guard only carries the call. This means a compromised or buggy adapter cannot widen its own authority, it can only ask, and KIFF decides. The conformance suite pins this too: the guard never injects a roles field.

Fail-safe posture

In enforce, a transport or guard error fails closed by default: a control tower that waves traffic through when its radar is down is the wrong default, so a withheld-on-error is safer than a silent allow. observe always allows (it never blocks anyway). Adapters expose a fail_closed=False override for hosts that prefer availability over the gate.

Where the code lives

The narrative is here on docs.kiff.dev. The runnable code, the core, every adapter, and the conformance suite, lives in the public MIT repo kiff/kiff-guard. Each framework page links out to the adapter and its example; this site never duplicates the code.