Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

toolgate

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.demo
Simulated 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.

What's implemented

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

Design decisions worth arguing with

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.

Testing

PYTHONPATH=src python3 -m pytest tests -q     # deterministic, stdlib only, instant

Pinned: 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.

Licence

MIT

About

A policy firewall and tamper-evident audit log for AI agent tool calls. Deny by default, rate limits, budgets, hash-chained history. Pure stdlib.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages