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 — this is the zero-config front door from the quickstart. Use it to get a real audit trail of what your agent already does, and to derive a starter domain from real traffic instead of a blank file.

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 kiffhq/kiff-guard. Each framework page links out to the adapter and its example; this site never duplicates the code.