# KIFF > KIFF gives each AI agent a Card: which actions it may take, how much per action and in total over a window, and what happens to a call outside those limits. KIFF decides every proposed consequential action before it runs, against the business's own state and the agent's Card. A call outside the Card waits for the owner or is refused. The owner changes or revokes the Card without changing the agent, and every decision has a signed, tamper-evident receipt. Agents now refund, credit, book, cancel and change accounts on their own. Each action costs almost nothing, so teams ask for many more of them, and a person approving each one becomes the whole cost. Payment networks now give agents credentials capped per payment. A payment cap knows one payment. KIFF covers what it cannot see: actions that are not payments, totals over time across agents, the state of the order or account, who answers when the agent goes over, and a record the agent's vendor did not write. In one line: payment networks cap a payment; KIFF caps the agent. ## Key facts - What a Card says: which actions the agent may take (refund an order, not cancel a subscription); the most per action; totals per window, counted as a number of actions or a sum of one parameter (for example €500 and 10 refunds a day); what happens outside it (hold for the owner, or refuse); an optional expiry. One agent holds a Card by default; a Card can also be shared by up to 20 agents drawing on one balance, and each decision still names the agent that asked. An agent holding several Cards is bound by the tightest. - How a call is checked, three ways: (1) the MCP gateway at `mcp.kiff.dev`, KIFF holds the tool's credential, so the agent's key can only ask, and the agent cannot reach the tool around its Card; (2) kiff-guard, one hook in the agent's tool loop in your own code (Python and TypeScript, eleven verified framework adapters); (3) one call to the decision API, `POST /v1/proposals/decide`, from any code. Outcomes: `allowed`, `approval_required`, `blocked`, `invalid`. - What "allowed" means: KIFF authorized the call. Through the gateway KIFF then forwards it to the tool; elsewhere the company's own system runs it. KIFF never moves money itself. - Outside the Card, the owner answers: a held call reaches the owner on Needs you, by email, and as a link in the agent's chat (Claude Code). The owner approves it once, changes the Card, or rejects it. Only the owner's signed-in session can answer; a hold nobody answers is refused when it expires (10 minutes by default). An agent key can never answer an approval it asked for. - The owner changes the Card, not the agent: raise it for a busy week, narrow it, move it to another agent or revoke it, and the next call uses the new terms. Revoking one Card withdraws only what that Card granted. - Correct under load: concurrent calls, retries and several workers draw on one balance held centrally. A retry counts once. Two runs cannot both spend the last of a balance. If the balance cannot be read, KIFF refuses; unknown is never read as zero. - The record: every change to a Card is attributed, every draw is on the Card's statement, and every decision has a signed, tamper-evident receipt the agent cannot write. The draw is recorded when the action is authorized, not when the caller executes it. - Under the Card is the operational domain (a versioned `kiff.yaml`: entities, states, actions, permissions, approvals). KIFF decides every proposed consequential action against it, from the real state rather than what the caller claims, and a caller cannot approve its own request. Where an action declares `requires_authority`, that decision also draws on a card the organization issued to the acting agent. A card narrows what the domain allows; it cannot make a forbidden action legal. - Where the Card is the final authority: wherever KIFF is on the only path to the capability, as through the gateway. An agent that can still act through a browser or a direct credential is outside it; close that path, or KIFF does not govern it. - Shipped today: Cards with per-window limits, shared Cards and statements; the MCP gateway (remote MCP, bearer credentials); held calls with owner approvals and expiry; eleven kiff-guard adapters; Claude Code and Codex through the gateway; signed receipts; the MIT framework. Direction, not product: controls across many agents at once, a view for builders running several clients, tools that accept only calls carrying KIFF's authorization, and payment credentials created only after an `allowed` decision. - Who it is for: businesses whose agents take consequential actions on their own money or operations (refunds, credits, payouts, plan and access changes), and the builders who deliver those agents to clients. Not sold to consumers using a personal agent; their protection belongs to the payment layer and the agent's own confirmations. - Open source and price: the KIFF Framework (Go) and the kiff-guard SDKs are MIT and need no account. Companies pay for KIFF Cloud, the hosted runtime with owner controls, statements and signed receipts, priced on governed operations (authenticated requests KIFF answers and records), not per agent, seat or token. During early access the price is agreed with each company. - For coding agents (Claude Code, Codex and similar): install the KIFF skill from `https://kiff.dev/skills/kiff.md`. It connects a tool through the gateway, drafts a Card for the owner to issue, and handles a held call: retry the identical call with the same `kiff_operation_id`, and never make the same change another way. - What it is not: not an agent platform, a workflow engine, a model gateway or an app builder. It works alongside them. ## Core - [KIFF](https://kiff.dev/): Give the agent a Card. A KIFF Card holds a set amount of authority that the agent uses up as it acts; the business gives it more or takes it back without a release or a change to the agent. The Card lives with the business (which actions, in which states, how much, until when), not in the agent’s code. The owner raises, lowers or revokes a card and the next request uses the new terms; each agent holds its own cards, so revoking a card withdraws only the authority that card granted and leaves other agents untouched; every change and decision stays on record. KIFF answers each proposed action before your system runs it. Free to build on the MIT framework; the company pays for KIFF Cloud Production. - [KIFF for agents](https://kiff.dev/agent): The canonical machine-entry page, what KIFF does, when an agent should (and shouldn't) use it, the real decision API (POST /v1/proposals/decide; outcomes allowed/approval_required/blocked/invalid), minimal integration, current status, and links to every machine surface. - [Why KIFF](https://kiff.dev/why): The founder's account: a Card for every AI agent, as familiar as a corporate card; why authority and credentials belong outside the agent; why agents acting on money and accounts is about to be normal; built for companies, with individuals named only as a hypothesis for later; the direction of one Card that travels across every assistant (ChatGPT, Claude, Grok and others), marked as not built yet; and why it has to be open source. - [Whitepaper](https://kiff.dev/whitepaper): The framework's design and principles. - [MCP walkthrough, English](https://kiff.dev/proof/videos/mcp-from-zero-en): Watch the end-to-end MCP setup, current Card limits, owner approval and a real tool call. KIFF production gateway, Stripe test mode. - [MCP walkthrough, Français](https://kiff.dev/proof/videos/mcp-from-zero-fr): The French narrated MCP walkthrough, with current Card limits and owner approval. KIFF production gateway, Stripe test mode. - [Customer operations, portrait](https://kiff.dev/proof/videos/customer-ops-portrait-en): A 58-second illustrated scenario for phones: connect tools, issue a Card, allow a €180 credit, hold €1,400, refuse a payment-terms change, and approve once. English narration; downloadable MP4. Fictional example, no live calls. - [Customer operations, landscape](https://kiff.dev/proof/videos/customer-ops-landscape-en): The same 58-second illustrated scenario for presentations, led by the isometric map. An interactive version is available on /integrate. - [Proof](https://kiff.dev/proof): A synthetic KIFF Card test run on KIFF’s production services (app.kiff.dev / api.kiff.dev) on 22 Sep 2026, with no customer data, recorded in the product: seven decisions (three allowed, four refused) with seven signed receipts, the owner raising the limit from €5,000 to €8,000 and then revoking the card. KIFF verified the decisions and what they drew from the card; "allowed" does not mean the refund executed, the company’s own system runs it. Also the agent and product demo videos (formerly /videos, which redirects here), and the runnable domain proofs, which show the state machine under a card and are not card evidence. - [Invariants](https://kiff.dev/invariants): The behavioural guarantees the cloud runtime makes, each anchored to the test that proves it with a file and line. Generated from the test suite by a generator whose own test fails the build when the page stops matching what passes, so it cannot describe a guarantee the code stopped making. Covers apps/api only; the page states plainly that a passing test proves the property it tests and nothing else. - [Production Launch](https://kiff.dev/on-rails): Optional: KIFF gets a company’s first agent action live with them. Count the drafts a person executes unchanged, observe the action on real traffic without refusing anything, issue the agent a card so the clear cases run and a person sees only the exceptions, control-test that path, then hand it over to the operations owner. Observing is agreed first, from the action alone; the rest is scoped from what observing shows. If almost everything needs a human, KIFF says so and the launch stops. - [Integrate](https://kiff.dev/integrate): The developer journey, the operational domain a card narrows against, Guard's adapters for each agent framework, the coding-agent domain skill that authors a kiff.yaml, and the three-line integration. - [Delegate](https://kiff.dev/delegate): Interactive walkthrough of a Card, run live on the page with sample agents and sample data. A delegation sentence is broken into its five parts (action, what it applies to, per-action limit, total limit, what happens otherwise), each becoming a line on the Card; the Card is given to a suggested agent, the agent draws it down, a request above a limit is refused, and the Card can be moved to another agent (keeping its balance and statement) or revoked. Ends with a gallery of example Cards for business and personal use, each with why its limits are set and where it is useful. - [Pricing](https://kiff.dev/pricing): Free to build: the framework and Guard SDKs are MIT, with no account required. KIFF Cloud Production is what the company pays for: workspace, owner controls for every card, dashboard and statements, full history and signed receipts. It is priced on what you govern (governed operations: authenticated requests KIFF answers and records; retries, bad requests and reads are free), not per agent, seat or token. During early access there is no published price; it is agreed with each company before it commits, and extra use is tracked, not billed automatically. An optional Production Launch gets the first action live with KIFF. Start with a free 30-minute review: gabriel@kiff.dev. - [For builders](https://kiff.dev/builders): For anyone building agents on KIFF, for clients or for their own business. Build the agent once and let the owner change its authority: KIFF is the reusable authority layer (limits, ownership, balance, revocation, evidence) that would otherwise be rebuilt for every deployment. Why supervision, not the work, limits autonomous agents; what a card is (a delegation closer to a licence than a payment card); why a hand-written limit fails under concurrency, retries, restarts and outages; and the phases of building with KIFF (integration and control testing, owner handover, then operations views once live). A clearly labelled section covers directions not built today. The walkthroughs are illustrative. Implementation scope, price and duration are agreed before work begins. ## Tutorials - [From a refund tool to an agent with a €50 Card](https://kiff.dev/blog/from-a-refund-tool-to-a-governed-mcp-agent): An end-to-end tutorial with the external Stripe test-mode MCP adapter, Lambda hosting, paid test order, KIFF connection and Card forms, Codex configuration, downloadable Python examples, and prompts for an allowed refund, owner approval, and changed limit. Real screenshots and recorded results; explains the generated gateway domain and the limits of this integration. ## Product notes - [An approved refund can still fail](https://kiff.dev/blog/an-approved-refund-can-still-fail): Why authorization and the tool's reported outcome are separate facts, how pending MCP calls are recovered, and how the optional Claude Code plugin brings back the owner's answer. The refund example is illustrative; accounting implementation remains separate. ## The framework (open source, MIT, Go) - [KIFF Framework on GitHub](https://github.com/kiff/kiff): The protocol kernel, events, state, decisions, actions, approvals, audit. Embed it in your backend; useful with zero cloud. Ships a CLI to scaffold and verify domains. - [Build a domain with the framework](https://kiff.dev/docs/framework-quickstart): Scaffold a runnable refund domain with `kiff new -scenario refund`, run it, and watch KIFF allow the good refund, hold the risky one for a human, and refuse the duplicate. Local, MIT, no account. - [What KIFF is](https://kiff.dev/docs/what-kiff-is): Scope and non-goals. - [How kiff-guard works](https://kiff.dev/docs/concepts): Observe vs enforce, the one-receipt rule, and the two adapter shapes. - [Domains](https://kiff.dev/docs/domains): The reusable operational contract. - [Decisions](https://kiff.dev/docs/decisions): The proposal-to-decision model. - [Receipts and proof](https://kiff.dev/docs/receipts-and-proof): Tamper-evident evidence that the decision ran before the action. - [What's real today](https://kiff.dev/docs/whats-real-today): An honest account of what ships now. ## kiff-guard (the on-ramp for existing agents) - [kiff-guard on GitHub](https://github.com/kiff/kiff-guard): Attach to your agent's tool calls, observe what it does, then migrate the risky actions onto the framework. Python and TypeScript, MIT, with adapters for common agent frameworks. - [Connect an agent](https://kiff.dev/docs/connect-an-agent): Three ways in: the MCP gateway for agents that call MCP tools, kiff-guard for agents in your own code (observe, then enforce against the agent's Card), or one call to the decision API from any code. - [KIFF Plugins](https://github.com/kiff/kiff-plugins) (beta): `kiff-cards` connects Claude Code to the gateway and installs both skills below; the optional `kiff` plugin adds Card status, held-call notices, owner-answer recovery through read-only `kiff_pending`, and `/kiff`. `/plugin marketplace add kiff/kiff-plugins`, then `/plugin install kiff-cards@kiff` and optionally `/plugin install kiff@kiff`. Claude Code asks for the agent's gateway key and stores it securely. [Installation, permissions and recovery](https://kiff.dev/docs/agent-skill). MIT. - [KIFF skill for coding agents](https://kiff.dev/skills/kiff.md) (beta): A portable SKILL.md for Claude Code, Codex and similar agents: connect an agent or remote MCP tool through KIFF, propose a Card for the owner to issue, handle held, refused and unknown results, and find the right repository or doc. The skill is guidance; the KIFF MCP gateway enforces the Card. Install guide: [kiff.dev/docs/agent-skill](https://kiff.dev/docs/agent-skill). - [Domain skill for coding agents](https://kiff.dev/skills/kiff-domains.md): A portable SKILL.md a coding assistant (Cursor, Kiro, Codex, Claude Code, and more) installs so it authors and extends your kiff.yaml domains against the real grammar, states, actions, approvals, permissions, executors. Fetch with one curl. - Adapter guides: [Agno](https://kiff.dev/docs/agno), [LangGraph](https://kiff.dev/docs/langgraph), [Hermes](https://kiff.dev/docs/hermes), [OpenAI Agents](https://kiff.dev/docs/openai-agents), [Google ADK](https://kiff.dev/docs/google-adk), [Pydantic AI](https://kiff.dev/docs/pydantic-ai), [Strands](https://kiff.dev/docs/strands), [Haystack](https://kiff.dev/docs/haystack), [Microsoft Agent Framework](https://kiff.dev/docs/microsoft-agent-framework), [LlamaIndex](https://kiff.dev/docs/llamaindex), [OpenClaw](https://kiff.dev/docs/openclaw). ## KIFF Cloud (hosted runtime) and Studio (the domain's rendered view) - [KIFF Cloud](https://app.kiff.dev): The KIFF runtime, hosted, agents and services call it over HTTP to propose an action and get a decision before execution, with approvals, signed receipts, and a governance evidence view. - [KIFF API](https://api.kiff.dev): The headless runtime contract, POST a proposal, get a decision before execution. - [Quickstart: give an agent a Card](https://kiff.dev/docs/quickstart): Connect a remote MCP tool and an agent in KIFF Cloud, issue the agent a Card, and answer its first held call. No code. - [Held calls and approvals](https://kiff.dev/docs/approvals): Owner answers in KIFF Cloud; the agent collects the same call. Read-only pending recovery and the optional Claude Code plugin bring back answers given in time, even when read late. Approval and the tool's reported outcome are separate facts; errors, unknown outcomes and older unrecorded outcomes do not establish success. An unanswered hold is refused after its expiry (10 minutes by default). - [MCP gateway](https://kiff.dev/docs/mcp-gateway): Connect an agent supporting remote MCP over HTTP to `mcp.kiff.dev`, connect remote MCP tools to KIFF, and check each call against the agent's Card before forwarding. Production smoke-tested with Claude Code and Codex using a temporary test tool. Set up in KIFF Cloud (Connected tools, then the agent's Tools tab) or with the gateway API; remote MCP only, bearer credentials only, no OAuth or arbitrary HTTP APIs. - [Tools that check KIFF](https://kiff.dev/docs/tool-permits): Choose who holds a connected tool's key: `sealed` (KIFF, encrypted), `verify` (the tool checks a short-lived Ed25519 execution permit KIFF signs for each allowed call, with the open-source verifier in `kiff-guard[verifier]`) or `relay` (a relay you run holds the key). The verifier runs each operation at most once and never resends a call whose answer was lost. - [Govern](https://kiff.dev/docs/govern): Decisions, receipts, approvals, and verifiability. - [KIFF Cards](https://kiff.dev/docs/kiff-cards): The instrument an organization issues to one named agent: the actions it may take, the entity states it may take them in, a ceiling counted as `count` or `sum()` over a window, and an optional expiry. Required only for actions the domain declares `requires_authority`, where absence is refused with `no_mandate_issued` and any proposal that would draw on a card needs an API key issued for its holder (`key_not_bound_to_agent`, `actor_not_key_holder`). Draws are recorded at authorization, not execution; issuing and revoking are management actions a runtime key cannot perform. ## Legal and trust - [Security](https://kiff.dev/security) - [Privacy](https://kiff.dev/privacy) - [Terms](https://kiff.dev/terms) - [Data Processing Addendum](https://kiff.dev/legal/dpa) - [EU AI Act](https://kiff.dev/ai-act) ## Optional - [llms-full.txt](https://kiff.dev/llms-full.txt): The full text of the docs and whitepaper concatenated into one file, for single-fetch ingestion. _Last updated: 2026-10-08._ --- # Full text The complete text of the KIFF docs and whitepaper follows, for single-fetch ingestion. Source of truth: https://kiff.dev/docs and https://kiff.dev/whitepaper. --- # Documentation ## Quickstart: give an agent a Card Source: https://kiff.dev/docs/quickstart # Quickstart: give an agent a Card Give one agent a [KIFF Card](/docs/kiff-cards) and watch KIFF check its calls against it. You connect a tool, issue the Card, point the agent at KIFF, and answer the first call that goes over the limit. No code, about ten minutes. You need: - an account at [app.kiff.dev](https://app.kiff.dev) (free); - an agent that can use a remote MCP server, such as Claude Code or Codex; - a remote MCP server at a public HTTPS address. Any public one works to try it. Already run an agent in your own code instead, with a framework such as LangGraph or the OpenAI Agents SDK? See [Connect an agent](/docs/connect-an-agent) for kiff-guard. The Card works the same way. ## 1. Say who the account is for The first time you sign in, KIFF asks **Who is this account for?** - **For me**: your own agents and tools. You see My Cards, My agents, Connected tools, Needs you and Activity. - **For a company**: the full workspace, with domains, contracts and API keys as well. It changes what you see, not what KIFF does. You can change it later in **Settings → Dashboard view**. ## 2. Connect a tool Open **Connected tools** and choose **Connect a tool**. 1. Enter the server's URL and, if it needs one, its bearer credential. KIFF lists the server's tools. Nothing is saved yet. 2. Choose the tool, the name your agent will see, and whether the tool only reads. If the tool takes a whole-number amount, choose it, so a Card can limit amounts as well as calls. KIFF stores the credential encrypted and never shows it again. ![Connected tools in KIFF Cloud: one connected read-only tool, with Give an agent a Card, Test, Replace credential and Remove.](/static/img/docs/connected-tools.jpg) *A connected tool, from a KIFF test account.* ## 3. Give the agent a Card Add the agent on **My agents** if it isn't there yet. Then, on the connected tool, choose **Give an agent a Card**: - **Agent**: the agent's name. - **Number of calls**, per day, per 24 hours or per hour. With an amount argument, also the most per call and a total. - **A call outside the Card**: hold it for you to answer, or refuse it. - **A held call waits**: 10 minutes by default, up to 7 days. Choose **Issue the Card**. The Card is in force at once, and its page shows the limit, what is used and what is left. ![An issued Card: holder claude-e2e-agent, authority wiki_structure, 2 per rolling hour, 2 remaining.](/static/img/docs/card-issued.jpg) *The Card's page right after it is issued, from a KIFF test account.* ## 4. Point the agent at KIFF Open the agent on **My agents**, go to its **Tools** tab, and choose **Connect to the KIFF gateway**. KIFF creates a key for this agent, which can ask for decisions and nothing else, and shows it once inside the configuration to paste. ![The agent's Tools tab: Use tools through KIFF, with the gateway URL, and the connected tools it can reach.](/static/img/docs/agent-tools-tab.jpg) *An agent's Tools tab, from a KIFF test account.* For Claude Code, the configuration looks like this: ```json { "mcpServers": { "kiff": { "type": "http", "url": "https://mcp.kiff.dev/mcp", "headers": { "Authorization": "Bearer " } } } } ``` Keep the key out of anything you commit. If the agent can also reach the same tool directly, remove that access: the Card only binds calls that go through KIFF. ## 5. Use it, and go over the limit once Ask the agent to use the tool. Calls inside the Card go through and return the tool's own result. When a call goes over the Card: - nothing is sent to the tool; - the agent is told the call is waiting for you, with a link; - you get an email, and the call appears on **Needs you**; - in Claude Code, the chat also offers to open the link. Open the link, signed in, and choose **Approve once**, **Change the card to allow it**, or **Reject**. The agent retries the same call and gets your answer. If nobody answers in time, the call is refused and nothing is sent. See [Held calls and approvals](/docs/approvals). ## What you have now - One Card that says what this agent may do with this tool, and how much. - A record of every call on **Activity**, and the Card's statement of every draw and change. - A way to stop the agent: revoke its key on its **Keys** tab, or revoke the Card. Other agents keep their access. ## Next - What a Card can say → [KIFF Cards](/docs/kiff-cards). - Agents in your own code → [Connect an agent](/docs/connect-an-agent). - The gateway in detail, with its API → [Use the KIFF MCP gateway](/docs/mcp-gateway). - What KIFF does and does not do → [What KIFF is](/docs/what-kiff-is). ## What KIFF is Source: https://kiff.dev/docs/what-kiff-is # What KIFF is KIFF lets your business decide what each AI agent may do, and change it without redeploying the agent. Today the rules for an agent usually live in its prompt, its config or its code, so every change is a release. With KIFF they live on a [KIFF Card](/docs/kiff-cards) the business controls: which actions the agent may take, in which states, how much, and until when. Before the agent acts, it asks KIFF. KIFF answers against the card and the current state, and your own system does the work. What that gives the company: - **Change a limit without a release.** The owner raises, lowers or revokes the card; the next request uses the new terms. - **Stop one agent, not all of them.** Each agent holds its own cards, and no two agents share one. Revoking a card withdraws only the authority that card granted: the agent's next request that needed it is refused, unless another active card of that agent also covers the action. Other agents are untouched. - **Start a new model small.** Give it its own card with a low limit and raise it when its statement looks right. - **Keep every change on record.** Each card has a statement: its limit, what was used, what is left, and every change with who made it. Only an owner or admin can issue, change or revoke a card. The agent's own key can only ask. ## How a decision is made KIFF puts an agent on the consequential actions you've kept it away from, refunds, payouts, payments, role changes, by **deciding the action before it runs**: whether this specific action, on this specific entity, is allowed right now. The useful action goes through, the out-of-policy one is refused, and the duplicate can't fire twice. The thing you build once is a [reusable operational domain](/docs/domains), the state, valid actions, authority, approvals, and evidence your agents share. This page is about the mechanism that makes the actions against it safe to hand over. ## What this does KIFF sits in front of an action and answers one question: *is this specific action, on this specific entity, allowed right now, given the entity's state and your policy?* The answer is one of four outcomes - `allowed`, `blocked`, `approval_required`, or `invalid`, and it is produced before the action executes, not after. The refund runs when the order is in a state that permits it. The same refund is refused once the order is already refunded. A high-value one waits for a human. You get to say yes to the agent doing real work because the cases you were afraid of are handled before the action, not by hoping the model behaves. Every answer leaves a record: a signed receipt of what was decided, on what basis, at that moment, support for the claim, not the headline. ## What KIFF is not KIFF is the **decision, not the runner.** This distinction is load-bearing, so it's worth being blunt about it: - KIFF does **not** execute your action. It does not move the money, send the email, or write to your database. - KIFF's direct API decision path does **not** hold provider credentials or proxy your action. Its separate MCP gateway stores connected remote MCP bearer credentials encrypted and forwards those tool calls only after an `allowed` decision. - KIFF does **not** run your agent or manage your workflow. In direct API integrations, your system stays the executor: it runs the action after `allowed` and withholds it otherwise. With the MCP gateway, KIFF forwards a connected tool call after `allowed`, and that remote tool performs the business operation. KIFF does not run your agent. In either path, only actions routed through KIFF are checked. ## What you own vs what KIFF decides A clean way to hold the split: **You own:** - the policy, which actions need approval, what the thresholds are, who may authorize what; - the state, what counts as the current state of an entity (an invoice is `PENDING` or `PAID`, a refund is `REQUESTED`); - the thresholds and approval rules; - execution, your runtime runs the action when KIFF says `allowed`. **KIFF decides:** - whether a proposed action is permitted against that state and policy, before it runs; - and produces the receipt that proves what was decided. KIFF derives the *observed action surface* for you, when you attach kiff-guard in observe mode, it learns which tools your agent actually calls and hands you a starter domain from real traffic (see [Connect an agent](/docs/connect-an-agent)). But the policy, state model, and approval rules in that domain are yours to set. KIFF never invents authority you didn't declare. ## The shortest path through the product 1. **Give one agent a Card**, connect a tool, issue the Card, and answer its first held call. See the [quickstart](/docs/quickstart). 2. **Connect the agents you already run**, through the MCP gateway, with kiff-guard in your own code, or with one call to the decision API. See [Connect an agent](/docs/connect-an-agent). 3. **Decide how you're asked**, what is held, for how long, and where you hear about it. See [Held calls and approvals](/docs/approvals). 4. **Build the domain underneath when you need it**, the state and rules that let a Card say *refund, only on a paid order*. See [Domains](/docs/domains). If you only read one more page, read [KIFF Cards](/docs/kiff-cards). ## KIFF Cards Source: https://kiff.dev/docs/kiff-cards # KIFF Cards Every other check in KIFF asks whether **one action** is allowed: is the entity in the right state, does this actor have the permission, are the parameters valid, does this need an approval. A card carries the answer to the one question no single action can answer, **what has this agent already done, and is that enough?** An action can be correct in every way and still be the one that should not run, because of the twenty that came before it. That is what a card catches. ## Why cards exist As agents make consequential work cheap, approving each action by hand becomes the limit on how much of it they can do. A card is how an owner delegates instead of supervising: defined actions, the conditions the domain already enforces, and a bounded total, which the owner can widen as the record earns it or withdraw at once. It is closer to a licence than to a payment card or an access badge. It counts any consequence you can count, not only money, and a limit bounds exposure without making an invalid action valid. That stays the domain's job. ## What a card is A KIFF Card, a **mandate** in the API, the package and the schema, is a bounded, revocable instrument issued to one named agent: > `refund-agent` may run `REFUND_ORDER`, only while the order is `PAID`, > up to a total of `500000` summed over the `amount_cents` parameter, > each calendar day, until it is revoked. What it says: - **Who holds it**, one agent by default. It must match the `actor_id` the agent already sends when it proposes an action. Two agents doing the same work usually hold two cards, each with its own balance. A card can also be **shared** with up to 20 agents: every holder draws on the same totals, and each decision still names the agent that asked. One agent may hold several cards, see below, where the tightest binds. - **What it may do**, the actions it covers. An action you leave out is still governed by the contract; it just does not draw on this card. - **Under what conditions**, the entity states the grant is confined to. Leave them unset and the action's own states apply, which is every state the domain already allows it in. A card narrows; it never widens. - **How much, over what period**, a quantity, either `count` (how many times those actions are *authorized*) or `sum()` (the total of one numeric parameter the actions declare), with a ceiling and a window: each calendar day, any rolling 24 hours, or any rolling hour. - **When it stops**, an optional expiry, or the moment an owner revokes it. ## When a card is required By default a card **constrains without conferring**: an agent with no card covering an action is unaffected, and the check is a no-op. That is what lets you adopt cards one agent at a time without the first one breaking something already running. An action opts out of that by declaring `requires_authority` in the domain: ```yaml actions: - name: REFUND_ORDER allowed_states: [PAID, PARTIALLY_REFUNDED] requires_authority: true ``` For that action, no card is a refusal rather than a no-op: ```json { "outcome": "blocked", "reasons": ["no_mandate_issued"] } ``` That reason is its own, deliberately. The first integrator to meet it has wired everything correctly, and a generic `blocked` would send them to audit their domain, their permissions and their state machine before discovering that nothing has been issued yet. It is declared per action, not per tenant: the author of the domain knows which consequences are worth an instrument, and a tenant-wide switch would make that judgement for every action at once. ## Which key can use a card A card names its holder, and the proposal names its actor in `actor_id`. So that one agent cannot name another and spend its card, the API key has to be issued for the holder: - **A key issued for an agent acts only as that agent.** A proposal from it naming anyone else is refused with `actor_not_key_holder`, on every action. - **Drawing on a card needs such a key.** A key not issued for any agent is refused with `key_not_bound_to_agent` on an action that requires a card, and on any action where the named agent holds a card that covers it, even if the card is optional there. It keeps working for actions no card covers. "Create a key for this agent" on a card's page issues the right key: the role the job needs, bound to the card's holder. For a card scoped to an environment, the key is also bound to that environment. Its requests are decided there whether or not they name it, and a request naming another environment is refused, so the key cannot draw on a card for a different environment by asking for it. If the agent already has its own key, for example the one issued when it was added to watch in observe, the card's page offers to give that key the job's role instead of issuing a second one. The agent keeps the key it has. Roles are only ever added to a key: to take one away, revoke the key and issue another. Through the API this is `PATCH /v1/keys/{id}` with `{"add_roles": [...]}`, under the same rules as creating a key: only an owner or admin may do it, and never a role the caller does not hold. A test request sent from the card's page while you are signed in is not held to any of this; it is you, the owner, exercising the card you issued. ## What happens at the ceiling **The action is refused.** It is not sent for approval. "At the ceiling" means the proposed action *would exceed* what the card has left for the window, not that the card is empty. A €2,000 refund against €1,000 of remaining authority is refused, and a €500 one right after it is allowed, same card, same window, same reason code for the first. This is deliberate. A cap that produces an approval request someone clicks through at the end of a long day is not a cap; it is a notification with extra steps, and it re-creates exactly the rubber-stamping that let the decision drift to the machine in the first place. A proposal the card cannot cover comes back as: ```json { "outcome": "blocked", "reasons": ["mandate_limit_reached"], "message": "this action exceeds the remaining authority under mandate refund-agent-card: sum(amount_cents) limit 500000 per calendar_day reached: 300000 used, this action draws 250000" } ``` The engine's own words in that `message` are the precise rule: 300000 used of 500000, and an action drawing 250000 does not fit, €2,000 of authority is still on the card for something smaller. If an agent needs more room, you **raise the ceiling**, one deliberate act, recorded with who made it, rather than approving past it one action at a time. That is the same decision, made visible and attributable instead of ambient. ## Some properties worth knowing **A card narrows; it never confers.** It selects from what the domain and its permissions already allow and caps how much of it happens. It cannot grant an action the domain does not have, or legalise one in a state it forbids. This is the opposite of a capability system. **Several cards can cover one action, and the tightest binds.** Each is checked; the first that refuses decides. There is no precedence rule to reason about, and no way to widen an agent's authority by adding another card. **A card that cannot be read refuses.** If the ledger is unreachable, the action is blocked rather than allowed, because allowing means acting on authority nobody could confirm. Cards marks such a balance *unknown* rather than showing a comforting zero, an unused card and an unreadable one are very different facts. **A revoked card refuses; it does not disappear.** Revoking does not return the agent to "unbounded" for an action that requires one, and re-issuing the same name does not quietly bring it back: a proposal under a revoked card is blocked with `mandate_expired`, and its statement stays readable. **Draws are recorded when an action is authorized, not when it executes.** KIFF decides; your system executes, and reports no outcome back. An authorization your code chose not to run still counts against the window. A retry of the same proposal does not draw twice. **A limit binds the approved path too.** When an operator grants an approval, the re-decision runs the same check. Approval does not buy authority. ## The domain's observe posture A card is checked even when the domain is in `observe`. In that posture the decision is advisory and the caller continues its existing flow; the authorization still draws on the card, whether or not the caller ultimately executes it. KIFF never learns which, so what the statement shows is authorized authority, never confirmed spend. Counting it anyway is exactly what lets you see **what your agent would have drawn** before you enforce anything. That is the recommended on-ramp: issue a card deliberately high, run for a week, read the statement, and set the real ceiling from your own traffic instead of from a guess. (This is the *domain's* observe posture, which still calls the decide API. Guard's own observe mode is a different thing: it watches an agent's tool calls without asking KIFF at all, so nothing is decided and no card is drawn.) ## The statement **Cards** in the dashboard shows, for each card: the ceiling, how much has been drawn in the current window, what remains, and every movement that got it there. A movement names what it was for, `REFUND_ORDER on ord_8812`, carries how much it drew, and says how much of that limit's window had been consumed once it landed. Where a card caps the same quantity over more than one window, each subtotal names its own: five an hour and twenty a day are both true of the same movement. Two things the statement deliberately does not do: - **It never claims execution.** Every movement is an authorization. KIFF decided; your system ran the action afterwards and does not report back, so an action you authorized and never ran still drew. - **It never lists refusals.** A refusal moved no balance, so it is not a movement. Refusals live in the domain's activity, which the card links to, filtered to its holder. Below the movements, **Issued and changed** is the card's history: what its ceiling was at each issuance, and who changed it. Re-issuing a card overwrites the row it lives in, so this is the only record of what the authority used to be. ## Changing the ceiling **Change ceiling** on the card opens the issuance form with its current terms, because a change *is* a re-issue under the same name. Draws already recorded still count, so raising a ceiling adds only the difference rather than opening a fresh window, and the revision records who made the change. ## Setting one **Cards → Issue a card** in the dashboard. The form offers the actions your domain declares, the entity states each of them allows, and only the parameters it types as a number, so a quantity that cannot be summed is not offered in the first place. Or over HTTP, with a management credential: ```bash curl -sS -X POST https://api.kiff.dev/v1/me/mandates \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "id": "refund-agent-card", "subject": "refund-agent", "domain": "card-refund", "grants": [{"action": "REFUND_ORDER", "entity_states": ["PAID"]}], "aggregates": [ {"quantity": "sum(amount_cents)", "limit": 500000, "window": "calendar_day"} ], "valid_until": "2027-01-01T00:00:00Z" }' ``` Issuing and revoking require a **management** identity, the signed-in account owner or an admin. An agent's own runtime key is refused. A credential that could grant itself authority would make every card advisory. ## What this is not - **Not a payment card.** No money moves through KIFF. A payment card knows a merchant and an amount; a KIFF Card knows that this order is refundable, and counts whatever the consequence is, three `DELETE_CUSTOMER` an hour is the same instrument as €5,000 a day. - **Not a rate limit.** A rate limit protects your infrastructure from volume. A card bounds consequence: what an agent is permitted to *do*, in the units the action is measured in. - **Not a spend tracker.** Nothing here reads your ledger or your bank. It counts what KIFF authorized, and says so on the statement. - **Not portable.** A card is enforced by your KIFF tenant. Signing one so a third party could verify it independently is a later question. ## See also - [Decisions](/docs/decisions), the proposal flow a limit is checked in, and the outcomes it can produce. - [Govern](/docs/govern), the evidence view per action, and the observe/enforce posture a limit is checked under either way. - [Held calls and approvals](/docs/approvals), what happens to a call outside the card, and how the owner answers. ## Connect an agent Source: https://kiff.dev/docs/connect-an-agent # Connect an agent An agent gets a [Card](/docs/kiff-cards) the same way however it is built. What differs is how its actions reach KIFF. Pick the path that matches the agent: | Your agent | Path | What changes in the agent | |---|---|---| | Calls tools over MCP (Claude Code, Codex, other MCP clients) | [The KIFF MCP gateway](/docs/mcp-gateway) | Its MCP server URL and key. No code. | | Runs in your own code, on a framework | **kiff-guard**, below | One hook in the agent's code. | | Any code that can make an HTTP call | **The decision API**, [below](#call-the-decision-api-directly) | One call before the action. | In every path KIFF decides before the action runs, and only actions that go through KIFF are checked. Give each agent its own key, from its page in KIFF Cloud: that is how KIFF knows which agent is asking, and whose Card to draw on. > **Using Claude Code or Codex?** Install the > [KIFF skill](/docs/agent-skill) and ask it to connect your agent. It > walks you through the right path below, drafts the Card, and knows what > to do when a call is held. ## Connect with kiff-guard kiff-guard is an open-source (MIT) library that sits in the agent's own tool-calling loop. Attach it in observe mode, watch what the agent actually does, then switch to enforce so each tool call is checked against the agent's Card first. The agent keeps doing the work; no rewrite. ### 1. Install the guard The guard is an open-source (MIT) package, [`kiff/kiff-guard`](https://github.com/kiff/kiff-guard), published on PyPI and npm. Install the core plus the adapter for your framework, here, Agno: ```bash pip install "kiff-guard[agno]" ``` The core has zero required dependencies; each framework adapter pulls its own framework in as an optional extra. For TypeScript (the OpenClaw adapter), `npm install @kiff/kiff-guard`. ### 2. Attach it in observe mode One guard, one hook. `observe` runs every tool, records a receipt, and learns your action catalog. It needs no client, no tenant, no API key - it never calls KIFF and never blocks a tool. ```python from kiff_guard import Guard from kiff_guard.adapters.agno import agno_hook guard = Guard(mode="observe") # no client, no tenant needed agent = Agent( model=..., tools=[refund_order, send_email], tool_hooks=[agno_hook(guard)], ) ``` On a different stack? The hook is the only line that changes. Every [framework page](/docs/agno) shows the same attach with that framework's one-liner, and [How the guard works](/docs/concepts) explains the model behind all of them. ### 3. Run as usual, and see what it did Nothing about your agent changes, run it the way you already do. The guard rides along on each tool call and turns each into a receipt. ```python agent.run("Refund order ord_4821 and email the customer") for r in guard.receipts: print(r.state, r.tool, r.outcome) # observed refund_order observed # observed send_email observed ``` ### 4. Derive a domain from real traffic Because the guard learned the catalog while it watched, it hands you a starter KIFF domain drafted from what the agent actually does, so you never face a blank `kiff.yaml`: ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ```yaml # KIFF domain draft for 'my-domain' # Auto-derived from observed agent traffic (instrument-first). domain: my-domain actions: - name: refund_order parameters: [amount_cents, order_id] risk: low # TODO(human): low | medium | high requires_approval: false # TODO(human) - name: send_email parameters: [body, to] risk: low # TODO(human): low | medium | high requires_approval: false # TODO(human) ``` The draft is a starting point, you set the risk, thresholds, and approval rules. KIFF derives the *observed action surface*; it never invents authority you didn't declare. ### 5. Give the agent a key and a Card, then enforce 1. In KIFF Cloud, open the agent on **My agents** (or **Agents**), go to its **Keys** tab and choose **Create a kiff-guard key**. The key is bound to this agent and shown once. Put it in the agent process's environment: ```bash export KIFF_CLOUD_URL=https://api.kiff.dev export KIFF_CLOUD_API_KEY='kiff_live_...' ``` 2. Give the agent a Card for the actions it should take, from its **Cards** tab or **Issue a card**. 3. Switch the guard to `enforce` and give it the client that carries the key. KIFF now decides **before** each tool runs: `allowed` proceeds, `approval_required` waits for the owner (see [Held calls and approvals](/docs/approvals)), `blocked` refuses. If KIFF can't be reached, the guard fails closed. ```python guard = Guard(mode="enforce", client=client) # client carries the key ``` Keep two kinds of credential apart: - the **agent's KIFF key** only asks KIFF for decisions. It cannot grant itself authority or change its own Card; - your **provider credentials** (the model API, your payments API) stay where they already live. kiff-guard never sends them to KIFF. To cut the agent off, revoke its key on its **Keys** tab. Its next call with that key is refused; other agents are untouched. ## Call the decision API directly No framework, or code you'd rather not hook? Immediately before a consequential call, send the proposed action to KIFF and branch on the answer. KIFF returns the decision; your code still runs the action. ```python import os import requests def ask_kiff(*, entity_id, entity_type, action, parameters, actor_id): response = requests.post( f"{os.environ['KIFF_CLOUD_URL']}/v1/proposals/decide", headers={"Authorization": f"Bearer {os.environ['KIFF_CLOUD_API_KEY']}"}, json={ "entity_id": entity_id, "entity_type": entity_type, "action_name": action, "actor_id": actor_id, "parameters": parameters, }, timeout=5, ) return response.json() decision = ask_kiff(entity_id=order_id, entity_type="Order", action="ISSUE_REFUND", parameters={"amount": amount}, actor_id="refund-agent") if decision["outcome"] == "allowed": process_refund(order_id, amount) ``` | Outcome | Meaning | What your code does | |---|---|---| | `allowed` | State, contract and Card permit it. | Run the action. | | `approval_required` | The owner must decide first. | Hold the action. | | `blocked` | State, permissions, parameters or the Card forbid it. | Do not run it. | | `invalid` | The request or domain configuration is incomplete. | Fix the request or domain. | | `limit_exceeded` | The account's Cloud allowance is used up. | Do not run; check billing. | Branch on `outcome`, not only the HTTP status. Where the action draws on a Card, `actor_id` must be the agent the Card was issued to, and the key must be that agent's own key: a key issued for another agent is refused, so one agent cannot spend another's Card. This governs only the calls you route through it. It proves what KIFF authorized before the call, not that your function completed. ## Where to go next - **Agents that call MCP tools** → [Use the KIFF MCP gateway](/docs/mcp-gateway). - **The model behind observe and enforce** → [How kiff-guard works](/docs/concepts). - **Your framework's hook** → pick it from the sidebar; each adapter ships in kiff-guard and passes its conformance suite. - **What a Card can say** → [KIFF Cards](/docs/kiff-cards). The guard, every adapter, and the conformance suite live in the public MIT repo [`kiff/kiff-guard`](https://github.com/kiff/kiff-guard). ## Use the KIFF MCP gateway Source: https://kiff.dev/docs/mcp-gateway # Use the KIFF MCP gateway Already have an AI agent that can connect to remote MCP servers? You can put KIFF between that agent and the tools it uses: ```text your agent ──MCP──▶ mcp.kiff.dev ──MCP──▶ your connected tool │ └── checks the agent's Card first ``` KIFF does not host the agent. The agent still plans the work; the connected tool still performs it. KIFF checks each call against the Card before it forwards the call. For this to be a boundary, connect the tool to KIFF and remove the agent's direct access to that same tool. This is a different integration from [kiff-guard](/docs/connect-an-agent), which you add to code you can change. Using Claude Code or Codex? The [KIFF skill](/docs/agent-skill) walks through these steps with you. The gateway is for remote MCP tools. New to KIFF? The [quickstart](/docs/quickstart) walks through the same steps in the dashboard. ## What you need - A KIFF account, signed in as its owner or an admin. - A remote MCP server reachable at a public HTTPS URL. Private, loopback, link-local, metadata and reserved addresses are refused. - An agent that can connect to a remote MCP server over HTTP. The production flow has been tested with Claude Code and Codex. - A bearer credential for the tool server, if it requires one. KIFF stores connected tool credentials encrypted. To keep the key away from KIFF entirely, the tool can check KIFF's execution permit itself instead: see [Tools that check KIFF](/docs/tool-permits). OAuth and arbitrary non-MCP web APIs are not supported yet. ## 1. Connect a remote MCP tool Signed in to KIFF Cloud as the account's owner or an admin, open **Tools** and choose **Connect a tool**: 1. Enter the server's URL and, if it needs one, its bearer credential. KIFF lists the server's tools with that credential; nothing is saved yet. 2. Choose the tool to connect, the name your agent will see, the amount argument a Card limits (whole-number arguments only), the idempotency argument KIFF fills (text arguments only), and whether the tool only reads. From the same page you can test a connected tool, replace its credential (the old one is kept unless the new one works), change who holds its key ([verify or relay mode](/docs/tool-permits)) or remove it. The credential is never shown again after it is saved. ### With the gateway API The same steps work over the API for a caller that already holds an owner or admin API key (see the note on API keys below). Prepare the request body in a local file readable only by you, such as `connection.json`: ```json { "name": "issue_refund", "url": "https://tools.example.com/mcp", "tool": "refund", "amount_argument": "amount_eur", "amount_unit": "EUR", "amount_scale": 0, "idempotency_argument": "idempotency_key", "credential": "" } ``` Then send it with your owner or admin key: ```bash curl -s -X POST https://mcp.kiff.dev/v1/connections \ -H "Authorization: Bearer $OWNER_KEY" \ -H "Content-Type: application/json" \ --data-binary @connection.json ``` `tool` is the name exposed by the remote server; `name` is the name your agent will see. Set `amount_argument` to the tool's integer amount parameter if the Card should limit amounts. Set `idempotency_argument` if the tool accepts an idempotency key. Omit either field if it does not apply. Use the same units your tool expects when you set Card limits. `amount_unit` and `amount_scale` are optional and only change how amounts are shown: a currency code or a word for the unit, and the number of decimal places the tool's integer carries. A tool that takes cents uses `"amount_unit": "EUR", "amount_scale": 2`, so a limit of 5000 reads as 50.00 EUR. KIFF still checks the whole numbers either way. KIFF checks that the URL, tool and credential work before saving the connection. The gateway then updates the account's tool contract. Treat the tool credential as a secret: do not commit the request file or paste a real credential into a command that will be saved in shell history. Remove the local request file after the connection is saved. ## 2. Give the agent a Card In KIFF Cloud, open **Tools** and choose **Give an agent a Card** on the connected tool. Name the agent and set, in the tool's own units, the most per call, a total and a number of calls per day, per 24 hours or per hour, what happens to a call outside the Card (held for you, or refused), and how long a held call waits for your answer. Issuing again for the same agent and tool changes its Card; **Change limits** on the Card starts from its current terms. Each agent needs its own key bound to that agent and limited to the `gateway_agent` role. The key must be created by a caller that holds the `admin` role, because a key can only grant roles its creator already holds. Signed in as an admin, open the agent under **Agents** (**My agents** in a personal account), go to its **Tools** tab and choose **Connect to the KIFF gateway**. KIFF creates the key and shows it once, already inside the configuration for Claude Code and Codex; copy it then. In a company account, only an admin can do this, and anyone else is told so. To do the same with the API, use your admin API key (`$ADMIN_KEY`): ```bash curl -s -X POST https://api.kiff.dev/v1/keys \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"label":"my-agent","roles":["gateway_agent"],"agent_id":"my-agent"}' ``` A solo account owner has the `admin` role, and so does their signed-in session in KIFF Cloud, which is why the dashboard steps above work. An API key created on **API keys** (`/dashboard/keys`) holds no roles, so it cannot connect tools or create gateway keys: there is no way yet to create an `admin` API key from the dashboard. Use the dashboard for these steps; the API calls are for an operator who already holds an admin key. A gateway key is good only for the gateway. Like a sign-in connection for the same agent, it reaches only the routes the gateway calls: it cannot list the account's Cards, read another agent's held work, or change anything on the account. ## 3. Point the agent at KIFF Configure the agent's remote MCP server URL as `https://mcp.kiff.dev/mcp` and give it the gateway key as a bearer token. For Claude Code, the MCP server entry has this shape: ```json { "mcpServers": { "kiff": { "type": "http", "url": "https://mcp.kiff.dev/mcp", "headers": { "Authorization": "Bearer " } } } } ``` For Codex, set `mcp_servers.kiff.url` to that URL and `mcp_servers.kiff.bearer_token_env_var` to the environment variable holding the gateway key. Keep the secret out of the configuration file if that file is checked in. ## 4. Let KIFF check each call When the agent calls the connected tool, KIFF checks the agent's Card first: - If the call is within the Card, KIFF forwards it to the tool. - If the Card requires an owner decision, KIFF holds the call and returns a link. KIFF also emails the account owner, once per held call, with what the agent asked for and the same link. Open it in KIFF Cloud, signed in, and choose an answer; nothing can be approved from the email itself. Approval authorizes the call; the agent must retry before KIFF forwards it. In Claude Code (2.1.287 or later), the chat also offers to open the link, on accounts where that is on. See [Held calls and approvals](/docs/approvals). - A held call waits for the owner's answer for the Card's hold expiry, 10 minutes unless the Card sets another (from 1 minute to 7 days, `hold_expiry` when the Card is issued). If nobody answers in time it is refused, nothing is sent, and a late answer is not accepted; the agent has to ask again with a new `kiff_operation_id`. An answer given in time stands until the agent collects it. - If the call is refused, KIFF does not forward it. If the client runs tool calls as **MCP tasks** (the gateway declares the `tasks` capability), a held call is answered with a working task instead of a result that says to retry. The client waits on the task; it completes once the call is forwarded, refused or expired, and the conversation continues with the outcome. Only the key that made the call can read its task. The gateway records calls so retries do not forward the same held action twice. If the tool's response is lost after a call was sent, the result may be unknown; KIFF will not send that action again automatically. When the tool supports idempotency, configure its idempotency argument as shown above. The gateway adds an optional `kiff_operation_id` argument to every tool. Give each new action a new id, and reuse the id when retrying the same action: - A call with an id KIFF has seen is the same call, at any time: it gets that call's outcome and is never forwarded again. The same id with different arguments is refused. - Without an id, an identical call (same tool, same arguments) within 10 minutes is treated as a retry of the first and gets its outcome, with a note saying so. Two genuinely separate identical actions in that window are done once; give each its own id to do both. - A tool connected as read-only is never answered with an earlier call's result: a second identical read reaches the tool again once the first has finished. ## What the agent knows before it calls Each tool the gateway lists ends with a line saying what the agent's Card allows for it: the most one call may carry, how much of each total is left in its window, and whether a call over the Card is held for an approver or refused. A tool no Card of the agent's covers says that calls to it are refused. For example: ```text Your KIFF Card: Card mdt-refunds covers this tool: up to 1000 amount_eur per call; 3800 of 5000 amount_eur left today. A call over it is held for a person to approve. ``` The gateway also lists its own read-only tool, `kiff_card`. It shows the same Cards with the numbers as they are now, the same ones the Card's statement shows. Calling it is not a governed call: nothing is recorded, decided or drawn, and nothing reaches a connected tool. No connected tool can be named `kiff_card`. These numbers are information, read when the agent asks. They can change between that read and the call, for example when another agent on the same Card draws from it. The decision KIFF makes when the call arrives is what counts. `kiff_card` shows only the Cards the agent holds, and reading a Card gives the agent no way to issue, change or revoke one. The gateway also lists `kiff_pending`, read-only as well. It lists this credential's own calls that wait for the owner, are being sent, or have an unknown outcome, newest first, from the last 7 days, 50 at a time. When there are more, the result has a `next_cursor` to pass back as `cursor`: ```json {"name": "kiff_pending", "arguments": {}} ``` These are the parameters for an MCP `tools/call` request. For the next page, use `{"name": "kiff_pending", "arguments": {"cursor": ""}}`. For a held call, the result includes the original arguments and the owner's answer: `waiting`, `approved`, `refused`, or `ended` without approval. `unavailable` means KIFF could not read the answer; it is not a refusal or proof that nobody approved. A client must preserve recovery and check again. For every call, the list gives the `kiff_operation_id` that collects it. A call made without one is listed under KIFF's own id, so it can still be collected after the 10-minute identical-call window. Reuse the listed original arguments, including any idempotency argument the agent supplied; the downstream tool still receives KIFF's own idempotency value. Reading the list sends nothing, uses no approval and draws nothing from the Card. Collecting a call is the agent's own call, made again with the same tool, original arguments and listed id; an approved call is sent at most once. When recovering work in a new session, read the pending list first using the same credential that made the calls. In Claude Code, the optional `kiff` plugin checks held calls and brings the owner's answer into the conversation. Its bounded scans continue across checks and its notices are announced once. See [installation, permissions and recovery](/docs/agent-skill#optional-card-status-notices-and-recovery-in-claude-code). ## What the agent gets back A successful authorization and a successful tool response are separate facts. A forwarded call can still return a tool error, and a missing response leaves the outcome unknown. KIFF Cloud records the tool's reported outcome alongside the decision; see [approval and the tool's outcome](/docs/approvals#approval-and-the-tool-s-outcome). A forwarded call returns the tool's own result, unchanged. Every result KIFF writes itself (held, refused, still being decided, sent with no answer yet, failed, or unknown) has text for the model with the next step, and the same facts for the program running the agent in the result's `_meta`, under `dev.kiff/call`: ```json { "state": "held", "invocation": "tc-…", "operation_id": "order-1042-refund", "sent": "no", "exception_id": "exc-…", "review_url": "https://app.kiff.dev/needs-you/exc-…", "hold_expires_at": "2026-10-01T12:10:00Z", "retry": "same_call", "retry_after_s": 30 } ``` - `state`: `held`, `refused`, `failed`, `unknown`, `deciding`, `forwarding`, or `forwarded` on a repeated call. - `sent`: whether the tool received the call: `no`, `yes` or `unknown`. - `retry`: `same_call` (repeat the identical call to get the answer), `new_call` (this call is over; a new attempt needs a new `kiff_operation_id`, after the next step in the text) or `none` (do not retry, for example when the owner rejected it or the result is unknown). - `reasons`: machine-readable codes, such as `no_mandate_issued`, `mandate_per_action_exceeded` or `mandate_owner_expired`. - `hold_expires_at`: on a held call, when it stops waiting for the owner and is refused. - `repeat`: `true` when an identical call without an id was answered with an earlier call's outcome. ## Limits to understand - Only calls to tools connected through KIFF are governed. A direct connection, shell, browser or another route to the same service sits outside this Card. - The gateway currently connects remote MCP servers only. It does not connect arbitrary HTTP APIs or use OAuth. - The prompt in the chat works only with clients KIFF has verified: today Claude Code 2.1.287 or later, on MCP revision 2026-07-28. Every other client gets the held result with its link, and the owner still gets the email and Needs you. - Claude Code and Codex passed production smoke tests with a temporary test tool. That verified the gateway path; it was not a customer transaction or a test of every MCP client and server. - Codex prompts for approval before every MCP tool call by default. Add `default_tools_approval_mode = "approve"` under `[mcp_servers.kiff]` in your Codex config (not at the top level) to let it forward calls without pausing. For the product story and the production test details, see [Putting KIFF between an agent and its tools](/blog/putting-kiff-between-an-agent-and-its-tools). ## The KIFF skill for coding agents Source: https://kiff.dev/docs/agent-skill # The KIFF skill for coding agents **Beta.** The skills, plugins and their install steps may change between versions. The KIFF skill is a `SKILL.md` file that teaches Claude Code, Codex and similar agents how to work with KIFF. With it installed, an agent can: - **connect** an agent or a remote MCP tool to KIFF, step by step, through the [MCP gateway](/docs/mcp-gateway), [kiff-guard](/docs/connect-an-agent) or the decision API; - **propose a Card**: draft the terms an agent needs (which tool, the most per call, totals per day, hold or refuse, how long a hold waits) and explain each line, for the owner to issue; - **handle a held call** correctly: tell the person it is waiting, retry the identical call with the same `kiff_operation_id`, and never make the same change another way; - **find the right place**: the KIFF repositories, SDKs, endpoints and doc pages. It is a plain markdown file, served at [kiff.dev/skills/kiff.md](https://kiff.dev/skills/kiff.md). Read it before you install it. ## What enforces the Card **The skill is not the security boundary.** It is instructions to the agent. The [KIFF MCP gateway](/docs/mcp-gateway) (`mcp.kiff.dev`) enforces each agent's Card on KIFF's servers, for every call that goes through it, whatever the instructions say. Installing the skill alone does not enforce anything. A Card applies only to calls that go through KIFF; a call the agent makes some other way is outside it. ## Install it Claude Code, as a plugin. It adds this skill, the [domain skill](https://kiff.dev/skills/kiff-domains.md) and the KIFF MCP gateway as a server named `kiff`: ```text /plugin marketplace add kiff/kiff-plugins /plugin install kiff-cards@kiff ``` Claude Code asks for this agent's **gateway key** when you enable the plugin and keeps it in its secure storage (from the agent's **Tools** tab, **Connect to the KIFF gateway**). Leave it empty and the skills still work; the server just does not connect. The skill itself never reads or handles a key. Source: [github.com/kiff/kiff-plugins](https://github.com/kiff/kiff-plugins). ### Optional: Card status, notices and recovery in Claude Code The marketplace contains two plugins. **`kiff-cards`** adds the gateway connection and skills described above. **`kiff`** adds the interface: Card status, notices for held calls, and the `/kiff` pane. It needs a gateway connection and a Claude Code version that runs plugin hooks modules. Install it after connecting `kiff-cards`: ```text /plugin install kiff@kiff ``` If you installed the former `kiff-cards-ui` plugin, uninstall it; `kiff` replaces it. The interface can also use a gateway connected through a KIFF connect link or the claude.ai KIFF connector. For an existing installation, update the marketplace and plugin from your terminal, then restart Claude Code to apply the update: ```bash claude plugin marketplace update kiff claude plugin update kiff@kiff ``` - **Status line:** shows what is left on the agent's Card. While a call is held, it shows that the call is waiting for approval instead. - **Held-call notice:** explains that nothing was sent and links to the call in KIFF Cloud, where the owner or an admin answers. - **Owner-answer notice:** while this session has an unanswered held call, the plugin reads `kiff_pending` every 15 seconds. When it finds an approval, refusal or ended hold, it shows one notice and submits one turn telling the agent. Claude Code runs the turn when idle. The agent decides whether to collect an approved call by calling the same tool again with the original arguments and operation id. - **`/kiff`:** opens this session's KIFF calls and refreshes the Card and pending answers. It also provides a way to resume reads after a permission refusal. The plugin reads `kiff_card` for the Card and `kiff_pending` for owner answers. Both are read-only: these reads send nothing to a connected business tool, use no approval and draw nothing from the Card. The plugin never approves a call or invokes the held business tool itself. ### Allow the two read-only tools Claude Code's permissions apply to these reads. Type `/kiff`; if a read was refused, Claude Code asks whether to allow the tool. Choose to allow it for future reads. In auto mode, Claude Code decides without asking; you can instead add the rules below through **`/permissions` → Allow**. Use the names matching your gateway connection. Each rule is just the tool name, with no `allow` prefix. Save under **User settings** to apply across folders, or under project settings for that project only. | Gateway connection | Card read | Pending-call read | | --- | --- | --- | | `kiff-cards` plugin | `mcp__plugin_kiff-cards_kiff__kiff_card` | `mcp__plugin_kiff-cards_kiff__kiff_pending` | | Connect link added as `kiff` | `mcp__kiff__kiff_card` | `mcp__kiff__kiff_pending` | | claude.ai KIFF connector | `mcp__claude_ai_KIFF__kiff_card` | `mcp__claude_ai_KIFF__kiff_pending` | After a permission refusal, the plugin explains it once and stops background reads of that tool. Once you grant permission, run `/kiff` to read again and resume background checks. The plugin cannot grant itself permission. ### Recovery after a delayed answer or outage Pending reads are paginated. Each check reads a bounded number of pages and keeps its cursor for the next check, including checks started with `/kiff`. A hold behind more than 500 newer pending calls can still be found; recovery does not keep restarting at page one. Two minutes after the hold's expiry, the plugin finishes one last read before stopping automatic checks for that hold. A partial scan, failed read or `unavailable` owner answer does not finish that check. Recovery continues until the answer can be read, or a complete scan establishes that the hold is absent. An authoritative `waiting` answer counts as a completed read; `unavailable` does not. If the answer stays unavailable, checks can continue every 15 seconds for the rest of the session. An approval given before the hold expired still counts when read late. The plugin announces an answer once, however late it reads it. This does not extend the time the owner has to approve: a new approval after expiry is refused. After automatic checks have ended, `/kiff` checks again. See [Held calls and approvals](/docs/approvals). ### Install only the skill in another agent Claude Code, the skill only: ```bash mkdir -p ~/.claude/skills/kiff && curl -fsSL https://kiff.dev/skills/kiff.md \ -o ~/.claude/skills/kiff/SKILL.md ``` Codex: ```bash mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills/kiff" && curl -fsSL https://kiff.dev/skills/kiff.md \ -o "${CODEX_HOME:-$HOME/.codex}/skills/kiff/SKILL.md" ``` Any other agent that reads an instructions file (Cursor, Gemini CLI, Aider, Amp, OpenCode, Windsurf): save the skill as its own file, then point your instructions file at it once. ```bash curl -fsSL https://kiff.dev/skills/kiff.md -o KIFF.md echo "Read KIFF.md before working with KIFF." >> AGENTS.md ``` Use your agent's own instructions file in place of `AGENTS.md` if it has one (for example `GEMINI.md`). Start a new session so the agent picks it up. To update, run the `curl` line again: it overwrites `KIFF.md`, and the line in `AGENTS.md` stays as it is. ## What to ask ```text Connect my refund tool to KIFF and give this agent a Card. What should this agent's Card be? It issues refunds of about 30 euros, about ten a day. KIFF held my last call. What do I do? ``` ## What it will not do The skill tells the agent to stay on its side of the Card. Where KIFF also enforces the rule on its servers, the line says so: - It never issues or changes a Card itself. It drafts the terms; the owner issues them in KIFF Cloud. An agent's key is refused if it tries. - It never answers a held call. Only a person on the account who may approve, signed in at `app.kiff.dev`, can; an agent's key is refused. A "yes" in the chat is not an approval. - It never asks for an owner or admin credential, and keeps keys out of files you commit. - When KIFF holds or refuses a call, it does not make the same change another way (a direct API call, a shell, a browser). It reports what happened and stops. It also says plainly that KIFF governs only the calls that go through it, and suggests removing any direct route the agent still has to the same tool. ## Related - The owner's answer and the tool's outcome → [Held calls and approvals](/docs/approvals). - Pending calls and collection arguments → [Use the KIFF MCP gateway](/docs/mcp-gateway). - Authoring a `kiff.yaml` domain instead? Use the [domain skill](https://kiff.dev/skills/kiff-domains.md). - The whole site as text for an agent: [llms.txt](https://kiff.dev/llms.txt) and [llms-full.txt](https://kiff.dev/llms-full.txt). ## Held calls and approvals Source: https://kiff.dev/docs/approvals # Held calls and approvals When an agent asks for something outside its [Card](/docs/kiff-cards), and the Card says to hold such calls, KIFF holds the call and asks the owner. Nothing runs until the owner answers. If nobody answers in time, the call is refused. ## Where the owner hears about it A held call reaches the owner in three places. All three lead to the same page, and the answer is given there. 1. **Needs you**, in KIFF Cloud, and the bell. Every held call is listed with the agent, the action, the Card and how far over it is. 2. **Email.** One email per held call, to the account owner, with what the agent wanted, which Card, how far over, and when the hold expires. Each button opens the call's Needs you page. 3. **The agent's chat**, for clients KIFF has verified (Claude Code 2.1.287 or later). The chat shows a prompt to open the Needs you link. This is on by default for a personal account and off for a company's, where the person running the agent is usually not the owner. Change it in **Settings → Account → Held calls**. ## Who can answer Only the owner, or an admin, signed in to KIFF Cloud. - The email and the chat only carry a link to the Needs you page. Neither can approve anything, so a forwarded email or a copied link answers nothing without the owner's session. - The agent's key cannot answer, and a "yes" typed in the chat is not an approval. - KIFF approval links always go to `app.kiff.dev`. A link to answer from anywhere else is not from KIFF. ## The three answers - **Approve once**: this call only. The Card stays as it is, and its balance does not move. - **Change the card to allow it**: raises the Card's limits just enough for this call, and for everything after it. The change is recorded on the Card's statement. - **Reject**: the call is refused, and the agent is told not to retry it. ## What the agent does The agent gets a result that says the call is waiting for the owner, with the link and when the hold expires. Nothing has been sent to the tool. To get the answer, the agent retries **the same call**: the same tool, the same arguments, and the same `kiff_operation_id` if it used one. Through the [MCP gateway](/docs/mcp-gateway), an approved call is then forwarded once; a rejected or expired one is refused. A program can read the same facts in the result's `_meta` (`state`, `retry`, `retry_after_s`, `hold_expires_at`). ### Read the answer without executing the call The gateway's read-only `kiff_pending` tool lists this credential's held, sending and unknown calls. A held entry includes the owner's answer, the original tool arguments, and the `kiff_operation_id` used to collect it. Reading the list sends nothing to the business tool and uses no approval. Follow `next_cursor` as `cursor` to read older pages. Collect an approved call with the same tool and original arguments. Use its original operation id, or KIFF's listed id if the original call had none. The latter lets the agent collect that held call after the usual 10-minute identical-call window. An unknown outcome is for reconciliation: the gateway does not automatically resend it. ### Bring the answer back into Claude Code The optional **`kiff` plugin** reads `kiff_pending` while this session has a held call. It shows the owner's answer once and submits a turn telling the agent; the agent decides whether to collect an approved call. The plugin does not approve or execute the call itself. If permissions stopped the reads, allow `kiff_card` and `kiff_pending` and run `/kiff` again. Checks continue through older pages and preserve recovery when KIFF cannot read the owner's answer. An answer given in time still counts when read after an outage. Install steps and exact permission rules are in the [plugin guide](/docs/agent-skill#optional-card-status-notices-and-recovery-in-claude-code). ## Approval and the tool's outcome Approval records who authorized the request. It does not establish that the tool performed it. KIFF records the tool's reported outcome beside the authorization decision: - **Tool reports success:** the delivered call has a successful response. This is what the tool reported, rather than independent verification of the external action. - **Tool reports an error:** the call was authorized and delivered, but the tool returned an error. Do not describe it as done or assume that the error proves there were no partial effects. - **Outcome unknown:** the call was sent but its response was lost or timed out. Reconcile with the tool before deciding what to do next. - **Outcome not recorded:** an older call has no explicit outcome record. While its stored tool response is retained, KIFF uses that response; after it is purged, an approved call is shown with an outcome-not-recorded warning. Missing evidence does not establish success. For example, a person can approve a €25 refund and the provider can refuse it because only €20 remains refundable. The approval and the refusal both belong in the record. Recording the outcome does not itself restore the Card's budget. ## How long a call waits Each Card sets how long a held call waits: **10 minutes** by default, from 1 minute to 7 days. A held call never runs on silence: - if nobody answers in time, the call is refused and nothing is sent; - a late answer is not accepted; - the agent has to ask again, as a new call. The prompt in the chat does not extend this. An approval given before expiry remains valid when the agent collects it later; a late read is different from a late approval. ## Next - Set what is held and for how long → [KIFF Cards](/docs/kiff-cards). - What the agent sees, field by field → [Use the KIFF MCP gateway](/docs/mcp-gateway). - Install notices and recovery in Claude Code → [The KIFF skill for coding agents](/docs/agent-skill). ## Reusable operational domains Source: https://kiff.dev/docs/domains # Reusable operational domains A **domain** is the thing you build once with KIFF: the state, the valid actions, the authority and approvals, the outcomes, and the evidence that an agentic system would otherwise rebuild around every new agent. A domain is the operational contract your software runs against, and the unit you reuse. That is the apex idea. The decision, the controls, the cards you issue against it, the receipts, and Govern are all things a domain *has*. A [KIFF Card](/docs/kiff-cards) can only narrow what the domain already declares, which is why the domain comes first. Start here, and the rest of these pages are detail. ## What a domain holds A domain gathers, in one place, the operational facts about a slice of your business: - **State**, what counts as the current status of an entity (an invoice is `PENDING` or `PAID`; a refund is `REQUESTED` or `SETTLED`), derived from the events KIFF has seen. - **Actions**, the consequential operations that can be taken (`ISSUE_REFUND`, `CREATE_PAYOUT`, `PAY_INVOICE`) and the states each is allowed in. - **Authority and approvals**, who may propose what, what needs a human sign-off, and the thresholds that trigger it. - **Outcomes and evidence**, the terminal result of each proposal and the signed receipt that proves what was decided, before execution. A [KIFF Card](/docs/kiff-cards) is issued on top of a domain: it names actions the domain defines and narrows how much of them one agent may take. A Card can never grant an action the domain does not have, or allow it in a state the domain forbids. ## Reuse is the point The reason a domain is worth building is that it outlives any single agent. Once the operational contract exists, you can: - connect a new agent, or replace the one you have, without rewriting what "a legitimate refund" means; - put a human, a scheduled job, or a service through the same contract the agent acts under; - change the framework underneath without changing the contract above. You build the refund domain, the payout domain, the access-change domain once. The agents that act through them are interchangeable. That is the difference between governing an agent and owning an operational domain. ## How you build one A domain is a **`kiff.yaml`, a versioned artifact that lives in your own repository**, like a `docker-compose.yml`: committed, diffed, and reviewed in git alongside the code it governs. You don't hand-edit it in a browser form. You author it with your own coding agent, two on-ramps: - **Derive from what your agent already does.** Attach the guard to an agent you run, in observe mode, and it derives a **starter domain** from real traffic, the actions your agent actually calls, ready to review. This removes the blank page. See [connect an existing agent](/docs/connect-an-agent). - **Author it with your coding agent.** Install the [kiff-domains skill](/skills/kiff-domains.md) in Cursor, Claude Code, Codex, or Gemini and let your agent write and extend the `kiff.yaml` against the real grammar, states, actions, approvals, permissions, executors, then `kiff verify` it locally. You bring the finished `kiff.yaml` to KIFF Cloud to validate and activate it. The dashboard's **Build** view is a read-only render of the running version, the mirror of what you have deployed, not an editor. The source of truth stays in your repo. See [Build a domain with the framework](/docs/framework-quickstart) and [how kiff-guard works](/docs/concepts). ## The Cloud unit: a governed operation When you run a domain on KIFF Cloud, the thing that's counted is a **governed operation**: one unique authenticated proposal that KIFF evaluates against current state and the active domain contract and records with a terminal outcome. Idempotent retries of the same proposal do not create another governed operation, the duplicate that state refuses is not a second billed event. Domains, actions, agents, integrations, and development environments are **not metered, and there is no cap on how many you create**, build as many as you want. They expand what the platform is worth; only governed operations are counted. The model is usage-based, analogous to how Twilio meters messages or Stripe meters payments: what you pay for is a **continuous, verifiable evidence stream**, every governed operation records a signed, tamper-evident receipt, and spend stays predictable through **budgets and alerts**, not through limits on the objects you model. See the [pricing page](/pricing) for how Cloud operation is bought. ## Running more than one domain A workspace is not limited to one domain, and each domain has its own lifecycle. The **all-domains view** is the portfolio: one row per domain with its state, the freshness of the runtime backing it, its recent governed-operation count, and the single next action to take. - **Independent posture.** Governance posture (`observe` / `enforce`) is per domain, one domain can be enforcing while another is still observing in observe. You flip a domain's posture from its home page; it takes effect on the next decision. See [Govern](/docs/govern). - **Approvals belong to a domain.** An approval is recorded against the domain whose contract required it, and it can only satisfy an action in that domain. A key scoped to one domain sees that domain's approval queue and no other; a workspace-level identity sees all of them. Two domains that happen to share an action name stay separate, an approval granted under one never authorizes the other. - **Independent lifecycle.** A domain you're still building appears as a **draft**, in the switcher and the portfolio, while your other domains keep operating. Adding or drafting a domain never puts the whole workspace back into setup. - **Finish a draft with your agent.** A draft's home shows a status checklist (runtime observed → action derived → finish with your agent → activate in observe → first decision). The milestones reflect observed progress; the only deliberate actions are finishing the contract with your coding agent and activating the verified `kiff.yaml`. ## Honest boundaries - A domain is **yours**. KIFF decides whether a proposed action conforms to the contract you declared; it does not invent authority you didn't set, and it never becomes your executor. - Reuse is a property you build toward, not a marketplace claim. KIFF does not yet offer a public exchange of domains authored by other customers; reuse here means reuse across your own agents, humans, and services. ## Next - The mechanism a domain runs on → [What KIFF is](/docs/what-kiff-is). - Issue an agent what it may do → [KIFF Cards](/docs/kiff-cards). - Build one locally with the open-source framework → [Build a domain with the framework](/docs/framework-quickstart). - See what a decision proves → [Receipts & proof packs](/docs/receipts-and-proof). ## Build a domain with the framework Source: https://kiff.dev/docs/framework-quickstart # Build a domain with the framework The open-source KIFF framework (MIT, Go) is what a Card stands on: the domain that says which actions exist and in which states they are allowed. This page builds one locally. To give an agent a Card in KIFF Cloud without writing a domain, start with the [quickstart](/docs/quickstart) instead. Pick the action you'd never let an agent take unsupervised, a refund - and ship a governed version of it. You scaffold a runnable refund domain, run it, and watch KIFF let the good refund through and hold the risky one for a human, **before any money moves**. No account, no cloud, all local. One honest note before you start, because it decides whether this is worth your five minutes. The demo also refuses a repeat refund on an already-refunded order, and that part is table stakes: a unique constraint does it, and any competent tool implementation already does it. It is in the demo because it shows KIFF reading state, not because it is the reason to use one. The reason is [KIFF Cards](/docs/kiff-cards), the instrument that bounds what an agent does in total, which no per-call check can produce, and that is the hosted part today. This is the fast path: install → scaffold → run → read the decisions. KIFF is an MIT Go framework; this quickstart runs entirely on your machine. [KIFF Cloud](/docs/quickstart) is the hosted way to run the same contract later, optional, not required here. ## 1. Install and scaffold ```bash go install github.com/kiff/kiff/cmd/kiff@latest kiff new -scenario refund github.com/acme/refunds cd refunds go mod tidy make demo ``` `kiff new -scenario refund` generates a runnable refund domain: an `Order` entity, a `MARK_PAID` action, an approval-gated `REFUND_ORDER` action, a headless HTTP API, and a demo script. `make demo` runs it end to end. > Building against a local checkout of the framework? Add > `-replace-local /path/to/kiff`. Want a smaller starter without the > refund scenario? `kiff new github.com/acme/thing`. ## 2. The useful action runs `make demo` marks an order paid, then has the agent propose a refund on it. The refund is a real, consequential action, and it runs: ```text order-2 PAID agent → REFUND_ORDER $12.00 KIFF ✓ ALLOWED, the contract is satisfied → your executor refunds → REFUNDED ``` This is the point of KIFF: the agent does the work you'd otherwise keep it away from, because KIFF checked the action against the order's state and your policy first. KIFF returns the decision; **your code executes** the refund on `allowed`. ## 3. The risky path waits; the repeat is refused The same domain makes the dangerous cases safe without you writing defensive glue: ```text agent → REFUND_ORDER $999.00, high-risk KIFF ⏸ HELD, approval required (the executor did NOT run) human → approves KIFF ✓ ALLOWED, refund executes → REFUNDED agent → REFUND_ORDER (same order, retried) KIFF ✗ BLOCKED, order is already REFUNDED (refused before money moves again) ``` A high-risk refund holds for a human; a repeat on an already-refunded order is refused. The agent proposing the wrong thing is normal, the decision in front of it is what makes proposing it safe. Both refusals here are the same refusal: the entity is not in a state that allows the action. That is worth having and it is not worth buying, because your database and your tool code can both do it. What neither can do is answer what the agent has done *in total*, because each of them only ever sees one call. ## 4. It replays, and it's on the record Every decision and execution is protocol data, not an optional log. The entity's state rebuilds from the stored events alone: ```text replay from events → REFUNDED materialized == replayed ``` That replay, plus the recorded decisions and approvals, is the proof of what happened and why. You can verify it yourself; it isn't a number on someone's dashboard. ## What you just shipped, and what's next You put an agent on a refund and let it run, with the risky path routed to a human by a decision outside the agent, in five minutes, on your machine. - **Understand the decision** → [How a decision is made](/docs/concepts): events, state, typed decisions, approvals, and the no-self-approval rule. - **Already have an agent?** [Connect an agent](/docs/connect-an-agent): through the MCP gateway, kiff-guard, or the decision API. - **Give an agent a Card** → the [quickstart](/docs/quickstart). - **Go deeper** → [Reusable operational domains](/docs/domains) is the thing you build once and connect any agent to. The framework and every scaffold live in the public MIT repo, [`kiff/kiff`](https://github.com/kiff/kiff). docs.kiff.dev owns the guided journey; the repo owns the code. ## Decisions Source: https://kiff.dev/docs/decisions # Decisions A decision is KIFF's answer to "is this action allowed, right now?" - produced **before** the action runs. This page covers what's evaluated, the four outcomes, and the lifecycle from proposal to recorded decision. ## What this does Your runtime proposes an action. KIFF evaluates that proposal against the entity's current state and the control's policy, and returns a decision. Your runtime then acts on the answer: run the action on `allowed`, withhold it otherwise. The decision happens at the gate, not after the fact. ## The decision flow ``` your runtime: about to run an action │ ▼ propose to KIFF ──► KIFF evaluates: │ • entity's current state (from events) │ • the control's policy + thresholds │ • required parameters present? │ • does the actor's identity hold authority? │ • does this action need an approval? │ • and only if the action declares │ requires_authority: the card issued to │ this actor, and what it has left ▼ decision returned: allowed | blocked | approval_required | invalid │ ▼ your runtime acts on it: allowed → run the action blocked / invalid → do not run approval_required → hold until a human resolves it │ ▼ one signed receipt recorded, either way ``` ## The card, where an action calls for one The card is the last gate, and only for actions that ask for it. An action that does not declare `requires_authority` is decided without any card at all, state, permissions, parameters and approval, as before. Where it does declare it: - **`no_mandate_issued`**, nothing has been issued to this actor for this action. A `blocked` decision with its own reason, because the integrator who meets it has wired everything correctly and the answer is that nobody has issued anything yet. - **`key_not_bound_to_agent`**, the action requires a card, or the named agent holds a card covering it, and the API key that sent it is not issued for any agent, so nothing ties the `actor_id` it named to the card's holder. Use the key created for the agent on its card's page. - **`actor_not_key_holder`**, the API key is issued for one agent and the proposal names another. Checked on every action, not only those that need a card: a key issued for an agent only ever acts as it. - **`mandate_limit_reached`**, the proposed action would exceed the card's remaining authority for this window. Note what that does *not* say: the card need not be empty. A €2,000 refund against €1,000 of remaining authority is refused under this reason while €1,000 is still there for something smaller. Refused, not sent for approval: more room is an owner's decision, taken deliberately and recorded. - **The draw is recorded when the action is authorized**, not when your runtime executes it. KIFF never learns whether you ran it, so an authorization you chose not to act on still counts. See [KIFF Cards](/docs/kiff-cards). ## The four outcomes - **`allowed`**, the proposal satisfies the policy for the current state. Your runtime runs the action. - **`blocked`**, a governance answer: the proposal is well-formed, but the action is not permitted in this state or breaks a rule (over a threshold, wrong state, missing authority). KIFF evaluated it and said no. Your runtime does not run it. - **`approval_required`**, the action needs a human sign-off first. It is held; your runtime does not run it until the approval is resolved. - **`invalid`**, the proposal is a **broken request**, not a governance answer: it is malformed or misconfigured against the contract, a required parameter is missing, a parameter fails the contract's constraints, the contract is invalid, or it has no executor. There was nothing to decide; the request itself needs fixing. A useful invariant: anything that is not exactly `allowed` means the action should not run. When KIFF runs in enforce mode through the guard, that withholding is the default behavior, and an error fails closed (withheld) rather than silently allowing. See [How the guard works](/docs/concepts) for the fail-safe posture. ### `blocked` vs `invalid`, a decision vs a broken request The two are distinct on purpose, and your integration should treat them differently: - **`blocked` is a governance decision.** KIFF looked at a valid request and refused it. It returns successfully (the decision *is* the result), counts as a governed operation, and surfaces in your evidence and alerts, it's a real thing that happened. - **`invalid` is not a governance decision.** The request never got as far as one, either it was malformed/misconfigured, or the data plane couldn't evaluate it right then. Either way it comes back as an error (not a success), is **not** counted as a governed operation, and does not raise a governance alert. Two sub-cases, and they call for different handling: - **Malformed or misconfigured** (missing/invalid parameter, invalid contract, no executor) → a client/config error (an HTTP `4xx`) that is yours to **fix**, the same way a bad request to any API is the caller's to correct, not a policy event. - **Transient data-plane error** (the runtime, state store, or action registry was briefly unavailable) → returned as an HTTP `5xx` you should **retry**, not fix; nothing was wrong with your request. So a missing or invalid parameter is `invalid` (fix your request), not `blocked` (a policy said no). Branch on this: on `blocked`, respect the decision; on `invalid`, **fix** a `4xx` or **retry** a `5xx`. ## What data is evaluated A decision is made against: - **the entity's state** at the moment of the proposal, derived from the events KIFF has recorded, not a value the caller asserts; - **the control's policy**, permitted states, required parameters, thresholds, approval rules, which you defined; - **the actor's identity**, resolved server-side. The roles a request carries are descriptive; authority is resolved from policy keyed to an authenticated identity, never from what the caller claims about itself. An action taken with an API key is attributed to **that key**, not to whoever minted it, so give each agent its own key and the record distinguishes them; share one key across a fleet and they appear as a single actor. ## The proposal / decision lifecycle 1. **Proposal**, your runtime submits the action it intends to take, with the entity and parameters. 2. **Evaluation**, KIFF reads the current state and applies the policy. 3. **Decision**, one of the four outcomes is returned, synchronously, before execution. 4. **Hold (if `approval_required`)**, the proposal waits for a human resolution rather than running. 5. **Record**, exactly one signed receipt captures what was decided and on what basis. See [Receipts & proof packs](/docs/receipts-and-proof). ### Reading an entity's lifecycle The proposals, validations, approvals, and executions KIFF has recorded for a single entity can be read back as one chronological, read-only view, the entity's governed history in order, assembled from records KIFF already keeps. It's a projection, not a new decision path: it changes nothing and simply surfaces what happened. Like every read surface it is scoped to your tenant, you only ever see your own entities, and a valid entity with no history yet returns an empty lifecycle rather than an error. ## Example shape The exact request/response contract for the decide endpoint and the guard's `enforce` mode lives with the runnable code in the public guard repo, [`kiff/kiff-guard`](https://github.com/kiff/kiff-guard), and on each [framework page](/docs/agno). Conceptually, your runtime sends the action and entity, and receives a decision with an outcome and a trace reference for the receipt: ```text proposal: action=ISSUE_REFUND entity=order/ord_4821 amount_cents=2500 decision: outcome=blocked reason=state_not_allowed (order already FULLY_REFUNDED) → one receipt recorded; your runtime does not issue the refund ``` ## What KIFF does not do here KIFF returns the decision. It does not execute the action on `allowed` - **your runtime does**, and it does not undo anything downstream. The decision proves what was authorized before execution; it does not prove what your system did afterward. That distinction is the heart of [Receipts & proof packs](/docs/receipts-and-proof). ## Receipts & proof packs Source: https://kiff.dev/docs/receipts-and-proof # Receipts & proof packs Every decision KIFF makes leaves a **receipt**: a signed record of what was authorized, on what basis, before the action ran. A **proof pack** is the exportable bundle of that evidence for one control over a window. This page is precise about what a receipt proves, and, just as important, what it does not. ## What this does When KIFF decides on a proposed action, it records one receipt capturing the decision: the action, the entity and its state at that moment, the outcome, and the policy that was checked. The receipt is signed and tamper-evident, so the record of the decision can't be quietly altered after the fact. ## What a receipt proves A decision receipt proves **pre-execution authorization**: that, at a specific moment, this action on this entity was evaluated against the recorded state and your declared policy, and a specific decision - `allowed`, `blocked`, `approval_required`, or `invalid`, was reached *before* the action ran. It is verifiable and tamper-evident. In plain terms: the receipt is proof of *what was authorized and on what basis*, made at the gate. ## What a receipt does not prove This limit matters, and overstating it would be dishonest: - A receipt does **not** prove the action executed. KIFF is the decision point, not the runner, your system executes `allowed` actions, and what happened downstream is outside the receipt. - A receipt does **not** prove the outcome of the action (that the money actually moved, that the email was delivered). - A receipt does **not** assert reviewer attribution beyond what the product records; approval-to-control linkage is limited today (see [What's real today](/docs/whats-real-today)). So a receipt answers "was this authorized, before it ran, against what state and policy?", not "what did the system do after?" Those are different questions, and conflating them is exactly the trap honest evidence avoids. ## Why "before" is the whole point Most systems log what happened after the fact, in a store the acting system controls. That's an account, not proof, the actor wrote it, and it can be edited. A decision receipt is different on all three counts: it's made **before** the action, by KIFF rather than the actor, in a **tamper-evident** form. That's what makes it hold up when someone later asks you to show what was authorized. ## Proof packs A proof pack is the exportable form of this evidence for one Protected Control over a time window. The on-screen [Govern](/docs/govern) view and the exported proof pack are built from the **same evidence object**, so what you see and what you hand someone else cannot diverge. A proof pack covers what the evidence object can truthfully show for the window: the control's identity, the decisions by outcome, the signed receipts and their verifiability, and the posture (observe vs enforce). It carries an honesty disclaimer about scope, a sampled "all verified" never reads as a complete-window guarantee. ## Verifiability and tamper-evidence Receipts are signed, which is what makes them tamper-evident: a change to the recorded decision breaks the signature. Receipts can optionally be anchored to an external trail for independent verifiability; the production form of that anchoring is not shipped, so the honest claim today is **signed, tamper-evident, verifiable**, not more. See [What's real today](/docs/whats-real-today) for the exact line. ![A receipt detail in the dashboard: the decision it records and its signature status, shown as Signed.](/static/img/docs/receipt-detail.png) *A receipt detail from a clean demo tenant, the decision it records and its signature status (Signed.).* ![A proof-pack export: the signed evidence bundle for one control over a window, with the scope/honesty disclaimer.](/static/img/docs/proof-pack-export.png) *The proof-pack export for one control, the signed decisions and receipts in scope, led by the honesty disclaimer about what it does and does not assert.* ## Next - See receipts in context → [Govern](/docs/govern). - Understand what produced them → [Decisions](/docs/decisions). ## Govern Source: https://kiff.dev/docs/govern # Govern Govern is the evidence view for company accounts: where you watch the decisions on one action in a domain land on real traffic, the decisions it made, the receipts it produced, and the posture it's in. It's the evidence that lets you keep an agent on a consequential action and still answer "what happened, and why?", the on-screen view of the same records a [proof pack](/docs/receipts-and-proof) exports. ## What this does For one governed action over a time window, Govern shows, summary-first: - **Identity**, the action, entity type, environment, and current mode. - **Posture**, whether the control is in `observe` or `enforce`, and when that last changed. - **Decisions by outcome**, counts of `allowed`, `blocked`, `approval_required`, `invalid`, with a drill-down into individual decisions. - **Receipts**, the signed records, with their signature and verifiability status. - **Completeness**, whether the window shown is complete or truncated, so a partial view never reads as the whole story. It reads top-down: a one-line summary of the control's state, then detail on demand. ## Observe vs enforce A domain runs in one of two postures, and Govern is honest about which: - **`observe`** (advisory-only), KIFF evaluates every action and records what it *would* have decided, but does not withhold anything; your runtime proceeds as usual. This is the on-ramp: watch real decisions on real traffic before you turn the gate on. - **`enforce`**, KIFF's decision is acted on: `allowed` runs, everything else is withheld by your runtime. Observe always reads as *would-have-decided*, never as production enforcement. The point of observe is to build trust before you flip to enforce, and the moment you flipped is itself recorded. Posture is **per domain**, each domain carries its own, so one can be enforcing while another is still in observe. You flip it from the domain's home page. Changing enforcement is a management action: a runtime key can propose and be governed, but it cannot flip the posture of the domain it runs under. A personal account doesn't show Govern: its Cards, Needs you and Activity cover the same decisions for each agent. ## Honest by design Govern is built to state what it can and cannot show. Every section renders a distinct **recorded**, **empty**, or **not-yet-recorded** state rather than implying completeness: - An action with no traffic yet shows an honest empty state, not a staged success. - A section whose data isn't recorded yet says so plainly. - Verifiability is scope-aware, a sampled result never presents itself as a complete-window guarantee. This matters because most dashboards imply more than they know. Govern's value depends on the opposite: if it says something happened, it happened, and if it can't show something, it says so. ## What it does not show - It does not show that an action executed downstream, only what KIFF decided. KIFF is the decision, not the runner. - It does not present demo or synthetic data as real customer evidence. - It does not claim reviewer attribution the product doesn't record yet (see [What's real today](/docs/whats-real-today)). ## Next - What the receipts here prove → [Receipts & proof packs](/docs/receipts-and-proof). - The full honesty line → [What's real today](/docs/whats-real-today). ## Tools that check KIFF: no key at KIFF Source: https://kiff.dev/docs/tool-permits # Tools that check KIFF: no key at KIFF By default, a tool you connect to the [KIFF MCP gateway](/docs/mcp-gateway) gives KIFF a credential. KIFF stores it encrypted and sends it with each allowed call. That works, but KIFF then holds a key that could act on the tool without the agent. A connection can instead say who holds that key. There are three choices: | Mode | Who holds the tool's key | What KIFF sends the tool | |---|---|---| | `sealed` (the default) | KIFF, stored encrypted | the call and the stored credential | | `verify` | nobody: the tool checks KIFF itself | the call and an **execution permit** | | `relay` | your relay, from your own vault | the call and an execution permit | The mode is set per connection and shown per connection under **Tools**. An account with any `sealed` connection still gives KIFF that connection's key. ## The execution permit For each call the agent's Card allows, KIFF signs a short-lived permit and sends it in the `KIFF-Permit` header of the `tools/call` request. The permit names: - the KIFF account, and the connection's audience (an identifier you choose, such as the tool's URL); - the tool, and a hash of the exact arguments sent ([RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) canonical JSON, then SHA-256); - the operation id, which KIFF also passes as the tool's idempotency argument; - the agent, the Card that allowed the call, and the decision. It is a JWT signed with Ed25519 (`alg: Ed25519`, `typ: kiff-permit+jwt`). It lives 60 seconds, and KIFF issues at most one per operation. A held call gets its permit when the owner approves it and the agent retries, so a long hold never produces an expired permit. KIFF publishes its verification keys at `https://api.kiff.dev/.well-known/kiff-permit-keys.json`. ## Verify mode: the tool checks the permit Use this for a tool you run. Install the verifier and call it before the tool does anything: ```bash pip install "kiff-guard[verifier]" ``` ```python from kiff_guard.permit import JWKSKeys, SQLiteStore, Verifier verifier = Verifier( issuer="https://api.kiff.dev", audience="https://refunds.example.com/mcp", # the connection's audience tenant="", keys=JWKSKeys(), store=SQLiteStore("/var/lib/refunds/permits.db"), ) outcome = verifier.run(headers["KIFF-Permit"], "refund_order", arguments, adapter) ``` `adapter` makes the one downstream call (`execute`) and finds its result again (`lookup`). [`kiff_guard.permit.stripe.StripeRefunds`](https://github.com/kiff/kiff-guard) is the reference adapter for Stripe refunds. The verifier refuses the call unless all of these hold: - the permit is signed by a key KIFF currently publishes, with `alg: Ed25519` and `typ: kiff-permit+jwt`; - it was issued by `https://api.kiff.dev`, for this audience and this account; - it names this tool, and its arguments hash equals the hash of the arguments received; - its lifetime is at most 60 seconds, and it was not issued in the future; - for a new operation, it has not expired (with 30 seconds of clock tolerance, so at most 90 seconds after KIFF's decision). Then it runs the operation **at most once**: - The operation is claimed in a durable store before anything is sent, and the permit is checked again just before sending. - A second presentation, a retry or a concurrent request for the same operation gets the recorded result. It is never sent a second time. - If the process dies or the answer is lost, the verifier never resends. It looks the result up with the adapter's `lookup`. What it cannot settle is marked `unknown` and listed by `verifier.unknown()` for a person to check. Run `verifier.sweep(...)` on start-up and periodically, because KIFF does not send an operation whose answer was lost again. You can add your own limits with `policy=`. A permit never widens what your tool allows. ## Relay mode: your relay holds the key Use this for a service that will not check KIFF's permit (Stripe, Zendesk). Run `kiff_guard.permit.relay.Relay` next to your vault. It verifies the permit exactly as above, makes the one downstream call it is configured for with the key from your environment, and returns the result. It serves only the tools you list; it is not a proxy. KIFF reaches the relay with a transport credential (a bearer token; mTLS in front of it is recommended). KIFF holds that credential. On its own it runs nothing, because every call also needs a valid permit. ## Moving a connection Under **Tools**, choose **Change who holds the key** on the connection. Over the API: ```bash curl -s -X PUT https://mcp.kiff.dev/v1/connections/refund_order/mode \ -H "Authorization: Bearer $OWNER_KEY" -H "Content-Type: application/json" \ -d '{"mode": "verify", "audience": "https://refunds.example.com/mcp"}' ``` KIFF reaches the tool the new way before anything changes. The tool name and audience go into the permit from this connection, set by an owner or admin, never from the call itself, and the amount the Card checks is the one in the arguments the permit signs. Moving to `verify` deletes the credential KIFF held, in the same change. Moving back to `sealed` needs the credential again. A `verify` or `relay` connection needs an idempotency argument, because the permit binds the operation id the tool de-duplicates on. ## What does not change - **KIFF still decides.** Whoever holds KIFF's signing key could authorize calls until verifiers stop trusting it. Verifiers refetch the keys at least every five minutes, and stop trusting a removed key at the next fetch. A key set that cannot be refreshed for an hour fails closed. A pinned key set (`PinnedKeys`, for offline use) expires with each key, and `distrust=[...]` refuses a key whatever KIFF publishes. - **The gateway still sees the call.** Tool names and arguments reach KIFF, because the Card decides on them. The tool's response still returns to the agent through the gateway and is purged after 24 hours. What leaves KIFF is the downstream key, and only that. - **A compromised gateway can still ask for calls.** It carries each agent's own key to KIFF, so within each agent's Card it could ask for calls the agent did not make. Each such call is still a Card decision with a receipt, and your verifier's `policy=` still applies. - **Revoking a Card stops new permits at once.** A permit already issued can still start its call for up to 90 seconds. ## What's real today vs planned Source: https://kiff.dev/docs/whats-real-today # What's real today vs planned These docs describe what KIFF does, not what it might do. This page is the explicit line between the two, so you never build on a claim the product can't back yet. If something isn't here as shipped, treat it as not shipped. ## Real today - **The decision.** KIFF evaluates a proposed action against an entity's state and your policy and returns `allowed`, `blocked`, or `approval_required`, before the action runs. A malformed or misconfigured request is `invalid` (a client error to fix, not a governance decision, and not a governed operation). See [Decisions](/docs/decisions). - **Read an entity's lifecycle.** The recorded proposals, approvals, and executions for an entity can be read back as one chronological, tenant-scoped, read-only view. See [Decisions](/docs/decisions). - **Observe mode, zero config.** Attach the guard to an agent you already run, with no account and no config, and get a real audit trail plus a derived starter policy. See [Connect an agent](/docs/connect-an-agent). - **Enforce mode through the guard.** Switch the guard to enforce, point it at a tenant, and KIFF's decision is acted on before each tool runs, failing closed on error. See [How the guard works](/docs/concepts). - **Shipped framework adapters.** Eleven kiff-guard adapters are verified against the real framework and pass the conformance suite. The sidebar badges say so per framework. - **KIFF Cards, with three honest limits.** You can issue a card to a named agent, read its statement, change its ceiling and revoke it. The three things to know before building on it: - **A card constrains; it never confers.** It narrows what the domain and its permissions already allow and caps how much of it happens. It cannot grant an action the domain does not have, or legalise one in a state it forbids. - **It is only required where the action says so.** An action that declares `requires_authority` is refused with `no_mandate_issued` when nothing has been issued to the proposing actor. Every other action is decided without a card, exactly as before. - **The key has to be issued for the holder.** Wherever a proposal would draw on a card, an API key not issued for the proposing agent is refused, so one agent cannot name another and spend its card. - **A draw is recorded when the action is authorized, not when it executes.** KIFF decides; your system runs the action afterwards and reports nothing back, so an authorization you chose not to run still counts against the window. See [KIFF Cards](/docs/kiff-cards). - **MCP gateway for remote tools.** An agent can connect to `mcp.kiff.dev`; the account owner connects a remote MCP server to KIFF, and the gateway checks the agent's Card before forwarding each tool call. Claude Code and Codex passed production smoke tests with a temporary tool: allowed calls reached it, held calls waited for owner approval, and retries of an approved call reached it once. This tested the gateway path, not a customer transaction or every MCP client. See [Use the KIFF MCP gateway](/docs/mcp-gateway). - **Held calls and approvals.** A call outside its Card can be held for the owner: listed on Needs you, emailed once to the owner, and, for Claude Code 2.1.287 or later, offered as a link in the agent's chat. Only the owner's signed-in session answers. Each Card sets how long a hold waits (10 minutes by default); an unanswered hold is refused. See [Held calls and approvals](/docs/approvals). - **Personal and company accounts.** An account is "for me" (Cards, agents, tools, Needs you, Activity) or "for a company" (the full workspace with domains, contracts and API keys). It changes what is shown, not what KIFF decides. - **Signed, tamper-evident receipts.** Every decision records one signed receipt of what was authorized, before execution. See [Receipts & proof packs](/docs/receipts-and-proof). - **Govern + proof packs.** For company accounts, an evidence view per action in a domain on your real traffic, exportable as a proof pack from the same evidence object, with honest empty / not-yet-recorded states. See [Govern](/docs/govern). - **Per-domain observe → enforce posture.** Each domain carries its own governance posture, flippable from its home page, one domain can enforce while another observes, and the moment of the flip is recorded. Changing enforcement is a management action. See [Govern](/docs/govern). - **Many domains in one workspace.** Run more than one operational domain side by side. The all-domains view shows each domain's posture, runtime freshness, recent activity, and its one next action; an in-progress domain appears as a draft while the others keep operating. See [Domains](/docs/domains). ## Not shipped, do not assume - **Production anchoring.** Receipts are signed and tamper-evident, and can be anchored to an external trail, but the production form of public anchoring is not shipped. The honest claim is *signed, tamper-evident, verifiable*, nothing stronger. - **Reviewer attribution as a guarantee.** The full linkage between an approval and the specific control decision is limited; where it isn't recorded, the evidence says "not yet recorded" rather than inventing it. Don't rely on reviewer-attribution claims. - **Compliance certification.** KIFF produces evidence that's useful in a compliance conversation, but it is not a certification and does not assert legal sufficiency for any regime. - **KIFF as the agent or business operator.** KIFF does not run your agent or perform the connected tool's business operation. In direct API integrations, your runtime executes after an `allowed` decision. Through the MCP gateway, KIFF stores connected remote MCP bearer credentials encrypted and forwards calls after an `allowed` decision; the remote tool performs the operation. A connection in `verify` or `relay` mode gives KIFF no key: the tool, or a relay you run, checks a permit KIFF signs for each allowed call ([Tools that check KIFF](/docs/tool-permits)). OAuth and arbitrary HTTP APIs are not supported by the gateway yet. ## How we keep these docs honest Two rules govern this page and the rest of the docs: 1. **Copy never outruns the artifact.** A capability is described as real only if it's shipped and verifiable. If it isn't, it lives in the "not shipped" list above until it is. 2. **No demo data presented as real.** Screenshots and examples in these docs are either clearly labelled as examples or drawn from a clean, safe tenant, never synthetic data dressed up as production evidence. If you find a claim in these docs that contradicts this page, this page wins, and it's a bug in the other page. Tell us. ## Next - Start using it → [Quickstart](/docs/quickstart). - Understand the model → [What KIFF is](/docs/what-kiff-is). ## How kiff-guard works Source: https://kiff.dev/docs/concepts # How kiff-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. In enforce mode, each tool call is decided against the agent's [Card](/docs/kiff-cards) and the domain before it runs. ## 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](/docs/connect-an-agent): 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. Guard carries the proposal to the decide path; it does not decide. Where the action declares `requires_authority`, the [KIFF Card](/docs/kiff-cards) issued to the proposing actor is an additional gate on that decision, after state, permissions and parameters, and the call is refused with `no_mandate_issued` if none has been issued. Guard neither issues nor administers cards: issuing is a management action, and a runtime credential cannot grant itself authority. 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](/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: ```python 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: ```python 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`](https://github.com/kiff/kiff-guard). Each framework page links out to the adapter and its example; this site never duplicates the code. ## Agno Source: https://kiff.dev/docs/agno # Agno > **How this fits.** kiff-guard checks each tool call this Agno agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the Agno hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** Agno was the first adapter, and its seam was exercised live against the cloud decide endpoint ([kiff-cloud#242](https://github.com/kiff/kiff-cloud/issues/242)). **Shape: middleware.** Agno's `tool_hooks` hand the guard a continuation that runs the tool, so the guard runs it via `Guard.evaluate(run=...)`. See [how the guard works](/docs/concepts) for what that means. ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" # plus Agno itself, the framework you're guarding: pip install agno ``` ## Observe, audit with zero config ```python from agno.agent import Agent from kiff_guard import Guard from kiff_guard.adapters.agno import agno_hook guard = Guard(mode="observe") # no client, no tenant needed agent = Agent( model=..., tools=[refund_order, send_email], tool_hooks=[agno_hook(guard)], ) ``` Run the agent as usual, then read the trail: ```python for r in guard.receipts: print(r.state, r.tool, r.outcome) ``` `agno_hook` is one line of real logic, it matches Agno's hook signature and calls the guard: ```python def hook(function_name, func, args): return guard.evaluate(function_name, args, run=lambda: func(**args)) ``` ## Activate Derive a starter domain from what you observed, review it, and activate it in the [dashboard](/dashboard): ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ## Enforce, govern at runtime Once you have a tenant and an active domain, point the guard at KIFF and switch to `enforce`. Nothing about the agent wiring changes, only the guard: ```python from kiff_guard import Guard, HTTPClient, ToolMap from kiff_guard.adapters.agno import agno_hook client = HTTPClient( api_key="kiff_live_...", # mint in the dashboard tool_map=ToolMap().bind( "refund_order", action="REFUND_ORDER", entity_type="Order", entity_arg="order_id"), ) guard = Guard(client=client, tenant="", agent="support", mode="enforce") agent = Agent(model=..., tools=[refund_order], tool_hooks=[agno_hook(guard)]) ``` In enforce mode a withheld decision raises `kiff_guard.Hold`; the tool never runs. The host app maps `Hold` onto Agno's human-in-the-loop / approval flow if it wants the agent to see why. ## The seam (verified) Agno invokes each tool hook as middleware: `hook(function_name, func, args)`. The hook calls `func(**args)` to let the tool proceed, or skips it to block. That maps directly onto the guard's `run` continuation. ## Links - Adapter code: [`kiff_guard/adapters/agno.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/agno.py) - Seam research: [agno.md](https://github.com/kiff/kiff-cloud/blob/main/docs/integration/frameworks/agno.md) - Agno docs: [docs.agno.com](https://docs.agno.com/) (Hooks → Tool Hooks) ## LangGraph / LangChain Source: https://kiff.dev/docs/langgraph # LangGraph / LangChain > **How this fits.** kiff-guard checks each tool call this LangGraph agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the LangGraph hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) and passes the conformance suite. The seam was checked against the current LangChain middleware API, including the built-in `ShellAllowListMiddleware` that uses the same block pattern. **Shape: middleware.** The seam is `wrap_tool_call`, the guard runs the tool via the `handler` continuation, so `Guard.evaluate(run=...)` fits directly. (Not `interrupt()`; see the note below.) ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" pip install langgraph langchain ``` ## Observe, audit with zero config ```python from langchain.agents import create_agent from langchain.agents.middleware import wrap_tool_call from kiff_guard import Guard from kiff_guard.adapters.langgraph import kiff_wrap_tool_call guard = Guard(mode="observe") # zero-config audit kiff_mw = wrap_tool_call(kiff_wrap_tool_call(guard)) agent = create_agent(model="...", tools=[...], middleware=[kiff_mw]) ``` Run the agent, then read the trail from `guard.receipts`. ## Activate ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` Review the draft and activate it in the [dashboard](/dashboard). ## Enforce, govern at runtime ```python from kiff_guard import Guard, HTTPClient, ToolMap client = HTTPClient(api_key="kiff_live_...", tool_map=ToolMap().bind(...)) guard = Guard(client=client, tenant="", agent="support", mode="enforce") kiff_mw = wrap_tool_call(kiff_wrap_tool_call(guard)) agent = create_agent(model="...", tools=[...], middleware=[kiff_mw]) ``` On a withheld decision the middleware returns a `ToolMessage` (`status="error"`) carrying the reason **without** calling the handler - the tool never runs, and the model sees the message as the tool result. This is exactly the short-circuit LangChain's own `ShellAllowListMiddleware` uses to reject disallowed shell commands. ## The seam (verified) `wrap_tool_call(request, handler) -> ToolMessage | Command`. `request.tool_call["name"] / ["args"] / ["id"]` carry the call. Calling `handler(request)` runs the tool; returning a `ToolMessage` without calling it blocks. `langchain` is imported lazily, so `import kiff_guard` never requires it. ## Why not `interrupt()` `interrupt()` pauses the graph and persists state for a **human** to resume, the right tool for a human approval pause, but heavyweight and asynchronous, not a synchronous "is this tool allowed right now" gate. KIFF's gate is a machine decision that returns in milliseconds, so it belongs in `wrap_tool_call`. When a withheld decision *is* an `approval_required`, the host app can bridge it into an `interrupt()` on top of the gate. ## Links - Adapter code: [`kiff_guard/adapters/langgraph.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/langgraph.py) - Seam research: [langgraph.md](https://github.com/kiff/kiff-cloud/blob/main/docs/integration/frameworks/langgraph.md) - LangChain middleware: [docs.langchain.com](https://docs.langchain.com/oss/python/langchain/middleware/custom) ## Hermes Agent (Nous) Source: https://kiff.dev/docs/hermes # Hermes Agent (Nous Research) > **How this fits.** kiff-guard checks each tool call this Hermes agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the Hermes hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) and passes the conformance suite. The seam was source-verified against the `NousResearch/hermes-agent` source on `main`, not just the docs, and a doc-vs-source discrepancy was resolved in favor of the source (see below). **Shape: inverted-control.** Hermes runs the tool after the hook returns; the hook only votes. So the adapter uses `Guard.observe()` / `Guard.decide_only()`, not a `run` callback. See [how the guard works](/docs/concepts). Hermes is a CLI-first agent that takes real, irreversible actions (terminal, code execution, file writes, messaging gateways), exactly the "actor takes consequential action" profile KIFF governs. ## Install The guard ships to Hermes as a **plugin**, a directory with `plugin.yaml` + `__init__.py` exposing `register(ctx)`. No fork, no monkeypatch. ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" ``` Then create the plugin at `~/.hermes/plugins/kiff-guard/`. ## Observe, audit with zero config `__init__.py`: ```python from kiff_guard import Guard from kiff_guard.adapters.hermes import register_kiff_guard _GUARD = Guard(mode="observe") # zero-config audit; no KIFF account def register(ctx): register_kiff_guard(ctx, _GUARD) ``` The hook fires before every tool (terminal, browser, code-exec, file writes, MCP) in both the CLI and the gateway, records a receipt, and never blocks. ## Activate ```python from kiff_guard import export_yaml print(export_yaml("my-domain", _GUARD.catalog)) ``` Review and activate in the [dashboard](/dashboard). ## Enforce, govern at runtime Build the guard with a client and `mode="enforce"`: ```python from kiff_guard import Guard, HTTPClient, ToolMap client = HTTPClient(api_key="kiff_live_...", tool_map=ToolMap().bind(...)) _GUARD = Guard(client=client, tenant="", agent="hermes", mode="enforce") ``` On a withheld decision the adapter returns Hermes' `{"action": "block", "message": ...}` directive, Hermes short-circuits the tool and hands `message` to the model as the tool's error result. The tool never runs. Enforce **fails closed** on a guard error by default; pass `fail_closed=False` to override. ## The seam (verified) The pre-execution seam is the **`pre_tool_call`** plugin hook. In source (`model_tools.py` → `handle_function_call`), Hermes calls `get_pre_tool_call_block_message(...)` *before* dispatch; a non-None block result short-circuits the tool. The hook fires exactly once per tool execution and the first block wins. > **Doc-vs-source discrepancy (resolved).** Hermes' plugin-build guide > lists `pre_tool_call`'s return as "ignored" (observer-only), but the > hooks reference says it can block, and the **source confirms the > hooks reference is correct**. The block directive is supported and > stable. KIFF complements Hermes' own `DANGEROUS_PATTERNS` local check > by adding the tenant-wide policy + audit. ## Links - Adapter code: [`kiff_guard/adapters/hermes.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/hermes.py) - Seam research (full, source-verified): [hermes.md](https://github.com/kiff/kiff-cloud/blob/main/docs/integration/frameworks/hermes.md) - Hermes docs: [hermes-agent.nousresearch.com](https://hermes-agent.nousresearch.com/docs) ## OpenAI Agents SDK Source: https://kiff.dev/docs/openai-agents # OpenAI Agents SDK > **How this fits.** kiff-guard checks each tool call this OpenAI Agents SDK agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the OpenAI Agents SDK hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard), passes the conformance suite, and was checked against the real `openai-agents` v0.17.4 SDK plus a live model call, confirming the guardrail is accepted and `reject_content` genuinely skips the tool. **Shape: inverted-control.** The SDK runs the tool after the guardrail returns a verdict, so the adapter uses `Guard.observe()` / `Guard.decide_only()`, not a `run` callback. See [how the guard works](/docs/concepts). ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" pip install openai-agents # the framework you're guarding ``` (The `openai` extra maps to the `openai-agents` package.) ## Observe, audit with zero config Attach the guard as a **tool input guardrail** on a `function_tool`: ```python from agents import Agent, function_tool from kiff_guard import Guard from kiff_guard.adapters.openai_agents import kiff_tool_input_guardrail guard = Guard(mode="observe") # zero-config audit; no KIFF account kiff_gd = kiff_tool_input_guardrail(guard) @function_tool(tool_input_guardrails=[kiff_gd]) def refund_order(order_id: str, amount_cents: int) -> str: ... agent = Agent(name="support", tools=[refund_order]) ``` In observe mode the guardrail records + learns and always allows the tool to run. Read the trail from `guard.receipts`. ## Activate ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` Review and activate in the [dashboard](/dashboard). ## Enforce, govern at runtime Build the guard with a client and `mode="enforce"`; the `@function_tool` wiring is unchanged: ```python from kiff_guard import Guard, HTTPClient, ToolMap client = HTTPClient(api_key="kiff_live_...", tool_map=ToolMap().bind(...)) guard = Guard(client=client, tenant="", agent="support", mode="enforce") kiff_gd = kiff_tool_input_guardrail(guard) ``` On a withheld decision the guardrail returns `ToolGuardrailFunctionOutput.reject_content(reason)`, the SDK skips the tool and hands the reason to the model instead of the tool result. Enforce **fails closed** on a guard error by default. ## The seam (verified, v0.17.4) `@tool_input_guardrail` runs before the tool executes. The callback gets `data.context.tool_name`, `.tool_arguments` (raw JSON **string**, the adapter parses it to a dict), and `.tool_call_id`. It returns `allow()` to run or `reject_content(message)` to skip. > **Not `needs_approval`.** `needs_approval` is the heavyweight *human* > pause: it pauses the run, surfaces `interruptions`, and resumes via > `RunState`. KIFF's gate is a machine decision, so it belongs in the > synchronous tool input guardrail. A withheld `approval_required` can > still be bridged into the SDK's native `needs_approval` flow on top of > the gate. > **Boundary:** tool input guardrails apply to `function_tool`s only. > Hosted/built-in tools (web search, code interpreter, computer, shell, > apply-patch) and handoffs bypass this pipeline; governing those needs > a different seam. ## Links - Adapter code: [`kiff_guard/adapters/openai_agents.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/openai_agents.py) - Live check: [`live_openai_check.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/live_openai_check.py) - Seam research: [openai-agents.md](https://github.com/kiff/kiff-cloud/blob/main/docs/integration/frameworks/openai-agents.md) - SDK guardrails: [openai.github.io/openai-agents-python](https://openai.github.io/openai-agents-python/guardrails/) ## Google ADK Source: https://kiff.dev/docs/google-adk # Google ADK > **How this fits.** kiff-guard checks each tool call this Google ADK agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the Google ADK hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) and passes the conformance suite. The seam was verified against the real `google-adk` package. **Shape: vote (inverted-control).** ADK runs the tool itself; its `before_tool_callback` only votes, return `None` to allow, return a `dict` to block (the dict becomes the tool result). So the guard uses `observe()` / `decide_only()`, not a `run` callback. See [how the guard works](/docs/concepts). ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" # plus Google ADK, the framework you're guarding: pip install google-adk ``` ## Observe, audit with zero config ```python from google.adk.agents import LlmAgent from kiff_guard import Guard from kiff_guard.adapters.google_adk import kiff_before_tool_callback guard = Guard(mode="observe") # no client, no tenant needed agent = LlmAgent( model=..., tools=[refund_order, send_email], before_tool_callback=kiff_before_tool_callback(guard), ) ``` Run the agent as usual, then read the trail: ```python for r in guard.receipts: print(r.state, r.tool, r.outcome) ``` ## Activate Derive a starter domain from what you observed, review it, and activate it in the [dashboard](/dashboard): ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ## Enforce, govern at runtime Once you have a tenant and an active domain, point the guard at KIFF and switch to `enforce`. Only the guard changes: ```python from kiff_guard import Guard, HTTPClient, ToolMap from kiff_guard.adapters.google_adk import kiff_before_tool_callback client = HTTPClient( api_key="kiff_live_...", # mint in the dashboard tool_map=ToolMap().bind( "refund_order", action="REFUND_ORDER", entity_type="Order", entity_arg="order_id"), ) guard = Guard(client=client, tenant="", agent="support", mode="enforce") agent = LlmAgent(model=..., tools=[refund_order], before_tool_callback=kiff_before_tool_callback(guard)) ``` In enforce mode a withheld decision returns a `dict` to ADK, so the tool is skipped and the reason is handed back to the model as the result. An allowed decision returns `None` and the tool runs. On a transport error the guard fails closed (blocks) by default. ## The seam (verified) ADK calls `before_tool_callback(tool, args, tool_context)` before a tool's `run_async`. Returning `None` runs the tool; returning a `dict` skips it and uses that dict as the result, the block path. ## Links - Adapter code: [`kiff_guard/adapters/google_adk.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/google_adk.py) - Google ADK docs: [google.github.io/adk-docs](https://google.github.io/adk-docs/) (Callbacks → Before Tool Callback) ## Pydantic AI Source: https://kiff.dev/docs/pydantic-ai # Pydantic AI > **How this fits.** kiff-guard checks each tool call this Pydantic AI agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the Pydantic AI hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) and passes the conformance suite. The seam was verified against the real `pydantic-ai` package. **Shape: vote (inverted-control).** Pydantic AI runs the tool itself; the `before_tool_execute` hook only votes, return `args` to allow, raise `SkipToolExecution(result)` to block. So the guard uses `observe()` / `decide_only()`, not a `run` callback. See [how the guard works](/docs/concepts). ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" # plus Pydantic AI, the framework you're guarding: pip install pydantic-ai ``` ## Observe, audit with zero config ```python from pydantic_ai import Agent from pydantic_ai.capabilities import Hooks from kiff_guard import Guard from kiff_guard.adapters.pydantic_ai import kiff_before_tool_execute guard = Guard(mode="observe") # no client, no tenant needed agent = Agent( "...", tools=[refund_order, send_email], capabilities=[Hooks(before_tool_execute=kiff_before_tool_execute(guard))], ) ``` Run the agent as usual, then read the trail: ```python for r in guard.receipts: print(r.state, r.tool, r.outcome) ``` ## Activate Derive a starter domain from what you observed, review it, and activate it in the [dashboard](/dashboard): ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ## Enforce, govern at runtime Once you have a tenant and an active domain, point the guard at KIFF and switch to `enforce`. Only the guard changes: ```python from kiff_guard import Guard, HTTPClient, ToolMap from pydantic_ai import Agent from pydantic_ai.capabilities import Hooks from kiff_guard.adapters.pydantic_ai import kiff_before_tool_execute client = HTTPClient( api_key="kiff_live_...", # mint in the dashboard tool_map=ToolMap().bind( "refund_order", action="REFUND_ORDER", entity_type="Order", entity_arg="order_id"), ) guard = Guard(client=client, tenant="", agent="support", mode="enforce") agent = Agent("...", tools=[refund_order], capabilities=[Hooks(before_tool_execute=kiff_before_tool_execute(guard))]) ``` In enforce mode a withheld decision raises `SkipToolExecution`, so the tool never runs and the reason is fed back to the model. An allowed decision returns the args and the tool runs. On a transport error the guard fails closed (skips) by default. ## The seam (verified) Pydantic AI's `before_tool_execute` hook (on a `Hooks` capability) fires before the tool runs, with the validated `args` and a `call` carrying `tool_name`. Returning `args` runs the tool; raising `SkipToolExecution(result)` skips it, the block path. ## Links - Adapter code: [`kiff_guard/adapters/pydantic_ai.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/pydantic_ai.py) - Pydantic AI docs: [ai.pydantic.dev/hooks](https://ai.pydantic.dev/hooks/) (tool execution hooks) ## Strands Agents Source: https://kiff.dev/docs/strands # Strands Agents > **How this fits.** kiff-guard checks each tool call this Strands agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the Strands hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) and passes the conformance suite. The seam was verified against the real `strands-agents` package. **Shape: vote (inverted-control).** Strands runs the tool itself; its `BeforeToolCallEvent` hook only votes, set `event.cancel_tool` to a message to block, leave it unset to allow. So the guard uses `observe()` / `decide_only()`, not a `run` callback. See [how the guard works](/docs/concepts). ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" # plus Strands, the framework you're guarding: pip install strands-agents ``` ## Observe, audit with zero config ```python from strands import Agent from kiff_guard import Guard from kiff_guard.adapters.strands import kiff_hook_provider guard = Guard(mode="observe") # no client, no tenant needed agent = Agent( model=..., tools=[refund_order, send_email], hooks=[kiff_hook_provider(guard)], ) ``` Run the agent as usual, then read the trail: ```python for r in guard.receipts: print(r.state, r.tool, r.outcome) ``` ## Activate Derive a starter domain from what you observed, review it, and activate it in the [dashboard](/dashboard): ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ## Enforce, govern at runtime Once you have a tenant and an active domain, point the guard at KIFF and switch to `enforce`. Only the guard changes: ```python from kiff_guard import Guard, HTTPClient, ToolMap from kiff_guard.adapters.strands import kiff_hook_provider client = HTTPClient( api_key="kiff_live_...", # mint in the dashboard tool_map=ToolMap().bind( "refund_order", action="REFUND_ORDER", entity_type="Order", entity_arg="order_id"), ) guard = Guard(client=client, tenant="", agent="support", mode="enforce") agent = Agent(model=..., tools=[refund_order], hooks=[kiff_hook_provider(guard)]) ``` In enforce mode a withheld decision sets `event.cancel_tool` to the reason; Strands cancels the call and surfaces the message as a tool result with an error status. An allowed decision leaves the event untouched and the tool runs. On a transport error the guard fails closed (cancels) by default. ## The seam (verified) Strands fires `BeforeToolCallEvent` before executing a tool; `event.tool_use` carries the tool name and input, and `event.cancel_tool: bool | str` blocks the call. The guard registers via a `HookProvider` attached with `Agent(hooks=[...])`. ## Links - Adapter code: [`kiff_guard/adapters/strands.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/strands.py) - Strands docs: [strandsagents.com](https://strandsagents.com/docs/) (Hooks → BeforeToolCallEvent) ## Haystack Agents Source: https://kiff.dev/docs/haystack # Haystack Agents > **How this fits.** kiff-guard checks each tool call this Haystack agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the Haystack hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) and passes the conformance suite. The seam was verified against the real `haystack-ai` package. **Shape: vote (inverted-control).** Haystack runs the tool itself; a `ConfirmationStrategy` only votes, return a decision with `execute=True` to allow, `execute=False` to block. So the guard uses `observe()` / `decide_only()`, not a `run` callback. See [how the guard works](/docs/concepts). ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" # plus Haystack, the framework you're guarding: pip install haystack-ai ``` ## Observe, audit with zero config ```python from haystack.components.agents import Agent from kiff_guard import Guard from kiff_guard.adapters.haystack import kiff_confirmation_strategy guard = Guard(mode="observe") # no client, no tenant needed agent = Agent( chat_generator=..., tools=[refund_order, send_email], confirmation_strategies={ "refund_order": kiff_confirmation_strategy(guard), "send_email": kiff_confirmation_strategy(guard), }, ) ``` Run the agent as usual, then read the trail: ```python for r in guard.receipts: print(r.state, r.tool, r.outcome) ``` ## Activate Derive a starter domain from what you observed, review it, and activate it in the [dashboard](/dashboard): ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ## Enforce, govern at runtime Once you have a tenant and an active domain, point the guard at KIFF and switch to `enforce`. Only the guard changes: ```python from kiff_guard import Guard, HTTPClient, ToolMap from haystack.components.agents import Agent from kiff_guard.adapters.haystack import kiff_confirmation_strategy client = HTTPClient( api_key="kiff_live_...", # mint in the dashboard tool_map=ToolMap().bind( "refund_order", action="REFUND_ORDER", entity_type="Order", entity_arg="order_id"), ) guard = Guard(client=client, tenant="", agent="support", mode="enforce") agent = Agent(chat_generator=..., tools=[refund_order], confirmation_strategies={"refund_order": kiff_confirmation_strategy(guard)}) ``` In enforce mode a withheld decision returns `execute=False` with the reason as feedback, so the tool never runs. An allowed decision returns `execute=True` and the tool runs. On a transport error the guard fails closed (blocks) by default. ## The seam (verified) Haystack calls a `ConfirmationStrategy.run(*, tool_name, tool_description, tool_params, tool_call_id, ...)` before executing a tool; it returns a `ToolExecutionDecision(execute=bool, feedback=...)`. The guard's strategy is passed via `Agent(confirmation_strategies=...)`. ## Links - Adapter code: [`kiff_guard/adapters/haystack.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/haystack.py) - Haystack docs: [docs.haystack.deepset.ai](https://docs.haystack.deepset.ai/docs/human-in-the-loop) (Agent → human-in-the-loop) ## Microsoft Agent Framework Source: https://kiff.dev/docs/microsoft-agent-framework # Microsoft Agent Framework > **How this fits.** kiff-guard checks each tool call this Microsoft Agent Framework agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the Microsoft Agent Framework hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) and passes the conformance suite. The seam was verified against the real `agent-framework-core` package. **Shape: middleware (async).** A `FunctionMiddleware` wraps the tool call: `await call_next()` runs the tool, or set `context.result` and skip `call_next` to block. The guard decides first and only runs the tool when allowed. See [how the guard works](/docs/concepts). ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" # plus Microsoft Agent Framework, the framework you're guarding: pip install agent-framework-core ``` ## Observe, audit with zero config ```python from agent_framework import Agent from kiff_guard import Guard from kiff_guard.adapters.microsoft_agent_framework import kiff_guard_middleware guard = Guard(mode="observe") # no client, no tenant needed agent = Agent( client=..., name="assistant", tools=[refund_order, send_email], middleware=[kiff_guard_middleware(guard)], ) ``` Run the agent as usual, then read the trail: ```python for r in guard.receipts: print(r.state, r.tool, r.outcome) ``` ## Activate Derive a starter domain from what you observed, review it, and activate it in the [dashboard](/dashboard): ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ## Enforce, govern at runtime Once you have a tenant and an active domain, point the guard at KIFF and switch to `enforce`. Only the guard changes: ```python from kiff_guard import Guard, HTTPClient, ToolMap from agent_framework import Agent from kiff_guard.adapters.microsoft_agent_framework import kiff_guard_middleware client = HTTPClient( api_key="kiff_live_...", # mint in the dashboard tool_map=ToolMap().bind( "refund_order", action="REFUND_ORDER", entity_type="Order", entity_arg="order_id"), ) guard = Guard(client=client, tenant="", agent="support", mode="enforce") agent = Agent(client=..., name="assistant", tools=[refund_order], middleware=[kiff_guard_middleware(guard)]) ``` In enforce mode a withheld decision sets `context.result` to the reason and skips `call_next`, so the tool never runs. An allowed decision awaits `call_next` and the tool runs. On a transport error the guard fails closed (blocks) by default. ## The seam (verified) A `FunctionMiddleware.process(self, context, call_next)` fires before a tool runs; `context.function.name` + `context.arguments` carry the call. Awaiting `call_next()` runs the tool; setting `context.result` and skipping `call_next` blocks it. Attached via `Agent(middleware=[...])`. ## Links - Adapter code: [`kiff_guard/adapters/microsoft_agent_framework.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/microsoft_agent_framework.py) - Microsoft Agent Framework docs: [learn.microsoft.com/agent-framework](https://learn.microsoft.com/en-us/agent-framework/) (Middleware → Function middleware) ## LlamaIndex Agents Source: https://kiff.dev/docs/llamaindex # LlamaIndex Agents > **How this fits.** kiff-guard checks each tool call this LlamaIndex agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the LlamaIndex hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in [`kiff-guard`](https://github.com/kiff/kiff-guard) (`kiff_guard.adapters.llama_index`) and was source-verified against llama-index-core and passes the conformance suite. **Shape: middleware.** LlamaIndex's `AgentWorkflow` runs each tool in its `call_tool` step. The guard sits *inside* that step, you subclass `AgentWorkflow` as `GuardedAgentWorkflow` and the guard runs the tool via its continuation, the same middleware shape as Agno. See [how the guard works](/docs/concepts) for what that means. ## Install ```bash pip install "git+https://github.com/kiff/kiff-guard.git#subdirectory=packages/python/kiff-guard" # plus LlamaIndex itself, the framework you're guarding: pip install "kiff-guard[llama-index]" ``` ## Observe, audit with zero config ```python from llama_index.core.agent.workflow import FunctionAgent from llama_index.llms.openai import OpenAI from kiff_guard import Guard from kiff_guard.adapters.llama_index import GuardedAgentWorkflow guard = Guard(mode="observe") # no client, no tenant needed workflow = GuardedAgentWorkflow( agents=[FunctionAgent(tools=[refund_order], llm=OpenAI(model="gpt-4o-mini"))], guard=guard, ) ``` Run the workflow as usual, then read the trail: ```python for r in guard.receipts: print(r.state, r.tool, r.outcome) ``` ## Activate Derive a starter domain from what you observed, review it, and activate it in the [dashboard](/dashboard): ```python from kiff_guard import export_yaml print(export_yaml("my-domain", guard.catalog)) ``` ## Enforce, govern at runtime Once you have a tenant and an active domain, point the guard at KIFF and switch to `enforce`. Nothing about the workflow wiring changes, only the guard: ```python from kiff_guard import Guard, HTTPClient, ToolMap from kiff_guard.adapters.llama_index import GuardedAgentWorkflow client = HTTPClient( api_key="kiff_live_...", # mint in the dashboard tool_map=ToolMap().bind( "refund_order", action="REFUND_ORDER", entity_type="Order", entity_arg="order_id"), ) guard = Guard(client=client, tenant="", agent="support", mode="enforce") workflow = GuardedAgentWorkflow( agents=[FunctionAgent(tools=[refund_order], llm=OpenAI(model="gpt-4o-mini"))], guard=guard, ) ``` In enforce mode a withheld decision raises `kiff_guard.Hold` from inside the `call_tool` step; the tool body never runs and the exception propagates up through the workflow handler (the fail-safe path, an unknown outcome withholds rather than proceeds). ## The seam (verified) LlamaIndex's `AgentWorkflow` exposes a `call_tool` workflow step that resolves and runs each tool. `GuardedAgentWorkflow` overrides that step and runs the tool body through the guard's continuation, block by raising `Hold`, proceed by calling through. The adapter is fully async to match the workflow, and lazily imports LlamaIndex so `import kiff_guard` works without `llama-index-core` installed. ## Links - Adapter code: [`kiff_guard/adapters/llama_index.py`](https://github.com/kiff/kiff-guard/blob/main/packages/python/kiff-guard/src/kiff_guard/adapters/llama_index.py) - LlamaIndex docs: [developers.llamaindex.ai](https://developers.llamaindex.ai/python/framework/use_cases/agents/) ## OpenClaw Source: https://kiff.dev/docs/openclaw # OpenClaw > **How this fits.** kiff-guard checks each tool call this OpenClaw agent makes against the agent's [Card](/docs/kiff-cards) before the tool runs. Give the agent its own key and a Card in KIFF Cloud ([Connect an agent](/docs/connect-an-agent)); this page is the OpenClaw hook. If the agent calls MCP tools instead, use the [MCP gateway](/docs/mcp-gateway), with no code. The model behind it: [how kiff-guard works](/docs/concepts). **Status: verified.** The adapter ships in the JS guard SDK ([`@kiff/kiff-guard`](https://github.com/kiff/kiff-guard), `packages/js/src/adapters/openclaw.ts`) and passes the conformance suite ported from the Python guard. It was verified against the OpenClaw plugin SDK's `before_tool_call` hook. This is KIFF's first TypeScript adapter, the Python `kiff_guard` package can't host it, so it lives in the JS package instead. **Shape: vote / approval-native.** The `before_tool_call` hook runs the tool itself; the guard only votes via its return value, one governed receipt per call, same as the Python vote adapters. OpenClaw is the first framework where KIFF's middle outcome, `approval_required`, maps onto a **native** human-in-the-loop primitive (OpenClaw pauses the run and routes a real `/approve` flow) rather than collapsing to a block. ## Why OpenClaw matters OpenClaw isn't a quick fifth adapter, it widens the whole picture: 1. **It forces the TypeScript guard SDK.** Every shipped adapter is Python; OpenClaw is Node/TS. A TS port unlocks the entire JS agent ecosystem (LangGraph.js, Vercel AI SDK, Mastra), the vibecode crowd KIFF Cloud targets first. 2. **First native `approval_required`.** OpenClaw has a real approval primitive (title, severity, timeout, resolution) plus an `/approve` UX. This is the first integration where "clearance held for a human" renders as the experience the whitepaper describes, not just a refusal. 3. **Control-plane to control-plane.** OpenClaw is itself a Gateway with channels, approvals, and audit. KIFF governing OpenClaw is two control planes composing, KIFF as the shared clearance authority above OpenClaw's runtime. ## The seam Register the guard on your OpenClaw plugin's `api` via the shipped `registerKiffGuard` helper. It wires KIFF's outcomes onto the `before_tool_call` return contract for you: ```ts import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { Guard } from "@kiff/kiff-guard"; import { registerKiffGuard } from "@kiff/kiff-guard/adapters/openclaw"; const guard = new Guard({ mode: "observe" }); // switch to enforce when ready export default definePluginEntry({ id: "kiff-guard", name: "KIFF Guard", register(api) { registerKiffGuard(api, guard); // before_tool_call, priority 50 }, }); ``` Under the hood the adapter votes on each call: `allowed` proceeds and records that the tool ran; `approval_required` returns a `requireApproval` directive (title, severity, a `timeoutMs` that fails closed) so OpenClaw routes a real human approval; anything else returns `{ block: true }`. Enforce mode **fails closed**, if the guard can't reach KIFF, the tool is blocked. One governed receipt is recorded per call either way. ## Status and tracking Shipped. OpenClaw was the forcing function for the TypeScript guard SDK ([RFC 015](https://github.com/kiff/kiff-cloud/blob/main/docs/design/015-open-core-and-guard-sdk-repo.md)), which now lives in `packages/js` of the public guard repo. The adapter and its conformance test ship there ([`adapters/openclaw.ts`](https://github.com/kiff/kiff-guard/blob/main/packages/js/src/adapters/openclaw.ts)), which is what moved this page from preview to verified. ## Links - Seam research (full): [openclaw.md](https://github.com/kiff/kiff-cloud/blob/main/docs/integration/frameworks/openclaw.md) - OpenClaw plugin hooks: [docs.openclaw.ai](https://docs.openclaw.ai/plugins/hooks) --- # Whitepaper Source: https://kiff.dev/whitepaper # KIFF ## A coordination protocol for governed action **Working notes, May 2026** **Gabriel Sarmiento** Released under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Code referenced is part of the open-source MIT framework at [github.com/kiff/kiff](https://github.com/kiff/kiff). --- > **Scope: framework v0.9.** This document describes that version's > behavior and limitations. Section 3 includes the approval and state > checks revised after an adversarial audit. ## Abstract Humans, services, integrations and AI agents can act on the same order, claim or account. Each action needs checks against the entity's current state, the actor's permissions and any required approval. When those checks differ between callers, an action may run without the review or state check the business requires. KIFF is a Go framework that validates proposed actions before calling their executors. It provides events, state transitions, decisions, action contracts, approvals and audit records. Events can be replayed to reconstruct an entity's state. This paper describes the validation sequence, its trust boundary and the evidence supplied by the framework, demos and tests. It also identifies the checks an application must perform outside the framework. --- ## Contents 1. [The problem](#1-the-problem) 2. [The coordination loop](#2-the-coordination-loop) 3. [The trust boundary](#3-the-trust-boundary) 4. [Domain definitions](#4-mechanics-not-semantics) 5. [Implementation and tests](#5-evidence-what-we-built) 6. [Limits](#6-honest-limits) 7. [Future work](#7-where-this-goes) 8. [Appendix A: the action contract](#appendix-a-the-action-contract) 9. [Appendix B: integration boundaries](#appendix-b-what-kiff-is-not) --- ## 1. The problem An actor's access to a tool does not establish whether a particular request is allowed. A refund tool may accept an amount even when the order is unpaid; a restart command may work during a deployment freeze. The application needs to check the request before the side effect occurs. The following hypothetical examples illustrate checks that operational systems may need: | Context | Proposed action | Check needed before execution | |---|---|---| | E-commerce | Refund an unpaid order | Confirm the order's payment state | | Insurance | Pay a claim under fraud review | Confirm the claim is eligible for payment | | Healthcare | Create an order that conflicts with an active prescription | Check the relevant clinical state | | Fintech | Transfer an amount above a dual-control threshold | Obtain the required review | | Internal DevOps | Restart a service during a deployment freeze | Check whether the restart is permitted | These checks apply to requests from humans and software as well as AI agents. Teams implement them with state machines, validators, review queues and audit stores. KIFF connects those components through a shared action contract and runtime. ### Scope of enforcement KIFF evaluates requests submitted to its gate. In an embedded application, the runtime validates an action before invoking its registered executor. When an application uses a decision API or the guard SDK, the calling code must honor the verdict; the guard's pre-tool hook withholds a call on a non-`allowed` verdict. A code path that invokes a tool without consulting KIFF remains outside this boundary. Integration must route the relevant actions through the gate and control any credentials or alternative execution paths that could bypass it. --- ## 2. The coordination loop The framework connects six types of record: ``` event → state → decision → action → approval → audit ``` **Events** record what happened. Each append-only record includes a timestamp, source and domain-specific payload inside a common envelope. **State** describes an entity's current condition. Deterministic transitions update it from events, giving the runtime a stored value against which to validate an action. **Decisions** record an actor's intent, including any submitted reasoning, evidence and confidence. A decision remains available even if its proposed action never runs. The record preserves the actor's explanation; it does not verify that the explanation is correct. **Actions** declare a name, allowed states, required parameters, permissions, risk level, approval requirement and executor. The runtime checks the contract before execution. See [Appendix A](#appendix-a-the-action-contract). **Approvals** record the requester, reviewer, entity, action, status, reason and timestamps. The runtime resolves a granted approval from the store before marking the action context approved. **Audit records** cover ingestion, state transitions, decisions, validation, approval requests and reviews, execution results and failures. Their `trace_id`, `correlation_id` and `causation_id` fields link the steps of a request. An event updates state, an actor records a decision and the runtime validates the proposed action. If the action needs approval, execution waits for a granted record. The executor then runs and its outcome is audited. Replaying the entity's events allows comparison with its materialized state. --- ## 3. The trust boundary An action that requires approval must resolve a granted record from the approval store. A caller-supplied flag cannot substitute for that record. `ActionContext` carries an unexported `approved` field: ```go // pkg/kiff/action/action.go (excerpt) type ActionContext struct { ActionName string EntityID string EntityType string CurrentState string Actor actor.Actor Parameters map[string]any ApprovalID string approved bool // unexported. Only the runtime can set this. } ``` Go prevents a package outside `action` from setting that field in a struct literal. `GrantApproval` also requires a capability type from an `internal/` package, which external callers cannot name. These compiler restrictions alone do not establish a runtime security boundary. ### The approval bypass and its fix An adversarial audit used reflection to call the setter from a separate module: ```go // From a separate module. No unsafe, four lines. m := reflect.ValueOf(&ctx).MethodByName("GrantApproval") m.Call([]reflect.Value{reflect.Zero(m.Type().In(0))}) ``` This attack and an `unsafe` variant executed an approval-required action against an empty approval store. Both attempts produced `action_validated` and `action_executed` audit records. The earlier implementation therefore allowed a bypass while recording it as valid. The revised runtime checks approval independently of the supplied bit: - `applyApproval` clears any inbound approved value and looks up the approval by ID. It checks that the record matches the entity and action and has status `granted` before deriving the approved value. - The capability check rejects a zero `trust.Grant`. - After the pluggable `Validator` accepts an action, the runtime checks the approval requirement again, so a permissive validator cannot waive it. If a check fails, the context stays unapproved. Store errors propagate to the caller rather than allowing execution. By default, the reviewer must differ from the requester. A requester attempting to review their own approval receives `approval.ErrSelfReview`. This segregation of duties prevents the requester from providing their own sign-off. ### Stored state The runtime also verifies the state used for validation. Earlier code accepted a caller's `CurrentState` string without checking it, allowing the caller to name a state in which its action was permitted. `ValidateAction` now reads stored state and returns `ErrStateMismatch` when the caller's assertion disagrees with it. ### Tests Compile-time tests check that an external caller cannot set `ActionContext{approved: true}` or directly invoke the protected setter. Runtime fixtures execute reflection, `unsafe` and permissive-validator attacks from a separate module against an empty approval store. Each must receive a refusal. These fixtures fail against the earlier implementation and pass after the fix. A separate test checks that an invented approval ID causes `ExecuteAction` to return `action.ErrApprovalRequired` without running the executor. Both compiler and runtime tests are needed: the compiler tests passed even when the reflection bypass existed. ### Permissions The framework resolves roles from `permission.Policy`, keyed by `Actor.ID`. Role membership belongs to the policy and is assigned through `AssignRole`. The permission check does not trust `Actor.Roles` supplied in the action context. The unit test in `pkg/kiff/permission/permission_test.go` checks that submitting `Roles: ["admin"]` gives an actor no admin permissions unless the policy assigned that role. `Actor.Roles` remains descriptive metadata for audit and display. ### Host responsibilities The host authenticates the actor through a session, API-key record or identity-provider claim. It supplies that identity and its assigned roles to the policy. KIFF then validates state, parameters, permissions and approval in that order, then any aggregate limits. The aggregate check runs last, after the action contract accepts the request. If the host accepts a forged identity or assigns excessive permissions, the framework cannot correct that mistake. The runtime also requires explicit executors (`ErrExecutorMissing` on a missing executor), uses atomic counters and random bytes for audit IDs, and validates a supplied domain definition before processing events. --- ## 4. Domain definitions Each application defines the business vocabulary its contracts use: - entity types, such as `Order`, `Claim` or `Service`; - event types, such as `ORDER_PAID` or `CLAIM_FILED`; - states, such as `PAID`, `UNDER_REVIEW` or `RESOLVED`; - action contracts, such as `REFUND_ORDER` or `RESTART_SERVICE`; - permission identifiers, such as `orders.refund`. KIFF provides the record formats and validation sequence. Events carry an ID, type, entity, source, actor, timestamp, metadata and payload. Transitions update state; contracts define action requirements; approval records track review; audit records link the resulting steps. A domain must supply its own eligibility rules. For example, a refund contract needs payment-state checks, while an outreach contract may require verified consent. Sharing the runtime does not establish those rules or make one domain's checks sufficient for another. --- ## 5. Implementation and tests The repository provides the following implementations, examples and tests for the version covered by this paper. ### 5.1 The framework `pkg/kiff/` contains the core records, runtime, store interfaces, HTTP API, observability wrapper and test helpers. The Postgres backend uses `pgx/v5`; applications can use the in-memory and file-backed stores without that backend. The `event.Store`, `decision.Store`, `approval.Store` and `audit.Store` interfaces have in-memory, JSONL and Postgres implementations. The shared conformance suite checks ordering, filtering, payload round-trips, upserts, validation rejection and context cancellation. ### 5.2 The refund demo `examples/refund-agno/` runs an agent against a mock order database with the same prompts, model and fixture in two configurations. **Direct tool access.** The agent's `refund_order` tool mutates the mock database. A $999 refund on an unpaid order succeeds because the tool does not check payment state. **Through KIFF.** The tool sends its request to an HTTP server wrapping `pkg/kiff/runtime`. Small refunds (≤ $100) use `AUTO_REFUND` and execute immediately when their checks pass. Larger refunds use `REFUND_ORDER`, which requires approval. The runtime returns `approval_required`; after a human grants approval, the agent can repeat the request and execute it. The timeline records the proposal, validation, approval and execution. Its rebuild check reports `materialized = REFUNDED`, `replayed = REFUNDED`, `events = 3 ✓`. `agent.OfflineProvider` makes the fixture deterministic and runnable without an LLM API key. With credentials configured, the same example can use AWS Bedrock. Run it with `make demo`; the example's README includes sample output. ### 5.3 The breadth demo `examples/support-ops/` processes five support tickets with five tools: | Ticket | Tool | Outcome | Reason | |---|---|---|---| | 1 | `issue_refund` | executed | small amount, no approval needed | | 2 | `issue_refund` | approval_required → granted → executed | over the cap, granted on review | | 3 | `send_outreach` | blocked_consent_missing | structural rejection before any approval is opened | | 4 | `escalate_to_human` | executed | escalation never needs approval | | 5 | `close_ticket` | executed | only legal in `RESOLVED` | The `SEND_OUTREACH` validator rejects missing or false `consent_verified` before opening an approval request. This shows the order of eligibility and approval checks: review cannot make an ineligible request valid. ### 5.4 The conformance suite `pkg/kiff/store/storetest/` defines the persistence tests shared by the backends. To check a new backend, implement the four store interfaces, provide factories for clean instances and run the suite. Passing it establishes agreement on the behaviors covered by its test cases. ### 5.5 The Postgres backend `pkg/kiff/store/postgres/` uses four tables: `kiff_events`, `kiff_decisions`, `kiff_approvals` and `kiff_audit`. Payloads use `JSONB`, and connections use `pgx/v5/pgxpool`. The conformance tests use `KIFF_POSTGRES_TEST_URL` and have been run against `postgres:16-alpine`. Default `go test ./...` does not require a running database. The schema uses `CREATE TABLE IF NOT EXISTS`; operators manage production migrations separately. ### 5.6 The operator surface `pkg/kiff/httpapi` serves read-only HTML at `/admin` and `/admin/entities/{id}`. These pages list pending approvals and entity timelines, including denials and failures. Production deployments must protect access to them. The inspection command `kiff timeline -base -entity ` reads `/entities/{id}/timeline` and `/demo/rebuild` from a KIFF server. It displays audit records and the state rebuild result in a terminal table. ### 5.7 What the evidence establishes The examples exercise the proposal, validation, approval and execution sequence. The conformance suite compares three persistence backends, and the adversarial fixtures test specific attempts to bypass approval. The approval bypass demonstrates why compile-time restrictions need runtime checks and tests. The demos cover e-commerce and support workflows. `cookbook/` also contains seven runnable recipes with domain tests, including `insurance-claims-triage`, `healthcare-prior-auth` and `cloud-infra-remediation`. Those tested recipes and the hypothetical examples in section 1 do not establish sector readiness or regulatory compliance. Passing the documented tests also does not establish safety for every integration or attack. An adopter must test its own contracts and execution paths. --- ## 6. Limits The framework version covered here leaves these responsibilities to the application or supporting infrastructure. **Durable execution.** A workflow engine must handle steps that survive crashes or coordinate multiple processes over long periods. KIFF can check whether each step is allowed; it does not provide that engine's scheduling and recovery mechanisms. **Multi-tenant identity.** The framework supplies actors and permissions without an organization, project or tenant model. Its HTTP API requires an `Authenticator`, whose principal overrides the actor in the request body. The host must implement tenancy and identity management. **Distributed state.** The runtime assumes a single authoritative state per entity. Replication, regional sharding and eventual consistency require an appropriate state backend and application design. **Event ordering across producers.** A store preserves insertion order. Producers must handle cross-source ordering, clock skew and duplicate delivery. This does not provide exactly-once delivery across systems. **Observability.** `pkg/kiff/observability` wraps the audit store with `slog` and counters. Applications must add tracing, spans and metric histograms when they need them. **Audit integrity.** Framework stores are append-only through their interfaces. They do not provide hash chains or signatures, so an operator with direct store access can rewrite records. Tamper evidence requires an additional signing or verification mechanism. **Time-of-check to time-of-use.** There is no lease, version check or compare-and-swap between a decision and execution. Idempotency covers an identical retried request; it does not serialize different proposals against the same entity. Applications must account for state changes during that interval. --- ## 7. Future work Further work includes evaluating additional persistence backends and documenting the protocol independently of its Go types. SQLite could support single-binary deployments, while DynamoDB could serve AWS-based applications. These are possible extensions, not capabilities established by the examples in this paper. The framework's interfaces and the documents in `docs/architecture.md` and `docs/conventions.md` currently describe its contract. A separate protocol specification would make that contract easier to implement in another language. Adoption and additional domain tests should inform that work. --- ## Appendix A: the action contract Core fields from `pkg/kiff/action/action.go`: ```go // ActionContract describes when and how an action is allowed to run. type ActionContract struct { Name string AllowedStates []string RequiredParameters []string RequiredPermissions []permission.Permission Risk RiskLevel ApprovalRequirement ApprovalRequirement Executor func(context.Context, ActionContext) (ActionResult, error) } ``` - **`Name`** identifies the action in audit records, HTTP routes and proposal payloads. - **`AllowedStates`** lists the states from which it may run. The runtime returns `action.ErrStateNotAllowed` for any other state, before checking parameters, permissions or approval. - **`RequiredParameters`** lists values that must be present and non-nil. Missing values produce `action.ErrMissingParameter`. Domain validators must check any additional requirements on those values. - **`RequiredPermissions`** lists permissions resolved through `permission.Policy`. A missing permission produces `action.ErrPermissionDenied`. - **`Risk`** supplies metadata for reporting and downstream tooling; it does not change the runtime's validation sequence. - **`ApprovalRequirement`** is `ApprovalNever` or `ApprovalRequired`. An approval-required action returns `action.ErrApprovalRequired` until the runtime resolves a granted record. - **`Executor`** performs the side effect after validation and returns an `ActionResult`, including any follow-up events. The executor must handle failures and any safeguards required by the external system, including changes since validation. --- ## Appendix B: integration boundaries **Agent frameworks** provide model access, prompts, tools and agent execution. An agent built with LangGraph, Agno, OpenAI Agents SDK or another harness can submit actions to KIFF for validation. **Workflow engines** provide durable execution, retries, timers and recovery across steps. They can use KIFF to validate individual steps. **Agent hosting** runs the agent's code and manages its sessions and infrastructure. The framework does not deploy or orchestrate agents. **Model gateways** route requests between model providers and may shape or cache them. KIFF's action validation does not require it to route model requests. **Application frontends** provide the user interface. Beyond the read-only operator pages, applications build their own frontend against the JSON HTTP API. --- *This document is part of the KIFF Framework, MIT-licensed, at [github.com/kiff/kiff](https://github.com/kiff/kiff). The whitepaper itself is released under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).* *Comments, corrections, and counterexamples welcome at hello@kiff.dev or as issues on the repository.* --- # Why KIFF Source: https://kiff.dev/why # Why I'm building KIFF Everyone understands a corporate card. A company gives an employee a card with a limit. The employee buys what the job needs without asking each time. The company sees every charge, can lower the limit, and can cancel the card the day it needs to. Nobody thinks that is strange, and it is how a company lets people spend its money without approving every coffee. I'm building KIFF to give every AI agent the same thing: a Card. The Card says which actions the agent may take, how much per action and in total, and what happens when it asks for more. The agent does the work. The person who owns the money, the account or the customer keeps control, and can sleep at night. ## Agents are about to act for all of us Until now, most agents have suggested, drafted and summarized. That is changing quickly. Over the coming months, agents will refund orders, pay invoices, book travel, change subscriptions, move customer data and deploy code. Not as an experiment, but as the normal way work gets done. When that happens, letting an agent act can't be a luxury that only a team with its own engineers can build safely. It has to be ordinary, the way a card in a wallet is ordinary. Someone has to decide, in plain terms, how far each agent may go, and that decision has to hold every time the agent acts. ## Today, the authority lives inside the agent Ask where an agent's limits are written down and the answer is usually: in its prompt, in a config file, in a function someone wrote last quarter, or nowhere, behind an API key that can do anything the connected service allows. That has three problems. **Nobody responsible can read it.** The person who owns the money can't see a limit written in code, and every change to it is an engineering task. **It doesn't travel.** Each assistant and each framework has its own settings. If you use ChatGPT today, Claude tomorrow and Grok next week, your limits don't follow you. You set them again in each place, or you don't set them at all. **The agent holds the keys.** When the credential that can move the money sits with the agent, every limit is a request the agent is asked to honour, not a line it can't cross. ## A Card lives outside the agent KIFF keeps the authority outside the agent, and the keys out of its hands. An agent doesn't act by itself. It acts through its tools and skills: an MCP server that issues refunds, a plugin that changes a subscription, a skill that calls a payment API. Every one of them is a way to move money or change a record, so every one of them needs the same answer before it runs: is this agent allowed to do this, and how much is left? That answer can't live in each tool, written again by each vendor. It has to live in one place the owner controls, and every tool has to ask it. Through the KIFF gateway, the agent never holds the tool's credential: KIFF does, and the agent's own key can only ask. With kiff-guard or the decision API, the credential stays where your system already keeps it, and KIFF decides before it is used. Either way, every call is checked against the agent's Card before it runs. Inside the Card, it goes through. Outside it, it waits for the owner or is refused. The owner answers from KIFF, raises the limit for a busy week, or revokes the Card tonight, and the agent keeps running the same code. One balance is held for each Card, so retries, parallel runs and extra copies of the agent can't spend it twice. Every decision has a signed, tamper-evident receipt the agent can't write. The agent still has a part to play. The KIFF skill teaches it to propose a Card and to ask, and to stop and report when a call is held. But those instructions are not the security boundary. KIFF decides on its own servers, and the gateway enforces that decision before a governed call goes through. A prompt injection or an ignored instruction can't give the agent authority its credential doesn't have. There is one honest edge: KIFF governs only the calls that go through it. If the agent still holds a direct credential to the same tool, that path is outside the Card, so remove it. You won't run one agent. You'll run several: one in your support desk, one in your finance tools, one in the assistant you happen to use that day. They can share one Card and draw on one balance, so ten agents can't each spend the full amount. Every decision still names the agent that asked. However many agents hold it, the Card stays yours: you set it, you see every draw, and revoking it stops all of them at once. Because the authority isn't inside the agent, the agent becomes replaceable. Models change, assistants come and go, frameworks churn. The Card stays where it is. ## Built for companies KIFF is built for companies whose agents act on their own money and operations. A company issues Cards to the agents that work for it: the support agent that refunds, the finance agent that pays, the ops agent that deploys. Each has its own limits and its own record. Several agents doing one job can share a Card, the way a team shares a budget. The builders who deliver those agents to clients can build the agent once and let each client set its Card. That is the offer today. Handing an agent authority should feel as familiar as handing an employee a card. ## Where this is going The part that excites me most is portability. Your authority should be one thing, held in one place you control, and every agent you use should ask that place before it acts. It shouldn't matter whether the agent runs in ChatGPT, Claude, Grok, a framework your team wrote, or an assistant that doesn't exist yet. The agent knows your limits because it asks the same Card. The keys never sit in the agent. Some of this works today. Agents that use remote MCP tools connect to KIFF through one gateway; we have verified Claude Code and Codex. Agents in your own code connect with one hook, and any code can make one call to the decision API. Other assistants are where we are going, along with: - controls across many agents at once, - tools that accept only calls KIFF has authorized, - payments that follow the Card. Those aren't products yet, and the site says so wherever they come up. I also think the idea reaches past companies. A person whose agent books travel or pays bills will want the same thing: limits they set, held in one place, that every assistant asks. That is a hypothesis about where this goes, not something we offer today. The goal is plain. In a few years, giving an agent a Card should be as normal as giving an employee a corporate card, and KIFF should be the place that Card lives. I intend to build the company that makes that normal. ## Why it has to be open Something that sits between an agent and your money can't be a black box, and it can't be locked to one vendor. If authority is going to travel with you across every assistant, the way it's checked has to be something anyone can read, run and build on. So the core is open source. The framework that makes the decision is MIT-licensed: you can run it yourself and read every line that decides, including the attacks we wrote against our own approval checks, which run on every commit. The SDK, the scanner, the skills and the Claude Code plugin are open too. The hosted service adds what you'd rather not run yourself: Cards you issue and revoke from a dashboard, the owner answering held calls, one balance per Card across every run, and signed, retained receipts. ## How we work **We say what is real.** A Card is the final authority only where KIFF is on the only path to the capability. If an agent can still act through a browser or a credential of its own, KIFF doesn't govern that path until it is closed, and we tell you that. The [proof page](/proof) was recorded on our production services, and it says what it does and doesn't prove. **People keep the authority.** The goal isn't to remove people. It is to move them from approving every action to deciding what each agent may do and how much, and to answering what falls outside it. ## Start Businesses whose agents take consequential actions on their own money or operations can start with one action and its Card. [The ten-minute quickstart →](/docs/quickstart) People who build agents, for clients or for their own company, can build the agent once and let each owner set its Card. [What building with KIFF includes →](/builders) If you have an agent that should be doing real work and isn't allowed to yet, write to me at [gabriel@kiff.dev](mailto:gabriel@kiff.dev). Bring one action and we'll work out its Card together.