Skip to content

Repository files navigation

ActGate

ci PyPI Python License: MIT

Local IntentLedger and MCP stdio proxy: propose a tool action, approve or deny in an append-only hash-chained ledger, then let an identical tools/call reach one upstream MCP server.

This is not a SaaS. The ledger path stays on disk. dry-run and approve record decisions only; they do not execute tools. The MCP proxy is what executes, and only after approve.

pending, then approve, then one execute

The drawing is those three steps. The same steps against a real echo server:

./examples/mcp/demo.sh

First tools/call returns ACTGATE_PENDING and does not reach upstream. actgate approve writes the ledger and still does not. The identical call then runs once. CI runs that script on Python 3.12.

Install

pip install actgate

Dev:

pip install -e .[dev]

CLI quickstart

actgate init
actgate propose --tool shell.exec --args '{"cmd":"ls"}' --blast-tags fs.read
actgate pending
actgate dry-run <intent_id>
actgate approve <intent_id>
actgate verify
actgate list

MCP proxy

Point your MCP client at ActGate instead of the upstream server:

actgate init
actgate mcp --upstream python -m some_mcp_server

Use the same --root (or the same working directory) for init, approve/deny, and mcp, so they share one ledger.

Flow:

  1. Client tools/list is forwarded to upstream. Only tools/call is gated; other methods are forwarded.
  2. First tools/call for a tool+args writes a propose event and returns ACTGATE_PENDING intent_id=... (upstream is not called).
  3. Human: actgate approve <intent_id> (or actgate deny <intent_id>).
  4. Identical subsequent tools/call (same tool and args) runs upstream once and appends an execute event. A third call returns already-executed.
  5. Denied intents never hit upstream.

Bare verify checks hash-chain integrity only. Set ACTGATE_SEAL_KEY for optional HMAC seals, or pass verify --require-seal.

Human loop (HITL)

Typical MCP approval loop:

  1. Client hits actgate mcp --upstream ... and gets ACTGATE_PENDING.
  2. Human: actgate pending (or actgate pending --watch) to see undecided proposes.
  3. actgate approve <intent_id> or actgate deny <intent_id>.
  4. Client retries the same tools/call; proxy executes once.

pending is the propose-without-decision queue. --watch polls (default 1s) and prints newly pending intents as JSON until Ctrl-C (clean exit 0).

Exit codes

Code Meaning
0 ok (propose, approve, verify clean, show/list/pending)
1 deny recorded, or verify found a broken chain / bad seal
2 setup error (missing ledger, bad path, invalid args)

Intent shape

{
  "tool": "shell.exec",
  "args": {"cmd": "ls"},
  "args_hash": null,
  "blast_tags": ["fs.read"],
  "requested_mode": "execute",
  "created_at": "2026-09-06T00:00:00+00:00"
}

What this is not

  • Not a hosted approval product
  • Not a policy DSL
  • No network calls in the ledger core path (the MCP proxy talks to a local upstream process)

Development

pip install -e .[dev]
pytest

About

Local IntentLedger + MCP stdio proxy: propose, approve/deny, hash-chained ledger. Optional HMAC seals.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages