ToolLeash
Synthesize and enforce least-privilege tool-call policies from OpenTelemetry GenAI traces.
ToolLeash ingests OTel GenAI execute_tool spans emitted by a real agent run, synthesizes a
deny-by-default policy that allows only observed behaviour (with inferred per-argument
constraints), emits it in multiple formats (Cedar, OPA/Rego, an MCP allow-list, or its own
canonical policy-ir JSON), and enforces it deterministically at the tool-call boundary. Mental
model: iamlive / AWS Access Analyzer, but for AI-agent tool calls.
Status: pre-1.0, under active construction. Every claim in this README is backed by a test or a command you can run yourself — see
STATUS.mdfor the exact test IDs and CI run per capability,PLAN.mdfor the roadmap, andCLAUDE.mdfor the engineering rules this project holds itself to. Nothing is claimed here that a test does not prove.
Install
pip install . # from a clone; no PyPI release yet
toolleash --help
Zero API keys, zero external binaries (no Node, no Docker) — every dependency is pip-installable.
Quickstart
python examples/run_quickstart.py
One command, no traces to record by hand. It runs a real Python MCP server, records a handful of
real execute_tool spans, synthesizes a policy from them in-process, then enforces that policy on
the same live session: an allowed call proceeds, a policy-violating call (never seen during
training) is denied. See examples/README.md for what it does and why. This exact script is run
as a real CI integration test (tests/integration/test_examples_quickstart.py).
CLI
toolleash record --listen 127.0.0.1:4318 # OTLP/HTTP receiver: capture a real staging run
toolleash inspect ./traces # sessions, tools, calls, argument-capture coverage
toolleash synthesize --traces ./traces --format cedar -o policy --strictness balanced
toolleash diff --policy policy-ir.json --traces ./new-traces # day-2 drift report (always exits 0)
toolleash validate --policy policy-ir.json --traces ./ci-run # CI gate: exits 1 on any denial
toolleash evaluate ./traces # self-graded / de-biased holdout table
Every command accepts --json for machine-readable output (all but record, which is a blocking
server, not a value-returning command). Formats available from synthesize: cedar, rego,
mcp-allowlist, policy-ir (Progent-JSON is not implemented — see Limitations).
Library
from toolleash.enforce.engine import PolicyEngine, ToolCallRequest
from toolleash.enforce.adapters import guarded_tool
engine = PolicyEngine(policy) # a PolicyIR from toolleash.policy.ir.build_policy_ir
decision = engine.decide(ToolCallRequest(agent="my-agent", tool="write_file", arguments={...}))
@guarded_tool(engine) # framework-agnostic: no framework imports
def write_file(path: str, content: str) -> None: ...
PolicyEngine.guard/enforce raise PolicyViolation on a policy-denied call in enforce mode;
shadow mode records what would have been blocked without blocking it — the safe rollout path.
See src/toolleash/enforce/engine.py and examples/run_quickstart.py for the full contract.
Limitations
These are not boilerplate — read them before you rely on a synthesized policy.
- Every enforcement mechanism here has only ever been probed by this project's own
adversarial-reviewer, never an independent security researcher. The traversal and
injection bypasses fixed this phase (see
docs/THREAT_MODEL.md's summary table) were each found by this project deliberately widening its own search after the previous one — not by an external red team. "No known bypass" and "no bypass" are different claims; today this project can only make the first one. Treat every "Yes" indocs/THREAT_MODEL.md's summary table as "this project's own tests say yes," not as an independently audited guarantee. - Progent-JSON is not implemented. Its real DSL can't be pinned from code without cloning
and running the actual Progent repo — outside this project's zero-asset guarantee, and
inferring the schema from the paper instead was explicitly ruled out (honesty over feature
count). So it is absent from
--formatentirely, not stubbed, not silently degraded.cedar,rego,mcp-allowlist, andpolicy-irare the only real formats; seePLAN.mdT-3.5 for the full locked decision. - Policies reflect observed behaviour, nothing more. An under-exercised staging run yields an over-restrictive policy (legitimate calls the agent will make in production were never observed, so they're denied); a staging run that's too broad — say, one that happens to touch paths or hosts wider than production ever will — yields an over-permissive policy. ToolLeash cannot tell which case it's in from the traces alone; the recorded window's coverage is on you.
- False-deny is a measured floor, not a product guarantee. On the two available free-tier
model-driven corpora (
gemini-flash-lite-latest,tests/fixtures/recorded/model_driven/), the held-out envelope false-deny rate at ToolLeash's default (balanced) strictness measures 11.1%–14.9%; at the tightest (exact/tight) presets it measures 24.6%–35.6%. Reproduce it yourself:toolleash evaluate tests/fixtures/recorded/model_driven/open(or.../shared). These numbers come from three toy agent personas over two toy tool suites — they are expected to improve with richer, more diverse training data, not a bound on what ToolLeash can achieve in your environment. - Permissiveness figures are lower bounds.
toolleash evaluate's "security-vacuous" / permissiveness column measures how much of a sampled foreign corpus a constraint admits; a thin corpus makes a wide-open constraint look artificially tight. A low permissiveness score is "not yet shown to be permissive," never "shown to be tight" — see the CLI's ownLOWER BOUNDcaption on everyevaluaterun. - Cedar output is a conservative under-approximation, not equivalent to the IR. The Cedar
emitter cannot express
string_length,char_class_regex, orurl_constraintfamilies; a rule containing one of these is omitted entirely (fail-closed — deny-by-default then denies those calls, never silently rendered as accept-any). A Cedar user therefore gets a strictly stricter policy than the canonical IR and MUST check which rules were dropped:synthesizeprintsOMITTED: ...to stderr and the emitted.cedarfile carries// OMITTEDheader comments; reproduce withtoolleash synthesize --traces tests/fixtures/recorded/filesystem --format cedar -o /tmp/policy --strictness balanced. Rego is more expressive (onlyurl_constraintis its own gap) but is not equivalent to the IR either — always checkomissions()/ the CLI output, for either format, before trusting the emitted policy's coverage. - Tool-argument capture is opt-in, and inference is only as good as what it's given. If a
traced agent's instrumentation does not capture call arguments (
arguments=Noneon the recorded span), ToolLeash has nothing to constrain — the synthesized policy degrades to a tool-name-only allow-list (any arguments accepted for an observed tool).toolleash inspectprints a loudWARNING: ... content capture appears OFFwhen this is detected; check that warning before trusting a policy's argument-level constraints. - Dependency upper bounds cap majors, not minors — the versions below are what was actually
verified, not a guarantee every version in range behaves identically. Every dependency is
capped at its next major (ADR-0020) after a real TestPyPI round-trip resolved
mcp>=1.28(no ceiling) to a genuinely breakingmcp==2.0.0on a fresh install — a class of bug no gate run ever exercises, since CI installs from the pinneduv.lock, never a fresh resolve. Last verified against:cedarpy4.8.7,regopy1.5.2,mcp1.28.1/1.29.0,pydantic2.13.4,typer0.26.8/0.27.0,opentelemetry-*1.43.0/1.44.0.regopyis the sharpest risk of this group: its RE2 regex-matching semantics are load-bearing for the char-class containment guarantee (ADR-0015), and a future major changing that would not crash the waymcpdid — it could silently change which policies validate. A ceiling is only ever raised after re-running the full round-trip against the new major (ADR-0020), never preemptively. - The OTel GenAI semantic conventions this project reads are Development/experimental status
(the
_incubatingnamespace) — attribute names may still change upstream. ToolLeash pins the exact version it targets indocs/specs/otel_genai_semconv.mdandsrc/toolleash/semconv.py; a future semconv release could require a re-pin.
See docs/THREAT_MODEL.md for what ToolLeash defends against and — just as important — what it
explicitly does not.
Requirements
Python ≥ 3.11. Zero API keys, zero external binaries, zero Node, zero Docker — every dependency
is pip-installable (see PLAN.md "Zero-asset guarantee").
Developing
uv sync # create the environment from pyproject + uv.lock
uv run python scripts/check.py # run the full quality gate (the definition of "green")
make check is equivalent where make is available; both just invoke scripts/check.py.
License
Apache-2.0. See LICENSE.
Metadata
Release files for toolleash 0.1.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 | Provenance | Actions |
|---|---|---|---|---|
| toolleash-0.1.1.tar.gz | 625.5 kB | Without provenance | Details for toolleash-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| toolleash-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for toolleash-0.1.1-py3-none-any.whl |
Total release size: 763.9 kB
Release files / toolleash-0.1.1.tar.gz
| Download URL | toolleash-0.1.1.tar.gz |
|---|---|
| Size | 625.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
55c20c305c2ba601a3079aaf56d65eec737312da53a6b8777e0606b889cc80e8
|
|
BLAKE2b-256 checksum How to use checksums |
1a87690e070dea52fd3280b74134445e47efe84a1ae8355732f84e431e733dd2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.0
|
Release files / toolleash-0.1.1-py3-none-any.whl
| Download URL | toolleash-0.1.1-py3-none-any.whl |
|---|---|
| Size | 138.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3b06c27830556e84fa0e3dc98496665471bc9ebccd3b9aabcd042f7f896c4a42
|
|
BLAKE2b-256 checksum How to use checksums |
adc42e2c189ebed038a54980adc7f5a4e65823ae8ceab9324f6860d0fb97983b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.0
|
Security for toolleash 0.1.1
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.