v0.5.0: vendor-neutral HITL protocol, Teams without a bot, and the gate an agent cannot route around - #7
Merged
Conversation
…vals Adds a vendor-neutral wire protocol between draftcat and any external presenter, so reaching a new operator surface no longer means draftcat taking on that vendor's bot lifecycle. draftcat stays the gate: policy, quorum, expiry, payload hash, approver membership and the audit trail. A relay owns only presentation. The relay is untrusted -- five checks (body-bound HMAC, clock-skew window, single-use nonce, payload-hash echo, approver membership) mean a compromised relay can deny by staying silent, which times the gate out and fires nothing, but can never authorise. Microsoft Teams is reached with one Power Automate flow in the customer's own tenant: HTTP trigger, "Post adaptive card and wait for a response", HTTP POST back. No Azure app registration, no bot, no tenant admin consent. The same three steps serve Slack, email or a shell script. Closes #5. - internal/relay: hitl/v0 envelopes, signing, and the verification rules, reusing the existing t=<unix>,v1=<hex> webhook signature scheme rather than adding a second one - relay_channel.go: complete OperatorChannel round trip including the callback server, with distinct-approver quorum counting. This is what qualifies `relay` for internal/channels, which admits a name only when a working implementation ships - run_context.go + state: one run id minted per pipeline run, carried on the envelope and recorded on action_approvals, so the trail can answer which run an approval released. Added as a correlation column outside the receipt signature, so receipts written before this release still verify - draftcat hitl verify <relay-url>: conformance suite that dispatches a real approval and checks the decision, including that a replay, a mutated payload hash and an out-of-scope approver are all refused - validate: relay config is checked at boot on the same terms as the Telegram channel, and quorum now counts against the operator pool of the channel the step actually names docs/hitl-protocol.md carries the specification and the Teams setup.
Completes the v0.5.0 set on top of the hitl/v0 relay. Risk tiers + approval_policy. A step declares risk: low|normal|high, and an operator can decide in advance that a declared class needs no fresh tap. The reason is that a gate people switch off protects nothing: the observed workaround for approval fatigue is disabling approval wholesale, which trades a narrow audited exemption for a total one. High risk is never eligible, enforced inside AutoApproveRule.Match 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 written as decision "policy_approve" with the rule that fired, so the trail never conflates a policy release with a human decision. Tool-call gate at POST /gate/tool-call. The gate covered declared pipeline steps; a harness calling MCP or SDK tools mid-run went around it. A harness now asks permission for one call and gets allow or deny back, with the same allowlist, risk tiers, human approval and audit trail. Denies by default, so an unconfigured tool fails closed, and the decision is bound to a hash of the exact arguments proposed rather than to the tool name alone. Arguments are hashed, never stored. Spend at the 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. 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. contrib/teams-relay ships a conformant relay plus the Adaptive Card, the Power Automate request schema and the setup both ways round. Verified end-to-end: draftcat hitl verify against the built relay passes 12/12 with a real approve round trip, and a deliberately forging relay fails the suite.
log.Fatalf on a relay that failed to start ran after `defer state.Close()` and `defer vbridge.Shutdown()` were already registered, so a misconfigured relay exited leaving the SQLite WAL unclosed and the voice bridge running. The relay only needs cfg.Relay, so it is now constructed right after startup validation, before either defer exists. A relay that cannot start still refuses the boot, but now fails while there is nothing to unwind.
Keeps the top block to the latest two releases, so v0.3.1 drops off as v0.5.0 lands. Folds the v0.5.0 governance entries into the existing list rather than appending to it: spend-at-decision joins the cost-budget line, run correlation joins durable gates, and the channel entry leads with what an operator does about Teams instead of restating the protocol. Adds `draftcat hitl verify` to the command list. Net shorter than before despite six new features.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5.
The six highest-ranked opportunities from a fresh mining pass, plus a working Microsoft Teams path.
What's here
hitl/v0protocol +relaychannelinternal/relay,relay_channel.go, spec, conformance commandapproval_policyrisk: low | normal | high, auditedpolicy_approverowsrun_idminted per run, on the envelope and onaction_approvalsPOST /gate/tool-call, default-deny, bound to an args hashescalate_after/escalate_toOn #5: the approach in that issue is no longer available
Researched fresh, and it changes the answer rather than just the estimate:
chatMessage— send-only; card actions other thanOpenUrlneed a botSo
teams.goplus a webhook URL cannot work at all. What remains in-binary is an Azure app registration and tenant admin consent, per vendor, forever.The design
Split 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, ±5min clock window, single-use nonce, 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 can never authorise.
Teams then needs one Power Automate flow in the customer's own tenant: HTTP trigger, Post adaptive card and wait for a response, HTTP POST back. No Azure app registration, no bot, no admin consent for draftcat. Slack, email, PagerDuty and a shell script are the same three steps.
relayearns its place ininternal/channelson the same terms as Telegram: a completeOperatorChannelround trip including the callback server ships in the binary.Verification
go vetclean, zero new lint issues (CI isonly-new-issues)draftcat hitl verifyagainst the builtcontrib/teams-relay, human taps Approve, signed decision returns, gate accepts. 12/12, exit 0Three calls worth a look
policy_approveis its own decision value, notapprovewith a flag, so an auditor scanningdecisionsees immediately that no human tapped. High-risk steps are excluded insideAutoApproveRule.Matchso no call site can route around it, and an unscoped rule is a validate error — that is how a policy tier silently becomes "no gate".escalate_toto the permitted approvers would let a slow operator promote someone the config never approved.run_idis a correlation column outside the receipt signature. Adding it toapproval.Fieldswould invalidate the HMAC on every row signed before this release, so existing stores would failaudit-verifyafter upgrading.Not in scope
Telegram is still required at boot in a relay-only deployment: draftcat runs one operator channel and Telegram still serves the command surface. Making it fully optional is a larger refactor than this release warranted.