Docs Product
Tools that check KIFF: no key at KIFF
By default, a tool you connect to the KIFF 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 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:
pip install "kiff-guard[verifier]"
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="<your KIFF account id>",
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 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: Ed25519andtyp: 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 markedunknownand listed byverifier.unknown()for a person to check. Runverifier.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:
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, anddistrust=[...]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.