Skip to content

feat: complete workflow-plugin-audit-chain (modules + steps + integration) — PR 4#3

Merged
intel352 merged 11 commits intomainfrom
feat/modules-and-steps
May 3, 2026
Merged

feat: complete workflow-plugin-audit-chain (modules + steps + integration) — PR 4#3
intel352 merged 11 commits intomainfrom
feat/modules-and-steps

Conversation

@intel352
Copy link
Copy Markdown
Contributor

@intel352 intel352 commented May 3, 2026

Summary

This PR delivers the complete audit-chain plugin implementation across three tasks, and closes the wfctl-strict-contracts CI gate that was failing on the admin-merged PR 1 and PR 3. All five step types now carry STRICT_PROTO contracts in plugin.contracts.json, satisfying the gate that checks for fully-typed proto dispatch end-to-end.

What's included

Task 13 — audit.ledger + audit.anchor_provider.* modules (commits fefd185, fd59afe)

  • modules.LedgerModule (audit.ledger): opens a pgx connection pool, registers chain.Appender and *sql.DB in global ledger/DB registries, full Init/Start/Stop lifecycle with double-init guard.
  • modules.OpenTimestampsProviderModule (audit.anchor_provider.opentimestamps): registers an OTS anchor provider, no network calls on Init.
  • modules.GitProviderModule (audit.anchor_provider.git): registers a Git-commit anchor provider.
  • modules.SigstoreProviderModule (audit.anchor_provider.sigstore): registers a Sigstore/Rekor anchor provider.
  • 16 module tests covering lifecycle, registry, and lifecycle error paths.

Task 14 — 7 step types with strict proto contracts (commits 4ef1f90, 6e4d1b5, 9fc52cc)

Seven steps, each wired via sdk.NewTypedStepFactory with STRICT_PROTO contracts:

Step type Handler Proto I/O
step.audit.append AppendHandler AppendRequestAppendResponse
step.audit.verify VerifyHandler VerifyRequestVerifyResponse
step.audit.merkle_root MerkleRootHandler MerkleRootRequestMerkleRootResponse
step.audit.anchor AnchorHandler AnchorRequestAnchorResponse
step.audit.proof ProofHandler ProofRequestProofResponse
step.audit.poll_anchor_confirmation PollAnchorConfirmationHandler PollAnchorConfirmationRequestPollAnchorConfirmationResponse
step.audit.public_receipt PublicReceiptHandler PublicReceiptRequestPublicReceiptResponse

Key correctness invariants:

  • AppendHandler uses RETURNING created_at — timestamps come from the DB clock, not the Go process.
  • AnchorHandler inserts with ON CONFLICT (ledger, provider, range_start, range_end) DO NOTHING — anchor step is idempotent.
  • PollAnchorConfirmationHandler enforces a forward-only ordering guard (pending → confirmed → finalized).
  • 21 step tests + 44 chain tests.

Task 15 — gRPC plugin entrypoint + E2E integration test (commits 70937e3, 1aa6765, cf88b67)

  • cmd/workflow-plugin-audit-chain/main.go calls sdk.Serve(internal.NewPlugin()).
  • internal/plugin.go registers all 4 module types and 7 step types as TypedModuleProvider + TypedStepProvider, with compile-time interface assertions.
  • integration_test.go (repo root, package integration_test): end-to-end scenario over real gRPC transport:
    1. Spins up Postgres 16 via testcontainers.
    2. Compiles and starts the plugin binary as a subprocess via goplugin.NewClient + ext.Handshake.
    3. CreateModule → InitModule → StartModule with anypb.Any(LedgerConfig) over TCP gRPC.
    4. CreateStep + ExecuteStep(AppendRequest) — entry hashes collected from AppendResponse.
    5. ExecuteStep(VerifyRequest) — asserts valid=true, entries_verified=5.
    6. ExecuteStep(MerkleRootRequest) — cross-checks root against chain.MerkleRoot(entryHashes).
    7. Mock anchor inserted directly; ExecuteStep(ProofRequest) retrieves inclusion proof for entry 3.
    8. chain.VerifyInclusion(hash3, merklePath, root) — cryptographic closure.
    • Skips cleanly under go test -short (no Docker required).
    • 19 internal tests + 1 E2E gRPC integration test.

Test plan

  • GOWORK=off go build ./... — clean
  • GOWORK=off go test -short ./... — 146 tests pass, E2E skipped
  • GOWORK=off go test -v -count=1 -timeout=300s . — E2E integration test passes (~20s with Docker + binary build)
  • go test -short .--- SKIP: TestE2E_AuditChainScenario (Docker guard confirmed)

Note on Copilot Code Review: Attempted gh pr edit --add-reviewer Copilot — will note result below. If it fails (U-10: Copilot Code Review not enabled on this repo), this is non-blocking.

🤖 Generated with Claude Code

intel352 and others added 11 commits May 3, 2026 15:11
- 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>
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>
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>
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>
- 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>
- 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>
…le, 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>
…sport

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>
- 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>
…iders

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>
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>
@intel352 intel352 merged commit fb79b57 into main May 3, 2026
5 checks passed
@intel352 intel352 deleted the feat/modules-and-steps branch May 3, 2026 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant