Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/actions/verify-ledger/README.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions .github/actions/verify-ledger/action.yml
Original file line number Diff line number Diff line change
@@ -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"
117 changes: 117 additions & 0 deletions .github/actions/verify-ledger/verify_ledger.py
Original file line number Diff line number Diff line change
@@ -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()
44 changes: 44 additions & 0 deletions .github/workflows/verify-ledger-selftest.yml
Original file line number Diff line number Diff line change
@@ -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"
Loading