ActGate
by kartsan03
README.md
# ActGate
[](https://github.com/kartsan03/actgate/actions/workflows/ci.yml)
[](https://pypi.org/project/actgate/)
[](pyproject.toml)
[](LICENSE)
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.
## 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
```json
{
"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
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues