The open-source gateway for judgment-pack: a small service that attests the inputs a pack is judged on, under one protected signing identity, and seals each session so a verifier can catch replay and rollback.
It implements trustworthy input acquisition line (judgment-pack-evaluator-experiments, ADR-0002). That line asked a narrow question — where do a pack's inputs come from, and what makes them trustworthy? — and answered it with a ceiling stated up front: you can prove byte-lineage, not truth. A judgment is only as trustworthy as the bytes it was computed over, so the honest goal is to put those bytes on a proof path the model cannot forge, and to say exactly where that proof stops.
- Acquire. A caller asks the gateway to acquire a result from a configured source. The gateway runs the source, content-addresses the result, and signs its canonical form under the gateway's Ed25519 identity, chained to the session's prior receipts. The caller cannot supply a receipt — the gateway produces every one. This is what takes the model out of the proof path: an agent can assert anything, but it cannot manufacture the gateway's signature.
- Seal. When a session closes, the gateway seals its final receipt count in an append-only registry under the same key.
- Verify. A verifier fetches the registry from the gateway (not from the store) and checks the store against it using only the public key — checking grants no power to forge. Per-receipt integrity comes from the signatures; the seal adds what a store cannot attest about itself — that a whole session was replayed into it, or that a session's tail was rolled back.
flowchart TD
subgraph Producer["Producer side"]
S["Configured source"] -->|result bytes| A["Gateway acquire and sign"]
A -->|receipt / artifact| ST["Retain receipt/artifact"]
ST -->|final count| SE["Seal final count"]
SE -->|seal| R["Registry"]
end
subgraph Consumer["Consumer side"]
OB["Obtain snapshot"] --> FR["Fetch registry from key holder"]
K["Out-of-band pinned key"] --> V["Verify locally"]
FR --> V
V --> B["Bind accepted receipt"]
B --> D["Re-digest artifact"]
D --> U["Use result"]
end
ST -.->|snapshot| OB
R -.-> FR
GW["/verify: diagnostic convenience"] -. not the consumer boundary .-> V
PK["/publickey: consistency only"] -. not trusted key distribution .-> V
The executable form of the consumer flow is SPEC.md §5a and go/ceremony_test.go.
The registry is the reason the gateway exists. The inline attestation core proved
each receipt; it left two residuals it structurally could not catch on its own,
because both need an anchor outside the store. The gateway is that anchor. See
SPEC.md for the seal contract and the exact findings, and
go/service_test.go for the demonstration — including the
contrast that per-receipt verification passes the same replayed and truncated
stores the registry-anchored verification rejects.
Named Tavily and Google Cloud Search grounding connections are available through
Gateway's private connection service and the web-search acquisition source.
Desk can switch providers without changing its assistant model. Credentials stay
in Gateway; search hits remain distinct from fetched page evidence. See
configuration, limits and the provider contract.
One Go binary, standard library only, binds localhost. Build it as below, or take it
built: a release made by the release workflow carries the gateway and the adapters for
Linux, macOS and Windows, with checksums and a provenance attestation
(docs/releasing.md). Releases up to v0.4.0
carry notes only.
cd go && go build -o gateway .
./gateway keygen gateway.seed # prints the public key and key id to pin
The serve command below references ./my_source. Create it first in a POSIX/WSL shell:
cat > my_source <<'EOF'
#!/bin/sh
# Consume the canonical arguments supplied on stdin, then emit one JSON result.
cat >/dev/null
printf '%s\n' '{"synthetic":true,"subject":"acme"}'
EOF
chmod +x my_sourceThe fixture is synthetic: it demonstrates the source-command interface only, and its output is not evidence that a real screening occurred. On native Windows, replace it with any executable that reads the canonical arguments on stdin and writes one JSON result object to stdout.
./gateway serve ./store gateway.seed gateway:demo ./registry.jsonl \
--source screening='./my_source' --port 8787
keygen prints the public key precisely because it has to be pinned out of
band — see Honest bounds.
The key can be rotated, with the gateway stopped:
./gateway rotate-key --seed gateway.seed --registry ./registry.jsonl makes the next
seed, keeps the record it is about to append beside the seed, appends to the registry a
statement the current key signs naming the next key and one the next key signs naming
the current one (SPEC.md §3b), and only then switches gateway.seed to the next seed,
keeping the one rotated from beside it for you to destroy. On Unix a running gateway
holds a lock on its registry, and rotate-key refuses while it is held; elsewhere,
stop the gateway first. A rotation that stopped part way — the append torn, or the
switch not made — leaves the gateway refusing to start under either key until
./gateway rotate-key --resume --seed gateway.seed --registry ./registry.jsonl
finishes it; do not move the seeds by hand. Nothing new is pinned: a verifier follows
the rotations from the key it pinned from keygen, and the report names how many it
followed and the key it ended at. The gateway refuses to start under a key it rotated
from.
curl -s localhost:8787/acquire -d '{"session":"s1","source":"screening","arguments":{"subject":"acme"}}'
curl -s localhost:8787/seal -d '{"session":"s1"}'
curl -s localhost:8787/verify # the gateway grading itself -- a diagnostic, not evidence (SPEC.md §5a.3)
curl -s localhost:8787/registry # the anchor a verifier fetches from the key holder
curl -s localhost:8787/publickey # consistency only: a real verifier uses the key pinned from keygen, never this
curl -s localhost:8787/posture # which isolation protections this host enforces, as gateway posture --json
./gateway posture says which of the isolation protections in SECURITY.md
this host enforces — the user switch for sources, the reaping of a source's descendants, the
engine image's custody, the seed file's owner and mode, the confinement of the programs the
gateway starts — one line each, enforced, not-enforced or not-applicable, with the reason.
Give it serve's own command line, with or without the word serve, or --config engine.json,
to judge that configuration's seed and sources as well; --json prints the report in canonical
JSON. It exits 0 whatever it reports, and a not-enforced line's reason says what is missing: a
user of its own for each source (--source-user), a seed at mode 0600 owned by the gateway's
user, the engine image.
The gateway mints version 3 receipts (SPEC.md §1.2a): /acquire answers
{result, receipt, salts}, where salts.args is the 32-byte salt behind the receipt's
arguments commitment, returned here and retained nowhere. Revealing the arguments to an
auditor takes the salt, so whoever holds it can, and nobody else; this reference hands it
over plaintext localhost HTTP, so the holder is whoever can read that exchange. A bare
--source command is the "command" shape: its acquisition record names the command
and the digest of the file its first word resolved to, read just before the source is
started, and says nothing else was known — a directly executed script is digested as the
script, while python3 fetch.py is digested as the interpreter and the script is an
argument the record does not repeat; observedAt is the gateway's own stamp of when
it had read the source's output in full, since a command records nothing.
--receipt-version 2 keeps the version 2 form for a consumer not yet updated.
An engine-governed write goes through the same boundary as a read, as an action (docs/design/executor.md):
POST /act names a platform, a tool the platform's write binding offers, the arguments, the
decision record the write relies on and the receipts that record relied on. The engine refuses
the request before any executor runs unless the requester is authenticated, the platform sets
write: true, the tool is the binding's, every cited receipt is in this engine's own store and
verifies under its own key, and a decision record with the stated digest is under the
configured decision-record directory — and then runs adapter-mcp on the write binding, retains
the target's response, and mints one action receipt in the session chain naming the requester,
the decision, the citations and the tool. The receipt is lineage of a request and a response: it
says who asked, never that the write was right or that anyone approved it.
For a write tool the operator holds to a decision policy in the configuration
(ADR-0011), the engine also reads the record
before anything is sent: it must be shaped as the runtime's evaluation record (recordVersion
1, kind evaluation, a pack digest), the request's pack digest and citations must be the
record's, and the record must state an outcome the policy allows, no handoff, a pack and
reviewed law where the policy asks for them, and facts equal to the write's arguments where the
policy binds them. From engine configuration version 9 a policy may also fix an argument to a
value of its own (fix) and refuse any argument it neither binds nor fixes (closed,
ADR-0016), and from
version 10 hold the value at a pointer to an array or an object, an array to a length (shape,
ADR-0018); the engine compares and never
fills a value in. reviewed alone holds a write to whatever reviewed-set
lock is current, so an edited pack locked again passes it; list packs to hold a write to particular reviewed bytes.
The receipt then names the policy by its digest. For a tool held to none, nothing in the record
is read. gateway verify compares an action's pack digest and citations with a runtime
evaluation record it names, and says of any other record that it did not compare it. Without a
policy, decision on a receipt means only that the record existed; SPEC.md §1.2a says what it
does and does not establish.
A record is found by its shape, so a policy holds a write to whatever record a writer of the
decision-record directory put there, one written by hand included. A policy that sets
requireSignedRecord to the runtime's public keys (engine configuration version 5,
ADR-0012) also requires the record to be
signed by one of them, in its exact bytes, in the runtime's sidecar beside it,
signatures.jsonl; the receipt then says, in decision.recordBytes, that the record was found
by its exact bytes, and gateway verify finds it the same way. A writer who cannot use one of those keys then cannot satisfy the policy
with a record of their own. It establishes nothing against whoever holds a named key, the
operator included, who can sign any record. The gateway trusts exactly the keys the policy names
and follows no key rotation: when the runtime's key changes, add the next key to the policy
before the project signs with it, and remove the old one once no record it signed is still to be
acted on.
So that only the user the runtime writes as, the signer and root can put a record where the engine
looks for one, the engine refuses to start, and gateway connect to add a platform, while anyone
else could write a record in the decision-record directory: the directory, and every directory and
regular file beneath it that the walk reads, must be owned by one of those three and writable by
nobody else, the sticky bit excusing nothing there; every directory on the way to it must be owned
by one of them and writable by nobody else unless it is sticky, as /tmp is; a directory not there
yet must be in a directory only its owner can write, since whoever makes it first owns it; and a
link at its path must be owned by root, and the directory the link is in is held the same way, since
whoever may write it could put a directory of records in the link's place. The refusal names the
path, its owner or mode, and why. It holds whatever policy the tools have, since without
requireSignedRecord a policy trusts every writer of the directory, and without a policy decision
means only that a record existed there. It establishes the state at start-up, as unix owners and
permission bits show it, and no more: a mode or a file changed later, an access-control list the
bits do not show (macOS's), a mount, and root are not seen; the engine runs only on unix, so there
is no Windows check.
Two platforms write back after a decision (issue #248): azure-devops, through an organization's
remote Azure DevOps MCP server at https://mcp.dev.azure.com/{organization}, and databricks,
through the managed MCP server of one Unity Catalog function at
https://{workspace}/api/2.0/mcp/functions/{catalog}/{schema}/{function} (the bindings in
catalog/). Their reads are the connections', so each binding states a write
operation alone. A binding's URL and tools may be a template, filled from the platform's
parameters (engine configuration version 8): the binding's digest pins the template and the
transport, the scheme and the host are written in it (or the host is {workspace} alone, held to a
Databricks workspace's host forms), and the action receipt names the resolved URL, beside
action.tool.transport "http", under which gateway verify and verify-ts hold it to the
endpoint form of SPEC.md §1.2a. Each write tool
is held to a decision policy: for Azure DevOps, whose write tools are dispatchers that an action
argument steers, the worked policy fixes action to update and the form of its operations, binds
the work item's id and revision to the record's facts, holds updates to an array of two, and is
closed, so a request that names another action, adds an operation, adds a member or writes its
operations as an object is refused before anything is sent (engine configuration version 10,
ADR-0016,
ADR-0018);
for Databricks, each of the function's named parameters is bound to a fact. Raw SQL is no write
tool, since a decision could then authorize an arbitrary statement, which no policy can bind
argument by argument; the postgres binding, whose server takes a statement, has no write
operation, and a platform that pins it with write: true is refused at start. An action receipt for these platforms establishes who asked, which judgment was cited and
found verifying, what was sent and to which resolved endpoint, and what the target answered; it
does not name the credential, which the configuration names, nor the TLS identity the write was
answered under, and it does not establish that the target applied the write as intended, that the
tool did only what its name says, or anything of the service's permissions beyond the
credential's (docs/design/executor.md, "The write-back platforms").
Personal Drive, S3 and local-vault file management is a separate authenticated
host control surface in gateway-connections: bounded listing/search, selected
file reads, and reviewed create/update/delete plans. It issues no signed action
receipt and is not available to autonomous jobs or model tool calls. See
storage file management for scope, consent,
conditional writes and recovery limits.
A source declared with --source-shape NAME=airbyte|mcp|http is an adapter
(ADR-0002): its stdout is an
envelope, {acquisition, result, page?}, and the receipt records the acquisition as the
adapter reported it — endpoint, snapshot, schema, the adapter that fetched — under the
shape the operator declared, with the statement committed under its own salt
(salts.statement) and, for a page, the digest of every item. A defective envelope
fails the acquisition; nothing is minted. What that record is, exactly, is the
adapter's testimony under the gateway's signature, and SECURITY.md says so. The envelope is
published as a JSON Schema with vectors, schema/adapter-envelope.v1.json, and
gateway adapter-conform --shape SHAPE --vectors DIR 'ADAPTER-COMMAND' runs an adapter of your
own as serve would and says, request by request, whether serve would accept its envelope,
minting nothing (write an adapter).
A consumer does not stop at those endpoints: it runs gateway verify itself, over
a store it holds, under the key it pinned from keygen's output, then binds the
receipt and re-digests the artifact before using a byte — the normative sequence
is SPEC.md §5a, executable in go/ceremony_test.go.
A store to hand to an auditor, or to back up, is exported as one consistent cut:
./gateway export --config engine.json --out /path/to/export [--observations journal.json]
./gateway export --check /path/to/export # every file against the manifest, byte for byte
./gateway verify /path/to/export/store /path/to/export/registry <authority> \
/path/to/export/decision-records < pinned-public-key
The export holds the receipts and artifacts, the registry, the decision records, the
descriptor snapshots the platforms pin (a platform's own, or each of its operations') and any observation journal given, in the store layout
gateway verify reads (SPEC.md §1.3), with manifest.json naming the authority, the signing
key by its id and every file by its digest and size. The cut reads the registry before the
receipts, so a session sealed while the export runs is in it with its seal and every receipt
the seal counts, or without the seal, never as a rollback. The seed is in no file of it, and the public key is not either: a
verifier pins it out of band, as for a live store. A link among the store's receipts or artifacts
refuses the export, and no file is read through one. --out must not be there yet, nor inside the
store or the decision records. --check reports {"findings":[...],"ok":...} and exits 0 when it
reaches a verdict, as verify does; a manifest it cannot read is no verdict. A restore is a copy
of the export's files back into place, which --check and verify then read as they read the
export. The journal comes from the connection custody, which the signer never reads:
gateway-connections --state-dir <dir> --principal <name> --export-observations writes it, as
the custody holds it, to stdout. The export carries the artifacts and decision records as they
are, so keep it as private as the store. The store contract behind it, and what a storage kind
other than the filesystem must satisfy, is
docs/design/store-contract.md.
gateway serve --config engine.json is the engine's form of the same command: the file
names platforms — a catalog binding pinned by digest, a credentials file, the OS user the
platform's adapters run as — and serve derives every source from it, refusing to start
under a configuration in which an adapter could read the seed or the signer a credential, or in
which, as owners and modes stand at start-up, anyone but root and the signer could replace an
adapter it launches, which must be a native executable of the host (ELF on Linux, Mach-O on
macOS, for its CPU), not a script; it launches each adapter by the path that adapter resolves to
(docs/design/engine-config.md). gateway connect --config engine.json <platform> --binding <name> --credentials-file <operation>=<path>... --user <name>
writes a platform entry: it holds the configuration that would result to the same refusals, runs each
of the platform's adapters once in check mode as the platform's user, prints what the platform
answered, and writes only then — nothing is acquired and no receipt is minted. The bindings
the engine ships, with their pins and licences, are in catalog/. With an
identity in the configuration — an issuer, the audience the engine is named as, and a file of
the issuer's public keys — every request that acquires, seals or verifies carries a bearer token
the issuer signed, and the receipt names the caller it proved; without one, every receipt carries
caller: null.
A source is any command that reads the canonical arguments on stdin and writes a JSON result on stdout. The gateway attaches no transport of its own; it attests whatever bytes a source returns — proof of the bytes, not proof of their truth.
A source is started with the environment declared for it, plus PATH, and nothing
else of the gateway's. --source-env NAME=KEY=VALUE sets a variable for source NAME;
--source-env NAME=KEY copies that one variable from the gateway's environment at
spawn time, which is for passing a path through by name, not a secret — a secret in
the gateway's environment is in the signer's memory whatever is declared. On Windows,
SYSTEMROOT is the one variable os/exec adds undeclared. --source-user NAME=USER
runs a source as another OS user with that user's own groups (Unix; root may switch, and
so may a process that is not root but holds CAP_SETUID, CAP_SETGID and CAP_KILL on
Linux; a start that cannot switch is refused rather than run as the signer).
--source-max-output BYTES bounds
what a source may write on stdout, from 1 byte to 64 MiB (one mebibyte by default);
past it the source is killed — on Unix its whole process group, elsewhere the direct
child — and the acquisition fails, and a descendant that outlives the kill gets a bounded wait rather
than the acquisition. --source-timeout NAME=SECONDS gives source NAME a timeout of its
own, a whole number of seconds from 1 to 604800 (thirty by default); past it the source is killed
the same way and the caller is told it did not finish within its timeout. A source's timeout
starts before its adapter does — the gateway resolves the command, digests it for a version 3
receipt where the source is a bare command, and starts it, on Unix behind a process-group
anchor — so keep an adapter's own --timeout, plus the cleanup waits it budgets for stopping,
under its source's timeout, with further margin for that start and for the adapter's own report.
That margin is what a read reaching the adapter's deadline needs to be reported by the adapter
rather than killed with it (adapters). --max-request BYTES bounds an /acquire body, from 1 byte to 64 MiB (one mebibyte by default); /seal and
/act keep one mebibyte, and every body must still arrive within the server's thirty-second
read timeout. --source-timeout, --max-request and --source-max-output are command-line
options: under an engine configuration the derived sources keep the thirty-second timeout,
/acquire keeps one mebibyte, and a source's stdout keeps its own mebibyte. An /acquire body's arguments are refused past 524,288 JSON values, a count a
one-mebibyte body cannot reach, and raising the bound does not raise it.
On Unix, serve refuses a seed file that is not a regular file
owned by the gateway's own user and readable by it alone, says which chmod to run,
and marks every descriptor its launcher left open close-on-exec so none reaches a
source; on Windows it checks that the seed is a regular file and says the rest is the
filesystem's. What this does not give: a source that runs as the gateway's own user can
read what that user can read, and a gateway that runs as root can read the files a
source user holds. The separation is as strong as the identities the operator gives
the two sides; SECURITY.md states it in full.
cd go && go test ./... && ./gateway conform # the frozen corpus is the arbiter
Stated plainly because the whole line is about being exact where proof stops:
- A receipt (and a seal) is an Ed25519 signature under an operator-configured authority label — proof of byte-lineage to the key holder, not proof that a genuinely-named source produced the bytes. The recorded source/authority is a label, not an authenticated origin.
- Verification needs only the public key, so checking grants no power to forge. But the public key must be pinned out of band: fetching it from the gateway you are auditing proves consistency, not authenticity.
- Signing uses Go's standard library
crypto/ed25519— vetted and constant time. (An earlier revision carried hand-written pure-Python Ed25519 that was neither; retiring it removed the caveat rather than mitigating it.) - The registry closes replay and rollback relative to a verifier that trusts the gateway's registry over the store. It does not defend a compromised gateway: key disclosure forges everything.
- This is a single-identity, single-operator reference, localhost, no authentication
on the HTTP surface unless an
identityis configured — and that decides who may call, never from where — no authorization, no HA. It is for self-hosting a trust root and demonstrating the mechanism — not a hardened public deployment.
The source contract above is the gateway's whole integration surface, and every source
that exists is a script someone wrote by hand. docs/adr/0001
records the direction: a second module, adapters/, that reaches the catalogs that
already exist — connector images for bulk and historical reads, the MCP servers vendors
publish for live reads and writes — and a release that ships the gateway, the adapters
and the runtime as one image with one configuration file. Inside that image the signer
runs alone with the seed, each adapter runs as its own process with one platform's
credentials, and neither imports the other; go/boundary_test.go and
adapters/boundary_test.go make the import rule a test, and the design notes state what
the tests cannot: what each process may read, and which deployment choices would undo it.
The receipt format
this needs, version 3, is normative in SPEC.md §1.2a with vectors under
corpus/v3/, and is what serve mints; its design record is
docs/design/receipt-v3.md. The adapters are in
adapters/: adapter-airbyte, a pinned connector image run through
the operator's container runtime, one page of one stream per acquisition;
adapter-mcp, a client of a vendor's MCP server over stdio or, at a URL the operator
fixes, over Streamable HTTP, one tool call per acquisition; and adapter-http, one request over TLS to an endpoint the operator fixed —
a search provider's JSON API, a reader service that renders a page or a PDF as text — with
a credential the adapter holds, one request per acquisition — wired to serve with
--source-shape NAME=airbyte, NAME=mcp and NAME=http. That a record reached through
both the connector and the MCP paths derives to the same facts is checked on a golden record
(docs/design/both-paths-agreement.md). A workflow
tool reaches the same surface through a client package under plugins/ — an n8n
community node and an Activepieces piece, each carrying result, receipt and salts into
the workflow as data and verifying nothing (docs/design/plugins.md);
plugins/smoke/ runs the node inside n8n, and the piece's actions
as the framework calls them, against a live engine. An MCP client reaches the engine's
platforms' live tools through gateway mcp, a fifth process that forwards each call to
/acquire under the caller's token and answers with the receipt
(docs/design/mcp-server.md, ADR-0003).
A document a person attaches in a desk — a PDF, a scanned form, a text export — is to reach
the same surface as a bare source, adapter-document, whose contract is written ahead of it:
the adapter establishes the document's identity by digest, extracts what it can within stated
bounds, names the pages it could not read, and writes one versioned record the gateway attests
under the command shape
(docs/design/attachments.md, ADR-0004).
Content a desk wants as a document goes the other way through a second bare source,
adapter-render: it takes the content as a closed structure of blocks, writes a Word file, or
has a program the operator configured make a PDF of that file, and writes one versioned
record that holds the file, so the receipt covers the file's bytes
(docs/design/rendering.md, ADR-0006).
go/ the core: canon, sign, seal, verify, conform, serve — standard library only
adapters/ what reaches outside, spawned by the core over the source contract, never linked
plugins/ the engine reached from workflow tools: an n8n node, an Activepieces piece
corpus/ the frozen vectors both answer to
docs/adr/ why the repository is shaped this way
docs/design/ what is designed and not yet decided
The format, the registry contract, and the verifier are all here and inspectable, on
purpose: an attestation nobody can verify is worth nothing. Verifiability is a
property of the ecosystem, not of one operator, so the parts a third party needs to
check a receipt or a seal without trusting the operator — the receipt format,
canonicalization, the seal contract, and verify_with_registry — are open and stay
open. That is the mandatory-open core.
This repository contains the gateway, its receipt and seal formats, the registry, and the verifier needed to inspect and validate those records. The shipped implementation acquires configured sources, signs the resulting records, and supports independent verification of their byte lineage.
scripts/dev/check.sh runs every check CI runs, in order, and prints one summary block of passes,
failures and skips; --setup runs the install steps first and --only NAME re-runs one check. The
checks are listed in dev.json, the manifest of the Judgment Pack development kit
(v0.1.0), whose runner is vendored here unchanged; the longer ones are scripts under
scripts/checks/. AGENTS.md and CLAUDE.md carry the same instructions for agent sessions,
shared rules first and this repository's own after them; CI checks that the two are identical
(scripts/sync-agent-docs.sh writes CLAUDE.md from AGENTS.md).
Apache-2.0. See LICENSE.
Operators can set --source-timeout NAME=SECONDS up to seven days (604800
seconds). The default remains 30 seconds; adapters keep their own timeout limits.
Set the adapter timeout below the Gateway timeout, allowing cleanup time.
Durable waiting belongs to the caller. Runner's independent jpack-source-worker
calls the normal /acquire API and retains its response in the caller's private
store. Gateway never stores the response's commitment salts or a replayable
request record. See source custody and recovery.