Skip to content

Releases: renezander030/draftcat

v0.6.0 — the gate holds under load

Choose a tag to compare

@renezander030 renezander030 released this 05 Sep 06:51
19f2d9a

The tool-call gate now holds for real harnesses, and the operator's taps no longer race each other.

Everything is additive for a config without tool_gate:. Three defaults change behaviour where the tool gate or OpenRouter is already in use — see Compatibility.

Decisions that survive the harness

A decision that needs a human takes as long as the human takes, and an HTTP client that gave up after 30 seconds used to lose it. POST /gate/tool-call now takes "mode": "async" (answers 202 with an approval_id at once) or "wait": "30s" (holds that long, then 202 with the id while the approval keeps running). The harness collects the decision from GET /gate/tool-call/<id>, optionally long-polling with ?wait=30s. An unknown id is 404 deny — the fail-closed answer for "the gate restarted, ask again". The plain sync call is unchanged and now also carries the approval_id.

A tool call waiting on a human is written to pending_approvals before the prompt goes out, exactly like a pipeline gate: it appears in /pending, and a process that dies mid-wait reconciles it as interrupted at next boot instead of the gate vanishing.

Rules that read the arguments

tool_gate:
  tools:
    - name: send_email
      risk: high
      require_approval: true
      args:
        to: {glob: "*@example.com"}   # inside the rule: ask as usual
      on_mismatch: deny               # outside it: refuse without asking

args: constrains top-level arguments with equals, one_of, glob, regex, min/max and optional. A match keeps the rule's base behaviour. A mismatch either asks a human (on_mismatch: approve, the default) or refuses (deny) — it can only tighten a rule, never widen it. draftcat validate refuses a regex that does not compile, a malformed glob, an empty constraint, min > max, and remember_approval on a high-risk tool. Every decision the gate makes on its own now names its rule in the response.

One prompt per call, not per retry

Inside repeat_window (default 10m, 0 disables) an identical call — same agent, tool and argument hash — gets the gate's remembered answer. A call the operator or a rule denied is denied again without a new prompt. An identical call still being decided joins the open prompt and receives the same decision. max_repeats caps how many prompts one identical call may raise. Approvals are not reused unless a rule opts in with remember_approval: true, because the next send has the same side effect as the last.

Nothing refused silently

A refusal the gate makes on its own — an unlisted tool, an argument outside a deny rule, the repeat guard — is reported to the operator channel, one notice per agent, tool and reason inside notify_window (default 10m). notify_denials: false turns it off.

One Telegram update pump

Every approval waiter and the command loop each polled getUpdates on their own ticker against one unsynchronised offset. With scheduled, webhook-triggered and tool-gate approvals all running concurrently, two pollers were routinely live at once, and whichever fetched the operator's tap kept it — a tap that landed in the command loop was dropped and the gate timed out. Now one goroutine polls and routes: a callback to the waiter that owns the message, adjustment text to the waiter that asked for it (slash commands always reach the command loop), everything else to the command loop. Same buttons, same security checks, same messages. Also fixed: an Adjust tap dereferenced a nil message, and the per-user rate limiter's map was written from concurrent gates without a lock.

Budgets on the real charge, calls that wait their turn

On OpenRouter every call asks for usage accounting and the cost caps are enforced on the charge the provider reports, reasoning tokens and cached-token discounts included; the configured per-1k rates remain the fallback and the only source for other OpenAI-compatible endpoints. provider.usage_accounting: false opts out. Transient failures (429, 408, 5xx, network) are retried with jittered backoff that honours Retry-After; other 4xx fail immediately as before. provider.max_retries sets the budget.

What is waiting on you

/pending on the channel and draftcat pending [--json] on the host list every gate waiting on a human — pipeline steps and tool calls — with age, time left and quorum. /status shows tokens and spend against every configured cap and the count of open gates.

Compatibility

Where the tool gate is already in use: the repeat guard is on (repeat_window: 0 restores re-prompting) and denial notices are on. Where the provider is OpenRouter: cost caps now count the provider's reported USD charge rather than the configured rates. The state schema is unchanged.

Upgrading

docker pull ghcr.io/renezander030/draftcat:0.6.0, or rebuild from source. Existing configs need no changes. Full contract for the gate: docs/tool-gate.md.

v0.5.0 — approvals anywhere, and the tool calls too

Choose a tag to compare

@renezander030 renezander030 released this 02 Aug 08:38
27ffac7

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 only

Steps 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.

v0.4.0 — the gate holds

Choose a tag to compare

@renezander030 renezander030 released this 26 Jul 05:51
cc5a921

Governance you can prove: approvals that survive anything, spend capped in money, and an audit trail you can read back.

Everything is additive. With no cost keys, no approvers, and no signature header, v0.3.1 behavior is unchanged.

Approval gates that survive a restart

Every gate is written to SQLite before the draft goes out, so an approval in flight outlives a redeploy, a crash, or a host reboot. Whatever happens to the process, the outcome of every gate lands in the audit trail, and the operator is told where things stand.

Spend caps in money

budgets:
  per_day_cost:      5.00   # same unit as your model rates
  per_pipeline_cost: 0.50

Token caps tell you how much the model thought. These answer what it costs. 0 leaves them off.

Per-operator approver scoping

- name: release-payment
  type: approval
  quorum: 2                     # how many must approve
  approvers: [111111, 222222]   # which ones

Quorum says how many. approvers says which ones, so the person who clears inbox drafts is not automatically the person who releases a payment. It narrows the channel's allowed_users and can never widen it.

Body-signed webhooks

Bind each inbound trigger to its exact body and a timestamp:

X-Draftcat-Signature: t=<unix>,v1=<hex hmac-sha256(t + "." + body)>

Enable with webhook.require_signature: true. Requests outside the skew window are refused and each signature is spent once, so a captured request cannot be re-fired. Verified whenever the header is present, even when not required.

Config validated on the boot path

The engine now runs the full draftcat validate check set at startup and refuses to start on errors, so problems surface at boot rather than mid-run. Typos get a suggestion:

ERROR pipelines[0].steps[1].type: invalid type "ia" — did you mean "ai"? (must be one of: deterministic, ai, approval)

DRAFTCAT_SKIP_VALIDATE=1 overrides.

draftcat runs

Read the governance record back out: what ran, when, and who decided what.

draftcat runs [pipeline] [--limit N] [--json]

2026-07-26T05:37:31Z  invoices    ok    60.0s
    release-payment      adjust      by 111 (0/2)
    release-payment      approve     by 222 (2/2) [signed]

--json gives the archivable form on stdout, ready to pipe into jq or a log shipper. Per-step timings and token counts stay in the observability spans.

Also in this release

  • WhatsApp intake (whatsapp_intake) for governed inbound messages.
  • internal/channels is the single registry the validator and engine share, so channel: only accepts what the binary can actually route to. Telegram remains the operator channel.

Issues #1, #2, #3 and #4 are closed by this release.

Full diff: v0.3.1...v0.4.0

v0.2.0

Choose a tag to compare

@renezander030 renezander030 released this 04 Jun 13:34

Three additions to the governed-pipeline engine, plus sharper positioning.

Webhook triggers (schedule: webhook)

Opt-in HTTP server: authenticated POST /hooks/<pipeline> runs a pipeline on demand. Bearer auth (constant-time compare), body-size cap, 409 when already running, no port opened unless enabled, refuses to start without a secret. The request body reaches the pipeline as {{webhook_body}} / {{input}}. A trigger only starts a pipeline — the approval gate still runs, so an inbound request can never make the LLM fire an outbound action.

Observability spans (internal/obs)

Opt-in structured JSON spans, one per pipeline and one per step (duration, status, tokens, cost). Off by default; enable with observability.spans: true or DRAFTCAT_TRACE=1. One line per span, ready for a log collector or a future OpenTelemetry exporter.

Schema enforcement: enum + number

output_schema now enforces enum membership and the number type at runtime. Skills already used both, but they were silently ignored. The validator accepts them and flags non-list enums.

Positioning

README adds an agent-harness (Flue / Claude Code) comparison column, framing Draftcat as the deterministic-boundary inverse of an autonomous agent harness.

v0.1.1 — renamed to Draftyard

Choose a tag to compare

@renezander030 renezander030 released this 24 May 14:53

Maintenance release that completes the rename from FixClaw to Draftyard.

No new features versus v0.1.0; this tag captures the renamed source so anyone pinning a version gets Draftyard-named code.

What changed

  • Repo renamed renezander030/fixclawrenezander030/draftyard (old URL still 301-redirects)
  • Go module path: github.com/ramaris/aiopsgithub.com/renezander030/draftyard
  • Default env var: AIOPS_TG_TOKENDRAFTYARD_TG_TOKEN (both config.yaml and the main.go fallback)
  • Log prefix: [aiops][draftyard]
  • Boot messages: "aiops starting/running" → "draftyard starting/running"
  • LLM system prompt: "You are aiops..." → "You are draftyard..."
  • Binary: make build now writes ./draftyard
  • Logo: new minimalist wordmark + geometric icon (yard with gate)
  • README: rewritten with comparison table vs n8n and LangChain agents

Breaking change

If your environment sets AIOPS_TG_TOKEN=..., rename it to DRAFTYARD_TG_TOKEN or override token_env: in config.yaml to match your existing variable. The engine refuses to start without a Telegram token resolved.

Upgrade

```bash
cd draftyard
git pull
go build -o draftyard . && ./draftyard
```

🤖 Generated with Claude Code

v0.1.0 — first tagged release (as Draftyard)

Choose a tag to compare

@renezander030 renezander030 released this 22 May 17:05

Draftyard's first tagged release. Single-business, single-operator deployments.

The project was originally published as FixClaw; renamed to Draftyard at v0.1. The old GitHub URL still forwards.

What's in v0.1

Engine

  • Pipeline runner with three typed step types: deterministic, ai, approval
  • YAML-defined pipelines and skills (no visual editor, no DSL)
  • Operator approval channel over Telegram (Slack WIP)
  • Token budgets enforced per-step, per-pipeline, per-day
  • Output schema validation against each skill's JSON schema
  • Input sanitization for operator messages (prompt-injection patterns scrubbed)
  • Channel security: allowlists, rate limits, input-length caps — engine refuses to start without them

Connectors

  • Gmail / Microsoft 365 (OAuth, read + send)
  • GoHighLevel CRM (contacts, opportunities, conversations)
  • Telegram (operator approval channel)

Actions

  • gmail_unread, notify
  • ghl_new_contacts, ghl_stale_opportunities, ghl_unread_conversations
  • pdf_extract, pdf_verify_cite — parse PDFs with bounding boxes, resolve tags in AI output with a four-layer match

State & idempotency

  • SQLite-backed state store (pure-Go, no CGO) at cfg.State.Path (default ./state.db)
  • Per-(pipeline, scope) dedup of fetched item IDs
  • Append-only pipeline_runs audit log
  • WAL mode, synchronous=NORMAL

Developer experience

  • draftyard validate [--strict] — config + skills linter
  • draftyard test — fixture-driven dry-run (no real APIs touched)
  • Pre-commit hooks via lefthook: gofmt, vet, build, test-short in parallel
  • Pre-push: draftyard validate --strict

Not in v0.1

  • Webhook triggers (schedule-only for now)
  • Generic HTTP action
  • Slack approval channel (mentioned in config schema, not built)
  • Retry / circuit breaker on step failures
  • Structured JSON logging / Prometheus metrics
  • Multi-tenant / per-pipeline secrets

See the README "Roadmap signals" for what's next.

Quickstart

```bash
git clone https://github.com/renezander030/draftyard.git && cd draftyard
git checkout v0.1.0
cp secrets.yaml.example secrets.yaml
go build -o draftyard . && ./draftyard
```

Public APIs may change between minor versions until v1.0.