// Cross-runtime parity helper — Go side. // // Specification: /home/profit/lakehouse/docs/specs/SUBJECT_MANIFESTS_ON_CATALOGD.md §5 Step 8. // // Counterpart of crates/catalogd/src/bin/parity_subject_audit.rs. // Both helpers MUST produce byte-identical output for the same input. // // Modes: // // --known-answer // Print canonical-JSON + HMAC for a hardcoded fixture. Compared // byte-for-byte against the Rust helper's output. If they // differ, the canonical-JSON or HMAC algorithm has drifted. // // --verify --key // Replay the HMAC chain on a real audit JSONL. Print one JSON // object: {mode, count, tip, verified, error}. package main import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" cat "git.agentview.dev/profit/golangLAKEHOUSE/internal/catalogd" ) const genesis = "GENESIS" func deterministicKey() []byte { k := make([]byte, 32) for i := range k { k[i] = byte(i) } return k } // knownAnswerOut is intentionally identical to KnownAnswerOut in the // Rust helper so a stdout diff is a one-line semantic comparison. type knownAnswerOut struct { Mode string `json:"mode"` Canonical string `json:"canonical"` Hmac string `json:"hmac"` CanonicalBytesLen int `json:"canonical_bytes_len"` } type verifyOut struct { Mode string `json:"mode"` Count int `json:"count"` Tip string `json:"tip"` Verified bool `json:"verified"` Error *string `json:"error"` } func runKnownAnswer() { row := cat.SubjectAuditRow{ Schema: "subject_audit.v1", Ts: time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC), CandidateID: "WORKER-FIXED", Accessor: cat.AuditAccessor{ Kind: "gateway_lookup", Daemon: "gateway", Purpose: "parity_test", TraceID: "trace-fixed", }, FieldsAccessed: []string{"name"}, Result: "success", PrevChainHash: genesis, } canonical, hmacHex, err := cat.CanonicalAndHmac(&row, deterministicKey(), genesis) if err != nil { die("canonical/hmac: %v", err) } out := knownAnswerOut{ Mode: "known_answer", Canonical: string(canonical), Hmac: hmacHex, CanonicalBytesLen: len(canonical), } emit(out) } func runVerify(auditPath, keyPath string) { entries, err := cat.ReadAuditLog(auditPath) if err != nil { die("read audit log: %v", err) } key, err := os.ReadFile(keyPath) if err != nil { die("read key: %v", err) } count, tip, verr := cat.VerifyChain(entries, key) out := verifyOut{ Mode: "verify", Count: count, Tip: tip, Verified: verr == nil, } if verr != nil { s := verr.Error() out.Error = &s // Reset count + tip to match the Rust helper's error semantics. out.Count = 0 out.Tip = genesis } emit(out) } func emit(v any) { bs, err := json.Marshal(v) if err != nil { die("marshal output: %v", err) } fmt.Println(string(bs)) } func die(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) os.Exit(2) } func main() { args := os.Args[1:] var ( knownAnswer bool auditPath string keyPath string ) for i := 0; i < len(args); i++ { switch args[i] { case "--known-answer": knownAnswer = true case "--verify": if i+1 >= len(args) { die("--verify needs a path") } auditPath = args[i+1] i++ case "--key": if i+1 >= len(args) { die("--key needs a path") } keyPath = args[i+1] i++ case "-h", "--help": fmt.Fprintln(os.Stderr, "subject_audit_helper --known-answer") fmt.Fprintln(os.Stderr, "subject_audit_helper --verify --key ") os.Exit(0) default: die("unknown arg: %s", args[i]) } } if knownAnswer { runKnownAnswer() return } if auditPath == "" || keyPath == "" { die("need --known-answer OR (--verify --key )") } // Sanity: file naming convention .audit.jsonl. if !strings.HasSuffix(filepath.Base(auditPath), ".audit.jsonl") { die("audit log path must end with .audit.jsonl") } runVerify(auditPath, keyPath) }