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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

willow_gate-0.1.0.tar.gz (100.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

willow_gate-0.1.0-py3-none-any.whl (44.1 kB view details)

Uploaded Python 3

File details

Details for the file willow_gate-0.1.0.tar.gz.

File metadata

  • Download URL: willow_gate-0.1.0.tar.gz
  • Upload date:
  • Size: 100.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for willow_gate-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7813c749833d2e74a019e70be7d23f3f3c31b5f5861eb5ffecc86eb6773aed87
MD5 f9d0780d61056d6d05d8d4937eb5411c
BLAKE2b-256 00644b4d4274d55c4f0f5160b7aea453dd60fffef621a441ed977a4a4a50cf0e

See more details on using hashes here.

Provenance

The following attestation bundles were made for willow_gate-0.1.0.tar.gz:

Publisher: release.yml on willow-memory/willow-gate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file willow_gate-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: willow_gate-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 44.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for willow_gate-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3b92677d992aebe2f9a4820ae4b24e04ae444ff16bf0caa198c29e5b60314053
MD5 90ed9ccfcd80c3704f09184aa2f39b58
BLAKE2b-256 c6fcc8ace9dfff7d41d2e2940fc9eb28d664d8ececc2121b4971b2db19025d1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for willow_gate-0.1.0-py3-none-any.whl:

Publisher: release.yml on willow-memory/willow-gate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

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