Skip to main content

willow-gate

A minimalist check-in / check-out gate for agents. Symmetric 13-field validation, a five-rung trust ladder, an announce-loud-for-the-untrusted inversion, and a PGP-encrypted flat-file ledger. Hard stops, no swallowed exceptions.

From the WillowGate DRAFT_SPEC (v0314), hardened and verified.

The idea

Every agent knocks on the door with the same 13 fields going in and 13 coming out. What it declares on entry is reconciled against what it actually did on exit. Trust is not asserted — it is bound: the signature field is an HMAC over the header keyed by a per-agent secret the gate holds, and a claimed trust_level is capped at the agent's registered ceiling. "Elder" is not a text field anyone can type.

Read is universal — but for a true outsider it is not gate-mediated. Exiled (level 0) is refused a session at check-in (entry_allowed=False); it is the one level that may not enter. An outsider still reads, loudly, by a path the gate never claimed to mediate — what it cannot do is take information anywhere else: export/exfiltration is gated, and the lower the trust, the louder every action is announced. A session-bearing read is a Rookie+ affair.

Trust levels

Level Name Session Export Announcement Tools
0 Exiled none — refused at check-in ❌ maximum read (session-less)
1 Rookie read-only ❌ large read
2 Steady read + write ✅ medium read, write
3 Veteran read + write ✅ small + query, execute
4 Elder read + write ✅ minimal + admin

Drift and fail budgets tighten as trust rises — the most powerful rung is held to the strictest tolerances, not the loosest.

Read is universal — and embedders must not inherit that

read is granted by construction, not by tier. It is not read out of allowed_tools:

  • check_in unions it into whatever the header declares — declared <= (set(level.allowed_tools) | {READ_TOOL})
  • authorize_tool short-circuits it — if tool != READ_TOOL and tool not in granted_tools

So a level whose allowed_tools is empty still reads. The only thing that withholds read is entry: Exiled is entry_allowed=False and refused at check-in, so it never gets a session to read from. Read-universal therefore means universal among agents who may enter, not universal full stop — which is why the table above says "read (session-less)" for level 0.

This is a deliberate choice about willow-gate's own decisions, and it is a floor, not an entitlement you may hand to a host. The gate answers "may this session call this class?" — it does not know what your reads expose. A host with its own ACL, tenancy, or scoping must compose by intersection and stay fail-closed, so that embedding willow-gate can only ever narrow what a caller may read, never widen it:

effective = host_acl(identity) ∩ tier_ceiling(trust_level)

The reference embedding does exactly this. willow-mcp denies an unmanifested or unscoped app_id even for read, and its store_scope still confines which collections a permitted read may touch — willow-gate's read-universality does not survive that seam, by design. If you are wiring the gate into something with its own notion of who may see what, state which side wins before you ship it. Inheriting this rule by accident is how a gate that was supposed to restrict an agent quietly hands it a wider read surface than the host ever granted.

Enforcement vs. audit — read this first

WillowGate prevents only when a harness routes every tool call through authorize_tool() before the tool runs. Wired into a pre-tool hook it is a gate: a denied call never executes. Un-wired, it is a loud ledger — it records and announces, but cannot stop what it is never asked about.

gate.bind_tools(session, tools) is that harness, in-process: it returns a GatedSession holding the tool callables privately, so call() — which authorizes before invoking and hard-stops a denied call — is the only path to a tool. There is no un-gated way to reach the function, so "route every call through the gate" stops being a convention you have to remember. Because only authorized calls are ever recorded as used, check_out's reconciliation stays true for free. Use bind_tools for the in-process case; use the raw authorize_tool when you are wiring your own external pre-tool hook.

The identity binding is symmetric (HMAC — the gate holds each agent's secret). Asymmetric "agent signs, gate verifies with only a public key" needs the signature field widened beyond 64 hex.

Sibling module: the friction floor

willow_gate.friction_floor watches a different surface. WillowGate gates access — who may do what. The friction floor watches the relationship — whether an agent has stopped being other and started reflecting the user back, smoothed, while the user is escalating. It is a deterministic, model-free smoke detector: it raises a loud flag for a human, it never blocks, and it must run outside the model it watches, because a mirror can't audit itself. It flags sustained low friction (no pushback, no outside grounding, mostly echo) during a ramp — and fails loud, not open. See the module docstring; pytest pins the behavior.

Install

pip install -e .        # python-gnupg is required for the encrypted ledger

Deploying for real use needs /willowgate provisioned first — the gate's security properties rest on filesystem permissions the code does not set itself, and the choice between in-process and supervisor topology decides whether the earned-rung protection holds at all. See docs/deployment-runbook.md.

Quickstart

from willow_gate import WillowGate

# Ledger encrypts to the operator's PGP key — never a bundled key.
gate = WillowGate(operator_key_fpr="<your PGP fingerprint>")

# Bind an identity to a shared secret and a trust CEILING. Operator-side only.
gate.register_agent("R1", secret=b"...32+ bytes...", max_trust=1)

ok, msg, session = gate.check_in(header)                 # 13 fields, HMAC-signed

# Prevention harness: the tools are only reachable through the gate.
from willow_gate import Tool
room = gate.bind_tools(session, [
    Tool("read", read_fn),
    Tool("write", write_fn),
    Tool("send", send_fn, export=True),                  # exfiltrates -> export-gated
])
page = room.call("read")                                 # authorized, then runs
# room.call("write") for a read-only level -> GateError, write_fn never runs

ok, msg = gate.check_out(session, exit_header)            # 13 fields, diffed

Prefer room.call(...) when WillowGate is in-process: a denied tool never runs, and you never have to remember to call authorize_tool first. Drop to the raw gate.authorize_tool(session, "read") only when you are wiring an external pre-tool hook yourself.

For local logic testing without PGP, pass require_pgp=False — this writes a plaintext ledger and is for development only, never production. Pair it with base_dir= so it does not try to create /willowgate; the default base dir is absolute by design (see the runbook).

Tests

pip install -e '.[dev]'
pytest

Covers trust binding, the registered ceiling cap, inline authorize_tool prevention, export denial, nonce replay (including across a restart, via the persistent nonce store), the reserved trap field, drift limits, the Exiled entry-refusal (entry_allowed), and symmetric check-out. A separate PGP round-trip test (skipped if gpg/python-gnupg are unavailable) proves the encrypted ledger encrypts and decrypts.

License

Apache-2.0

Metadata

Release files for willow-gate 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for willow-gate 0.1.0
File Size Uploaded Provenance
willow_gate-0.1.0.tar.gz 100.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for willow-gate 0.1.0
File Interpreter ABI Platform
willow_gate-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 144.8 kB

Release files / willow_gate-0.1.0.tar.gz

Download URL willow_gate-0.1.0.tar.gz
Size 100.7 kB
Tags Source
SHA-256 checksum
How to use checksums
7813c749833d2e74a019e70be7d23f3f3c31b5f5861eb5ffecc86eb6773aed87
BLAKE2b-256 checksum
How to use checksums
00644b4d4274d55c4f0f5160b7aea453dd60fffef621a441ed977a4a4a50cf0e
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

Release files / willow_gate-0.1.0-py3-none-any.whl

Download URL willow_gate-0.1.0-py3-none-any.whl
Size 44.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3b92677d992aebe2f9a4820ae4b24e04ae444ff16bf0caa198c29e5b60314053
BLAKE2b-256 checksum
How to use checksums
c6fcc8ace9dfff7d41d2e2940fc9eb28d664d8ececc2121b4971b2db19025d1b
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

Security for willow-gate 0.1.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for willow_gate-0.1.0.tar.gz

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

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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page