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→ oneobservedreceipt; 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.