121 lines
5.4 KiB
Text
Executable file
121 lines
5.4 KiB
Text
Executable file
#!/usr/bin/env nu
|
|
# scripts/witness.nu — dumb executor for standing laws (sow/laws.ncl).
|
|
#
|
|
# Verifies laws.ncl's signature before running anything (no witness, no start),
|
|
# runs every Active law's `check` command against the current catalog state, and
|
|
# appends one JSONL receipt line per law. Prints the exact command to sign the
|
|
# resulting receipt — it does NOT sign it.
|
|
#
|
|
# Signing the receipt is a deliberate, separate, human-executed step, never
|
|
# performed by this script. Confirmed 2026-07-03 while wiring T5: minisign has no
|
|
# password environment variable BY DESIGN (`minisign -h`) — the reference
|
|
# implementation refuses to let a passphrase-protected key be used
|
|
# non-interactively, specifically to prevent scripting/automation from handling it.
|
|
# A witness.nu that tried to call `minisign -S` internally could therefore never
|
|
# succeed against a real, passphrase-protected key; it would either hang forever
|
|
# waiting for interactive input (observed) or require weakening the key with `-W`
|
|
# (no password), which defeats the point. The correct split, matching J3's own
|
|
# precedent: this script produces content; the human signs it, in their own shell,
|
|
# every time — the same boundary as `laws.ncl`'s ratification, now applied to
|
|
# receipts too.
|
|
#
|
|
# Whole-file signing, not per-line: minisign signs files, and a per-line `sig`
|
|
# field would need to reference a signature computed over the line's own bytes
|
|
# minus that same field — the exact self-referential trap found and reverted in
|
|
# laws-schema.ncl's `signature` field (see
|
|
# provisioning/.coder/2026-07-03_laws-catalogo-provisioning.plan.md §3, §9 J3).
|
|
#
|
|
# Usage:
|
|
# nu scripts/witness.nu [laws_path] [receipt_path] --wo <id> [--pubkey path]
|
|
#
|
|
# Exit 0: every Active law's check passed (or none are Active). Exit 1: a guard
|
|
# failed (missing/invalid signature) or at least one Active law's check failed.
|
|
# Exit code reflects the LAW VERDICT only — receipt signing happens after, by the
|
|
# human, regardless of this script's exit code (a Rejected verdict is still signed,
|
|
# so the rejection itself is witnessed).
|
|
|
|
def main [
|
|
laws_path: string = "sow/laws.ncl"
|
|
receipt_path: string = "sow/receipts/catalog-witness.jsonl"
|
|
--wo: string = "catalog-witness"
|
|
--pubkey: string = "~/.minisign/witness.pub"
|
|
]: nothing -> nothing {
|
|
let pubkey = ($pubkey | path expand)
|
|
let sig_path = $"($laws_path).minisig"
|
|
|
|
# Guard 1: no witness, no start. The public key and the detached signature must
|
|
# both exist, and the signature must verify, before any check runs.
|
|
if not ($pubkey | path exists) {
|
|
print $"REFUSED: public key not found at ($pubkey)"
|
|
exit 1
|
|
}
|
|
if not ($sig_path | path exists) {
|
|
print $"REFUSED: signature not found at ($sig_path) — laws are non-binding until signed"
|
|
exit 1
|
|
}
|
|
|
|
let verify = (do { ^minisign -V -p $pubkey -m $laws_path -x $sig_path } | complete)
|
|
if ($verify.exit_code != 0) {
|
|
print $"REFUSED: signature verification failed for ($laws_path)"
|
|
print $verify.stderr
|
|
exit 1
|
|
}
|
|
print $"Signature verified: ($laws_path)"
|
|
|
|
# Guard 2: laws.ncl must export cleanly against its own schema contract.
|
|
let laws_export = (do { ^nickel export $laws_path --format json } | complete)
|
|
if ($laws_export.exit_code != 0) {
|
|
print $"REFUSED: ($laws_path) failed to export — ($laws_export.stderr)"
|
|
exit 1
|
|
}
|
|
|
|
let all_laws = ($laws_export.stdout | from json | get laws)
|
|
let active_laws = ($all_laws | where status == "Active")
|
|
let deferred_count = ($all_laws | where status == "Deferred" | length)
|
|
if ($deferred_count > 0) {
|
|
print ("Skipping " + ($deferred_count | into string) + " Deferred law(s) — not enforced, see laws.ncl")
|
|
}
|
|
|
|
let ts = (date now | format date "%+")
|
|
let laws_hash = (open $laws_path | hash sha256)
|
|
|
|
let results = ($active_laws | each {|law|
|
|
let out = (do { ^bash -c $law.check } | complete)
|
|
let verdict = (if $out.exit_code == 0 { "pass" } else { "fail" })
|
|
let scope_hash = ({scope: $law.scope, exclude: $law.exclude, exempt: $law.exempt} | to json | hash sha256)
|
|
{
|
|
wo: $wo,
|
|
sow_ref: $laws_path,
|
|
sow_hash: $laws_hash,
|
|
contract: $law.id,
|
|
phase: $law.kind,
|
|
exit: $out.exit_code,
|
|
verdict: $verdict,
|
|
detail: ($out.stderr | str trim | str substring 0..300),
|
|
scope_hash: $scope_hash,
|
|
ts: $ts,
|
|
}
|
|
})
|
|
|
|
let receipt_dir = ($receipt_path | path dirname)
|
|
^mkdir -p $receipt_dir
|
|
|
|
let lines = ($results | each {|r| $r | to json --raw } | str join "\n")
|
|
$"($lines)\n" | save --append --raw $receipt_path
|
|
|
|
let receipt_sig_path = $"($receipt_path).minisig"
|
|
print ""
|
|
print "Receipt written, NOT signed. Sign it yourself, in your own shell:"
|
|
print (" minisign -S -s ~/.minisign/witness.key -m " + $receipt_path + " -x " + $receipt_sig_path + " -t \"wo=" + $wo + " " + ($results | length | into string) + " law(s) checked at " + $ts + "\" -c \"provisioning catalog witness receipt\"")
|
|
print ""
|
|
|
|
let failed = ($results | where verdict == "fail")
|
|
if ($failed | is-not-empty) {
|
|
let failed_count = ($failed | length)
|
|
print ("REJECTED: " + ($failed_count | into string) + " law(s) failed")
|
|
$failed | select contract exit detail | print
|
|
exit 1
|
|
}
|
|
let pass_count = ($results | length)
|
|
print ("ACCEPTED: " + ($pass_count | into string) + " law(s) pass")
|
|
}
|