Problem
In-conversation approval shipped in #4 (v0.1.0/0.1.1): when a payment needs
human approval, pay.stage() returns a staged intent and the MCP server
exposes approve_payment / reject_payment keyed by intent_id. Today the
approval is recorded as a stream of audit events (approval_staged →
approval_received → payment_completed|payment_denied), not as a single
structured, queryable decision record.
@rpelevin raised this in #4:
"the human said yes in chat" should be a verifiable decision boundary, not a
vague transcript claim. The approval should mint a record that binds exactly
what was approved.
What's bound today
The staged intent + audit chain already carry:
intent_id
amount, currency
recipient_wallet, recipient_url (the target endpoint)
intent (the agent's own stated reason for the spend)
rule_fired (the policy threshold that was crossed)
- audit events keyed by
intent_id
Proposed: a single ApprovalRecord
Bind, in one structured object returned at approval time and persisted:
mcp_server + tool (provenance)
intent_id
amount, currency, recipient_wallet, recipient_url
actor / principal — who initiated; matters once more than one agent or human shares a wallet. New.
reason + policy threshold crossed (rule_fired)
expires_at — driven by the policy's own timeout_seconds, not a fixed TTL. Overlaps the approval-timeout gap (today the MCP server uses a fixed 1h TTL).
params_hash — hash of the exact request (url + method + body + amount) so the approved request == the settled request, closing the approve→settle TOCTOU. New, and worth it.
route: allow | revise | human_review | stop — revise (counter-offer a lower cap instead of a binary yes/no) is new.
Open questions
- Append-only audit JSONL with these fields added, or a separate signed/structured record? blacktea keeps the audit log narrow on purpose; this is the natural place to widen it.
revise semantics: a real counter-offer flow, or out of scope for v1?
- Where does
actor/principal come from across the SDK / CLI / MCP surfaces?
Related
Design push credit: @rpelevin.
Problem
In-conversation approval shipped in #4 (v0.1.0/0.1.1): when a payment needs
human approval,
pay.stage()returns a staged intent and the MCP serverexposes
approve_payment/reject_paymentkeyed byintent_id. Today theapproval is recorded as a stream of audit events (
approval_staged→approval_received→payment_completed|payment_denied), not as a singlestructured, queryable decision record.
@rpelevin raised this in #4:
"the human said yes in chat" should be a verifiable decision boundary, not a
vague transcript claim. The approval should mint a record that binds exactly
what was approved.
What's bound today
The staged intent + audit chain already carry:
intent_idamount,currencyrecipient_wallet,recipient_url(the target endpoint)intent(the agent's own stated reason for the spend)rule_fired(the policy threshold that was crossed)intent_idProposed: a single ApprovalRecord
Bind, in one structured object returned at approval time and persisted:
mcp_server+tool(provenance)intent_idamount,currency,recipient_wallet,recipient_urlactor/principal— who initiated; matters once more than one agent or human shares a wallet. New.reason+ policy threshold crossed (rule_fired)expires_at— driven by the policy's owntimeout_seconds, not a fixed TTL. Overlaps the approval-timeout gap (today the MCP server uses a fixed 1h TTL).params_hash— hash of the exact request (url + method + body + amount) so the approved request == the settled request, closing the approve→settle TOCTOU. New, and worth it.route:allow | revise | human_review | stop—revise(counter-offer a lower cap instead of a binary yes/no) is new.Open questions
revisesemantics: a real counter-offer flow, or out of scope for v1?actor/principalcome from across the SDK / CLI / MCP surfaces?Related
blacktea approve <intent-id>#3 — out-of-band approval (separate trust model).Design push credit: @rpelevin.