The enforcement core of KIFF Cloud: it checks every call an AI agent makes
against the Card its owner issued, before anything is sent.
kiff.dev · What KIFF is · How it works · Proof · Invariants · Whitepaper · Docs · Status
AI agents can now refund an order, pay a bill or answer a dispute. That's useful right up until one of them is tricked, confused or compromised. Then the question isn't "is the model smart?" any more. It's "what can it actually do?"
Today the answer lives in a prompt, a config file, an if amount > 500
someone wrote last quarter, or nowhere at all, behind a key that can do
anything the service allows. The business can't read it, every change is a
release, and the agent holds the keys.
KIFF answers it outside the model. Each agent gets a Card, like a corporate card: which actions it may take, on which records, how much per action and in total, and what happens when it asks for more. The agent asks; KIFF decides, before anything is sent. The tool's sign-in never reaches the agent. Anything past the Card waits for the owner. Every decision leaves a signed receipt anyone can check.
This repository is where that's enforced: the MCP gateway at mcp.kiff.dev
and the parts of the API at api.kiff.dev that decide a call, mint its
permit, apply a halt and write its receipt. Below is its blueprint, drawn
from the code in this repository.
tafs is KIFF's own app, for iPhone, iPad and Android. It is in a closed test on Android and on TestFlight for iPhone and iPad. A business hands routine jobs to AI agents, called tafs, and each taf works under its own Card. Every action a taf takes goes through the gateway and the decision path in this repository:
- Inside its Card, a taf acts, and the action leaves a signed receipt. The app's Record shows it, with the receipt checked on the owner's device.
- Outside its Card, it waits. The held call appears in Needs you, and the owner approves it once or refuses it, signed on their own device.
- Halt all stops every taf, and nothing new is sent until the owner resumes.
- Coach mode sets a taf up: it connects the tools, proposes the Card, and the owner approves both.
The tools' sign-ins stay in planque; the tafs themselves never hold them. The same code decides for tafs and for any agent you connect yourself.
| Diagram | Answers | |
|---|---|---|
| 1 | System context | Who touches a call, and who holds which key |
| 2 | How a Card decides | What a Card checks, and why one balance per Card is the key |
| 3 | Connected tools | sealed, verify and relay: where the credential lives; what a connection profile adds |
| 4 | One allowed call | A call from the agent to the vendor and back |
| 5 | The hold | A call past the Card, and the owner's approval |
| 6 | Lifecycle of a call | Why a call is never sent twice |
| 7 | The halt | Stopping an agent, confirmed on the vendor's side |
| 8 | Receipts | From a decision to a public anchor |
| 9 | Components | The two Go modules and their packages |
| 10 | Data model | The tables behind Cards, holds, halts and proof |
| 11 | Deployment | What runs, and what it keeps |
| 12 | Network topology | Trust zones, and the only way out |
| 13 | Cloud architecture | How kiff/cloud fits with tafs and the rest of the family |
The diagrams are drawn with Archify;
their sources are in .github/readme/src.
- The agent asks, KIFF decides. Every
tools/callgoes through the gateway, and KIFF answers allowed, held or refused before anything is sent. A clever prompt changes nothing: the Card is outside the model. - Nobody hands the agent a key. The agent holds only its KIFF agent key. The tool's sign-in is sealed in KIFF, kept by planque, or not needed at all because the tool checks KIFF's permit.
- Proof anyone can check. Every decision leaves a signed receipt, and receipts are anchored on a public network.
A Card is authority you can hand over, watch being used, and take back.
| On a Card you set | The owner can, at any time, without touching the agent |
|---|---|
| The action, such as refunding an order | Raise or narrow the limits: €5,000 a day, €20,000 for Black Friday, back on Monday |
| Which records it applies to, such as one order or paid orders only | Give extra room once, for a busy hour |
| A limit per action, such as €500 per refund | Share it, so several agents draw on one balance |
| A total per day, week or month, as an amount or a count | Choose approvers, who answer the calls it holds |
| An expiry, if you want one | Revoke it at 2 a.m., and only that authority stops |
| What happens over the limit: it waits for you, or it's refused | Read its statement: what was drawn, what's left, every change and who made it |
The key is the running total. A per-action limit bounds the size of one mistake. Only a cumulative limit bounds a bad day, and that is the number that grows as agents get cheaper to run. So a Card keeps one balance, locked for every draw: two agents sharing a Card can't both spend its last €100, a retry is counted once, and a balance that can't be read is refused, never waved through.
This is the part that takes more than a weekend, and the reason KIFF decides better than a limit in code.
A Card doesn't float on its own. It narrows a domain: a model of how your
business actually works. The records you care about, the states they move
through, the actions that are valid in each state, and who may take them. An
order is CART → PAID → FULFILLED → RETURNED. A refund is valid on a paid
order and refused on one already refunded, whoever asks and however the
prompt is worded.
- The domain lives in a
kiff.yamlin your repository. Your coding agent writes it with the kiff-domains skill, from what your agents already do: kiff-guard in observe mode learns which tools they call and hands you a starter domain from real traffic. - A Card constrains; it never confers. It can only narrow what the domain allows. It can never make a forbidden action legal.
- Observe, then enforce. A new domain first decides and records on your real traffic while refusing nothing. When the numbers look right, it enforces.
- Build once, reuse everywhere. Every agent you add, in any framework, uses the same domain and gets its own Card.
Why not write the limit yourself? Anyone can write if amount > 500.
Nobody wants every agent to own the lifecycle of authority: who holds it,
what's left, who changed it, how to take it back, and what happens with two
runs at once, a retry, a restart or an outage. That lifecycle is what this
repository is.
Each connected tool has a custody mode that says who holds its credential:
| Mode | Who holds the tool's credential | What the tool receives |
|---|---|---|
sealed |
KIFF, encrypted at rest and redacted from results and logs | the call, with that credential |
verify |
nobody | the call with a KIFF-Permit header, which the tool checks |
relay |
planque, on your side or KIFF's | the call with its permit; planque adds the sign-in |
A permit is an Ed25519 JWT (typ kiff-permit+jwt) over the SHA-256 of
the call's JCS-canonical arguments. It covers one execution of one exact
call, its lifetime is capped, and arguments the owner fixed (such as the
Stripe operation) are ones the agent can't set. The keys are published at
https://api.kiff.dev/.well-known/kiff-permit-keys.json
(docs).
A connection can carry a profile: what each of its operations does, in terms a Card can count. A profile applies only once the owner has approved it, signed on their own phone; a connector's key can propose one, never apply it. For each operation it can say:
- The amount, read from the record. Some writes act on an amount the call doesn't carry, such as answering a dispute. The gateway reads the amount from the record, as the agent and through its own Card, before KIFF decides. An amount the agent sends itself is refused, and so is a call whose amount can't be read: nothing is sent.
- A verify read after the write. When the vendor answers a write, the gateway reads it back: the amount the vendor used, or a field that shows it took effect. If the vendor used more than was decided, the Card counts the larger figure. A difference, a failed read or a missing field flags the call and pauses that operation until the owner resumes it. It finds a mismatch after the fact; it can't undo the first one.
- Probation. The first sent calls of a money operation that no test call has proved are on probation.
"Did this happen?" A money call whose outcome KIFF can't prove (sent and its answer lost, or refused by the vendor without proof that nothing was done, or on probation) stays counted on the Card until the owner answers whether it happened.
Whose run it is. When a person runs an agent, the run carries a grant KIFF signed for them, and the gateway passes it with each call. KIFF checks it on every call: for this agent, unexpired, and the person may still use it. KIFF records the call, and any hold it opens, as that person's, so nobody answers a hold they asked for. A call without a grant is decided as before.
Every outbound call leaves through one egress guard. Tool URLs are untrusted, so they're checked when a tool is connected and again on the resolved IP of every connection: no internal or metadata addresses, no redirects, no proxies.
When a call goes past the Card, nothing runs until the owner answers. They approve that exact call, once, on the resource it named. An approval that isn't used is withdrawn, a new breach after it waits for room, and a hold nobody answers expires as refused. If a call like this should be normal, the owner changes the Card instead.
The gateway records a call before it is decided; a call it hasn't
recorded is never sent. One attempt wins the move to forwarding, and every
other retry reads that attempt's answer. A call whose outcome is lost is
recorded as unknown and never sent again.
One tap halts an agent, or the whole account. KIFF decides none of its calls from that moment: new ones, held ones and retries are refused. The halt reads as halted, not halting, only once each planque has confirmed it can see it, so a permit issued just before the halt can't slip through on the vendor's side. After a resume, only new calls go through.
Don't take our word for it.
- Get a receipt.
GET https://api.kiff.dev/v1/receipts/{trace_id}, with your account's key, returns it (docs). Itsattestationis a secp256k1 signature over the receipt's causal-chain hash;receipt.VerifyAttestationrecovers the signer and compares it withattestation.wallet_address. - Check the anchor, in public. Receipts are anchored on Base Sepolia,
a public test network, by the KiffAnchor contract at
0x2F5468eE0De25c6a86D5F1445F37E6a79039DEa4. With the batch for a receipt,kiff-verifychecks the record is in a batch that contract anchored:
cd api && go run ./cmd/kiff-verify --rpc https://sepolia.base.org \
--contract 0x2F5468eE0De25c6a86D5F1445F37E6a79039DEa4 \
--record-id <record id> --batch batch.json
A test network shows the mechanism end to end, in public, but it carries no economic finality and isn't a durability guarantee. Anchoring on Base mainnet is supported in the code and not offered: every plan anchors on Base Sepolia. The signed receipt is the guarantee; the anchor is a public witness.
gateway/ the MCP gateway (its own Go module)
cmd/gateway the server
cmd/testtool an MCP tool for tests that counts the calls it gets
internal/gateway tools/call: ask KIFF, hold or refuse, send once;
amounts read from the record, verify reads, run grants
internal/connection connecting tools, in batches, and applying approved profiles
internal/invocation the call ledger: one send per approval, never re-sent
internal/egress which tool URLs may be reached (no internal addresses)
internal/secret sealed tool credentials, redacted from results and logs
internal/kiffapi the gateway's client for the API
api/ the decision path (a second Go module)
internal/mandate Cards: limits per subject, holds and the decision
internal/proposals decided operations
internal/permit execution permits: Ed25519 JWTs over the exact arguments;
run grants and read grants
internal/tooloutcome how an allowed call ended at the tool; "Did this happen?"
internal/halts the owner's halt, and planque's confirmation of it
internal/receipt receipts, their attestation and its verification
internal/anchor anchoring receipt batches; Merkle proofs
cmd/kiff-verify checks a record against an anchored batch
internal/... what those packages import
Not yet published: the orchestration that ties a decision together in the API's HTTP layer, including refusing a halted agent's calls at decision time. That layer is bundled with billing, support and operations code today; the orchestration moves to its own package after v1 and is published then.
Key columns only; the full schemas are in each package's sql/ folder. A
Card is one mandates row, and its balance comes from its draws in the
window. A held call is a mandate_exceptions row; approving it lets it draw
once. The gateway's ledger is separate: it decides nothing, it makes sure
nothing is sent twice. Relationships are by id; not all of them are
database foreign keys.
Two services, two modules. The gateway reaches the API only over HTTP
(KIFF_API_URL). Key material stays server-side: Ed25519 for permits and
signed halt asks, secp256k1 for receipt attestations and anchoring. Hosting,
networks and deploy configuration aren't part of this export.
status.kiff.dev runs apart from KIFF's own
services.
What crosses each line: agent to KIFF, the call and never a vendor key; KIFF to the tool, the call with its permit; planque to the vendor, the call with the sign-in planque keeps.
| Repository | What it is |
|---|---|
| kiff/cloud | You are here: the enforcement core. |
| kiff/tafs | KIFF's own app: hand routine work to agents, each under its Card, and keep the keys. The app and its cloud service. |
| kiff/planque | Keeps your tools' sign-ins and sends the calls KIFF allows. |
| kiff/agents | The runtime tafs' agents run on. |
| kiff/kiff | The KIFF framework (MIT): the open-source engine behind KIFF Cards, run on your own machine. |
| kiff/status | The source of status.kiff.dev. |
| kiff/kiff-plugins | KIFF for Claude Code: the gateway, and skills to connect an agent and handle held calls. |
| kiff/kiff-guard | Put an agent you already run under a Card (MIT): Python and TypeScript, with adapters for Agno, LangGraph, OpenAI, Google ADK, Strands, n8n and your own code. |
| kiff/kiff-scan | What can your AI agent do without asking? Finds functions a model can call that delete data, deploy, move money or run a shell, with nothing on the path that can refuse. uvx kiff-scan scan . (MIT) |
Each guarantee is stated as narrowly as its tests prove, and every test named
here is in this repository. Tests whose doc comment carries an
// Invariant: line are also listed on
kiff.dev/invariants.
| Guarantee | Proved by |
|---|---|
Every call is decided. The gateway sends a call only once KIFF has allowed it, and a call it hasn't recorded is never sent.gateway/internal/gateway |
TestWithinCardForwardsOutsideHoldsTestUnrecordedCallIsNotSent |
No key in the agent's hands. On verify the tool gets the call with its permit and no credential. On relay the call goes to planque with its permit and a transport credential for the relay; the tool's own sign-in stays in planque. A call allowed without its permit isn't sent.gateway/internal/gateway |
TestVerifyModeSendsThePermitAndNoCredentialTestRelayModeSendsTransportCredentialAndPermitTestAllowedWithoutPermitIsNotSent |
A permit covers one exact call. An Ed25519 JWT over the SHA-256 of the call's canonical arguments; the arguments sent are those it was minted for.api/internal/permit, gateway/internal/gateway |
TestPermitBindsTheFixedArgumentsTestArgsSHA256IgnoresOrderAndWhitespaceTestLifetimeIsCapped |
Limits hold. A call past a Card's limit is refused or held, totals count every allowed call, and an expired Card doesn't fall through to no limit. Room comes back only on the tool's own trusted, final outcome that nothing was done; the operation stays spent and is never sent again.api/internal/mandate |
TestPerActionLimitTestCumulativeLimitRefusesTheActionThatCrossesItTestExpiredMandateDoesNotFallThroughToUnboundedTestMandateFailsClosedUnlikeTheUsageLimiterTestReleasedEffectsRestoreRoomWithoutReturningAttemptsTestMixedEffectReleaseIdempotentAndOperationStaysSpent |
Approval is narrow. Approving a held call covers that call once, on the resource it named. An unused approval is withdrawn, and a new breach after it waits for room.api/internal/mandate |
TestApproveOnceTestResourceOutsideComesHomeAndApprovalBindsTheResourceTestUnusedApprovalIsWithdrawnTestNewCumulativeBreachAfterApprovalWaitsForRoom |
An amount comes from the record, never from the agent. A write is limited by the amount its resource has; an amount the agent sends is refused, and a call whose amount can't be read isn't sent.gateway/internal/gateway |
TestAWriteIsLimitedByTheAmountItsResourceHasTestAnAmountTheAgentSendsIsRefusedTestACallWhoseAmountCantBeReadIsNotSent |
What the vendor did is read back. A verify read that differs pauses the operation, a vendor's larger figure is counted on the Card once, and an untried money operation starts on probation.gateway/internal/gateway, api/internal/mandate |
TestAVerifyReadThatDiffersPausesTheOperationTestEffectSurchargeCountsTheLargerFigureOnceTestTheFirstThreeCallsOfAnUntriedOperationAreOnProbation |
A profile needs the owner. A connection profile applies only with the owner's signed approval.gateway/internal/connection |
TestProfileAppliesOnlyWithTheOwnersApproval |
One send per approval. An approved call is sent once, even under concurrent retries. A call whose outcome is lost is recorded as unknown and never re-sent.gateway/internal/gateway |
TestApprovedHoldForwardsOnceUnderConcurrentRetriesTestCrashWhileForwardingIsUnknownAndNotResent |
The halt reaches the vendor's side. A halt is shown as done only once each planque has confirmed it, over a request signed for that halt.api/internal/halts |
TestPlanquesConfirmOnlyWhenEachSeesTheHaltTestPlanquesAskIsSignedForThatHalt |
Receipts are verifiable. A receipt's causal chain has one pinned encoding, its signature fails on any change, and a tampered receipt fails verification against its anchor.api/internal/receipt |
TestCausalChainCanonicalEncodingIsPinnedTestAnchorAttester_TamperedReceiptFailsOnchainVerify |
- Credentials handed to an agent outside KIFF and planque. "No key in the agent's hands" holds on supported connections, the ones KIFF's gateway and planque make.
- Undoing what a vendor already did. A halt stops new calls; a request already sent may still complete.
- The trusted boundary is KIFF, planque and your approval device. Attacks on your own servers, or on a device you approve with, are outside it.
- Part of the halt isn't here yet. Refusing a halted agent's calls at decision time lives in the API's HTTP layer, which isn't published yet. planque's own check at dispatch is public in kiff/planque.
Go 1.25 or later. Each module builds and tests on its own:
cd gateway && go test -race ./...
cd api && go test -race ./...
Tests that need Postgres skip unless KIFF_TEST_POSTGRES_URL points at a
database the tests may create databases in, for example
postgres://kiff:kiff@localhost:5432/postgres?sslmode=disable. Run them with
TZ=UTC.
It's an export of KIFF's private repository, taken at the commit named in
SOURCE, so the code you read is the code that runs. Pull
requests aren't merged here: changes land in the private repository and
reach this one with the next export.
Found a way around a Card, a permit, a halt or a receipt? We want to hear it first. Report it privately: see SECURITY.md.
FSL-1.1-ALv2: use, change and redistribute it for any purpose other than a competing product. Each version becomes Apache 2.0 two years after it's released.



