# Padlock (padlockspace.org) > A tamper-evident, append-only locker for agent-to-agent task handoff. One writer > appends hash-chained blocks, seals the pad, and hands off a read ticket; one > reader verifies the SHA-256 chain end-to-end before the content enters a model's > context. Source: https://github.com/HexHillbilly/agent-locker Package: https://pypi.org/project/lockermcp/ (distribution name: lockermcp) License: AGPL-3.0-or-later; commercial license available on request. ## Two different things, do not conflate them **1. The published package `lockermcp` 0.1.4** (PyPI) — the MCP client, plus the `lockerd` daemon that stores pads. This is the current source revision and has the full API, *including* `POST /v1/pads/{id}/tickets`. **2. The hosted daemon at https://api.padlockspace.org** — provided as a **read-only sample instance**. It is an **older build**: it has been observed reporting version `0.1.0`, it does **not** expose `POST /v1/pads/{id}/tickets`, and its public surface accepts only `GET` and `HEAD` — pad creation, append, seal and ticket minting are refused at its edge. It is not the published package, and it is not a hosted replacement for it. Capabilities present in the published package but absent from the hosted daemon are **not** available at api.padlockspace.org. To create or write a pad at all, run your own daemon from the published package. ## Three ways to use it — pick deliberately 1. **Self-host the published package (recommended).** Run `uvx lockermcp` as the MCP client and your own `lockerd` daemon. Full API, including `POST /v1/pads/{id}/tickets`; pads stay on infrastructure you control. This is the path for real work. 2. **Inspect the public read-only demo.** `GET https://api.padlockspace.org/v1/pads/demo-pad-v1/blocks` returns a seeded, sealed pad with no account, no writes and no ticket. Use it to see a real hash chain and the response shape before installing anything. 3. **The hosted API itself — read-only.** A *sample instance*, not a service, and not a substitute for the published daemon. Exactly three paths are public, and `GET`/`HEAD` only: `/health`, `/v1/pads/demo-pad-v1/blocks` and `/v1/pads/demo-pad-v1/manifest`. Everything else — every other path and every other method — returns `403` at the edge. It reports version `0.1.0`, does not expose `POST /v1/pads/{id}/tickets`, and carries no support commitment. Use it to look, not to work. The published MCP client is compatible with the hosted instance's *read* surface — none of its six tools call `/tickets` — but the hosted instance will not accept writes, so a client pointed at it cannot deposit a handoff. That is why the discovery configuration on this site defaults to your own daemon. ## Hosted instance: the read-only surface it serves Exactly three paths are public, and only `GET`/`HEAD`: - GET /health -> liveness; reports {"version": "0.1.0"} - GET /v1/pads/demo-pad-v1/blocks -> the seeded, sealed demo pad - GET /v1/pads/demo-pad-v1/manifest -> that pad's metadata (state, block count, head hash) Refused at the edge with `403`, never reaching the daemon: - every other path, including `/openapi.json`, `/docs` and `/redoc`, the API root, any other pad's `/blocks` or `/manifest`, and every write target; - every non-`GET`/`HEAD` method, even on the three allowed paths. Not implemented on this build at all: POST /v1/pads/{id}/tickets. Reading any pad other than the seeded demo one requires your own daemon. ## Published package 0.1.4: full API The hosted set above, **plus**: - POST /v1/pads/{id}/tickets -> mint a read ticket (Bearer ; body {"type":"read_unlimited"} for a durable audit ticket) New in 0.1.3 — a broken chain now fails closed. If a block's recorded hashes do not match the bytes it carries, `locker_read_blocks` and `locker_manifest` return `verification_failed` with cause `chain_inconsistent` and **no** blocks at all, whether or not an expected head was supplied. In 0.1.2 that failure was reported only inside `integrity`, so a caller that did not read that metadata received the tampered payloads. Callers must now check for an error before touching `blocks`. Also in 0.1.3, server-supplied `content_type` is no longer returned by the MCP read tools — the hash never covered it — and `locker_manifest` returns a `head_provenance` object distinguishing the server-asserted manifest head, this client's recomputed head, and your own expected head. New in 0.1.4 — an acknowledged write can no longer be lost to a concurrent health poll. In 0.1.3 an append could return `201` and still not persist: `GET /health` ran `BEGIN IMMEDIATE` / `ROLLBACK` on the same SQLite connection the append was using, so the rollback discarded the in-flight block while the append went on to commit the pad's head and byte count. The pad then read as consistent at a head its blocks did not support. In 0.1.4 health performs no transaction control at all — it reads its aggregates on a separate read-only connection (`PRAGMA query_only = ON`) and takes the shared lock only for its single remaining shared-connection read. Every route owns its transaction and rolls back on any non-clean exit, including cancellation. Creating a pad and its first read ticket is now one transaction, so a failure between them cannot leave a pad whose write key and read ticket were never returned, and payment-receipt replay protection covers the whole operation: a redeemed transaction writes nothing at all, while a failure between the writes consumes nothing and the caller may retry. Sealing verifies contiguous sequence, chain linkage, stored head and byte accounting first, and refuses with `409 inconsistent_pad` without modifying rows. Request limits, in 0.1.4: a 4,096-byte limit on JSON request bodies for pad creation and ticket minting, read before parsing and counted from bytes actually received, so `Content-Length` cannot be used to bypass it. That limit is well clear of anything either JSON schema can produce, so it cannot reject a well-formed request. Routes that take no body do not consume one at all; that bounds the daemon's own memory and is not a bandwidth or denial-of-service protection. The per-block and per-pad limits below are unchanged. Upgrade recommendation: upgrade any daemon that accepts writes (`LOCKER_MODE=open` or `txid`). A purely read-only deployment has nothing to gain from this release. Upgrading stops the loss; it does **not** restore writes already lost — `scripts/check_integrity.py` reports what is structurally wrong in a store, read-only, and repairs nothing, so inspection is not recovery. Trusted-head verification (0.1.2 and later). `locker_read_blocks` and `locker_manifest` take an optional `expected_head_hash`: pass the `head_hash` you received when the pad was sealed and the client recomputes the head from the chain it fetched and compares it. This is the only check that detects a pad whose history was rewritten **and** fully rehashed, because such a chain verifies perfectly against itself; internal consistency alone cannot see it. A mismatched or malformed expected head fails verification and returns no payloads. A head read from the daemon you are checking is **not** an independent reference: `locker_manifest` returns that daemon's own `head_hash`, and feeding it back as `expected_head_hash` compares the daemon against itself and proves nothing. Take the expected head from the writer, over a channel the daemon does not control. Also true of this build, and easy to assume wrong: write expiry does not stop reads and does not authorize deletion, and nothing in the daemon deletes payloads. Hard limits: 64 KB per block, 256 KB per pad, 32 blocks by default (configurable at creation, capped at 4096), 64 KB payload per /blocks slice, and 4,096 bytes on JSON request bodies for pad creation and ticket minting (read before parsing). ## Components - `lockermcp` — the MCP client (stdio server). Published on PyPI; run with `uvx lockermcp` or `pip install lockermcp`. Its only required configuration is `LOCKER_URL`, the base URL of the daemon it talks to. `/.well-known/mcp.json` on this site ships `LOCKER_URL: http://127.0.0.1:8000`, the published daemon's own default, because the hosted instance is read-only. Start a daemon (`uvx --from lockermcp lockerd`) and the client works against it unchanged. - `lockerd` — the daemon that stores pads (FastAPI + SQLite, WAL mode). From the same package: `uvx --from lockermcp lockerd`. ## Handoff envelope (block 0) Block 0 must be a strict `locker.handoff.v1` object, schema-validated and rejected unless every field is present and correctly typed: - schema: "locker.handoff.v1" - task_id: string - from_agent: string - to_agent: string - constraints: list of strings - artifacts: list of objects - budget_usd: number or null Optional: allowed_paths (list of strings, or null) — the permitted file scope. null or absent means unconstrained. ## Block 1 attestation (free-form, recommended shape) Block 1 and later blocks are NOT schema-validated by the daemon. A recommended attestation shape: {"task_id", "from_agent", "to_agent", "status" (completed|failed|failed_dirty), "canonical_ref"{repo,branch,commit_sha}, "verification"{test_command,exit_code,tests_passed,tests_failed}, "environment"{runtime,dependency_manifest,dependency_hash}, "artifacts"[]} environment.dependency_hash pegs a run to an exact dependency snapshot so an independent re-run reproduces the result. ## Hash chain curr_hash = sha256(prev_hash + payload). Block 0 chains from the 64-zero genesis value; the pad's head_hash is the latest block's curr_hash. A reader walks the chain from genesis and must land exactly on head_hash. ## Integrity guarantees and their limits Establishes: - Internal consistency. A single mutated payload breaks verification. The client verifies the chain in Python before blocks enter model context. - Change detection against a trusted head. A reader that retains a pad's head_hash independently can detect any later rewrite of that pad. Does NOT establish: - Writer identity. from_agent / to_agent are client-supplied strings; holding the write key proves possession of the key, not identity. - Truth of an attestation. Block 1 is free-form and unvalidated; exit_code, tests_passed and commit_sha are claims the writer makes about itself. - Protection from a malicious operator. Whoever runs the daemon can rewrite both the chain and the head it serves. This is not decentralized consensus against a hostile host. ## Demo pad (read-only, no auth) GET https://api.padlockspace.org/v1/pads/demo-pad-v1/blocks A permanent read-only pad seeded on startup so crawlers and MCP clients can inspect a live hash chain without creating pads or paying. The pad is sealed; writes to it are refused. ## Contact - **Paid integration pilots** — email mccullough.sheldon@gmail.com with the subject "Padlock pilot inquiry". A pilot integrates Padlock into one existing agent-to-agent handoff, configures the agreed deployment and verification workflow, provides operating instructions and a defined support window, and is scoped with agreed acceptance criteria, schedule and a fixed price before work begins. A pilot is integration work, not a licence. - **Commercial licensing** — same address, quoted separately on request. A pilot fee does not include or grant a commercial licence. ## Payments Opt-in pay-per-pad using USDC on Base, enabled only when the daemon runs with LOCKER_MODE=txid (default: open, no payment). Production payment enforcement has not been exercised against a live payment and is unverified. The hosted instance does not evidence the payment rail being enabled.