v0.5.0 — approvals anywhere, and the tool calls too
Approvals that reach the operator wherever they already are, and a gate that now covers an agent's individual tool calls.
Everything is additive. With no relay:, no approval_policy:, no tool_gate: and no risk: on any step, v0.4.0 behavior is unchanged.
Approvals on any operator surface
Adding an operator channel used to mean draftcat taking on a vendor's bot lifecycle. Microsoft Teams made the cost concrete: incoming webhooks and O365 connectors were disabled in May 2026, Graph chatMessage cannot receive a card submit, the Graph Approvals API is still beta, and the Python Bot Framework SDK is archived. What is left in-binary is an Azure app registration and tenant admin consent, per vendor, forever.
The hitl/v0 protocol splits the gate from the presenter. draftcat keeps what must not be delegated — policy, quorum, expiry, the payload hash, approver membership, the audit trail. A relay owns only presentation.
The relay is untrusted. Five checks stand between it and a forged approval: body-bound HMAC, a clock-skew window, a single-use nonce, the payload-hash echo, and approver membership checked against draftcat's own config. A compromised relay can deny by staying silent, which times the gate out and fires nothing, but it can never authorise.
Teams then needs one Power Automate flow in your own tenant: HTTP trigger, Post adaptive card and wait for a response, HTTP POST back. No bot, no Azure app registration, no admin consent. Slack, email, PagerDuty and a shell script are the same three steps.
draftcat hitl verify https://prod-42.westeurope.logic.azure.com/workflows/...The conformance suite dispatches a real approval and checks the decision that comes back, including that a replay, a mutated payload hash and an out-of-scope approver are all refused. contrib/teams-relay/ ships a working relay, the Adaptive Card and the request schema.
relay earns its place in internal/channels on the same terms as Telegram: a complete round trip ships in the binary, callback server included.
A gate an agent's tool calls cannot route around
For a declared pipeline the gate was structural. For a harness also calling MCP or SDK tools mid-run it held by convention only — those calls never reached it.
POST /gate/tool-call
{"tool": "send_email", "args": {"to": "anna@example.com"}, "agent": "harness-1"}
Allowlist, risk tier, human approval and audit row, same as a pipeline step. It denies by default, so a tool nobody listed fails closed, and the decision binds to a hash of the exact arguments proposed — approving send_email for one recipient does not approve it for another. Arguments are hashed, never stored.
Risk tiers you declare in advance
A gate people switch off protects nothing, and the usual workaround for approval fatigue is disabling approval wholesale — trading a narrow exemption for a total one.
approval_policy:
auto_approve:
- risk: low
pipeline: invoices
max_cost: 0.50
reason: internal drafts onlySteps declare risk: low | normal | high. High risk is never eligible, enforced where the match is decided so no call site can route around it, and an unscoped rule is a validate error rather than a silent removal of the gate. Every exemption is recorded as policy_approve with the rule that fired, so the trail never mistakes a policy release for a human decision.
Spend at the moment of decision
Cost caps were enforced between calls but never shown to the person releasing the action. The approval prompt and the hitl/v0 envelope now carry what the run has spent against its caps.
Escalation before the gate dies
A pending gate ran silently to timeout, so a run could die waiting on an operator who never saw the prompt. escalate_after re-notifies while the gate is still open and escalate_to names who else to tell. It widens who is told, never who may decide — authority is a config decision, not a timer.
Run-correlated audit
Every approval decision now records the pipeline run it released, so the trail answers which run an approval let proceed rather than only which pipeline. run_id is a correlation column outside the receipt signature, so receipts written before this release still pass draftcat audit-verify.
Upgrading
docker pull ghcr.io/renezander030/draftcat:0.5.0, or rebuild from source. The state schema migrates itself on open. Existing configs need no changes.