Pipelock writes a signed receipt every time it adjudicates an outbound agent action. Receipts compose into a hash-chained file an auditor or CI runner can read without trusting any vendor dashboard. This page is the shortest path from zero to “I ran the verifier, it passed, I tampered one byte, it failed.”
The fixtures here come from the public conformance corpus that verifier authors and Audit Packet producers can test against. There is no vendor-only verification path: every command below runs on public fixtures with a public test key.
The verifier commands
The Pipelock verifier is available as the built-in CLI, a standalone Go
binary, and TypeScript, Rust, and Python ports. They expose the same receipt
and chain verification surface, with matching exit-code semantics (0 valid,
1 invalid, 2 runtime error, 64 usage error).
| Binary | Source | What it covers |
|---|---|---|
pipelock verify-receipt | Built into the main Pipelock binary | Single receipt JSON, single evidence.jsonl, or a recorder session directory via --chain |
pipelock-verifier (Go) | cmd/pipelock-verifier, shipped as its own GoReleaser archive | Subcommands receipt, chain, audit-packet. No proxy, no scanner, no network surface. |
pipelock-verifier-ts | sdk/verifiers/ts | Node/Bun port via @noble/ed25519 and ajv |
pipelock-verifier-rs | sdk/verifiers/rust | Rust port with embedded schema |
pipelock-verify | pipelock-verify-python | Python port for receipt chains and Python audit scripts |
Core Go invocations:
# Verify a single receipt with the main Pipelock binary
pipelock verify-receipt receipt.json
# Verify a single receipt with the standalone Go verifier
pipelock-verifier receipt receipt.json
# Verify a full receipt chain (one jsonl file or a session directory)
pipelock-verifier chain evidence.jsonl
# Verify a complete Audit Packet bundle
pipelock-verifier audit-packet ./audit-packet/
The receipt and chain commands accept --key HEX_OR_FILE to pin a signer key
for trusted verification. The Go, TypeScript, and Rust CLIs also accept
--json for machine-readable output. The standalone Go verifier additionally
accepts --offline, --allow-self-consistent-only, --no-trust-required,
and --expect-sha256 on audit-packet for stricter or more permissive
policies depending on the relying party.
Walk the seven-case quick path
The full
public conformance corpus
ships 28 fixtures (8 golden, 8 edge, 12 malicious) covering every
reject_reason in the closed enum. A verifier author runs the whole corpus.
A first-time reader can grasp the format in seven hand-picked cases:
| # | Fixture | Category | What it shows |
|---|---|---|---|
| 1 | golden/01-allow-clean-get | golden | A clean allowed GET request. The basic happy-path signed receipt. |
| 2 | golden/03-block-dlp-aws-key | golden | A blocked DLP hit still produces a signed receipt. Blocked is not silent. |
| 3 | malicious/m01-forged-signature-wrong-key | malicious | Reject reason: signature_invalid. Attacker rewrote signer_key to claim the pinned key; the foreign signature fails. |
| 4 | malicious/m03-expired-timestamp | malicious | Reject reason: expired. Receipt is past the verifier’s max_age window. |
| 5 | malicious/m08-tampered-chain-prev-hash | malicious | Reject reason: chain_break. A receipt’s chain_prev_hash does not match the SHA-256 of the prior receipt. |
| 6 | malicious/m11-body-tampered-after-sign | malicious | Reject reason: body_tampered. Signature was valid for the original body; action_record.target was rewritten post-signing. |
| 7 | edge/e04-edge-of-freshness-window | edge | A receipt right at the freshness boundary. A correct verifier still accepts; off-by-one verifiers do not. |
Each .json fixture has a sibling .expect.json describing the verdict any
correct verifier MUST emit, the reject_reason enum value, the expected
signer key, and notes for verifier authors. Drop your verifier in, run it
across the corpus, compare line by line.
Try it locally
Clone the bench, pin the test key, run the verifier on a golden receipt:
git clone https://github.com/luckyPipewrench/agent-egress-bench.git
cd agent-egress-bench/receipts/v0/conformance
# Public test key (committed for reproducibility; production deployments pin their own)
TEST_KEY=$(jq -r .public_key_hex _generator/test-key.json)
# Golden case: accept
pipelock-verifier receipt golden/01-allow-clean-get.json --key "$TEST_KEY"
echo "exit: $?" # 0 (valid)
# Malicious case: reject with signature_invalid
pipelock-verifier receipt malicious/m01-forged-signature-wrong-key.json --key "$TEST_KEY"
echo "exit: $?" # 1 (invalid)
# Chain case: reject with chain_break
pipelock-verifier chain malicious/m08-tampered-chain-prev-hash.jsonl --key "$TEST_KEY"
echo "exit: $?" # 1 (invalid)
A correct verifier emits the reject reason on stderr or in the --json
output. The exit code is the relying party’s contract: CI runners gate on
0; auditors investigate any 1; 2 and 64 indicate the verifier itself
is misconfigured.
Same corpus, three other languages
Each language port exposes the same receipt and chain verification surface. Use the public corpus to confirm whether a verifier agrees with the expected verdicts before trusting it on production receipts.
# TypeScript
cd sdk/verifiers/ts && npm install && npm run build
node dist/src/cli.js receipt path/to/golden/01-allow-clean-get.json --key "$TEST_KEY"
# Rust
cd sdk/verifiers/rust && cargo build --release
./target/release/pipelock-verifier-rs receipt path/to/golden/01-allow-clean-get.json --key "$TEST_KEY"
# Python (separate repo)
pip install pipelock-verify
pipelock-verify path/to/golden/01-allow-clean-get.json
If a verifier disagrees with the others on any fixture, the disagreement itself is the bug report. The corpus is the contract.
What a passing verification actually proves
Three things, in order, every time:
- Signature math. The signature is a well-formed Ed25519 signature over
SHA-256(canonical-JSON(action_record)), and it verifies against the embeddedsigner_key. A wrong-key forgery fails here. - Body integrity. The verifier recomputes the canonical-JSON hash from
the current bytes of
action_record. If a single byte changed after signing, the recomputed hash disagrees with the one the signature was produced over. A post-sign edit fails here. - Chain linkage. When verifying a chain, each receipt’s
chain_prev_hashis checked against the SHA-256 of the previous receipt’s bytes. Cutting one receipt out, reordering, or splicing in a foreign receipt breaks the chain.
Add the verifier-side policy checks on top: pinned signer key, max age, replay window, schema version, strict JSON parsing. The conformance README documents the exact policy assumptions every conformance run uses.
Where to go next
- Agent action receipts: the primitive these commands verify, framed for procurement and audit.
- Verifiable egress control: the umbrella category combining binary-enforced mediation with signed evidence.
- Agent security control layers: where signed receipts fit in the four-lane agent-security map.
- Pipelock action receipt format spec: the exact bytes on the wire.
- AARP v0.1 spec: the assurance profile layered over a frozen receipt.
- Audit Packet v0 schema: the JSON Schema the bundle producers and verifiers both honor.
- pipelock-agent-egress-action: the GitHub Action that produces complete Audit Packets in CI.
- Conformance corpus README: the closed reject-reason enum, policy assumptions, and the generator that produces the fixtures.
Receipts or it didn’t happen. The corpus is public, the verifiers are public, the test key is public. The only thing that varies between a trustworthy run and an untrustworthy one is which public key the relying party chose to pin.
Frequently asked questions
What does verifying a receipt actually prove?
chain_prev_hash matches the SHA-256 of the prior one). A correctly written verifier does all three offline, with no network and no vendor dashboard.What's the difference between `pipelock verify-receipt` and `pipelock-verifier`?
pipelock verify-receipt is built into the main Pipelock binary; an operator who already runs Pipelock has it without installing anything else. pipelock-verifier is a separate binary with no network surface, no proxy, no scanner, and no config reload. It exists so a CI runner, an auditor’s laptop, or a third party can drop in just the verifier without provisioning the full firewall stack. Same verification logic. The standalone verifier adds three subcommands for receipt, chain, and audit-packet, and matches exit codes with the TypeScript and Rust ports.Why are there four verifiers in four languages?
Where do receipts come from in production?
Can a malicious actor with the test key forge receipts that the verifier accepts?
_generator/test-key.json for reproducibility. That key is for conformance testing only. Production verifiers pin a different public key per deployment; the corpus is structured so a verifier configured to pin the test key will accept the golden fixtures and reject the malicious ones. If a production verifier is misconfigured to trust the test key, that is a deployment mistake, not a property of the format.What happens if the corpus changes upstream?
fixture_id and an .expect.json that pins the exact verdict and reject_reason. Adding new fixtures does not change the verdict on existing ones. The corpus is versioned at receipts/v0/; if the receipt envelope itself ever changes in a way that breaks v0 verifiers, the new corpus will live at receipts/v1/ next to v0, not on top of it.