feat: scaffold workflow-plugin-audit-chain (ADR 0008/0009)#1
Merged
Conversation
Rename all workflow-plugin-TEMPLATE placeholders to workflow-plugin-audit-chain (per ADR 0009). Declare all 7 step types, 6 module types, and 1 trigger type per the prereq design spec. Add proto/audit.proto with full typed contracts for all step input/output messages (AppendRequest/Response through PublicReceiptResponse) including PollAnchorConfirmationResponse swallowed/error_message fields. Commit generated gen/audit.pb.go. Add Makefile with proto-gen target. All 10 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace remaining workflow-plugin-TEMPLATE placeholders in CLAUDE.md (title, cross-compile command, cmd path). Add README.md describing all 7 step types, 6 module types, 1 trigger type, quick-start YAML, and build instructions per task spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… improvements Important: add actor (field 8) and metadata (field 9) to Entry message so ProofResponse carries the full appended record matching AppendRequest fields. Minor: add start_sequence/end_sequence to MerkleRootResponse so the covered range is self-contained. Add canonical preimage comment to Entry.entry_hash documenting the exact hash input (sequence, ledger, event_type, payload_hash, prev_entry_hash, created_at — sorted keys, no whitespace). Fix variable shadowing 't' → 'typ' in TestModuleTypes_Declared and TestStepTypes_Declared. Add TestCreateTrigger_KnownType_ReturnsNotImplemented to match Module/Step pattern. All 11 tests pass; go build ./... exits 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Canonicalize() uses json.Decoder with UseNumber() to decode, then json.Marshal() to re-encode — encoding/json sorts map keys automatically, producing RFC 8785-compliant output (keys lexicographically sorted at every level, no whitespace, arrays preserve order). UseNumber() prevents float64 precision loss for integers > 2^53. 13 tests: sort/whitespace/nested/idempotent (required), plus array order, deeply nested, booleans, null, empty object, large integer preservation, objects inside arrays, invalid JSON error, and string idempotency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PayloadHash: SHA-256(Canonicalize(data)) — key-order-invariant payload hash
using the RFC 8785 canonical helper from Task 7.
EntryHash: SHA-256 of canonical JSON {event_type, ledger, payload_hash,
prev_entry_hash, sequence} — ties each entry cryptographically to its
predecessor via prev_entry_hash. created_at/actor/metadata excluded per design.
MerkleRoot: standard binary tree over []string leaves (SHA-256 each leaf,
combine pairs as SHA-256(left||right), duplicate last when odd count).
InclusionProof: returns direction-prefixed sibling path ("L"|"R" + 64 hex).
VerifyInclusion: recomputes root from leaf + proof and compares.
38 tests: all 5 required TDD cases plus determinism, adversarial tamper,
out-of-range, all-indices verification, single/two/four/seven-leaf trees.
go build ./... exits 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e compare Important 1: add RFC 6962 §2.1 domain separation to Merkle tree. leafNode: SHA-256(0x00 || leaf_bytes); combineNodes: SHA-256(0x01 || left || right). Prevents an adversary from supplying a 64-byte internal-node preimage as a leaf to forge a proof path. Test helpers in merkle_test.go updated to match. Important 2: PayloadHash returns (string, error) instead of panicking. gRPC does not validate proto bytes fields as JSON; a malformed payload reaching PayloadHash would crash the append goroutine. Callers now handle the error. Added TestPayloadHash_InvalidJSON_ReturnsError test. Minor 3: fix TestMerkleProof_TamperedProof_Fails — previously tested invalid-hex truncation; now flips a single nibble to a different valid hex digit using the full-length proof, properly testing hash-mismatch rejection. Minor 4: VerifyInclusion uses crypto/subtle.ConstantTimeCompare on raw bytes instead of string equality for the final root comparison. 39 tests pass; go build ./... exits 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation Implements Append + AppendTx (BMW PR 11 Task 47 dual-entry-point contract), 4 up/down SQL migrations, testcontainer integration tests (8 cases including 50-goroutine × 10-entry concurrency test confirming gap-free sequences 1..500), and a test-migrations Makefile target. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add metadata []byte param to Append+AppendTx (fixes dead schema column; Task 47 can now pass metadata without a breaking change) - Fix all unchecked Scan calls in tests (false-positive risk on rollback test where zero-value matched expected value) - Add SET LOCAL lock_timeout='5s' before FOR UPDATE (surfaces stalled holders as errors rather than indefinite blocks) - Use DB-side NOW() for created_at (avoids multi-node clock skew) - Drop unused gID goroutine closure parameter; use range-over-int idiom Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…il wfctl strict-contracts validator)
The wfctl plugin validate --strict-contracts CI check requires contracts
array elements to be objects (map[string]interface{}), not filename
strings. Other GoCodeAlone plugins use null or [] for empty contracts;
adopting [] to match template default. The plugin.contracts.json file
remains in-tree for the future when contracts are formalized.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intel352
added a commit
that referenced
this pull request
May 3, 2026
…tion) — PR 4 (#3) * feat(modules): add audit.ledger + audit.anchor_provider.* modules - Add dsn field to LedgerConfig proto + three new anchor provider config proto messages (OpenTimestampsProviderConfig, GitAnchorProviderConfig, SigstoreProviderConfig); regenerate gen/audit.pb.go. - modules/ledger.go: LedgerModule typed with LedgerConfig; Init opens Postgres via sql.Open, creates chain.Appender, registers in ledger registry keyed by partition name; Stop unregisters + closes DB. - modules/anchor_provider.go: anchorProviderModule wraps each provider; typed factory functions (NewOpenTimestampsProviderModule, NewGitAnchorProviderModule, NewSigstoreProviderModule) construct the underlying provider and register it on Init. - 14 unit tests; all pass without a running Postgres instance. - internal/plugin.go: implement TypedModuleProvider (CreateTypedModule) so the gRPC server uses the typed proto path — zero map[string]any at the module creation boundary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(modules): address code-review issues in Task 13 Critical: - NewLedgerModule now validates config.Name is non-empty; silently registering under key "" would corrupt the ledger registry. Important: - Fix AuditChainPlugin doc comment: remove false TypedStepProvider claim (Task 14 adds that interface). Add compile-time assertion `var _ sdk.TypedModuleProvider = (*AuditChainPlugin)(nil)`. - Add 5 CreateTypedModule tests (the primary gRPC path was unexercised): valid LedgerConfig, nil config, mismatched anypb type, deferred providers, unknown type. Minor: - TestLedgerModule_StopUnregisters: add missing t.Cleanup guard. - LedgerModule.Init: guard against double-call to prevent DB pool leak. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(steps): add 7 audit-plugin step types with typed proto contracts Implements Task 14. All step handlers use sdk.TypedStepProvider (zero map[string]any). Key changes: - proto/audit.proto: add ledger field (5) to PollAnchorConfirmationRequest so poll handlers can retrieve the right *sql.DB from the registry. - gen/audit.pb.go: regenerated. - modules/ledger.go: add RegisterDB/GetDB/UnregisterDB registry; Init() and Stop() register/unregister both the chain.Appender and *sql.DB. - modules/anchor_provider.go: add AnchorProviderNames() for step.audit.anchor "all configured providers" fallback. - steps/: new package — AppendHandler, VerifyHandler, MerkleRootHandler, AnchorHandler, PollAnchorConfirmationHandler (swallow-transient-errors contract), ProofHandler (RFC 6962 inclusion proof), PublicReceiptHandler (verifiable receipt JSON + SHA256 hash, optional field redaction). - internal/plugin.go: wire TypedStepProvider; stepFactories slice maps each step type to its sdk.NewTypedStepFactory; legacy CreateStep path returns "not yet implemented via legacy path" for known types. - internal/plugin_test.go: TestTypedStepTypes_Declared, TestCreateTypedStep_UnknownType_ReturnsError, TestCreateTypedStep_KnownType_ReturnsInstance. All packages pass: GOWORK=off go test ./... -race -count=1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(steps): address spec-reviewer feedback on Task 14 Issue 1 — poll_anchor_confirmation transient/hard-error test coverage: - steps/testutil_test.go: add mockAnchorProvider (controls Verify return) and openFakeDB / fakeSQLDriver (fake database/sql driver returning confirmation="pending" for any SELECT without a real Postgres instance). - steps/poll_anchor_confirmation_test.go: add TestPollAnchorConfirmationHandler_TransientError_Swallowed — asserts swallowed=true, transitioned=false, error_message set, no gRPC error. Add TestPollAnchorConfirmationHandler_HardError_PropagatesGRPC — asserts non-nil error when provider.Verify returns a hard error. Issue 2 — public_receipt pseudonym format and replace-not-delete: - ApplyRedactions (now exported for direct testing): assigns "contributor_N" labels (N increments per unique original value within the receipt scope); REPLACES the field value with the pseudonym string instead of deleting the field; deduplicates: identical originals share one label. - steps/public_receipt_test.go: add TestApplyRedactions_TwoDistinctFields, TestApplyRedactions_DuplicateOriginalValue, TestApplyRedactions_NoRedactFields. Issue 3 — map[string]any in public_receipt.go: - Replaced map[string]any receipt document and anchorRecordSlice with typed structs: receiptDocument, receiptEntry, receiptMerkleProof, receiptAnchor. Zero map[string]any in the package. All packages pass: GOWORK=off go test ./... -race -count=1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(task14): address all 4 code-reviewer issues - chain: Append/AppendTx return DB-assigned createdAt via RETURNING created_at; steps/append.go propagates DB timestamp to AppendResponse (not app clock) - chain: update all call sites in append_test.go to 4-return destructuring - poll_anchor_confirmation: guard against downgrade with forward-only confirmationOrder map (pending<confirmed<finalized) - anchor: INSERT ... ON CONFLICT (ledger,provider,range_start,range_end) DO NOTHING for idempotent re-runs; matching unique index added to 004_indexes.sql + down Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(task15): add integration tests for plugin entrypoint - TestIntegration_BinaryBuilds: shells out to go build to verify the cmd/workflow-plugin-audit-chain binary compiles and links end-to-end - TestIntegration_AllStepsAreTyped: creates all 7 step types and verifies Execute() returns the typed-step dispatch error, proving each is a TypedStepInstance (not a legacy step) - TestIntegration_LedgerModuleLifecycle: exercises the full Init→Start→Stop lifecycle, verifying ledger+DB registries are populated after Init and cleared after Stop; also verifies double-Init guard - TestIntegration_AnchorProviderModule_OpenTimestamps: lifecycle smoke test for the OTS anchor provider module (no network calls on Init) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(task15): root-level E2E integration test (append-5, verify, Merkle, proof) integration_test.go at the module root exercises the full audit-chain scenario through the plugin factory chain: 1. Spins up ephemeral Postgres 16 via testcontainers + applies all 4 migrations. 2. Declares audit.ledger via internal.NewPlugin().CreateTypedModule — exercises the typed module factory wiring in internal/plugin.go. 3. Appends 5 entries via step.audit.append; verifies sequence 1-5 and 64-char hashes. 4. Verifies chain integrity over all 5 entries via step.audit.verify; expects Valid=true, EntriesVerified=5. 5. Computes Merkle root over entries 1-5 via step.audit.merkle_root; cross-checks against independently computed chain.MerkleRoot. 6. Records a mock audit_anchors row for range 1-5 with the computed root. 7. Retrieves and cryptographically verifies the Merkle inclusion proof for entry 3 via step.audit.proof + chain.VerifyInclusion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(task15): rewrite E2E integration test to use gRPC subprocess transport Addresses all 3 spec-reviewer rejection issues: 1. Delete internal/integration_test.go (wrong location — was internal package, not repo root). 2. Root integration_test.go now includes the full audit chain scenario: append-5, verify, Merkle root, cross-check, mock anchor, inclusion proof for entry 3, and chain.VerifyInclusion cryptographic check. 3. All step executions go through real gRPC proto serialisation: - testGRPCPlugin wraps go-plugin and dispenses pb.PluginServiceClient directly (bypassing *ext.PluginClient's unexported fields). - buildBinary/startPlugin compile the actual binary and start it as a subprocess via goplugin.NewClient + ext.Handshake. - Each request is packed as anypb.Any, sent over TCP gRPC to the subprocess, and unpacked from the typed response — genuine wire serialisation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(task15): address code-reviewer Important #1, #3 and Minor #4 - Important #1: add testing.Short() guard at top of TestE2E_AuditChainScenario so Docker-dependent test skips cleanly under `go test -short ./...`; remove redundant guard from buildBinary (test-level guard fires first). - Important #3: log StopModule errors in t.Cleanup instead of discarding with blank identifier. - Minor #4: run `GOWORK=off go mod tidy` — promotes testcontainers-go and testcontainers-go/modules/postgres from // indirect to direct deps. Important #2 (direct step handler calls) was already resolved in the prior commit (1aa6765) which rewrote the test to use goplugin.NewClient subprocess + pb.PluginServiceClient gRPC calls. Code-reviewer reviewed an earlier commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: strict-contracts — add kind/mode fields, drop unimplemented providers plugin.contracts.json: rewrote all 12 descriptors with required "kind", "type", and "mode": "strict_proto" fields (previously all had neither "kind" nor "mode", causing the wfctl validator to skip every descriptor and report 14 coverage-gap errors). plugin.json: remove audit.anchor_provider.ethereum and audit.anchor_provider.aws_qldb from capabilities.moduleTypes — those providers are not implemented and must not appear in the advertised contract surface. Validator now passes: OK workflow-plugin-audit-chain v0.1.0 (plugin.json) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct trigger output proto type AuditEntry → Entry The trigger.audit.entry_appended contract referenced a non-existent proto type workflow.plugin.audit.v1.AuditEntry. The actual message is workflow.plugin.audit.v1.Entry (proto/audit.proto line 242). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the full scaffold for
workflow-plugin-audit-chain— a new public/MIT Go plugin providing tamper-evident, hash-chained audit logging for the GoCodeAlone/workflow engine (ADR 0008). Namedaudit-chainper ADR 0009 to avoid collision with the existingauditplugin.plugin.json,plugin.contracts.json,proto/audit.proto, generatedgen/audit.pb.go) — 7 step types, 6 module types, 1 trigger type;Entry.entry_hashpreimage documented as SHA-256 of RFC 8785 canonical JSON of{event_type, ledger, payload_hash, prev_entry_hash, sequence}chain/canonical.go) — RFC 8785 key-sorted, whitespace-free JSON usingjson.NewDecoder.UseNumber()for numeric precisionchain/hash.go,chain/merkle.go) —PayloadHash([]byte) (string, error),EntryHash(...), RFC 6962 §2.1 domain-separated Merkle tree (0x00 leaf / 0x01 internal prefixes), direction-prefixed inclusion proofs,VerifyInclusionwithcrypto/subtle.ConstantTimeComparechain/append.go,migrations/001–004) —Appender.Append(owns its own tx) andAppendTx(participates in caller's tx, required by BMW PR 11 Task 47);SELECT … FOR UPDATEonaudit_ledgersfor gap-free monotonic sequences;SET LOCAL lock_timeout='5s'; DB-sideNOW()forcreated_at;metadata []bytecolumn wired throughchain/append_test.go) — testcontainers-go postgres:16-alpine; 8 tests including 50-goroutine × 10-entry concurrency test verifying sequences 1..500 with no gaps under-raceproto-gen,build,test,test-migrations,vet.github/workflows/ci.yml,release.yml,.goreleaser.yaml) — cross-platform binaries for linux/darwin/windows × amd64/arm64Test plan
GOWORK=off go build ./...— clean buildGOWORK=off go test ./... -v -race -count=1— all 40 tests pass (8 integration + 32 unit)make test-migrations— migrations up/down cycle passesGOWORK=off go vet ./...— no issues🤖 Generated with Claude Code