@adlc/gate-manifest
v1.4.1
Published
Hash-chained evidence ledger recording what each ADLC gate verified — agentic provenance with optional HMAC signing (C11).
Maintainers
Readme
gate-manifest
ADLC C11 — cross-cutting provenance. A hash-chained evidence ledger that records what each ADLC gate verified, proving to auditors (and CI) that agentic code was checked before it shipped. Set ADLC_MANIFEST_KEY to add HMAC-SHA256 signatures so the chain attests authorship, not just internal consistency (see Signing & provenance).
ADLC phase
Serves C11 (cross-cutting provenance / agentic SLSA). Consumed by P6 human-gate reviewers who need attest output as a PR comment, and by CI which runs verify as a blocking gate.
Usage
gate-manifest record <gate-name> [--ticket id] [--data '{json}'] [--files a,b,c] [--dir path] [--json]
gate-manifest verify [--json] [--dir path]
gate-manifest show [--ticket id] [--json] [--dir path]
gate-manifest attest [--ticket id] [--dir path]
gate-manifest repair-chain --reason "..." [--write] [--attest-unsigned] [--json] [--dir path]Prosecution, runner acceptance, rails evidence, and manual gate records use the
same atomic chain writer. repair-chain recovers ledgers made
by older raw appenders: it refuses valid ledgers, preserves the original bytes
in a hash-named backup, rechains entries in order, and records the repair as the
final entry. Signed ledgers require the original matching ADLC_MANIFEST_KEY;
repair verifies every existing signature before it rewrites anything. If keyed
repair would sign previously unsigned entries, it refuses unless the operator
passes --attest-unsigned; the repair plan and audit entry disclose their count
and original line numbers.
record
Append one entry to .adlc/manifest.jsonl.
gate-manifest record spec-lint --ticket T-42 --data '{"model":"haiku","pass":true}' --files src/foo.mjs,src/bar.mjsThe entry stored:
{
"seq": 3,
"gate": "spec-lint",
"ticket": "T-42",
"ts": "2024-01-01T00:00:00.000Z",
"data": { "model": "haiku", "pass": true },
"files": { "src/foo.mjs": "<sha256>", "src/bar.mjs": "<sha256>" },
"prev": "<sha256 of the previous raw JSONL line, or null>",
"sig": "<HMAC-SHA256 over the canonical entry bytes — present only when ADLC_MANIFEST_KEY is set>"
}When ADLC_MANIFEST_KEY is set, record appends a sig (the human output prints (signed) / (unsigned)). See Signing & provenance below.
| Flag | Description |
|------|-------------|
| --ticket id | Associate this entry with a ticket id (optional) |
| --data '{json}' | Arbitrary JSON payload (must be valid JSON; malformed → exit 1) |
| --files a,b,c | Comma-separated paths; each is SHA-256 hashed (missing files hash to null) |
| --dir path | Override ledger directory (default .adlc) |
| --json | Print the recorded entry as JSON |
verify
Walk the raw ledger lines and validate the hash chain. Every entry's prev must equal sha256 of the exact raw bytes of the previous line; sequence numbers must be strictly monotonically increasing.
gate-manifest verify # human-readable
gate-manifest verify --json # machine-readableExit 0 when valid (or empty manifest). Exit 2 when the chain is broken — reports the seq and line number of the first break.
When ADLC_MANIFEST_KEY is set, verify additionally checks every entry's HMAC signature. A missing sig (unsigned entry) or a wrong sig (signature invalid) breaks the chain. The JSON result includes signed: true only when a key was present and every entry verified cryptographically; otherwise signed: false.
| Flag | Description |
|------|-------------|
| --json | Emit { valid, message, count, signed, break } |
| --dir path | Override ledger directory |
show
Print entries from the ledger, optionally filtered by ticket.
gate-manifest show
gate-manifest show --ticket T-42
gate-manifest show --ticket T-42 --json| Flag | Description |
|------|-------------|
| --ticket id | Filter to entries with this ticket id |
| --json | Emit { entries, skipped } |
| --dir path | Override ledger directory |
attest
Generate a Markdown summary suitable for a PR comment.
gate-manifest attest --ticket T-42Output example:
## Gate evidence for T-42
| seq | gate | ts | files | data |
|-----|------|-----|-------|------|
| 1 | spec-lint | 2024-01-01T… | 0 | — |
| 2 | hollow-test | 2024-01-01T… | 3 | model=haiku |
Chain status: **valid** (2 entries)| Flag | Description |
|------|-------------|
| --ticket id | Filter entries and use ticket id in heading |
| --dir path | Override ledger directory |
Exit codes
| Code | Meaning |
|------|---------|
| 0 | Gate passes (record, show, attest always; verify when chain is valid) |
| 1 | Operational error (bad input, unreadable file, malformed --data JSON) |
| 2 | Gate fails (verify detects chain break) |
Chain integrity
The atomic writer derives prev from the byte-exact ledger state while holding
the append lock—never from parsed and re-serialized entries. The prev field is
sha256(previous raw JSONL line) (null for the first entry). Tampering any
middle line breaks all subsequent links and is detected by verify.
Signing & provenance
The hash chain alone proves internal consistency, not authorship. sha256 is a public, keyless function: anyone who can write manifest.jsonl can recompute every prev and forge a clean chain from scratch. On its own the chain is therefore not cryptographic provenance — do not represent a hash-chain-only pass as in-toto/SLSA attestation.
To get real provenance, set a secret signing key:
export ADLC_MANIFEST_KEY="$(openssl rand -hex 32)" # store in your CI secret manager, never in the repo
gate-manifest record spec-lint --ticket T-42
gate-manifest verify --json # → { ..., "signed": true }- record keeps its compatible v1 signature over
{ seq, gate, ts, ticket?, data?, files, prev }. Generalized first-party evidence usessigVersion: 2and signs canonical JSON for every entry field exceptsig, including ticket, revision, and provenance data. - verify (run with the key) requires every entry to carry a valid sig — comparison is constant-time (
crypto.timingSafeEqual). A missing sig →unsigned entry; a wrong sig →signature invalid. Either breaks the chain (exit 2). This defeats the forge-from-scratch attack: without the key, an attacker cannot produce valid signatures. - verify without a key still checks the hash chain but reports
signed: false, so callers cannot claim cryptographic provenance.
Zero-dependency: HMAC comes from Node's built-in node:crypto. Key management (rotation, distribution) is out of scope for this tool — supply the key via the environment.
Sibling tools
rails-guard(C5) — appends its own proof here after verifying diff is rails-clean.hollow-test(C4) — appends coverage and mutation results.review-calibration(C8) — appends prosecution verdicts and calibration score.
Core gaps
None for ledger/CLI primitives — sha256, hashFiles, appendEntry, readEntries, ledgerPath, ADLC_DIR, parseArgs, pass, gateFail, opError, printJson from @adlc/core cover them. Core exposes sha256 but no keyed-MAC primitive, so HMAC signing uses Node's built-in node:crypto (createHmac, timingSafeEqual) directly in lib/sign.mjs — still zero runtime dependencies.
