Toolgate
The capability control plane for embedded AI agents.
Portal & live simulation: maglionejm.github.io/toolgate — try the gate and tamper with a real hash chain in your browser.
Agents should never hold credentials — not the user's OAuth token, not a tenant API key, not anything. Toolgate sits between agents and the tools they call:
- the agent authenticates with its own Ed25519 key and a delegation grant from a human;
- it receives short-lived capability tokens (
sub= the human,act.sub= the agent, RFC 8693 semantics) that are useless if stolen (proof-of-possession bound); - every tool call is policy-checked (allow / deny / require human approval), metered against the grant's budget, and executed by the gate, which injects the real credential server-side;
- every decision — including denials — lands in a hash-chained, Ed25519-signed audit trail.
Agent (no secrets) ── token + one-time proof ──▶ GATE ── real credential ──▶ Upstream API
│
verify → decide → budget → inject → execute → audit
Try it
uvx --from toolgate-io toolgate demo # zero-install, straight from PyPI
or from source:
uv sync
uv run toolgate demo
The demo boots Toolgate plus two credential-guarded mock APIs and runs a six-act scenario: an allowed CRM read (the upstream rejects anything without its live key — proving injection), a policy denial, an external email parked for human approval and executed against the approved args only, budget exhaustion, revocation that kills a live token instantly, and audit chain verification with the full decision trace.
[OK ] read_contact executed -> {'contact': {...}}
[DENIED ] TG_DENIED: matched deny rule never-delete
[PARKED ] approval apr_... pending — agent is blocked, not trusted
[HUMAN ] Sam approved the exact parked arguments (args are hash-bound)
[OK ] send_email executed after approval
[BUDGET ] blocked: delegation grant budget exhausted
[REVOKED ] TG_REVOKED: live token died with the grant, no TTL wait
[AUDIT ] chain of 10 records — verification: VALID
How it works
- Register a tenant, its users, agents (public keys only), and upstreams. Upstream credentials are sealed into the vault (AES-256-GCM) and never leave the server.
- Delegate: a user grants an agent bounded authority — which upstreams/tools (RFC 9396-style
authorization_details), what budget (cost units), which policy, until when. - Exchange: the agent presents a signed client assertion (RFC 7523 style) and receives a capability token — TTL ~2 minutes with jitter, audience-bound, sender-constrained via
cnf.jkt. - Call: each gate call carries the token plus a one-time DPoP-style proof signed by the agent key (bound to method, URL, and token hash; replays rejected).
- Enforce: token bounds → policy rules (first match wins, glob matching, dot-path argument constraints, cost ceilings) → default deny → atomic budget charge.
- Approve:
require_approvalparks the call; a human decides on the exact argument set (hash-bound — no post-approval swaps); the agent polls and executes. - Audit: every decision appends to a hash chain signed by the gate key.
GET /v1/control/audit/verifyproves nothing was edited, removed, or reordered.
CLI
Everything an operator does is a toolgate command (full reference):
pip install toolgate-io
toolgate init # profile + connectivity check
toolgate keys generate --out agent-key.json # agent identity (private key stays local)
toolgate grants create -t tnt_... --user usr_... --agent agt_... \
--policy pol_... --budget 100 --authz "crm:*" # bounded delegation
toolgate approvals watch -t tnt_... --by usr_... # interactive human-in-the-loop inbox
toolgate audit export --out audit.json && toolgate audit verify --file audit.json # verify an exported chain
toolgate dev call crm read_contact --grant grt_... --key agent-key.json # act as the agent
Offline-verification caveat.
audit verify --fileis genuinely offline/third-party only when you supply the gate's public key out-of-band via--jwk. Without--jwk, the verifier fetches the key fromGET /v1/keyson the very server being audited — so a server that forged the chain could also serve a matching key. For independent verification, pass--jwkwith a key you obtained separately.
Agent-side SDK
from toolgate.sdk import ToolgateClient, PendingApproval, generate_ed25519_key_pair
client = ToolgateClient(
base_url=base_url,
agent_id=agent_id,
agent_private_jwk=agent_private_jwk, # the only secret an agent ever holds
grant_id=grant_id,
)
result = client.call("crm", "read_contact", {"contactId": "c-001"})
if isinstance(result, PendingApproval):
result = client.wait_for_approval(result.approval_id)
# Denials, budget exhaustion, and revocation raise typed ToolgateCallError
# (TG_DENIED / TG_BUDGET_EXCEEDED / TG_REVOKED / TG_PROOF_INVALID / ...).
Layout
| Module | Purpose |
|---|---|
toolgate.core |
Capability tokens, client assertions + PoP proofs, policy engine, audit chain |
toolgate.server |
Control plane (registry, grants, token endpoint, approvals, revocation, audit) + gate (enforcement pipeline, vault) |
toolgate.sdk |
Agent-side client: token exchange, signed calls, approval flow, typed errors |
toolgate.demo |
End-to-end scenario (uv run toolgate-demo) |
Documentation
Full suite in docs/: Quickstart · API Reference · Token Spec · Security Model · Deployment · Operations
Design
docs/ARCHITECTURE.md— components, token design, threat modeldocs/adr/0001— delegation, never user-credential impersonationdocs/adr/0002— JWT on OAuth rails (RFC 8693/9396/7800) over Biscuit/Macaroon/UCANdocs/adr/0003— original TS runtime decision (superseded by 0005)docs/adr/0004— approvals bound to args hashes; hash-chained signed auditdocs/adr/0005— Python as the reference implementation
Roadmap lives in the issue tracker: MCP compatibility, upstream OAuth brokering, Merkle checkpoint anchoring, approvals via Slack/webhooks/CIBA push, dashboard, Postgres scale-out, TypeScript SDK rebuild.
Development
uv sync
uv run ruff check src tests
uv run pytest tests/ -q # 39 tests: core unit + server integration + SDK-vs-server
uv run toolgate-demo # the six-act scenario
uv run toolgate-server # standalone server (logs only the admin-key fingerprint at boot; set TOOLGATE_ADMIN_KEY explicitly)
Production env vars: TOOLGATE_MASTER_KEY, TOOLGATE_ADMIN_KEY, TOOLGATE_PUBLIC_URL, TOOLGATE_DB, PORT.
Contributing & security
Contributions welcome — see CONTRIBUTING.md. Security findings go through private vulnerability reporting, never public issues.
License
Apache License 2.0 © 2026 Juan Martin Maglione. Toolgate is early-stage software (pre-1.0): the wire format is a compatibility surface we take seriously, but expect movement before 1.0.
Release files for toolgate-io 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| toolgate_io-0.3.1.tar.gz | 133.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| toolgate_io-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 194.7 kB
Release files / toolgate_io-0.3.1.tar.gz
| Download URL | toolgate_io-0.3.1.tar.gz |
|---|---|
| Size | 133.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
783fd3b30d7bdb3f5cfc9eab08183c567a7057c60022b5343f7f6dee5929de2d
|
|
BLAKE2b-256 checksum How to use checksums |
ef3fd01bbae225839618c755fb36aaab81bd44dbcdd395d3079f2652c0772a0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.
Transparency logRelease files / toolgate_io-0.3.1-py3-none-any.whl
| Download URL | toolgate_io-0.3.1-py3-none-any.whl |
|---|---|
| Size | 61.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
57d262648a0acc1554bea1a170348eb6fe39d74c48e2b6935948297349d5b1ef
|
|
BLAKE2b-256 checksum How to use checksums |
ed743f8562661adab275e9886864857ad4bcd69e01b6b9d1ed3fa8d0177f426a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.
Transparency log