From 6c6e89c0ac3146f00baf0c3098164cfe0d765f4f Mon Sep 17 00:00:00 2001 From: ares <285551516+New1Direction@users.noreply.github.com> Date: Sat, 6 Jun 2026 08:07:31 -0700 Subject: [PATCH] =?UTF-8?q?feat(ci):=20'verify-ledger'=20GitHub=20Action?= =?UTF-8?q?=20=E2=80=94=20verify=20AI=20work=20as=20a=20CI=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reusable composite Action that recomputes a korgex receipt/journal's hash-chain (+ causal DAG + Ed25519 signature) in CI and FAILS the build if anything was tampered — zero trust in the tool that produced the ledger. Turns the verifiable- cognition moat into a one-line check any repo can adopt: - uses: New1Direction/korgex/.github/actions/verify-ledger@main with: { path: '.korg/journal.json' } # or '**/*.korgreceipt.json' - verify_ledger.py: resolves a verifier (KORG_VERIFY_BIN override · npx @korgg/ledger-verify · cargo-installed korg-verify [default]) — all three impls emit the same --json verdict — globs the path, verifies each, writes a markdown verdict to . Exit 0 all-valid / 1 any-invalid / 2 setup error. No-match fails loudly (no silent pass with nothing verified). Optional --pubkey pins the signer. - action.yml: composite; caches the korg-verify binary. - Self-test workflow proves it on real CI: an intact frozen vector passes the gate, a tampered one fails it (asserted). Logic LIVE-validated locally against the committed conformance vectors with the real korg-verify binary: intact->exit0, tampered->exit1 (exact seq error), missing->exit2, glob handled, step-summary table rendered. --- .github/actions/verify-ledger/README.md | 62 ++++++++++ .github/actions/verify-ledger/action.yml | 46 +++++++ .../actions/verify-ledger/verify_ledger.py | 117 ++++++++++++++++++ .github/workflows/verify-ledger-selftest.yml | 44 +++++++ 4 files changed, 269 insertions(+) create mode 100644 .github/actions/verify-ledger/README.md create mode 100644 .github/actions/verify-ledger/action.yml create mode 100644 .github/actions/verify-ledger/verify_ledger.py create mode 100644 .github/workflows/verify-ledger-selftest.yml diff --git a/.github/actions/verify-ledger/README.md b/.github/actions/verify-ledger/README.md new file mode 100644 index 0000000..6630a38 --- /dev/null +++ b/.github/actions/verify-ledger/README.md @@ -0,0 +1,62 @@ +# Verify korg ledger — GitHub Action + +A CI gate that **verifies what your AI agent actually did**. Point it at a korgex +receipt or ledger journal; it recomputes the hash-chain (+ causal DAG, + Ed25519 +signature if present) and **fails the build if anything was tampered** — with zero +trust in the tool that produced the ledger. + +It runs one of the three independent `korg-ledger@v1` implementations (Rust +`korg-verify` from crates.io by default, or the `@korgg/ledger-verify` JS impl), all +of which reproduce the same frozen conformance vectors. + +## Usage + +```yaml +# .github/workflows/verify.yml +name: verify-ai-work +on: [push, pull_request] +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: New1Direction/korgex/.github/actions/verify-ledger@main + with: + path: ".korg/journal.json" # or "**/*.korgreceipt.json" +``` + +Pin the signer (recommended for signed receipts), so a green check proves authorship +against a key you trust: + +```yaml + - uses: New1Direction/korgex/.github/actions/verify-ledger@main + with: + path: "deliverable.korgreceipt.json" + pubkey: ${{ vars.KORG_SIGNER_PUBKEY }} +``` + +## Inputs + +| Input | Default | Description | +|---|---|---| +| `path` | `.korg/journal.json` | File or glob to verify (receipt or journal; `**` supported). | +| `pubkey` | — | Hex pubkey to **pin** the expected signer; any other key is rejected. | +| `verifier` | `cargo` | `cargo` (korg-verify from crates.io) or `npx` (`@korgg/ledger-verify`). | +| `fail-on-invalid` | `true` | Fail the job on a bad verdict. Set `false` to report-only. | +| `summary` | `true` | Write a verdict table to the GitHub step summary. | + +## Exit codes + +`0` every file valid · `1` at least one invalid (the gate) · `2` setup error (no file +matched, or the verifier couldn't be installed). + +## What a green check proves + +The recorded events hash-chain intact and form a well-formed causal DAG +(tamper-evident); a receipt's recorded tip matches the chain head; and — if signed — +the named key attests to that exact tip. It does **not** prove *when* it happened +(needs an external time anchor) or that the key maps to a real-world identity (pin it +with `pubkey`). + +> Tip: the same logic runs locally — `KORG_VERIFY_BIN=/path/to/korg-verify python3 +> verify_ledger.py` (with `INPUT_PATH=…`) — so you can reproduce a CI verdict by hand. diff --git a/.github/actions/verify-ledger/action.yml b/.github/actions/verify-ledger/action.yml new file mode 100644 index 0000000..64782db --- /dev/null +++ b/.github/actions/verify-ledger/action.yml @@ -0,0 +1,46 @@ +name: "Verify korg ledger" +description: "Verify a korgex receipt or ledger journal in CI (hash-chain + causal DAG + Ed25519 signature). Fails the job if verification fails — zero trust in the tool that produced it." +branding: + icon: "shield" + color: "green" + +inputs: + path: + description: "Path or glob to a korg receipt (*.korgreceipt.json) or journal (.korg/journal.json, *.jsonl). Supports ** recursive globs." + required: false + default: ".korg/journal.json" + pubkey: + description: "Optional hex pubkey to PIN the expected signer — a green check then proves authorship against a key you trust, not just the one the receipt carries." + required: false + default: "" + verifier: + description: "Which implementation to run: 'cargo' (korg-verify from crates.io, default) or 'npx' (@korgg/ledger-verify, the JS impl)." + required: false + default: "cargo" + fail-on-invalid: + description: "Fail the job when verification fails (the gate). Set false to report-only." + required: false + default: "true" + summary: + description: "Write a verdict table to the GitHub step summary." + required: false + default: "true" + +runs: + using: "composite" + steps: + - name: Cache korg-verify + if: ${{ inputs.verifier == 'cargo' }} + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/korg-verify + key: korg-verify-bin-${{ runner.os }} + - name: Verify korg ledger + shell: bash + env: + INPUT_PATH: ${{ inputs.path }} + INPUT_PUBKEY: ${{ inputs.pubkey }} + INPUT_VERIFIER: ${{ inputs.verifier }} + INPUT_FAIL_ON_INVALID: ${{ inputs.fail-on-invalid }} + INPUT_SUMMARY: ${{ inputs.summary }} + run: python3 "$GITHUB_ACTION_PATH/verify_ledger.py" diff --git a/.github/actions/verify-ledger/verify_ledger.py b/.github/actions/verify-ledger/verify_ledger.py new file mode 100644 index 0000000..eaacc5f --- /dev/null +++ b/.github/actions/verify-ledger/verify_ledger.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""CI gate: verify korg receipts / ledger journals — fail the build if the hash-chain +doesn't verify. + +Resolves a verifier and runs it on every matched file: + - ``KORG_VERIFY_BIN`` (env) — use this binary directly (also how this script is + tested locally without a network install); + - ``verifier: npx`` — ``npx @korgg/ledger-verify`` (the JS implementation); + - ``verifier: cargo`` (default) — ``korg-verify`` from crates.io (installed if absent). + +All three implementations emit the SAME ``--json`` verdict shape, so parsing is +uniform. Exit 0 = every file valid · 1 = at least one invalid · 2 = setup error. + +Inputs arrive as ``INPUT_*`` env vars (GitHub composite-action convention). +""" +from __future__ import annotations + +import glob +import json +import os +import shutil +import subprocess +import sys + + +def _bool(name: str, default: str = "true") -> bool: + return os.environ.get(name, default).strip().lower() in ("1", "true", "yes", "on") + + +def _fail_setup(msg: str) -> None: + print(f"::error::{msg}") + sys.exit(2) + + +def resolve_verifier() -> list[str]: + """The verifier argv prefix (file + flags are appended per call).""" + override = os.environ.get("KORG_VERIFY_BIN") + if override: + return [override] + verifier = os.environ.get("INPUT_VERIFIER", "cargo").strip().lower() + if verifier == "npx": + return ["npx", "--yes", "@korgg/ledger-verify"] + # cargo (default): the published korg-verify binary + if not shutil.which("korg-verify"): + print("Installing korg-verify from crates.io…", flush=True) + r = subprocess.run(["cargo", "install", "korg-verify"], capture_output=True, text=True) + if r.returncode != 0: + _fail_setup(f"`cargo install korg-verify` failed:\n{r.stderr[-2000:]}") + return ["korg-verify"] + + +def write_summary(rows: list[tuple[str, str, str]], run: list[str]) -> None: + path = os.environ.get("GITHUB_STEP_SUMMARY") + if not path or not _bool("INPUT_SUMMARY"): + return + lines = [ + "### 🔗 korg ledger verification", + "", + f"Verifier: `{' '.join(run)}` — three independent implementations reproduce the " + "same hashes, so a green check needs no trust in the tool that produced the ledger.", + "", + "| file | result | detail |", + "|---|---|---|", + ] + lines += [f"| `{f}` | {res} | {detail} |" for f, res, detail in rows] + with open(path, "a", encoding="utf-8") as fh: + fh.write("\n".join(lines) + "\n") + + +def main() -> None: + glob_pat = os.environ.get("INPUT_PATH", ".korg/journal.json").strip() + pubkey = os.environ.get("INPUT_PUBKEY", "").strip() + run = resolve_verifier() + + files = sorted(glob.glob(glob_pat, recursive=True)) + if not files: + # A gate pointed at a ledger that isn't there is a misconfig or a missing + # receipt — fail loudly rather than silently "pass" with nothing verified. + _fail_setup(f"no korg receipt/journal matched: {glob_pat!r}") + + rows: list[tuple[str, str, str]] = [] + any_invalid = False + for f in files: + args = list(run) + [f, "--json"] + (["--pubkey", pubkey] if pubkey else []) + p = subprocess.run(args, capture_output=True, text=True) + try: + v = json.loads(p.stdout) + except (ValueError, TypeError): + any_invalid = True + rows.append((f, "❌ error", (p.stderr or p.stdout or "no verifier output").strip()[:200])) + print(f"✗ {f} — verifier produced no parseable verdict") + continue + if v.get("valid") is True: + sig = v.get("signer") or "" + detail = f"{v.get('event_count', '?')} events" + if v.get("dag_ok"): + detail += " · DAG ok" + if sig and v.get("signature_ok"): + detail += f" · signed {sig[:12]}…" + rows.append((f, "✅ valid", detail)) + print(f"✓ {f} — {detail}") + else: + any_invalid = True + errs = "; ".join(v.get("errors", [])[:3]) or "verification failed" + rows.append((f, "❌ invalid", errs)) + print(f"✗ {f} — {errs}") + + write_summary(rows, run) + if any_invalid and _bool("INPUT_FAIL_ON_INVALID"): + print("::error::korg verification failed — see the job summary.") + sys.exit(1) + print(f"All {len(files)} ledger artifact(s) verified.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/verify-ledger-selftest.yml b/.github/workflows/verify-ledger-selftest.yml new file mode 100644 index 0000000..eea6c29 --- /dev/null +++ b/.github/workflows/verify-ledger-selftest.yml @@ -0,0 +1,44 @@ +name: verify-ledger self-test + +# Proves the verify-ledger action on GitHub CI: an intact ledger passes the gate, and +# a tampered one fails it (and we assert the failure). Uses the committed frozen +# korg-ledger@v1 conformance vectors as fixtures. + +on: + push: + paths: + - ".github/actions/verify-ledger/**" + - ".github/workflows/verify-ledger-selftest.yml" + pull_request: + paths: + - ".github/actions/verify-ledger/**" + - ".github/workflows/verify-ledger-selftest.yml" + workflow_dispatch: + +jobs: + intact-passes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: an intact ledger verifies (gate passes) + uses: ./.github/actions/verify-ledger + with: + path: spec/korg-ledger-v1/vectors/basic-intact.jsonl + + tampered-fails: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: a tampered ledger is rejected (gate is expected to fail) + id: verify + continue-on-error: true + uses: ./.github/actions/verify-ledger + with: + path: spec/korg-ledger-v1/vectors/tampered-content.jsonl + - name: assert the gate failed + run: | + if [ "${{ steps.verify.outcome }}" != "failure" ]; then + echo "::error::tampered ledger should have FAILED the gate, but did not" + exit 1 + fi + echo "✓ tampered ledger correctly rejected by the gate"