tollgate
by Akiyo974
README.md
# countersigned
[](https://github.com/Akiyo974/countersigned/actions/workflows/ci.yml) [](LICENSE)  
**Your agent proposes. A human countersigns. Then it runs, verified.**
Terraform-style `plan` / `apply` for AI agent actions.
Your agent can read anything. When it wants to *change* something, it proposes a plan: the current state is read, business rules are checked, the exact effects are previewed. A human (or a policy) approves that exact plan. Then `apply` re-checks that the world has not moved, executes with idempotency keys, **verifies every step against the real system**, rolls back in reverse if anything fails, and writes it all to a tamper-evident ledger.
Zero dependencies. TypeScript. Speaks MCP out of the box, so it works with Claude, Cursor, VS Code, OpenAI Agents and anything else that talks Model Context Protocol.

```
agent ──plan──▶ observe state · check rules · preview effects · policy decides
│
human approves the exact plan hash (CLI, UI, chat button) ◀── the agent has no "approve" tool
│
agent ──apply─▶ recheck state (drift? refuse) ─▶ execute(idempotencyKey) ─▶ verify against the system
│ failure
▼
compensate earlier steps, in reverse
every event ─▶ hash-chained ledger
```
## Why
Agents moved from *answering* to *doing*. Writing to real systems (an ERP, a CRM, a billing API, a database) breaks in ways a better prompt cannot fix:
| What goes wrong | What countersigned does |
| --- | --- |
| The agent acts on state that changed after the human said yes | Snapshot at plan time, re-observe before apply, refuse on drift (`STALE_STATE`) |
| A retry or a crash sends the email twice, books the order twice | Stable idempotency key per step, forwarded to your system; completed steps are never repeated |
| "Done!" says the agent. It was not. | `verify()` is mandatory and re-reads the source of truth; the report carries the evidence |
| Step 3 fails, steps 1 and 2 already changed things | Saga compensation in reverse order; irreversible leftovers are flagged for a human |
| The approved plan is not the plan that runs | Approval is pinned to a SHA-256 of the plan; any edit invalidates it |
| The agent approves itself | Approval lives outside the agent's process and tools; requester cannot approve |
| "What exactly happened at 3am?" | Append-only, hash-chained ledger; `countersigned ledger verify` detects edits and deletions |
Terraform solved this for infrastructure years ago. Nobody had generalized it to the rest of the systems agents now touch.
## See it
```bash
git clone https://github.com/Akiyo974/countersigned && cd countersigned
node examples/erp/demo.ts # Node >= 22.18, no install needed
```
The GIF above is generated from real runs of `examples/erp/agent.ts` and the CLI (`python3 scripts/render-demo.py . docs/assets/demo.gif`). The longer narrated demo runs an agent against a deliberately annoying fake ERP: documents locked by users, writes that report success but do not stick, emails that cannot be unsent. Excerpt:
```
5. The ERP says "OK" but the write did not stick: verification catches it, earlier steps are rolled back
Plan pl_mw2gf0p3b5f8be729d: COMPENSATED
Step 1 (set_reorder_point) failed: verification failed: ERP shows 3, expected 6.
Every earlier effect was compensated; the system is back to its approved state.
0. reschedule_order: compensated
1. set_reorder_point: failed (verification failed: ERP shows 3, expected 6; compensated)
```
## Define an action
An action is five small functions. Writing them forces the questions that matter *before* the agent runs in production.
```ts
import { defineAction, t } from "countersigned";
export const setReorderPoint = defineAction({
name: "set_reorder_point",
description: "Change the reorder point of an inventory item. Reversible.",
reversibility: "reversible", // read | reversible | compensable | irreversible
input: t.object({ sku: t.string(), reorderPoint: t.integer({ min: 0 }) }),
// 1. What state does this depend on? Snapshotted at plan time, re-checked before apply.
observe: ({ sku }) => erp.getItem(sku),
// 2. Business rules. Any violation denies the plan before a human is bothered.
check: (input, item) => (item ? [] : ["item does not exist"]),
// 3. What will change, in words a human can approve.
preview: ({ sku, reorderPoint }, item) => [
{ target: `item:${sku}`, op: "update", before: item.reorderPoint, after: reorderPoint },
],
// 4. Do it. Pass the idempotency key downstream.
execute: ({ sku, reorderPoint }, { idempotencyKey }) => erp.update(sku, { reorderPoint }, idempotencyKey),
// 5. Prove it. Re-read the system; never trust the return value alone.
verify: ({ sku, reorderPoint }) => erp.getItem(sku)?.reorderPoint === reorderPoint,
// The way back. Required for reversible and compensable actions.
compensate: ({ sku }, _result, { before }) => erp.update(sku, { reorderPoint: before.reorderPoint }),
});
```
`defineAction` refuses, at boot, a writable action without a way back, an irreversible action pretending to have one, or a description too thin for a model to choose correctly.
Already using zod, valibot or ajv? Wrap it with `t.custom(jsonSchema, parse)`.
## Use it from code
```ts
import { FileStore, rules, Gate } from "countersigned";
const gate = new Gate({
store: new FileStore(".countersigned"),
rules: [rules.irreversibleNeedsApproval(), rules.autoApprove("set_reorder_point"), rules.maxEffects(20)],
}).register(setReorderPoint, rescheduleOrder, emailSupplier);
const plan = await gate.plan(steps, { intent: "absorb the supplier delay", requestedBy: "agent-42" });
// plan.status: "approved" (policy allowed) | "pending_approval" | "denied"
await gate.approve(plan.id, { by: "christen" }); // or from the CLI, or your UI
const report = await gate.apply(plan.id); // safe to call again
```
## Use it as an MCP server
```ts
import { serveStdio } from "countersigned/mcp";
serveStdio(gate, { agentId: "claude-desktop" });
```
The agent sees `countersigned_list_actions`, `countersigned_plan`, `countersigned_get_plan`, `countersigned_apply`, plus every read-only action as a direct tool. It never sees an approve tool. Example config (Claude Desktop, Claude Code, Cursor):
```json
{
"mcpServers": {
"erp": { "command": "node", "args": ["/path/to/countersigned/examples/erp/mcp-server.ts"] }
}
}
```
Then, in another terminal, the human side:
```bash
npx countersigned list # what is waiting
npx countersigned show pl_... # intent, before/after, why it needs you
npx countersigned approve pl_... # signs this exact plan hash
npx countersigned ledger verify # chain and plan records intact?
```
## Storage
```ts
import { FileStore, MemoryStore } from "countersigned";
import { SqliteStore } from "countersigned/sqlite";
new MemoryStore(); // tests
new FileStore(".countersigned"); // plain files, zero setup
new SqliteStore("countersigned.db"); // transactional, built on node:sqlite (still zero dependencies)
```
Any database works: a `Store` is six async methods (`createPlan`, `getPlan`, `updatePlan`, `listPlans`, `appendLedger`, `readLedger`).
## Policies
Rules are plain functions `(step, plan) => { decision, reason } | undefined`. Strictest opinion wins and `deny` can never be relaxed. With no rules: reads run, every write waits for a human.
Built in: `autoApprove`, `requireApproval`, `deny`, `irreversibleNeedsApproval`, `tagNeedsApproval`, `maxEffects` (blast radius), `maxAutoSteps`. A custom rule that lets small changes through:
```ts
(step) => step.action === "set_reorder_point" && Math.abs(after - before) <= 2
? { decision: "allow", reason: "change of 2 units or less" }
: undefined
```
## Guarantees, and what is not guaranteed
Honest list, because safety tooling that oversells is worse than none.
- **Drift detection is as good as your `observe()`.** Observe exactly what the action depends on, from the source of truth.
- **The gate's recheck is time-of-check.** A change between the recheck and the write is still possible unless the target system can refuse it. Keep the version (ETag, row version) in `observe()` and send it with the write (`If-Match`): the server then rejects any last-instant change, and `execute()` throws `NotAppliedError` so the gate rolls back cleanly. [`examples/rest`](examples/rest) shows the full pattern, tested.
- **Compensation is not a transaction.** It is a best effort correction. That is why reversibility is declared per action, and irreversible effects that already happened are reported as `unrecoverable` instead of hidden.
- **If `execute()` throws, the outcome is unknown.** countersigned does not guess: the step is reported for a human, the key lets you look it up. When you *know* nothing happened (a 409, 412 or 422 from the API), throw `NotAppliedError` instead.
- **An interrupted apply is never resumed implicitly.** If the process dies mid-apply, every further `apply` is refused (`APPLY_IN_PROGRESS`), so an agent cannot crash its way into a re-run. An operator calls `gate.resume(id, { by })` (not exposed over MCP): ownership is taken atomically (a second resume, or the original process if still alive, stops before its next step), finished steps are skipped, a step that started without a recorded outcome is checked with `verify()` against the real system and is never blindly re-executed, and steps that never started are re-checked for drift and expiry (overridable with `ignoreDrift` / `allowExpired` when the operator has looked).
- **FileStore and SqliteStore are single-machine.** Both are safe across processes on one host. For several hosts, implement the `Store` interface on your database (six async methods).
- **The ledger is tamper-evident, not tamper-proof.** Edits, insertions and deletions break the chain; entries cut from the end are caught because every plan records the last entry written for it (`countersigned ledger verify`). Someone able to rewrite both plans and ledger can still forge history, so publish `countersigned ledger head` somewhere the host cannot rewrite (a git commit, a log service) and verify with `--head`.
## Status
v0.1, working and tested (65 tests, including a real MCP client loop over stdio, a real HTTP API with ETags, multi-process SQLite, and regression tests for every finding of an adversarial security review). APIs may still move before 1.0.
## Roadmap
- [ ] Python SDK with the same plan format, so one ledger serves both ecosystems
- [x] SQLite store (`countersigned/sqlite`, built on `node:sqlite`)
- [ ] Postgres store
- [ ] Approval channels: Slack / Teams buttons, a small web inbox, email links
- [ ] Multi-approver and role-based policies (two signatures over 10 000 $)
- [ ] Adapters: Dynamics 365 Business Central / NAV OData, Odoo, Shopify, Stripe, generic REST with ETags
- [ ] Remote MCP (Streamable HTTP) transport with OAuth
- [ ] OpenTelemetry export of ledger events
- [ ] Ledger anchoring (periodic hash published to git or a transparency log)
## Contributing
Issues, adapters and ideas are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). Found a way around a safety guarantee? Please report it privately, see [SECURITY.md](SECURITY.md).
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing