feat(encryption): Stage 5 PR-C — BootstrapEncryption + bootstrap CLI + Validate()#759
Conversation
…+ Validate()
Stage 5 PR-C of the data-at-rest encryption rollout (design doc
docs/design/2026_04_29_partial_data_at_rest_encryption.md, §5.6
bootstrap, §6.1 admin.go, §6.6 admin commands). Closes the last
mutating-RPC gap in the proto.EncryptionAdmin surface and adds
the production-wiring guard the PR-B review carry-forward
identified.
## adapter/encryption_admin.go
- BootstrapEncryption proposes the §5.6 0x04 OpBootstrap entry on
top of Stage 3's raft envelope and Stage 4's fsmwire body
encoder. Validates:
- storage_dek_id != 0
- raft_dek_id != 0
- storage_dek_id != raft_dek_id (no slot collision)
- non-empty wrapped DEK pair
- writer_batch contains at least one member
- each writer.full_node_id != 0 (§6.1 sentinel rule)
- each writer.local_epoch <= 0xFFFF (§4.1 16-bit bound)
- 2*len(writers) <= bootstrapBatchRowCap = 16384 (matches
fsmwire.maxBootstrapBatchCount; rejected at the gRPC
boundary so a too-large batch does not round-trip through
Propose)
- buildBootstrapBatch / validateBootstrapWriter expand each
writer into the §5.6 step 1a two-row layout (one
storage-DEK row + one raft-DEK row) so the §4.1
nonce-uniqueness invariant covers both envelope purposes from
the first post-bootstrap entry. Row ordering is fixed: storage
row i precedes raft row i, locked by the wire-layout
round-trip test.
- New Validate() method enforces the option-pairing invariant:
proposer wired without leaderView fails closed. Production
wiring in main.go must call this; tests are explicitly allowed
to skip it (the proposer-without-leaderView path is a useful
test affordance for the propose-side wiring).
## cmd/elastickv-admin/encryption_mutators.go
- New `encryption bootstrap` subcommand:
--storage-dek-id, --raft-dek-id (uint32, non-zero, distinct)
--wrapped-storage-dek, --wrapped-raft-dek (base64)
--writer=<full_node_id>:<local_epoch> (repeated; required)
- parseBootstrapArgs / validateBootstrapDEKIDs /
decodeBootstrapWrappedDEKs split out for cyclop.
- parseWriterBatch enforces the same per-writer invariants the
server enforces (zero node id, epoch overflow, malformed
string) so the CLI fails fast before the Propose round-trip.
## cmd/elastickv-admin/encryption.go
- encryptionMain dispatches the new subcommand and updates the
usage text + unknown-subcommand error.
## Tests
adapter/encryption_admin_test.go:
- BootstrapEncryption happy path round-trips through
fsmwire.DecodeBootstrap. Verifies storage/raft row pairing
per writer.
- BootstrapEncryption rejects every boundary input (8 cases in
a table: zero ids, equal ids, empty wrapped DEKs, empty
batch, zero full_node_id, epoch overflow).
- BootstrapEncryption rejects oversize batch (just above the
bootstrapBatchRowCap boundary).
- BootstrapEncryption inherits the no-proposer FailedPrecondition
contract via TestEncryptionAdmin_MutatingRPCs_RejectWithoutProposer.
- Validate() three-way coverage: proposer-without-leaderView →
error; read-only server → nil; both wired → nil.
cmd/elastickv-admin/encryption_test.go:
- runEncryptionBootstrap happy path against a stub gRPC server.
- runEncryptionBootstrap rejects 5 malformed --writer cases at
the CLI parse boundary.
## Out of scope (Stage 5D / Stage 6)
- §5.6 step 1a capability fan-out helper. PR-C lets the operator
ship a pre-built writer batch on the CLI; a future iteration
adds an in-CLI fan-out that dials every cluster member's
GetCapability and assembles the batch automatically.
- main.go gRPC wiring (register EncryptionAdminServer on the
shard gRPC listener, plumb proposer/leaderView from the
shard's raftengine.Engine, call Validate at startup). Stage 6
cluster-flag gate (`--encryption-enabled`) will turn this on.
## Self-review (CLAUDE.md 5 passes)
1. Data loss: leader-gated mutator on top of Stage 4 HaltApply
seam. Empty batch rejected at boundary. Oversize batch
rejected at boundary. Idempotency lives at the FSM apply
layer (rejects if active.storage != 0).
2. Concurrency: no new mutable state. Proposer is set once at
construction. The §5.6 step 1a fan-out itself is deferred.
3. Performance: hot path unchanged. Mutator is one-shot per
cluster lifetime.
4. Consistency: fsmwire encoder is the single source of truth
for the on-wire byte layout. Round-trip test
(Encode→DecodeBootstrap) locks server's view against
fsmwire's. Row-ordering pinned (storage_i, raft_i).
5. Test coverage: boundary table (8 cases), oversize batch,
no-proposer rejection, leader-gating (inherited from
requireLeader), Validate() three-way coverage, CLI happy
path, CLI parse-time error table.
## Verification
- go test -race -timeout=60s ./adapter ./cmd/elastickv-admin: PASS
- golangci-lint run on touched paths: 0 issues
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a leader-gated BootstrapEncryption RPC with validation and batch-building helpers, a CLI ChangesEncryption Bootstrap Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
adapter/encryption_admin_test.go (1)
411-447: 💤 Low valueConsider adding a test case for nil writer in the batch.
The implementation at
encryption_admin.go:425-428validates against nil writers, but this table doesn't include a test case forwriter_batchcontaining a nil entry. This would ensure the nil-check path is exercised.♻️ Suggested test case addition
{"writer with local_epoch above 0xFFFF", func(r *pb.BootstrapEncryptionRequest) { r.WriterBatch = []*pb.WriterRegistryEntry{{FullNodeId: 1, LocalEpoch: 0x10000}} }}, + {"nil writer in batch", func(r *pb.BootstrapEncryptionRequest) { + r.WriterBatch = []*pb.WriterRegistryEntry{nil} + }}, } {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@adapter/encryption_admin_test.go` around lines 411 - 447, Add a table-driven test case to TestEncryptionAdmin_BootstrapEncryption_RejectsBadInputs that inserts a nil entry into WriterBatch (e.g., append a nil *pb.WriterRegistryEntry) to exercise the nil-writer validation in BootstrapEncryption; ensure the test builds a valid request via validBootstrapEncryptionRequest(), mutates WriterBatch to include a nil entry, calls srv.BootstrapEncryption(...), and asserts the returned gRPC status code is codes.InvalidArgument just like the other cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/elastickv-admin/encryption_mutators.go`:
- Around line 305-313: The decodeBootstrapWrappedDEKs function currently calls
decodeWrappedDEKFlag which emits errors referencing --wrapped-new-dek; replace
that with a flag-specific helper (e.g., decodeWrappedDEKFlagWithName(flagName, s
string)) and call it twice from decodeBootstrapWrappedDEKs passing
"wrapped-storage-dek" for storageB64 and "wrapped-raft-dek" for raftB64 so
errors mention the exact bootstrap flag; ensure the helper validates empty
string, base64 decode errors, and zero-length decoded output and wraps/errors
use the provided flagName in their messages.
---
Nitpick comments:
In `@adapter/encryption_admin_test.go`:
- Around line 411-447: Add a table-driven test case to
TestEncryptionAdmin_BootstrapEncryption_RejectsBadInputs that inserts a nil
entry into WriterBatch (e.g., append a nil *pb.WriterRegistryEntry) to exercise
the nil-writer validation in BootstrapEncryption; ensure the test builds a valid
request via validBootstrapEncryptionRequest(), mutates WriterBatch to include a
nil entry, calls srv.BootstrapEncryption(...), and asserts the returned gRPC
status code is codes.InvalidArgument just like the other cases.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4d9325e0-b34d-4523-b4f9-f53648e5af31
📒 Files selected for processing (6)
adapter/encryption_admin.goadapter/encryption_admin_test.gocmd/elastickv-admin/encryption.gocmd/elastickv-admin/encryption_mutators.gocmd/elastickv-admin/encryption_test.godocs/design/2026_04_29_partial_data_at_rest_encryption.md
There was a problem hiding this comment.
Code Review
This pull request implements the BootstrapEncryption RPC and its corresponding CLI subcommand, completing a significant portion of the encryption roadmap. Key changes include the addition of the Validate method to EncryptionAdminServer to prevent misconfiguration, the implementation of the BootstrapEncryption logic with strict boundary checks, and the expansion of the administrative CLI to support cluster bootstrapping. Feedback focuses on strengthening the robustness of the bootstrap process by suggesting additional validation for node ID uniqueness within the writer batch to prevent fatal FSM errors and recommending size limits on wrapped DEK payloads to mitigate potential denial-of-service vectors.
| "encryption: writer_batch produces %d registry rows, exceeding the §4.1 bound %d", | ||
| totalRows, bootstrapBatchRowCap) | ||
| } | ||
| out := make([]fsmwire.RegistrationPayload, 0, totalRows) |
There was a problem hiding this comment.
The writer_batch should be validated for uniqueness of full_node_id and its 16-bit truncation. If the batch contains duplicates or collisions on the 16-bit node_id, the FSM apply will fail with ErrLocalEpochRollback or ErrNodeIDCollision. Since FSM apply errors are fatal (per §6.3), a malformed bootstrap request could halt the entire cluster. Validating this at the gRPC boundary ensures the proposed Raft entry is safe to apply.
seenFull := make(map[uint64]bool)
seenTrunc := make(map[uint16]bool)
for _, w := range writers {
if w == nil { continue }
id := w.GetFullNodeId()
trunc := uint16(id)
if seenFull[id] {
return nil, grpcStatusErrorf(codes.InvalidArgument, "encryption: duplicate full_node_id %d in batch", id)
}
if seenTrunc[trunc] {
return nil, grpcStatusErrorf(codes.InvalidArgument, "encryption: node_id collision on 0x%04x in batch", trunc)
}
seenFull[id], seenTrunc[trunc] = true, true
}
out := make([]fsmwire.RegistrationPayload, 0, totalRows)References
- Apply validation to external requests to reject malformed data that could cause fatal errors or OOM issues.
| if len(req.GetWrappedStorageDek()) == 0 { | ||
| return nil, grpcStatusError(codes.InvalidArgument, "encryption: wrapped_storage_dek is required") | ||
| } | ||
| if len(req.GetWrappedRaftDek()) == 0 { | ||
| return nil, grpcStatusError(codes.InvalidArgument, "encryption: wrapped_raft_dek is required") | ||
| } |
There was a problem hiding this comment.
It is recommended to validate the size of the wrapped_storage_dek and wrapped_raft_dek payloads. The fsmwire.EncodeBootstrap helper uses safeU32 which panics if the length exceeds math.MaxUint32. While gRPC message limits usually prevent this, explicit validation here avoids a potential DoS vector where a crafted request could crash the leader process.
if len(req.GetWrappedStorageDek()) == 0 {
return nil, grpcStatusError(codes.InvalidArgument, "encryption: wrapped_storage_dek is required")
}
if len(req.GetWrappedStorageDek()) > 4096 {
return nil, grpcStatusError(codes.InvalidArgument, "encryption: wrapped_storage_dek exceeds maximum allowed size")
}
if len(req.GetWrappedRaftDek()) == 0 {
return nil, grpcStatusError(codes.InvalidArgument, "encryption: wrapped_raft_dek is required")
}
if len(req.GetWrappedRaftDek()) > 4096 {
return nil, grpcStatusError(codes.InvalidArgument, "encryption: wrapped_raft_dek exceeds maximum allowed size")
}References
- When handling requests, always limit the size of the request body or payloads to prevent Denial of Service attacks from oversized payloads.
…bbit minor Addresses three findings on PR #759. The high-priority item is the cluster-safety regression that would crash a cluster on a malformed bootstrap; addressed first per the prioritization rule. ## gemini high — writer_batch uniqueness validation A bootstrap request with duplicate `full_node_id` OR two distinct `full_node_id`s colliding on the uint16 narrowing used as the §4.1 registry-row key (`!encryption|writers|<dek_id>|<uint16(node_id)>`) reaches the FSM with a payload that fails closed at apply time (case 3: ErrLocalEpochRollback for the duplicate, case 4: ErrNodeIDCollision for the collision). FSM apply errors halt the apply loop via the HaltApply seam, so a single malformed bootstrap would stop every replica in the cluster. New `validateWriterBatchUniqueness` rejects both cases at the gRPC boundary with codes.InvalidArgument before the Propose round-trip. The check uses a uint64→uint16 narrowing identical to the registry row key. Regression tests: - TestEncryptionAdmin_BootstrapEncryption_RejectsDuplicateFullNodeID - TestEncryptionAdmin_BootstrapEncryption_RejectsNodeIDCollision Caller audit: validateWriterBatchUniqueness is called only from buildBootstrapBatch which is called only from BootstrapEncryption. No existing happy-path test uses duplicates or 16-bit collisions. ## gemini security-medium — wrapped DEK size cap fsmwire.EncodeBootstrap uses safeU32 on the length prefix; a crafted oversize wrapped DEK could reach the panic guard or inflate the proposed Raft entry beyond practical limits. Add a 4 KiB per-field cap at the gRPC boundary (real KEK-wrapped 32B DEKs run hundreds of bytes; 4 KiB is ~10x headroom). New `validateWrappedDEK` enforces the cap and the existing non-empty check in one place. Both wrapped DEK validations route through it. Regression test: - TestEncryptionAdmin_BootstrapEncryption_RejectsOversizeWrappedDEK (covers both storage and raft fields with maxWrappedDEKSize+1 bytes) ## coderabbit minor — bootstrap wrapped-DEK error flag names decodeWrappedDEKFlag emitted "--wrapped-new-dek" in its error text (the rotate-dek flag name); bootstrap users got mixed flag names in one message. New decodeWrappedDEKFlagWithName takes the flag name and formats the error accordingly. decodeWrappedDEKFlag keeps its signature by delegating to the helper with the rotate-dek default. decodeBootstrapWrappedDEKs passes the correct bootstrap flag names. ## Verification - go test -race -timeout=60s ./adapter ./cmd/elastickv-admin: PASS - golangci-lint run on touched paths: 0 issues
|
@claude review |
|
Claude finished @bootjp's task in 3m 7s —— View job Code Review — Stage 5 PR-C (BootstrapEncryption + CLI + Validate)
Prior Review Comments — Status UpdateBoth Gemini's concerns and CodeRabbit's actionable comment are already resolved in the submitted code:
New Findings1. Dangling comment in
|
…r cleanups claude[bot] round-1 confirmed all prior Gemini/CodeRabbit findings resolved and surfaced 5 new items. This commit addresses every one. ## #1 bug — dangling narrowUint32 docstring (encryption_mutators.go) A prior commit collapsed the narrowUint32 doc comment into the runEncryptionBootstrap godoc block without a blank-line separator, so the body-level explanation ("callers MUST have validated v against math.MaxUint32 first") never reached its target function and tooling rendered the narrowUint32 rationale as part of runEncryptionBootstrap's godoc. Move the comment to immediately precede the actual narrowUint32 declaration. ## #2 inconsistency — RotateDEK lacks wrapped_new_dek size cap BootstrapEncryption gained a 4096-byte cap via validateWrappedDEK in round-1; RotateDEK still only checked for empty. A crafted oversize wrapped_new_dek could push fsmwire.EncodeRotation toward its safeU32 length-prefix guard (gRPC's 4 MiB default was the only upstream guard). Route RotateDEK through validateWrappedDEK("new", ...) so both mutators share the same fail-closed bound. Caller audit (fail-closed semantic change): RotateDEK now rejects wrapped_new_dek > maxWrappedDEKSize at the gRPC boundary. - CLI runEncryptionRotateDEK: --wrapped-new-dek base64 input. A user passing > 4 KiB hits the new bound; realistic KEK-wrapped DEKs run hundreds of bytes, well under the cap. - Other gRPC clients: future Stage 6+ wiring inherits the bound cluster-wide. - Tests: validRotateDEKRequest uses 13-byte "wrapped-bytes" fixture; unchanged. No silent semantic regression downstream. Regression test: - TestEncryptionAdmin_RotateDEK_RejectsOversizeWrappedDEK exercises the maxWrappedDEKSize+1 boundary. ## #3 nit — nil writer test case missing Add {"nil writer in batch", ...} to TestEncryptionAdmin_BootstrapEncryption_RejectsBadInputs so the validateBootstrapWriter nil-guard has an explicit table entry rather than being covered only transitively. ## #4 minor — uniqueness check runs after building output buildBootstrapBatch was building the full registry-row slice (up to bootstrapBatchRowCap = 16384 rows) before validateWriterBatchUniqueness ran. For a near-cap batch with a collision at the tail, the work was thrown away. Restructure: 1. Run validateBootstrapWriter over every entry (per-writer shape). 2. Run validateWriterBatchUniqueness (cross-entry). 3. Allocate and fill `out` from the validated slice. No behaviour change for valid batches; invalid batches now short-circuit before the allocation. Tests cover both halves. ## #5 minor — dead nil-guard in validateWriterBatchUniqueness With the reorder above, validateBootstrapWriter rejects any nil entry before validateWriterBatchUniqueness ever sees it. The `if w == nil { continue }` defensive check inside the latter is now unreachable; remove it and document the contract in the function's godoc. ## Verification - go test -race -timeout=60s ./adapter ./cmd/elastickv-admin: PASS - golangci-lint run on touched paths: 0 issues
|
@claude review |
|
Claude finished @bootjp's task in 2m 38s —— View job Code Review — Stage 5 PR-C (BootstrapEncryption + CLI + Validate) — Round 2
Prior Review Findings — All Resolved ✓Every issue raised in the round-1 reviews (CodeRabbit, Gemini, and prior Claude) is addressed in commits
Fresh Findings (post-fix)1. CLI test gap: empty
|
claude[bot] round-2 confirmed all actionable items resolved; the single remaining gap was a CLI-side test for the missing-entirely case (no --writer flags at all). The server-side "empty writer_batch" path is covered by adapter tests; this fills the parse-layer test gap with TestRunEncryptionBootstrap_RejectsEmptyWriterBatch. The parseWriterBatch bitSize=32 vs uint16 observation is intentional design (permissive parse so the custom epoch-bound error fires instead of strconv's parse error) and needs no change.
|
@claude review |
|
Claude finished @bootjp's task in 3m 9s —— View job Code Review — Stage 5 PR-C (Round 3)
Round 2 Finding — Resolved ✓
Fresh Pass — No New IssuesAll code paths reviewed:
One theoretical observation (not actionable): What's Well DoneThe three-commit fix sequence (
Verdict: All prior findings resolved. No new issues. Ready to merge. |
Codex P1: startRaftServers wired rt.spec.id (the Raft GROUP id) as full_node_id, but group ids are shared by every replica of a group (every node uses group 1 in the default config). All nodes therefore returned the same CapabilityReport.full_node_id, and BootstrapEncryption's validateWriterBatchUniqueness check (added in PR #759 for the gemini HIGH on writer batches) would reject the bootstrap with "duplicate full_node_id" — the very gate the batch validation was meant to enforce now turns into the failure mode that prevents the cluster from bootstrapping at all. Fix: derive a per-node-stable uint64 from --raftId via the canonical etcd.DeriveNodeID(id string) uint64 helper that raftengine itself uses for peer identification (FNV-1a hash, 0 sentinel collision mapped to 1). The call site now passes etcdraftengine.DeriveNodeID(*raftId) and the helper's parameter is renamed shardID → fullNodeID with a docstring spelling out the must-be-per-node-stable contract so a future refactor does not regress to the shard-id wiring. Caller audit: registerEncryptionAdminServer has exactly one production caller (main.go:1292), now passing the derived per-node id. The single test caller passes a literal uint64 (1) which is unaffected — its purpose is to verify the gRPC registration path, not the id-derivation correctness. Regression test TestEncryptionAdminFullNodeID_DistinctPerRaftId pins the canonical 3-node config (n1/n2/n3) produces three distinct non-zero ids so a future refactor breaking the derivation surfaces as a failing unit test rather than a cluster-wide bootstrap deadlock. go build, go vet, go test -race, golangci-lint all clean.
Summary
Stage 5 PR-C of the data-at-rest encryption rollout (design doc:
docs/design/2026_04_29_partial_data_at_rest_encryption.md, §5.6bootstrap, §6.1 admin.go, §6.6 admin commands). Closes the last
mutating-RPC gap in the
proto.EncryptionAdminsurface and adds theproduction-wiring guard the PR-B review carry-forward identified.
Production-inert:
main.godoes not yet registerEncryptionAdminServer; that wiring is Stage 5D along with the§5.6 step 1a capability fan-out helper.
What lands
BootstrapEncryptionserver impl: proposes the §5.6 0x04OpBootstrap entry. Validates
storage_dek_id != 0 != raft_dek_id,storage_dek_id != raft_dek_id, non-empty wrapped DEK pair,non-empty writer batch, each writer's
full_node_id != 0/local_epoch <= 0xFFFF, and2 * len(writers) <= bootstrapBatchRowCap(mirrorsfsmwire.maxBootstrapBatchCount).Expands each writer into a storage-DEK row + a raft-DEK row so
the §4.1 nonce-uniqueness invariant covers both purposes from
the first post-bootstrap entry.
elastickv-admin encryption bootstrapCLI: takes--storage-dek-id,--raft-dek-id,--wrapped-storage-dek(base64),
--wrapped-raft-dek(base64), and repeated--writer=<full_node_id>:<local_epoch>. The capabilityfan-out is deferred; the operator ships a pre-built batch.
Validate()method: fails closed when a proposer is wiredwithout a
LeaderViewso production code inmain.gocannotsilently let followers mutate state. Tests still get the
no-leaderView affordance for proposer-side unit tests.
shipped (feat(encryption): Stage 5 PR-B — RotateDEK + RegisterEncryptionWriter mutators #756), 5C in PR, 5D open.
Out of scope (Stage 5D / Stage 6)
batch by dialing every cluster member's
GetCapability).main.gogRPC wiring (registerEncryptionAdminServeron theshard gRPC listener, plumb proposer/leaderView from the shard's
raftengine.Engine, callValidate()at startup).--encryption-enabledcluster flag + §9.1 startuprefusal guards.
Test plan
go test -race -timeout=60s ./adapter -run TestEncryptionAdmin— adds:
BootstrapEncryption_HappyPath(wire-layout round-trip viafsmwire.DecodeBootstrap, row-ordering pinned).BootstrapEncryption_RejectsBadInputs(8-case table: zeroids, equal ids, empty wrapped DEKs, empty batch, zero
full_node_id, epoch > 0xFFFF).BootstrapEncryption_RejectsOversizeBatch(abovebootstrapBatchRowCap).Validate_*three-way: no proposer / both wired /proposer-without-leaderView.
MutatingRPCs_RejectWithoutProposerextended tocover Bootstrap.
go test -race -timeout=60s ./cmd/elastickv-admin -run TestRunEncryptionBootstrap— CLI happy path against a stub gRPC server + 5-case
parse-error table (
--writermalformed).golangci-lint runon touched paths — 0 issues.Self-review (CLAUDE.md 5 passes)
HaltApplyseam. Empty batch / oversize batch rejected atthe gRPC boundary. Idempotency lives at the FSM apply layer
(rejects if
active.storage != 0).§5.6 step 1a fan-out itself is intentionally deferred.
per cluster lifetime.
fsmwireencoder is the single source oftruth for the on-wire byte layout. The round-trip test locks
the server's view against
fsmwire's. Row-ordering ispinned (storage row i precedes raft row i).
no-proposer rejection, leader-gating (inherited from
requireLeader),Validate()three-way coverage, CLI happypath, CLI parse-time error table.
Notes
with
libprotoc 29.6; no wire-relevant difference vs thecanonical 29.3.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes / Tests
Documentation