Skip to content
2389-researchPublic

About

Local signed event ledger CLI with Ed25519 identities, verification, checkpoints, and a queryable SQLite index.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PACT

PACT is a local, append-only signed ledger CLI. This Go tool initializes a store, creates an external Ed25519 identity, trusts that identity, appends signed event commits, inspects local heads and objects, verifies the full store, makes official signed checkpoints, and queries a disposable SQLite index in deterministic causal batches.

The Phase 0 contract, Phase 1 single-replica core, Phase 2 bounded index and query surface, and five-step setup command are implemented. See the Phase 0 and Phase 1 status for exact gate, ledger, checkpoint, and recovery evidence, and the setup status for setup dogfood and review evidence.

Install

Development and source installation require Go 1.26 or later. The commands in this repository use mise exec; configure mise to expose Go 1.26 or later, or replace that wrapper with your equivalent toolchain command.

Install pact at the exact destination $HOME/.local/bin/pact:

mkdir -p "$HOME/.local/bin"
env -u GOROOT mise exec -- env GOBIN="$HOME/.local/bin" go install ./cmd/pact

Add $HOME/.local/bin to PATH if it is not already present.

Operator CLI foundation

On a capable terminal, bare pact plays a brief rotating signed-and-sealed welcome, then prints top-level help. Redirected bare output prints one static frame, plain under the default automatic color policy; --color always may color that frame. pact help and pact --help skip the animation and remain immediate. pact help index shows the nested index commands, and pact COMMAND --help remains seal-free.

Commands that need an existing repository, including status, heads, show, verify, log, query, and index, discover it from the working directory: PACT walks up through parent directories until it finds .pact. An explicit --repo PATH is authoritative. PACT resolves that exact path and does not search its parents.

pact status performs strict verification and checks whether indexed reads are ready; it never creates or rebuilds an index. A healthy status writes its summary to stdout and exits 0. An attention status means canonical state is valid but the index needs work; it writes the summary and pact index rebuild to stderr, then exits 9. A broken status writes its summary to stderr and uses the matching verification exit code: 4 for integrity or authorization failure, or 9 when missing dependencies are the only failure. A missing PACT store is a store error with exit code 3. With --json, healthy output is one JSON result on stdout; a non-healthy result is one JSON error envelope on stderr.

Human output accepts --color auto|always|never. auto is the default and adds color only on a suitable terminal; NO_COLOR and TERM=dumb disable it. always forces color, never disables it, and JSON output never contains ANSI escapes.

pact setup is the canonical bootstrap. A fully flagged command never prompts; with --json, it emits machine-readable output for automation:

pact setup \
  --repo /path/to/project \
  --namespace org/example/widget \
  --actor operator/alice \
  --key-file /path/outside/project/operator.key.json \
  --json

On a terminal, omit missing values for a guided setup. PACT asks for the namespace, actor, and external key path, prints one five-step plan, then asks for confirmation before writing anything:

pact setup --repo /path/to/project

The key must live outside the project root. An exact rerun accepts the existing store, key, and trust root, verifies canonical state, and accepts or rebuilds the derived index. It never overwrites a conflicting namespace, actor, key, or trust root. Run the same command after an interrupted setup to resume from the last durable action.

Write the standalone agent skill without initializing a repository:

pact quickstart > SKILL.md

Development recording and handoffs

pact record --profile development/v1 validates fixed development payloads before signing, checks selected evidence, admits one atomic operation, and refreshes the index. Use the canonical guide for the complete input object, executable example, exact Git proof, and recovery recipe. The embedded pact quickstart output is that same guide.

pact record --repo /path/to/project --profile development/v1 \
  --input /path/to/record.json --key-file /path/outside/project/operator.key.json --json
pact brief --repo /path/to/project --profile development/v1 \
  --namespace org/example/development --subject widget/task-1 --source-revision FULL_GIT_REVISION --json
pact query --repo /path/to/project --namespace org/example/development --subject widget/task-1 --json
pact show --repo /path/to/project EXACT_OBJECT_OR_EVENT_REF --json
pact evidence check --repo /path/to/project --event-ref EXACT_EVENT_REF --json
pact verify --repo /path/to/project --strict --json

Follow every query cursor and inspect payloads with show before interpreting acceptance. Keep a green check separate from an open Important finding and explicit acceptance. Keep concurrent conflicting decisions visible. Retry uncertain writes with the same input, operation ID, namespace, and signer. Read admission and index readiness separately; an index failure does not undo admission. Raw commit and checkpoint writes still require an explicit index rebuild.

pact brief returns complete development state over an exact source and captured frontier. Its default limit is 16,384 encoded bytes, including the newline. Use --max-bytes from 1,024 through 65,536; oversized required state exits 2 with empty stdout. Reads never repair an index. Use explicit index rebuild when readiness fails. Exit 0 means successful observation, not acceptance. The canonical guide explains cuts, limits, and exact proof. The earlier adoption evaluation retains its historical result. Schema checks, signatures, local-root authority, evidence availability, and supported acceptance remain separate layers. A shared key does not establish independent review. Local closure does not establish global completeness or organizational approval.

Build and run an operator lifecycle

The commands below run from this repository and need mise, go, and jq. They build the binary outside the project ledger, keep the private key outside it, admit two commits, inspect an event and the current head, create a checkpoint, and run strict verification.

work_dir="$(mktemp -d)"
repo="$work_dir/project"
key_dir="$(mktemp -d)"
pact="$key_dir/pact"
key_file="$key_dir/operator.key.json"

mkdir -p "$repo"
env -u GOROOT mise exec -- go build -o "$pact" ./cmd/pact

"$pact" setup \
  --repo "$repo" \
  --namespace org/example/widget \
  --actor operator/alice \
  --key-file "$key_file" \
  --json

cat >"$work_dir/first.json" <<'JSON'
{"events":[{"local_id":"e1","kind":"observation","type":"build.test.executed","subject":"build/linux/1","schema_ref":"pact:core/generic-object/v1","payload":{"result":"pass"},"evidence":[],"caused_by":[],"supersedes":[],"tags":["ci"]}]}
JSON

first_commit="$("$pact" commit \
  --repo "$repo" \
  --key-file "$key_file" \
  --events "$work_dir/first.json" \
  --json)"
first_id="$(printf '%s' "$first_commit" | jq -r .object_id)"
first_event="$(printf '%s' "$first_commit" | jq -r '.event_refs[0]')"

cat >"$work_dir/second.json" <<'JSON'
{"events":[{"local_id":"e2","kind":"observation","type":"build.package.created","subject":"build/linux/1","schema_ref":"pact:core/generic-object/v1","payload":{"artifact":"widget.tar"},"evidence":[],"caused_by":[],"supersedes":[],"tags":["ci"]}]}
JSON

"$pact" commit \
  --repo "$repo" \
  --key-file "$key_file" \
  --events "$work_dir/second.json" \
  --json

"$pact" index status --repo "$repo" --json
"$pact" index rebuild --repo "$repo" --json
"$pact" log --repo "$repo" --namespace org/example --limit 100 --json
"$pact" query \
  --repo "$repo" \
  --namespace org/example \
  --type build.test.executed \
  --tag ci \
  --limit 100 \
  --json

"$pact" show --repo "$repo" "$first_id" --json
"$pact" show --repo "$repo" "$first_event" --json
"$pact" heads --repo "$repo" --namespace org/example --json
"$pact" verify --repo "$repo" --strict --json

checkpoint="$("$pact" checkpoint \
  --repo "$repo" \
  --key-file "$key_file" \
  --scope org/example \
  --policy-ref sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
  --authority-epoch epoch-1 \
  --schema-ref sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
  --purpose 'operator lifecycle' \
  --json)"
checkpoint_id="$(printf '%s' "$checkpoint" | jq -r .object_id)"

"$pact" show --repo "$repo" "$checkpoint_id" --json
"$pact" verify --repo "$repo" --strict --json

Private keys must stay outside every initialized project root. pact keygen refuses an output path inside such a root. The project stores public identity and signatures only. Keep external key files owner-readable only and back them up through the operator's normal secret-storage process.

Official checkpoint creation has a stricter production rule than the bundled Python reference: the signing key must already be configured as a local trusted root with trust-add. Unknown signers cannot create a checkpoint, even when their signature would be authentic. PACT performs this authorization check and a full strict store verification before it persists checkpoint bytes.

Index and query operations

The shipped command inventory is quickstart, setup, init, keygen, trust-add, hash, record, commit, status, heads, show, evidence check, verify, checkpoint, index status, index rebuild, log, and query.

The live SQLite file is .pact/index/pact-v1.sqlite3. It is derived, disposable, and never canonical ledger history. Removing it does not remove an event, trust root, head, or checkpoint. Read commands never repair it as a side effect. Inspect the state, then rebuild after the derived filesystem shape is safe:

pact index status --repo /path/to/project --json
pact index rebuild --repo /path/to/project --json

missing, stale, corrupt, incompatible, and partial-build all require operator action. index rebuild recreates a missing .pact/index directory and repairs an absent live file or unusable regular database. Existing directory and live-file paths must be real, with no SQLite sidecars. Remove or repair unsafe paths and SQLite sidecars before rebuilding. A successful raw canonical write makes an existing index stale; this is expected. The validated record workflow also refreshes the derived index and reports failures separately. Rebuild scans and verifies canonical bytes, creates and validates a same-directory temporary SQLite file, then replaces the live file. Log and query refuse every state except current. show and canonical verification do not need a usable index.

log accepts repeated --namespace and --actor filters. query requires at least one filter and accepts repeated --namespace, --type, --kind, --subject, --actor, --tag, --schema-ref, --event-ref, --caused-by, and --supersedes filters. Repeated values in one family are OR; different families are AND. Namespace prefixes match the exact namespace or a slash-delimited descendant, so org/example does not match org/example2. Other filters are exact and case-sensitive after validation and field normalization. Subject and tag values use Unicode NFC normalization.

pact log \
  --repo /path/to/project \
  --namespace org/example \
  --namespace org/another-team \
  --limit 100 \
  --json

pact query \
  --repo /path/to/project \
  --type build.test.executed \
  --type build.package.created \
  --tag ci \
  --actor ed25519:sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
  --limit 100 \
  --json

The query means “either listed type, and tag ci, and that actor.” Results use stored causal batches based on known local dependency edges. observed_at is advisory metadata and never sorts results. A batch is part of a partial order, not a total sequence; a lower batch number does not prove a path to every later batch.

Replica status separates local closure from global knowledge. locally_closed means all dependencies named by the local object set are present. It does not mean no other object exists elsewhere. Missing dependencies yield stable blockers and local completeness incomplete; global completeness always remains unknown. A valid current index built from that incomplete replica has coverage=partial. An unavailable index has coverage=unavailable, while partial-build means SQLite diverges from its own declared source and cannot answer queries. Strict verification and official checkpoints refuse completeness blockers.

Pagination uses an opaque cursor bound to the command, normalized filters, page limit, schema, source fingerprint, logical digest, and causal order. Cursors survive process restart and a deterministic same-source rebuild. A changed query fails with cursor_query_mismatch. When canonical source changes, the old index first fails as index_stale; after rebuilding against the changed source, an old cursor fails with cursor_stale. A page may split a causal batch, in which case each affected page reports complete_in_page=false. Unresolved events remain a separate transport group and do not claim to occur later.

Phase 2 limits

The fixed profile is pact/resource-limits/phase2-v1.

Resource Maximum
One canonical object 4,194,304 bytes
Canonical objects per scan 100,000
Canonical bytes per scan 1,073,741,824 bytes
Events per commit 1,024
Events per scan 250,000
Parents per commit 64
Longest known causal path 4,096 signed dependency edges
Graph frontier 4,096 nodes
Total graph edges 1,000,000
Query results per page default 100; maximum 1,000
Values in one filter family 64
Values across filter families 256
Encoded cursor 4,096 bytes
Decoded cursor 3,072 bytes
Encoded JSON result, including the --json newline 16,777,216 bytes
Stored SQLite file 2,147,483,648 bytes
Diagnostic samples per axis 100
One diagnostic text field 512 UTF-8 bytes

Exact-bound inputs pass. With --json, the first excess fails safely with a bounded, machine-readable diagnostic instead of truncating authoritative results. Log and query use one streaming serializer for both the bound and CLI output, so this limit does not require a full-page encoding buffer. The code names the input contract: graph, scan, and filter excesses use resource_limit; an invalid page limit is a usage error; an oversized or malformed cursor is cursor_invalid; and an oversized stored index is corrupt.

Every query hashes the bounded canonical object set and verifies selected canonical commits, so query work grows with the canonical object set. Rebuild scans all bounded canonical bytes and holds the exclusive store lock through publication, pausing writers during rebuild. This fixed-snapshot tradeoff is intentional for the local Phase 2 scope.

Arbitrary external payload schemas, network sync, public policy execution, and delegated authority remain out of scope. The fixed development recorder validates its bundled schemas. PACT also has no trusted timestamps, hardware key service, or global completeness claim.

Repository gate

Install uv and jq, then run the full Go gate, compiled scratch index lifecycle, and bundled Python reference suite with:

env -u GOROOT mise exec -- ./scripts/check

About

Local signed event ledger CLI with Ed25519 identities, verification, checkpoints, and a queryable SQLite index.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages