An autonomous agent is an untrusted caller that happens to live in your process. toolgate treats it like one: every tool call passes through a deny-by-default policy, a rate limiter, and a session budget — and every decision, especially the denials, lands in a hash-chained audit log that can prove nobody rewrote history afterwards. Pure stdlib, zero dependencies.
git clone https://github.com/harshadkhetpal/toolgate
cd toolgate
PYTHONPATH=src python3 -m toolgate.demoSimulated coding-agent session through the gate
ALLOW fs.read reads inside the workspace are fine
ALLOW fs.write writes inside the workspace are fine
ALLOW shell.run read-only and test commands are fine
DENY fs.read no rule covers tool 'fs.read' with these arguments
DENY fs.read credential files are off limits even inside the workspace
DENY shell.run destructive or network shell is blocked
ALLOW http.get the GitHub API is the only approved remote
DENY http.get no rule covers tool 'http.get' with these arguments
HOLD git.push pushes wait for a human
ALLOW fs.read reads inside the workspace are fine
ALLOW fs.read reads inside the workspace are fine
DENY fs.read rate limit: fs.read called 3 times in the last 60s (max 3)
DENY fs.read rate limit: fs.read called 3 times in the last 60s (max 3)
audit chain intact: True (head 60b946c377547156...)
after editing entry 4: intact: False, first broken link: seq 4
That fourth line is the one to stare at: the agent asked for
/workspace/../etc/passwd. Paths are canonicalized before rule matching, so
the prefix rule never sees the disguise. The last two lines are the other half
of the product: an auditor edits one entry and verification names the exact
broken link.
| Piece | Detail |
|---|---|
| PolicyEngine | Rules as plain data (tool glob + argument constraints). All matching rules are evaluated; the most restrictive wins (deny > hold-for-approval > allow). No matching rule means deny |
| RateLimiter | Sliding window per tool — the difference between a mistake and an incident is a loop |
| BudgetMeter | Per-tool costs against a session cap; denied calls cost nothing |
| AuditLog | SHA-256 hash chain over canonical JSON. verify() catches edits, deletions, reordering, truncation — and names the first bad entry. verify_anchor() catches full-file rewrites |
| ToolGate | The composition: policy, then rate, then budget; everything logged with its reason |
Deny by default, and "no rule" is the reason. A tool nobody thought about is not a tool the agent may use. Allowlists age better than blocklists because new attack surface arrives disabled.
Rules are data, not callables. A policy you can serialize is a policy you can diff in a pull request — the same reason firewall rules and IAM policies are data. The cost is expressiveness; the constraint language is deliberately small (prefix, regex, forbidden-regex) and that is the point.
Paths are canonicalized before matching. must_start_with: /workspace
is security theatre if /workspace/../etc/passwd passes it. Normalization
happens in one place, before any rule looks at the arguments, so no individual
rule needs to remember the trick.
The chain's limitation is documented, not hidden. A hash chain proves
internal consistency, not authorship — an attacker who can rewrite the whole
file can re-chain it. That is what verify_anchor() is for: persist head()
somewhere the attacker cannot reach and compare later. Claiming more than
that would be selling snake oil; there is a test showing exactly this attack.
Denials are charged nothing and logged always. Budget should bound damage done, not curiosity. And after an incident, what the agent tried is exactly as important as what it did — the denial log is the forensic record of intent.
The clock is injected. Rate limiting is time-dependent; reading a real clock would make tests flaky and the demo non-reproducible. Everything is deterministic to the byte, which is also why the demo output above is real.
PYTHONPATH=src python3 -m pytest tests -q # deterministic, stdlib only, instantPinned: deny-by-default, deny-overrides-allow precedence, path traversal caught by canonicalization, sliding-window mechanics, budget charging only allowed calls, tamper detection at the exact entry for edits / deletions / reordering, the anchor catching a full rewrite, and a deterministic demo in which every hostile call is blocked.
MIT