Skip to content

v0.15.0: harness hooks, approved HTTP requests, scheduled anchors, Slack relay, trip, definition files, budget endpoint, shadow policy, call caps - #19

Merged
renezander030 merged 14 commits into
masterfrom
release/v0.15.0
Nov 6, 2026
Merged

renezander030 merged 14 commits into
masterfrom
release/v0.15.0

Conversation

@renezander030

Copy link
Copy Markdown
Owner

What's new

  • Harness hooks. draftcat gate hook --format claude|codex|copilot|gemini runs as the pre-tool hook of Claude Code, Codex, GitHub Copilot CLI or Gemini CLI: it asks the gate about each call, waits for the decision, consumes the permit and answers allow or deny in the harness's format. A gate that is down, refuses the token, does not decide in time, denies or refuses the consume blocks the call. draftcat gate install <harness> writes the hook into the harness's settings file, keeps every other key and hook, and --remove takes out only draftcat's entry.
  • http_request action. Sends one HTTP request after an approval step, with a credential read from auth_env that the agent never holds. The approval step shows the request itself and is bound to exactly that request; it is sent once with an Idempotency-Key, no retry, no redirects.
  • Scheduled receipt anchors. receipts.anchor: {every | at, timezone, file, notify} appends the anchor to a file and sends it to the operator channel when the history changed. receipts verify-anchor --log anchors.jsonl names the first changed receipt.
  • Slack relay. contrib/slack-relay, a stdlib-only hitl/v0 relay with Approve, Skip and Adjust buttons that verifies Slack's request signature on every click and maps Slack users to operator identities.
  • Trip. tool_gate.trip: {denials, within, scope} freezes an agent the gate keeps refusing (or the whole gate), tells the operator once, and is lifted with /unfreeze <agent> or gate unfreeze --agent.
  • Definition files. definition_file on a tool rule pins the approved definition itself; a changed definition is named field by field in the prompt and the denial reason, and gate diff-definition prints the same summary.
  • GET /gate/budget. Today's usage against the daily and per-pipeline caps, the frozen flag, and the caller's own capped tools, behind the gate's tokens, read-only.
  • Shadow policy. tool_gate.shadow: {config} decides every live call a second time under a candidate policy without touching the live decision, stores each difference by argument hash, and gate shadow groups them.
  • Voice call caps enforced. budgets.per_day_calls and per_day_call_minutes now refuse new calls (HTTP 429) and are recorded durably.
  • draftcat hitl verify binds its callback listener before dispatching.
  • Includes the 0.14.1 native-installation fix (interrupted or incomplete downloads fail, replacement executables are verified before the switch) as its first commit.

Verification

  • gofmt, go build ./..., go vet ./... and go vet -tags voice ./... pass.
  • go test -short ./..., go test -short -tags voice ./... and the full non-short suite pass; each feature carries its own tests (about 5,900 added test lines).
  • go run . validate reports 0 errors. golangci-lint run --new-from-rev on master reports 0 issues, also with --build-tags voice.
  • contrib/teams-relay and contrib/slack-relay pass gofmt, vet and their tests. The Slack relay passes draftcat hitl verify 12/12 in an end-to-end test against a fake Slack API.
  • npm ci --ignore-scripts && npm test && npm pack --dry-run pass (20 installer tests).
  • CLI help and usage texts of every command were compared before and after the tidy-up commit: identical.
  • Not run: a live Slack workspace, the harness hooks inside a live harness, and the release workflow's cross-builds.

Review notes

  • Base: c757b57 (master, v0.14.0). Version 0.14.0 → 0.15.0 in version.go, package.json and both package-lock.json entries.
  • Commit 1 is the 0.14.1 native-installation fix, unchanged. Commit 2, Tidy-up: 8 behavior-preserving refactors, is a self-contained cleanup and reviews on its own. Each feature follows as its own commit, then one integration fix (the shadow candidate reads definition_file), then the release commit.
  • Every new setting is optional; configurations without them decide exactly as before, and policy hashes and permit bindings are unchanged. New tables: receipt_anchor_state, gate_agent_freeze, gate_shadow_diffs, budget_voice_calls, plus an index on tool_actions(tool, created_at).
  • Behaviour changes to read: in pipelines with http_request the approval message is the request itself; voice builds record calls whether or not caps are set; /unfreeze <agent> lifts one agent's freeze.
  • The open 0.14.1 pull request is contained here as commit 1; merging this one releases both, and the 0.14.1 pull request can be closed afterwards. The CHANGELOG keeps its 0.14.1 entry below 0.15.0.

Evidence

  • The full CI set (build, vet with and without voice, short and full tests, validate, lint on new code, npm) passes on the final tree, and passed on each feature commit on its own before they were combined.
  • The Slack relay's conformance run (draftcat hitl verify, 12 of 12) is an automated test in contrib/slack-relay.
  • The definition-file pin keeps the golden policy hash of a definition_sha256 rule with the same content (TestToolGateHashesAreStable).

Door

Two-way. Every feature is opt-in behind a new setting, command or action, and a configuration without them behaves as v0.14.0; the new tables are additive and a downgrade ignores them. The release is undone by not tagging it, or by a follow-up revert.

Blast radius

  • Touched: the tool gate (tool_gate.go, tool_policy.go, freeze, shadow), config and validation, the state store (additive tables), the voice receivers (call caps), the deterministic action switch (http_request), the receipts commands, hitl verify, docs and contrib/.
  • Untouched: receipt formats and signatures, permit binding, the hitl/v0 protocol, existing actions and connectors, the release workflow.
  • Highest-care areas: the voice receivers now answer 429 or 503 when caps are set; the reload path now re-reads definition_file content and the shadow candidate.

- The tool gate binds a call's identity once and reads it from the request.
- One strict JSON body reader serves every gate endpoint.
- One way to spend a permit: unused state store entry points are gone.
- Config.Pipeline(name) answers which pipeline a name refers to.
- One config reader serves the subcommands.
- One run-history query in the state store.
- gate test reads and runs its cases in its own functions beside the compare.
- The startup checks return their verdict with validate.Summarize.

CLI help, config keys, stored formats and receipts are unchanged.
draftcat gate hook runs as the pre-tool hook of Claude Code, Codex,
GitHub Copilot CLI or Gemini CLI. It reads the harness payload, asks
POST /gate/tool-call with a stable action_id, waits and polls for a
human decision, consumes the permit and answers allow or deny in the
harness's own format. Anything short of permit: execute blocks the call.

draftcat gate install writes that hook into the harness settings file
(project or --user), keeping other keys and hooks, idempotent, with
--remove, --dry-run and a list of the tool names the matcher sends.
…t to the operator channel when the history changed
contrib/slack-relay is a stdlib-only hitl/v0 relay in its own module. It
verifies draftcat's signed dispatch (signature window, protocol, payload
hash against the draft, callback, expiry), posts the draft to a Slack
channel as Block Kit with a button per offered decision, verifies
Slack's v0 request signature on every click, maps the Slack user to an
operator identity, and posts one signed decision back. The message is
updated with the outcome and loses its buttons; a later click changes
nothing. A test runs `draftcat hitl verify` against it end to end.

`channel: slack` (and `channel: teams`) stay validation errors, now
pointing at `channel: relay` and the relay in contrib/.
The conformance suite listened in the background and dispatched at once,
so a relay that answered within milliseconds could find no listener, and
a callback port already in use left the suite waiting out its whole
timeout. It now binds first and fails at once if it cannot.
A tool rule can pin its definition to a file holding the approved
definition instead of to its hash. The pin is the file's canonical hash,
so a rule moved from definition_sha256 to a file with the same content
decides the same and keeps its policy hash. When a call's definition
differs, the approval prompt and the denial reason name the changed
fields and inputSchema paths, bounded and without echoing long text.
The file is read at start, by validate and the offline checks, and again
on every reload; a missing or unparsable file is refused. New command
draftcat gate diff-definition compares two definitions the same way.
…tial draftcat holds

The approval step before it shows the request itself and binds its
approval to a digest of exactly that request; the action sends only a
matching request, once, with an Idempotency-Key, no retry and no
redirects. The credential comes from auth_env and joins the redaction
set. Validation requires an approval step before each request.
…inutes

Calls are counted at session_start and their minutes recorded at
session_end, once per session id, in the budget ledger the other daily
caps use. At a cap, pre_call_context and session_start answer 429 and
dograh_staging_smoke is refused, with one operator notice per cap and
UTC day. With caps set, an unreadable ledger refuses the call; without
caps nothing is refused. /status and budget status show the recorded
usage against the caps.
tool_gate.trip counts, per agent, the denials the gate makes (allowlist,
caller tools, deny rules, daily caps, the repeat guard, operator refusals)
and freezes the agent, or with scope: gate the whole gate, once it reaches
the threshold inside the window. Frozen-gate and frozen-agent denials, timeouts
and unavailable stores are not counted. The agent freeze is stored in the new
gate_agent_freeze table, survives a restart, refuses the agent's consumes and
grants, is listed by /status, gate status and the digest, and is lifted with
/unfreeze <agent> or gate unfreeze --agent. One operator notice per trip.
tool_gate.shadow names a candidate config. After each live decision the
candidate's tool_gate decides the same call in the background, as gate check
does, with today's totals read from the live store; it never changes, delays
or fails the live decision, and failures and panics are logged and counted.
A differing verdict stores one row in the new gate_shadow_diffs table (time,
agent, tool, args hash, value-free live and shadow verdicts, candidate hash),
kept 30 days. draftcat gate shadow lists the differences grouped by tool and
decision pair. The candidate is re-read on reload and a broken one switches
the shadow off without holding up the reload. /metrics counts differences
and errors.
A candidate rule pinned by definition_file now decides on that file, as a
live rule does, and a candidate whose file cannot be read is refused.
@renezander030
renezander030 merged commit cf99fba into master Nov 6, 2026
@renezander030
renezander030 deleted the release/v0.15.0 branch November 6, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant