Docs Start here
Connect an external approver
A separately operated service answers requests held by an Ask Card. KIFF checks its bounded grant; the worker’s normal MCP collection executes the approved call. This connector holds no worker key, tool credential or admin session. Python 3.9+ is sufficient; there are no dependencies. Docker is optional.
The connector and configuration examples live in the kiff-guard cookbook.
1. Grant access in KIFF
Open Cards, choose an active Card, and expand External approver → Connect an external approver. The Card must use Ask and have a per-action amount limit. Choose a name, its worker/action, a maximum amount and an expiry (up to 90 days). Only an active admin can grant access. Human-only separation of duties prevents external grants.
Copy the kiff_approver_… credential shown once. Keep it in the separate service,
never in the worker’s prompt or environment. Lost it? Revoke that connection and
create another. The grant cannot add budget, revise the Card or execute a tool.
2. Run the example
git clone https://github.com/kiff/kiff-guard.git
cd kiff-guard/cookbook/external-approver
cp connector.example.json connector.json
cp refund-policy.example.json refund-policy.json
Edit connector.json to set context_file to refund-policy.json. Adapt the
policy to your connected action, amount parameter and target argument. Amounts
in the policy file use the tool’s stored integer units: 10000 means €100
when the tool declares EUR with scale 2. The example’s amount_eur uses whole
euros (scale 0). Give the grant a ceiling above the Card’s ordinary per-action
threshold, while retaining its cumulative budget.
The included synthetic facts let synthetic-order-1 be refunded up to 100 stored
units; synthetic-order-2 is not refundable. Other targets defer. In real use,
replace these fixtures with independently authenticated business facts. Never
copy the worker’s claims into this trusted file.
Enter the credential without placing it in shell history:
read -r -s KIFF_APPROVER_KEY
export KIFF_APPROVER_KEY
python3 connector.py --config connector.json
It polls every 15 seconds. After the first successful poll, reload the Card: Awaiting connection becomes Connected. More than two minutes without a successful poll shows Not responding. This proves API contact, not model health or an execution guarantee. Ctrl-C stops the service. Use your usual process supervisor to keep it running; KIFF does not host it.
To process one batch and exit:
python3 connector.py --config connector.json --once
Now have the worker make an MCP call above its Card’s per-action threshold and
inside the grant, using the synthetic target. The connector answers it. The
worker/plugin collects the approved call using kiff_pending and the same
arguments/operation id. KIFF rechecks all Card budgets; the tool runs once.
On Needs you, open the request to see the service’s name, decision, reason and the separately recorded tool outcome. Approval alone does not mean it ran. Stop the connector or call a target outside its policy: the request stays available for human review within the original deadline. An out-of-grant call is never listed for this connector. Revoke the connection from the Card to withdraw unused approvals; actions already executed remain historical.
Optional model providers
Set provider to jev and supply TYPESAFE_API_KEY through your secret manager,
or set it to strands and add strands_url pointing to your separately running
Decider’s /v1/systemone. Keep context_file: the model needs trusted policy
and facts separate from request content. Model errors or malformed answers
defer to a person; confidence never widens authority.
Model integration is unverified. The mappings follow the published Jev API and Strands Decider contract, but no credentialed model call has been validated. Deterministic connector and synthetic MCP validation are documented in the Cloud release PR. Do not treat these mappings as a tested model integration.
Optional Docker
docker build -t kiff-external-approver .
docker run --rm --read-only --cap-drop=ALL --security-opt=no-new-privileges \
--env KIFF_APPROVER_KEY \
--mount type=bind,src="$(pwd)",dst=/config,readonly \
kiff-external-approver
For Jev, pass --env TYPESAFE_API_KEY too. Mount only the intended config/facts
in production. No Docker socket, inbound port or worker credentials are needed.
The container example has not been validated unless the release evidence says
otherwise; the plain Python command is the supported runnable path.
Interoperability contract
A signed-in admin creates POST /v1/me/approver-grants with name, card_id,
worker, action, parameter, max, and RFC3339 expires_at. The principal is
generated when omitted. List status with GET /v1/me/approver-grants?card_id=…;
revoke with POST /v1/me/approver-grants/{id}/revoke.
The dedicated credential only accesses:
GET /v1/approver/holds: matching requests; a successful read records contact.GET /v1/approver/holds/{id}: current exact arguments, Card, grant and token.POST /v1/approver/holds/{id}/answer:review_token,decision(approve,reject,defer), requiredreason_code, optionalreasonandmodel.
The token binds the answer to the current held revision and Card. Duplicate matching answers are idempotent. 409 means stale/answered/expired; reread, never invent new authority. 403 means grant withdrawn, expired or no longer valid; stop and contact its admin. Uncovered holds return 404. Read failure returns 503 and grants nothing. Original deadlines, cumulative ceilings, human review and tenant isolation continue to apply. Only one worker/action/amount parameter and one Card are supported per grant; overlapping uncovered authority requires a person. The connector never supplies its own execution outcome.
python3 -m unittest -v test_connector.py