Skip to content

feat(encryption): Stage 5 PR-A — EncryptionAdmin proto + read-only RPCs + status CLI#754

Merged
bootjp merged 5 commits into
mainfrom
feat/encryption-admin
May 15, 2026
Merged

feat(encryption): Stage 5 PR-A — EncryptionAdmin proto + read-only RPCs + status CLI#754
bootjp merged 5 commits into
mainfrom
feat/encryption-admin

Conversation

@bootjp
Copy link
Copy Markdown
Owner

@bootjp bootjp commented May 15, 2026

Summary

First slice of Stage 5 of the data-at-rest encryption rollout
(design doc: docs/design/2026_04_29_partial_data_at_rest_encryption.md,
§5.6 initial DEK bootstrap, §6.1 admin.go service, §6.6 admin commands).
Production-inert: the new server is not yet registered on main's gRPC
listener; only the read-only RPCs are wired; the propose-side RPCs
return Unimplemented until PR-B.

This PR pairs the doc-lifecycle transition (_proposed_
_partial_, per CLAUDE.md design-doc-first workflow) with PR-A of
the implementation.

What lands

  • proto.EncryptionAdmin v1 surface: GetCapability,
    GetSidecarState, BootstrapEncryption, RotateDEK,
    RegisterEncryptionWriter, ResyncSidecar. The schema doc-comments
    pin the local_epoch <= 0xFFFF validation rule that every decode
    site must apply.
  • adapter/EncryptionAdminServer with the read-only RPCs fully
    implemented (sidecar-backed via encryption.ReadSidecar); the
    three propose-side RPCs are Unimplemented stubs.
  • elastickv-admin encryption status CLI subcommand: dispatches via
    argv[1] == "encryption" before flag.Parse() so subcommand flags
    do not bleed into the HTTP-server global flag namespace. Uses
    grpc.NewClient (no DialContext deprecation).

Out of scope (deferred to PR-B / Stage 6)

  • Wiring RegisterEncryptionAdminServer onto main.go's gRPC listener.
  • BootstrapEncryption / RotateDEK / RegisterEncryptionWriter
    implementations on top of raftengine.Proposer + Stage 4 fsmwire
    bodies, with leader-only routing.
  • Remaining CLI subcommands (bootstrap, rotate-dek,
    register-writer, resync-sidecar).
  • §5.6 step 1a capability fan-out across voters and learners.
  • Stage 6 cluster flags (--enable-storage-envelope,
    --enable-raft-envelope) and §9.1 startup-refusal guards.

Test plan

  • go test -race ./adapter -run TestEncryptionAdmin — 9 cases
    cover no-sidecar / missing-file / not-bootstrapped / bootstrapped /
    callback override / ResyncSidecar happy path / Unimplemented stubs.
  • go test -race ./cmd/elastickv-admin -run 'TestEncryptionMain|TestRunEncryptionStatus'
    — real gRPC server on a loopback listener (not bufconn) exercises
    the CLI dial path with both bootstrapped and unconfigured fixtures.
  • go test -race ./internal/encryption/... regression — pass.
  • golangci-lint run on touched paths — 0 issues.
  • CI verifies build / lint / Jepsen.

Self-review (CLAUDE.md 5 passes)

  1. Data loss — every read-only RPC snapshots the sidecar via
    encryption.ReadSidecar; no writes, no Raft proposals. Mutating
    RPCs return Unimplemented and never reach the FSM apply path.
    §5.1 crash-durable write protocol is untouched.
  2. Concurrency — server has no mutable state; per-call sidecar
    reads are inherently snapshot-safe under the §5.1 atomic-rename
    protocol. CLI uses grpc.NewClient (non-blocking, lazy connect).
    Test listener cleanup is bounded by a 2s GracefulStop deadline
    so a stuck client cannot hang go test.
  3. Performance — RPCs are operator-facing and off the data
    plane. ReadSidecar parses a small JSON file per call, which is
    fine at admin-RPC rates and matches the existing §5.5 fallback
    pattern.
  4. ConsistencyGetSidecarState reports the persisted
    post-fsync sidecar value; WithEncryptionAdminLatestAppliedIndex
    is the §5.5 escape hatch so PR-B can substitute the freshest
    in-memory apply counter when leadership wiring lands.
    writer_registry_for_caller is empty in PR-A (no Stage 7), and
    the cutover path sources its batch from GetCapability fan-out
    per §5.6 step 1a, not from this RPC.
  5. Test coverage — every read-only branch and every
    Unimplemented stub has a unit test; CLI status path has
    end-to-end coverage through a real gRPC server with both
    bootstrapped and unconfigured fixtures.

Notes

  • The proto was regenerated with libprotoc 29.6 locally (29.3 not
    installable on macOS via Homebrew); the only diff vs the canonical
    toolchain is the protoc v5.29.x version-comment string in
    generated headers. protoc-gen-go v1.36.11 and
    protoc-gen-go-grpc 1.6.1 match the proto/Makefile pins
    exactly, so the wire-relevant output is identical.
  • The pre-existing adapter/ test-package long timeout reproduced
    on main without my changes (go test -race -count=1 ./adapter/
    hangs after ~10 minutes on grpc dns_resolver goroutines); my
    tests run cleanly in isolation (go test -race -run TestEncryptionAdmin ./adapter/ finishes in 1.16s).

Summary by CodeRabbit

  • New Features

    • Added encryption admin RPC service for observability and follower repair operations.
    • Added elastickv-admin encryption status CLI command to report encryption capability and sidecar state.
  • Tests

    • Added comprehensive test coverage for encryption admin functionality and CLI commands.
  • Documentation

    • Updated encryption design documentation with milestone status and stage completion tracking.

Review Change Stack

bootjp added 2 commits May 15, 2026 16:30
…pped)

Stages 0-4 of the data-at-rest encryption rollout (foundation package,
sidecar crash-durable I/O, storage envelope, raft envelope + engine
pre-apply hook, FSM-internal Raft entry types) all landed via PRs
#719/#722/#742/#744/#748. Per CLAUDE.md's design-doc lifecycle, rename
the proposal to `_partial_` and add a milestone status table so the
remaining stages (5-9) and their section references are visible at a
glance.

The pipeline is now end-to-end byte-tag complete but production-inert:
no caller wires WithEncryption, no flag exists, no admin RPC proposes
a bootstrap entry. Stage 5 begins the operator-facing surface.
…Cs + status CLI

First slice of Stage 5 (operator-facing surface for data-at-rest
encryption). Lands the proto.EncryptionAdmin service, a server
scaffold serving the read-only RPCs, and a `elastickv-admin
encryption status` CLI subcommand. Stays production-inert: the
server is not yet registered on main's gRPC listener, the mutating
RPCs return Unimplemented, and Stage 4's WithEncryption applier
seam is still un-wired.

## proto/encryption_admin.proto

Full v1 surface per the design's section 6.1 admin.go:

- GetCapability / GetSidecarState - read-only, leaderless, fan-out
  source for the section 7.1 cutover capability check.
- BootstrapEncryption / RotateDEK / RegisterEncryptionWriter -
  mutating; proposers for the section 11.3 reserved opcodes
  0x03 / 0x04 / 0x05 wired in Stage 4. PR-A returns Unimplemented;
  PR-B will route them through raftengine.Proposer using the
  Stage 4 fsmwire encoders.
- ResyncSidecar - section 5.5 follower repair, read-only.

local_epoch travels as uint32 on the wire (proto3 has no uint16);
the schema comment documents the value <= 0xFFFF validation rule
that every decode site must apply.

## adapter/encryption_admin.go

EncryptionAdminServer implements pb.EncryptionAdminServer. Options:

- WithEncryptionAdminSidecarPath - the section 5.1 keys.json path;
  empty means "encryption not configured" and GetCapability returns
  encryption_capable=false gracefully.
- WithEncryptionAdminKeystore - reserved for the PR-B RotateDEK
  fast path.
- WithEncryptionAdminFullNodeID - reported in CapabilityReport for
  the section 5.6 step 1a writer-registry batch.
- WithEncryptionAdminBuildSHA - overrides runtime/debug auto-detect.
- WithEncryptionAdminLatestAppliedIndex - the section 5.5 escape
  hatch so a caller can substitute the in-memory apply counter for
  the sidecar's persisted value.

Read-only RPCs read the sidecar via encryption.ReadSidecar on every
call; the file is small and there is no shared mutable state in
the server itself. Sidecar errors map: not-exist on GetCapability
returns a graceful "not capable", not-exist on GetSidecarState /
ResyncSidecar returns FailedPrecondition, version mismatch returns
FailedPrecondition with the error embedded, anything else returns
Internal.

writer_registry_for_caller is an empty non-nil map in PR-A; Stage 7
fills it from the writer registry.

## cmd/elastickv-admin/encryption.go

main() dispatches to encryptionMain(os.Args[2:]) when argv[1] is
"encryption", before flag.Parse() so the encryption subcommand
tree owns a separate flag namespace from the HTTP-server surface.
Sub-subcommands: PR-A wires only `status`. It dials the configured
endpoint via grpc.NewClient (no DialContext deprecation), calls
GetCapability and GetSidecarState, and writes a plain-text table.
GetSidecarState returning FailedPrecondition on an unconfigured
node downgrades to a "<unavailable>" line rather than failing the
whole command, so `status` is informative everywhere.

## Tests

- adapter/encryption_admin_test.go covers: no-sidecar path, missing
  sidecar file, sidecar-present-but-not-bootstrapped, bootstrapped,
  GetSidecarState error code, GetSidecarState happy path with
  wrapped DEK ids, callback override of latest_applied_index,
  ResyncSidecar happy path, and the three Unimplemented stubs.
- cmd/elastickv-admin/encryption_test.go brings up a real gRPC
  server on a loopback listener (matching the production dial
  path, not bufconn) and exercises runEncryptionStatus end-to-end
  with both bootstrapped and unconfigured fixtures. Listener
  cleanup uses GracefulStop with a 2s ceiling so a stuck client
  cannot hang the test process.

## Five-pass self-review

1. Data loss - read-only RPCs, no Raft proposals, no sidecar
   writes. Mutating stubs return Unimplemented and never touch
   state. Section 5.1 sidecar write protocol untouched.
2. Concurrency - server has no mutable state; every call snapshots
   the sidecar via ReadSidecar. The CLI gRPC client uses
   grpc.NewClient (non-blocking, lazy-connecting). Test listener
   cleanup is bounded.
3. Performance - hot paths are unchanged; the new RPCs are
   operator-facing and off the data plane. ReadSidecar parses a
   small JSON file per call which is fine for an admin RPC.
4. Consistency - GetSidecarState reports the sidecar's view, which
   is the persisted, post-fsync value; the callback escape hatch
   lets PR-B substitute the freshest applied index when leadership
   wiring lands. writer_registry_for_caller is empty (no Stage 7
   yet) and the cutover path is documented to source its batch
   from GetCapability fan-out, not this RPC.
5. Test coverage - unit tests on every read-only path plus the
   Unimplemented stubs; CLI tests cover both bootstrapped and
   unconfigured fixtures end-to-end through a real gRPC server.

## Verification

- go test -race ./adapter -run TestEncryptionAdmin: PASS (1.16s)
- go test -race ./cmd/elastickv-admin -run 'TestEncryptionMain|TestRunEncryptionStatus': PASS (1.08s)
- go test -race ./internal/encryption/...: PASS
- golangci-lint run on touched paths: 0 issues
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

@bootjp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 9 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a47e3e8-072d-418a-a0ae-795d4827018c

📥 Commits

Reviewing files that changed from the base of the PR and between 37fc973 and d09a753.

📒 Files selected for processing (5)
  • adapter/encryption_admin.go
  • adapter/encryption_admin_test.go
  • cmd/elastickv-admin/encryption.go
  • cmd/elastickv-admin/encryption_test.go
  • docs/design/2026_04_29_partial_data_at_rest_encryption.md
📝 Walkthrough

Walkthrough

This PR introduces the EncryptionAdmin gRPC service for encryption observability and sidecar recovery. It defines proto contracts for capability reporting and sidecar state inspection, implements three read-only RPCs in a new gRPC server, adds an elastickv-admin CLI command to query encryption status via GetCapability and GetSidecarState, and updates the design document to track that Stages 0–4 of the encryption rollout are now shipped.

Changes

Encryption Admin Control Plane

Layer / File(s) Summary
Proto service and message contracts
proto/encryption_admin.proto
Defines EncryptionAdmin service with six RPCs (three read-only probes, three mutating leader-routed operations), plus wire-format messages for capability reporting, sidecar recovery state, DEK operations, and follower resync; documents on-wire constraints for local_epoch (≤ 0xFFFF).
Server structure and configuration
adapter/encryption_admin.go (lines 1–101)
Defines EncryptionAdminServer struct with optional sidecar path, keystore, node ID, and build SHA; implements functional option setters; includes constructor with auto-detection of VCS revision from build info.
GetCapability RPC implementation
adapter/encryption_admin.go (lines 103–277)
Implements GetCapability to report capability and sidecar presence; adds error mapping helper to convert sidecar read errors to gRPC status codes; auto-detects build SHA when not provided.
GetSidecarState and ResyncSidecar RPCs
adapter/encryption_admin.go (lines 150–216)
Implements GetSidecarState and ResyncSidecar to read sidecar, populate reports with active IDs and envelope metadata, compute latest applied index via callback, and remap wrapped DEKs; includes helpers for safe uint32 key parsing and DEK map remapping.
Server unit tests
adapter/encryption_admin_test.go
Validates GetCapability under four scenarios (no path, missing, not bootstrapped, bootstrapped), GetSidecarState error handling and state reporting with wrapped DEKs, callback override of applied index, ResyncSidecar wrapped DEK shipping, and unimplemented mutating RPCs; includes fixture and assertion helpers.
CLI command dispatcher and status subcommand
cmd/elastickv-admin/encryption.go
Implements encryptionMain dispatcher routing encryption status subcommand; implements encryption status command with --endpoint and --timeout flags, gRPC dialing, GetCapability and GetSidecarState calls, special handling for FailedPrecondition vs other errors, and human-readable output including sorted wrapped DEK IDs.
CLI integration tests
cmd/elastickv-admin/encryption_test.go
Validates CLI argument handling, end-to-end status reporting with bootstrapped sidecar, no-sidecar output format, and non-FailedPrecondition error propagation; includes test helpers for sidecar fixtures, local gRPC test server startup, and stub server implementations.
Main entry point
cmd/elastickv-admin/main.go
Adds early argument branch to intercept encryption subcommand before standard flag parsing; invokes encryptionMain with deferred error logging.
Design document update
docs/design/2026_04_29_partial_data_at_rest_encryption.md
Updates status to "Partial — Stages 0–4 shipped"; adds milestone table tracking stage delivery and PRs; clarifies Stages 0–4 deliver encryption pipeline but remain production-inert pending later wiring and flags.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 A rabbit hops with encryption delight,
State and capability now in sight,
The sidecar reveals its wrapped-key treasure,
Observability brings crypto pleasure;
Stages 0–4 hop toward the light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: implementation of the EncryptionAdmin proto service with read-only RPCs and a status CLI command for Stage 5 of data-at-rest encryption.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/encryption-admin

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the EncryptionAdmin gRPC service and the encryption status CLI subcommand, providing read-only observability for the data-at-rest encryption feature. The changes include server-side logic for capability and state probes, comprehensive tests, and updated design documentation. Review feedback identifies a critical logic error in the EncryptionCapable flag that could prevent cluster bootstrapping and notes inconsistencies in the ResyncSidecar RPC implementation relative to the design specifications. Additionally, the reviewer recommended logging errors during gRPC connection cleanup and adopting slices.Sort for improved performance.

Comment on lines +114 to +137
func (s *EncryptionAdminServer) GetCapability(_ context.Context, _ *pb.Empty) (*pb.CapabilityReport, error) {
if s.sidecarPath == "" {
return &pb.CapabilityReport{BuildSha: s.buildSHA}, nil
}
sc, err := encryption.ReadSidecar(s.sidecarPath)
switch {
case err == nil:
case errors.Is(err, os.ErrNotExist) || encryption.IsNotExist(err):
return &pb.CapabilityReport{BuildSha: s.buildSHA}, nil
default:
return nil, statusFromSidecarErr(err)
}
return &pb.CapabilityReport{
EncryptionCapable: sc.Active.Storage != 0,
BuildSha: s.buildSHA,
SidecarPresent: true,
FullNodeId: s.fullNodeID,
// LocalEpoch stays at 0 until Stage 7 wires the §4.1
// writer-registry counter. Bootstrap pre-check is
// expected to call GetCapability before any DEK exists,
// so 0 is the correct value at that point regardless.
LocalEpoch: 0,
}, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The EncryptionCapable logic in GetCapability appears to be incorrect based on the design doc §7.1 Phase 0. The doc states that encryption_capable should be true for any node running the new binary with the encryption flag set, allowing the leader to distinguish capable nodes from older ones during the Phase 0/1 capability poll.

Currently, if the sidecar is missing or sc.Active.Storage == 0 (not yet bootstrapped), the RPC returns false. This will block the bootstrap process itself, as the cutover command (§7.1 step 3) refuses to proceed unless all members report true. The capability check should return true if the node is configured for encryption (i.e., sidecarPath is set), regardless of whether it has already been bootstrapped.

Comment thread proto/encryption_admin.proto Outdated
Comment on lines +20 to +22
// default Raft group's leader (BootstrapEncryption, RotateDEK,
// RegisterEncryptionWriter, ResyncSidecar). They route to the
// leader; followers return FailedPrecondition with the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment groups ResyncSidecar under "Mutating RPCs that propose FSM-internal entries". However, the design doc §5.5 and the implementation in adapter/encryption_admin.go indicate that this is a read-only RPC used for follower repair that does not involve a Raft proposal. Please update the comment to reflect its read-only nature to avoid confusion for future implementers.

Comment thread adapter/encryption_admin.go Outdated
Comment on lines +178 to +192
func (s *EncryptionAdminServer) ResyncSidecar(_ context.Context, _ *pb.ResyncSidecarRequest) (*pb.ResyncSidecarResponse, error) {
if s.sidecarPath == "" {
return nil, grpcStatusError(codes.FailedPrecondition, "encryption: sidecar path is not configured on this node")
}
sc, err := encryption.ReadSidecar(s.sidecarPath)
if err != nil {
return nil, statusFromSidecarErr(err)
}
return &pb.ResyncSidecarResponse{
WrappedDeksById: wrappedDEKMap(sc),
ActiveStorageId: sc.Active.Storage,
ActiveRaftId: sc.Active.Raft,
LeaderLatestAppliedIndex: s.appliedIndex(sc.RaftAppliedIndex),
}, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ResyncSidecar implementation is missing the writer_registry_for_caller information required by the recovery protocol described in Design Doc §5.5. The doc specifies that a recovering follower uses the leader's recorded last_seen_local_epoch for that specific node to re-derive its local_epoch monotonically.

While the PR description notes that the writer registry is deferred to Stage 7, the ResyncSidecarResponse proto message is also missing this field entirely, whereas SidecarStateReport (from GetSidecarState) includes it. Given §5.5 says the fallback uses GetSidecarState, the purpose and completeness of ResyncSidecar as a separate RPC should be reconciled with the design.

Comment thread cmd/elastickv-admin/encryption.go Outdated
if err != nil {
return err
}
defer func() { _ = closeFn() }()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error from closeFn() (which closes the gRPC client connection) is silently ignored. Per the general rules for this repository, errors from Close() methods on resources like network connections should be logged to ensure that cleanup problems or resource leaks are visible.

Suggested change
defer func() { _ = closeFn() }()
defer func() {
if err := closeFn(); err != nil {
fmt.Fprintf(os.Stderr, "encryption: close connection: %v\n", err)
}
}()
References
  1. Do not silently ignore errors from Close() methods on resources like network connections. Log them to ensure resource leaks or other cleanup problems are visible.

Comment thread cmd/elastickv-admin/encryption.go Outdated
for id := range state.WrappedDeksById {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the project is using Go 1.21+ (as evidenced by the use of the slices package in keystore.go), it is preferred to use slices.Sort for sorting slices of ordered types like uint32. This is more concise and efficient than sort.Slice.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6799e520d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +116
if stateErr != nil {
_, _ = fmt.Fprintf(out, "sidecar_state: <unavailable: %v>\n", stateErr)
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return non-precondition sidecar-state failures

writeEncryptionStatus treats every GetSidecarState error as a soft “unavailable” condition and returns success, but only FailedPrecondition is expected for unconfigured nodes. If GetSidecarState fails for other reasons (e.g., Unimplemented, permission issues, transport faults after capability succeeds), elastickv-admin encryption status exits 0 and can mislead operators into thinking the check passed. Restrict this fallback path to codes.FailedPrecondition and propagate all other RPC errors.

Useful? React with 👍 / 👎.

Addresses the first review wave on PR #754. Per CLAUDE.md, the P1
defect fix is paired with a failing regression test in the same
commit.

## gemini P1 (high) - adapter/encryption_admin.go:137

`GetCapability` was gating `encryption_capable` on
`sc.Active.Storage != 0`, which is the post-bootstrap state. The
§7.1 Phase 0 cutover gate runs *before* bootstrap proposes the
first DEK, so gating on Active.Storage != 0 deadlocks the cutover
against the very entry it gates.

Fix: encryption_capable is now gated on "node started with
--encryption-enabled" (proxied here by a non-empty sidecar path).
SidecarPresent carries the orthogonal "bootstrap entry landed"
signal. The §6.1 doc-comment is rewritten so the next reader does
not re-introduce the bug.

Regression tests:
- TestEncryptionAdmin_GetCapability_SidecarMissing now asserts
  EncryptionCapable=true with SidecarPresent=false (Phase 0
  window: configured but not yet bootstrapped).
- TestEncryptionAdmin_GetCapability_NotBootstrapped now asserts
  EncryptionCapable=true with SidecarPresent=true (sidecar
  present but Active.Storage==0).

Caller audit: GetCapability has one production caller today
(cmd/elastickv-admin/encryption.go), which just prints the bool.
No semantic logic depends on the old gating. Future cutover
callers (PR-B / Stage 6) will read the new semantics fresh.

## gemini medium - proto/encryption_admin.proto:22

ResyncSidecar was grouped under "mutating RPCs". Move it into the
read-only group and document the §5.5 follower-repair semantics
(addressed at the leader because the recovery flow only consults
the leader's record, but no Raft proposal is involved).

## gemini medium - adapter/encryption_admin.go:192

ResyncSidecarResponse was missing the writer_registry_for_caller
field that §5.5 requires for the follower to re-derive its §4.1
local_epoch. Add the field on the proto and populate an empty
non-nil map in PR-A (Stage 7 wires the registry); the field-comment
documents the value <= 0xFFFF decode rule the schema requires.

## gemini medium - cmd/elastickv-admin/encryption.go:81

closeFn() error was silently ignored. Log to stderr so an FD leak
or transport bug stays visible.

## gemini medium - cmd/elastickv-admin/encryption.go:144

sort.Slice replaced with slices.Sort.

## codex P2 - cmd/elastickv-admin/encryption.go:116

writeEncryptionStatus treated every GetSidecarState error as the
"node not configured" soft fallback. Narrow the fallback to
codes.FailedPrecondition (the documented sentinel for unconfigured
nodes); any other code (Internal / Unavailable / Unimplemented)
now propagates so the CLI exits non-zero.

Regression test (TestRunEncryptionStatus_PropagatesNonPreconditionError)
spins up a stub gRPC server that returns codes.Internal from
GetSidecarState and verifies runEncryptionStatus returns a wrapped
error rather than silently exiting 0.

Caller audit: writeEncryptionStatus has a single production
callsite (runEncryptionStatus, just above the narrowing). The
narrowing happens before writeEncryptionStatus is called, so the
helper's contract is unchanged at its call boundary.

## Verification

- go test -race -timeout=60s ./adapter ./cmd/elastickv-admin (Stage 5
  scope): PASS
- golangci-lint run on touched paths: 0 issues
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 15, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @bootjp's task in 3m 31s —— View job


Claude's Review

Tasks

  • Gather context from PR and prior reviews
  • Read adapter/encryption_admin.go
  • Read adapter/encryption_admin_test.go
  • Read cmd/elastickv-admin/encryption.go
  • Read cmd/elastickv-admin/encryption_test.go
  • Read proto/encryption_admin.proto
  • Read cmd/elastickv-admin/main.go
  • Analyze and write review

Status of Prior Automated Review Comments

Before noting new findings, I verified the current state of every issue flagged by the earlier automated reviewers:

Finding Status
Gemini P1: EncryptionCapable gates on bootstrap instead of sidecarPath != "" Already fixedadapter/encryption_admin.go:122 correctly returns true when sidecarPath != "" regardless of Active.Storage. Comments on lines 107–120 explain the §7.1 Phase 0 rationale. Tests TestEncryptionAdmin_GetCapability_SidecarMissing and TestEncryptionAdmin_GetCapability_NotBootstrapped lock this in.
Gemini M1: ResyncSidecar mis-classified under "Mutating RPCs" comment group Not an issue in the current protoproto/encryption_admin.proto:9–24 lists ResyncSidecar in Group 1 (read-only, leaderless probes). Gemini's comment appears to reference a prior revision.
Gemini M2: ResyncSidecar missing writer_registry_for_caller By design, properly scoped — the field exists in ResyncSidecarResponse (proto field 5) and the server returns a non-nil empty map. Comments in both the proto and the implementation explain Stage 7 fills it.
Gemini M3: Silent closeFn() error Already fixedencryption.go:83–91 uses a proper defer with fmt.Fprintf(os.Stderr, ...).
Gemini M4: Use slices.Sort Already fixedencryption.go:161 uses slices.Sort(ids).
Codex P2: Non-FailedPrecondition errors silently treated as soft fallback Already fixedencryption.go:104 checks status.Code(stateErr) != codes.FailedPrecondition and propagates all other errors. TestRunEncryptionStatus_PropagatesNonPreconditionError pins this.

All automated reviewer concerns are resolved in the current code.


Independent Review

adapter/encryption_admin.go

[Minor] ResyncSidecar silently ignores CallerFullNodeId

adapter/encryption_admin.go:189 accepts *pb.ResyncSidecarRequest with CallerFullNodeId but never reads it. The server reads its local sidecar unconditionally:

func (s *EncryptionAdminServer) ResyncSidecar(_ context.Context, _ *pb.ResyncSidecarRequest) ...

The underscore drops the entire request. The test at encryption_admin_test.go:214 sends CallerFullNodeId: 5 which is also exercised in a no-op. The comment in the implementation says PR-B will add a leader-only guard that uses this field. This is acceptable as-is for PR-A, but the field is currently dead on the receive side. Adding a brief comment at the parameter site (e.g., // req.CallerFullNodeId is reserved for the PR-B leader-only guard) would signal to reviewers that the unused field is intentional, not an oversight.

[Minor] wrappedDEKMap silently skips corrupt key IDs without logging

adapter/encryption_admin.go:228–230:

id, err := parseSidecarKeyID(idStr)
if err != nil {
    continue
}

The comment says this can only fire on "programming errors" since validateSidecar already rejects bad IDs. That may be true today, but if a future sidecar migration introduces a format the current validateSidecar doesn't catch, the RPC will silently return an incomplete wrapped_deks_by_id map. A slog.Error(...) here would make the issue immediately visible to operators without changing the non-panicking behavior. Fix this →

cmd/elastickv-admin/encryption.go

[Minor] dialEncryption accepts a context.Context it explicitly ignores

encryption.go:61:

func dialEncryption(_ context.Context, flags *encryptionEndpointFlags) ...

grpc.NewClient is non-blocking so the context is unused. The signature is forward-compatible for when PR-B adds TLS/token auth. This is acceptable, but since the parameter is named _, callers (line 79 passes ctx) may wonder if the timeout is being respected during dial. A doc comment on the function clarifying that the context is reserved for PR-B's auth path would help.

[Design note] Shared context deadline for two sequential RPCs

runEncryptionStatus uses a single ctx (derived from *endpoint.timeout) for both GetCapability and GetSidecarState. If the first RPC consumes most of the budget, the second inherits only the remainder. This is a known tradeoff in operator CLIs and is fine here since both are fast read-only probes against a local or nearby node. No action needed, but worth noting for PR-B when mutating RPCs (with proposal round-trips) share the same surface.

cmd/elastickv-admin/encryption_test.go

[Minor] startEncryptionAdminTestServer discards Serve error

encryption_test.go:182:

go func() { _ = srv.Serve(lis) }()

If Serve fails before GracefulStop is called (e.g., port conflict on the test listener), the failure is invisible. A t.Logf inside the goroutine would surface it without failing the test (since GracefulStop closing the listener is the normal termination path). Fix this →

[Minor] ResyncSidecar test doesn't assert WriterRegistryForCaller contract

TestEncryptionAdmin_ResyncSidecar_ShipsWrappedDEKs at encryption_admin_test.go:203 verifies the DEKs and applied index, but unlike TestEncryptionAdmin_GetSidecarState_ShipsWrappedDEKs (which checks WriterRegistryForCaller != nil && len == 0), the ResyncSidecar test doesn't assert the non-nil-empty-map contract. Since ResyncSidecarResponse.WriterRegistryForCaller is supposed to mirror the same Stage 7 contract, adding the assertion pins it against accidental regression. Fix this →

proto/encryption_admin.proto

[Cosmetic] Service RPC ordering doesn't match comment grouping

In the service EncryptionAdmin block (lines 46–52), RPCs are ordered: GetCapability, GetSidecarState, BootstrapEncryption, RotateDEK, RegisterEncryptionWriter, ResyncSidecar. The comment groups describe Group 1 (read-only: GetCapability, GetSidecarState, ResyncSidecar) then Group 2 (mutating: Bootstrap, Rotate, Register). Reordering the service definition to match the comment grouping would make it easier to cross-reference. Wire format is unaffected. Low priority given proto stability.

Security / Design Notes

  • No auth on read-only RPCs in PR-A: GetCapability, GetSidecarState, and ResyncSidecar return wrapped DEK material. As the proto doc correctly notes, this is no worse than reading the on-disk sidecar. The PR description explicitly defers TLS/token auth to PR-B. Acceptable gap, but the server registration in PR-B must ensure these are not accidentally exposed without auth.
  • local_epoch validation deferred: The proto comments note local_epoch <= 0xFFFF MUST be validated at every decode site. The mutating RPCs are Unimplemented in PR-A so this is fine now, but PR-B must enforce this at the BootstrapEncryption boundary before the writer-batch goes into the Raft proposal.

Overall Assessment

This is a well-structured and carefully scoped PR. The implementation quality is high: the EncryptionCapable semantics are correct and documented, the error propagation strategy is sound, the tests exercise both the happy path and the edge cases (no-sidecar, missing-file, pre-bootstrap, bootstrapped, non-FailedPrecondition errors), and the design-doc lifecycle transition is properly handled. The issues I found are all minor — a few missing assertions and a couple of silent error swallows in test helpers. The PR is ready to merge once those minor items are addressed or acknowledged as acceptable deferred debt.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 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.go`:
- Around line 74-76: The flag parsing path using fs.Parse(args) treats any error
as fatal; modify the error handling in the encryption command (where
fs.Parse(args) is called in encryption.go) to treat flag.ErrHelp as a non-error:
after calling fs.Parse(args), if err == flag.ErrHelp return nil (or no wrapped
error) so the help invocation exits successfully, otherwise return
errors.Wrap(err, "parse flags") for other errors; ensure you import flag if not
already referenced and keep the existing errors.Wrap behavior for non-help
errors.
- Around line 31-40: The command treats help-style invocations as an
unknown-subcommand error; update the subcommand handling so that when sub is
"help", "-h", or "--help" it prints the usage string ("usage: elastickv-admin
encryption <subcommand> [flags]") to os.Stdout and returns nil (exit 0) instead
of falling through to errors.Errorf; modify the switch that dispatches on sub
(the same block that calls runEncryptionStatus(rest, os.Stdout)) to handle these
help tokens and emit the usage text rather than an error.
- Around line 131-133: The fmt.Fprintf call that writes "sidecar_state:
<unavailable: %v>\n" currently discards its error; change it to capture the
returned error from fmt.Fprintf(out, ...) and return (or wrap) that error
instead of ignoring it so write failures to the output stream are propagated.
Locate the call using fmt.Fprintf with the message "sidecar_state: <unavailable:
%v>\n", and use the returned err (and stateErr if desired for context) to
produce a non-nil error return from the enclosing function rather than using "_,
_ =" to drop it.

In `@docs/design/2026_04_29_partial_data_at_rest_encryption.md`:
- Line 16: Update the Stage 5 status for the "EncryptionAdmin gRPC + CLI" row in
the design doc: change the Stage column value currently set to `open` to
`partial` (or split into separate 5A/5B rows if you prefer) and add `#754` to
the "Landed in" column so the row reflects this PR as Stage 5 PR-A; locate the
row containing the text "EncryptionAdmin gRPC + CLI (§5.6 bootstrap, §6.6 admin
commands)" and make these two edits to keep milestone tracking accurate.
🪄 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: 7d42f2fa-0b40-4cf0-b149-4a978908ab51

📥 Commits

Reviewing files that changed from the base of the PR and between 2d92a17 and 37fc973.

⛔ Files ignored due to path filters (2)
  • proto/encryption_admin.pb.go is excluded by !**/*.pb.go
  • proto/encryption_admin_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (7)
  • adapter/encryption_admin.go
  • adapter/encryption_admin_test.go
  • cmd/elastickv-admin/encryption.go
  • cmd/elastickv-admin/encryption_test.go
  • cmd/elastickv-admin/main.go
  • docs/design/2026_04_29_partial_data_at_rest_encryption.md
  • proto/encryption_admin.proto

Comment thread cmd/elastickv-admin/encryption.go
Comment thread cmd/elastickv-admin/encryption.go
Comment thread cmd/elastickv-admin/encryption.go
Comment thread docs/design/2026_04_29_partial_data_at_rest_encryption.md Outdated
Addresses claude[bot]'s round-2 review on PR #754. claude[bot] LGTM'd
the round-1 fixes (gemini P1 + 4 mediums + codex P2) and surfaced
only minor follow-ups in the round-2 pass.

## adapter/encryption_admin.go

- ResyncSidecar now binds `req` and explicitly `_ = req` with a
  comment that PR-B's leader-only guard will read CallerFullNodeId.
  No semantic change; this just signals to reviewers that the
  unused field is intentional, not an oversight.
- wrappedDEKMap logs an `slog.Error` when parseSidecarKeyID rejects
  an entry. The comment continues to document the "programming
  error" expectation; the log makes a future sidecar-format
  migration that outpaces validateSidecar visible in operator
  logs rather than producing a silently shrunken wrapped_deks_by_id
  map.

## cmd/elastickv-admin/encryption.go

- dialEncryption gets a doc-comment clarifying that the context
  argument is reserved for the PR-B auth path (TLS + token);
  grpc.NewClient itself is non-blocking and ignores it.

## adapter/encryption_admin_test.go

- TestEncryptionAdmin_ResyncSidecar_ShipsWrappedDEKs now asserts
  the same non-nil empty-map contract on WriterRegistryForCaller
  that TestEncryptionAdmin_GetSidecarState_ShipsWrappedDEKs
  already checks. Mirrors the §5.5 promise so a future regression
  to nil is caught.

## cmd/elastickv-admin/encryption_test.go

- startCustomEncryptionAdminTestServer's Serve goroutine no longer
  silently drops the error. GracefulStop returns nil from Serve,
  so the happy-path exit prints nothing; an unexpected listener
  or accept failure now surfaces via t.Logf.

## Skipped (claude[bot] cosmetic)

- "Service RPC ordering doesn't match comment grouping" — reordering
  the rpc definitions would shuffle the generated ServiceDesc.Methods
  order. Wire format is unaffected but the cosmetic risk-to-reward
  is unfavourable; the existing comment grouping is clear enough.

## Verification

- go test -race -timeout=60s on ./adapter and ./cmd/elastickv-admin:
  PASS
- golangci-lint run on touched paths: 0 issues
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 15, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @bootjp's task in 1m 46s —— View job


Claude finished @bootjp's task


Claude's Review (Round 2)

Tasks

  • Gather context from PR and prior reviews
  • Read adapter/encryption_admin.go
  • Read adapter/encryption_admin_test.go
  • Read cmd/elastickv-admin/encryption.go
  • Read cmd/elastickv-admin/encryption_test.go
  • Read proto/encryption_admin.proto
  • Read cmd/elastickv-admin/main.go
  • Read design doc
  • Analyze findings and write review

Status: Prior Round-1 Findings

All five issues flagged in the previous Claude review are addressed in the current code:

Finding Status
wrappedDEKMap silently skipped corrupt key IDs ✅ Fixed — slog.Error(...) logged at encryption_admin.go:237–239
startEncryptionAdminTestServer discarded Serve error ✅ Fixed — t.Logf(...) at encryption_test.go:187–189
ResyncSidecar test missing WriterRegistryForCaller assertion ✅ Fixed — assertions at encryption_admin_test.go:229–235
ResyncSidecar unused req parameter undocumented ✅ Documented — _ = req with comment at encryption_admin.go:193–194
dialEncryption ignores context without explanation ✅ Documented — doc comment at encryption.go:61–65 explains PR-B reservation

All prior automated-reviewer findings (Gemini P1/M1–M4, Codex P2) remain resolved as verified in the prior round.


Remaining Open Items (CodeRabbit batch, not yet addressed)

1. encryption.go:137 — Write failure silently discarded for unavailable sidecar line

The current code:

_, _ = fmt.Fprintf(out, "sidecar_state: <unavailable: %v>\n", stateErr)
return nil

Every other fmt.Fprintf call in writeEncryptionStatus propagates the write error (e.g., lines 116–134 all return errors.Wrap(err, "write ...")). This one diverges from that pattern and silently succeeds even if the output stream is broken. The CodeRabbit suggested fix is correct and minimal:

if _, err := fmt.Fprintf(out, "sidecar_state: <unavailable: %v>\n", stateErr); err != nil {
    return errors.Wrap(err, "write sidecar_state unavailable")
}
return nil

Fix this →

2. encryptionMain and runEncryptionStatus-h/--help exits non-zero

Two issues combine here. First, encryptionMain (encryption.go:35–40) falls through to the default error case for any of -h, --help, or help:

switch sub {
case "status":
    return runEncryptionStatus(rest, os.Stdout)
default:
    return errors.Errorf("encryption: unknown subcommand %q (PR-A supports: status)", sub)
}

Second, runEncryptionStatus uses flag.ContinueOnError (line 77) but does not special-case flag.ErrHelp (line 79–81):

if err := fs.Parse(args); err != nil {
    return errors.Wrap(err, "parse flags")  // flag.ErrHelp hits this path
}

Both cause elastickv-admin encryption -h and elastickv-admin encryption status -h to exit non-zero, which will confuse shell scripts using $? to detect success. The fixes are independent and low-risk:

// In encryptionMain:
case "-h", "--help", "help":
    _, _ = fmt.Fprintln(os.Stdout, "usage: elastickv-admin encryption <subcommand> [flags]\n\nsubcommands:\n  status")
    return nil

// In runEncryptionStatus after fs.Parse:
if errors.Is(err, flag.ErrHelp) {
    return nil
}
return errors.Wrap(err, "parse flags")

Fix this →

3. docs/design/2026_04_29_partial_data_at_rest_encryption.md:16 — Stage 5 row not updated

The milestone table still shows:

| 5 | `EncryptionAdmin` gRPC + CLI (§5.6 bootstrap, §6.6 admin commands) | open | — |

This PR is Stage 5 PR-A. Per the CLAUDE.md design-doc lifecycle, the row should reflect that this is partially landed (suggest splitting into 5A/5B rows or marking as partial) and link to #754. Fix this →


New Independent Observations

No new correctness, data-loss, or concurrency issues found. The implementation quality is high and consistent with the rest of the codebase.

One cosmetic note: writeSidecarFixture in adapter/encryption_admin_test.go:256–263 and writeStatusFixture in cmd/elastickv-admin/encryption_test.go:145–153 are identical helpers in different packages. This is expected and acceptable given the package boundary (the admin test imports adapter but can't call internal test helpers from it); no action required.


Overall Assessment

This PR lands cleanly. The three remaining items are all cosmetic or ergonomic:

  • Item 1 (_, _ = fmt.Fprintf) is a one-liner fix that brings the unavailable-sidecar branch into consistency with the rest of writeEncryptionStatus.
  • Items 2a+2b (-h exit code) are three-liners; they don't affect correctness but are the kind of paper cut that trips up shell scripts.
  • Item 3 (design doc) is a one-row table update.

None block correctness. The security posture is appropriate for a PR-A read-only surface. The EncryptionCapable semantics, error propagation strategy, WriterRegistryForCaller non-nil contract, and test coverage are all solid. Ready to merge — the three items above are suitable for a quick cleanup commit or can be deferred to PR-B.

…bot])

claude[bot] round 2 says "Ready to merge"; coderabbitai round 1
filed 4 minor findings. All overlap in 3 distinct cleanups, all
addressed here. No correctness change; the diff is the kind of
paper-cut polish that keeps shell ergonomics + design-doc lifecycle
clean.

## cmd/elastickv-admin/encryption.go

- `encryption -h` / `--help` / `help` now print usage to stdout and
  exit 0 instead of falling through to the unknown-subcommand
  error path.
- `encryption status -h` now treats `flag.ErrHelp` as success.
  flag.ContinueOnError already wrote the per-flag usage to
  fs.Output(); we just stop wrapping the sentinel into an error.
- The unavailable-sidecar branch no longer drops `fmt.Fprintf`
  write errors; it now matches the propagation pattern every
  other Fprintf in `writeEncryptionStatus` uses.

## cmd/elastickv-admin/encryption_test.go

- TestEncryptionMain_HelpFlagExitsZero locks in the three help-flag
  forms (`-h`, `--help`, `help`) all returning nil.
- TestRunEncryptionStatus_HelpFlagExitsZero locks in
  `flag.ErrHelp`-on-subcommand-flags also returning nil.

## docs/design/2026_04_29_partial_data_at_rest_encryption.md

- Stage 5 milestone row split into 5A (this PR, shipped) and 5B
  (mutating RPCs + capability fan-out + remaining CLI + main.go
  wiring, open). `Landed in` column links #754 for 5A.

## Verification

- go test -race -timeout=60s on ./adapter and ./cmd/elastickv-admin:
  PASS
- golangci-lint run on touched paths: 0 issues
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 15, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @bootjp's task in 2m 30s —— View job


Claude's Review (Round 3)

Tasks

  • Gather context from PR and prior reviews
  • Read cmd/elastickv-admin/encryption.go (check Round 2 items 1 & 2)
  • Read docs/design/2026_04_29_partial_data_at_rest_encryption.md (check Round 2 item 3)
  • Read adapter/encryption_admin.go and tests for any new issues
  • Analyze findings and write review

Status of Prior Round 2 / CodeRabbit Findings

All three open items from Round 2 are resolved in commit d09a753:

Finding Status
_, _ = fmt.Fprintf discards write error for unavailable-sidecar line ✅ Fixed — encryption.go:153–157 captures and propagates the error
-h/--help exits non-zero in encryptionMain and runEncryptionStatus ✅ Fixed — encryptionMain (lines 38–47) handles the help case with proper error capture; runEncryptionStatus (lines 94–96) guards on errors.Is(err, flag.ErrHelp)
Design doc Stage 5 row stale (open / no PR link) ✅ Fixed — Stage 5 split into 5A (shipped, PR #754) and 5B (open, )

New pinning tests added for the help fixes:

  • TestEncryptionMain_HelpFlagExitsZero — covers -h, --help, help
  • TestRunEncryptionStatus_HelpFlagExitsZero — covers status -h

Independent Round 3 Observations

No new correctness, data-loss, or concurrency issues found.

One minor style note: adapter/encryption_admin.go:6 imports stdlib "errors" rather than "github.com/cockroachdb/errors". The stdlib package is used only in errors.Is() (functionally equivalent since cockroachdb/errors implements the standard unwrap interface) and in parseSidecarKeyID's errors.New() (an internal unexported function whose error never crosses a package boundary, so wrapcheck does not fire). The linter passes as-is. Flagged as a style note for completeness; no action is required to merge.

Everything else — the EncryptionCapable gate semantics, error propagation strategy, WriterRegistryForCaller non-nil contract, sidecarPath vs bootstrap distinction, flag.ErrHelp handling, fmt.Fprintf error propagation, and design-doc lifecycle — is clean.


Overall Assessment

This PR is ready to merge. All automated-reviewer findings (Gemini P1/M1–M4, Codex P2), all Round 1 Claude findings, and all three Round 2 Claude / CodeRabbit findings are resolved. The implementation quality is high, the test coverage is thorough, and the design-doc lifecycle (5A shipped / 5B open) is correctly maintained.

@bootjp bootjp merged commit ee42528 into main May 15, 2026
9 checks passed
@bootjp bootjp deleted the feat/encryption-admin branch May 15, 2026 17:07
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