Skip to content

feat: subscription lifecycle hardening and readiness accessors#63

Closed
amery wants to merge 11 commits into
mainfrom
pr-amery-fixes
Closed

feat: subscription lifecycle hardening and readiness accessors#63
amery wants to merge 11 commits into
mainfrom
pr-amery-fixes

Conversation

@amery

@amery amery commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Subscription lifecycle correctness across the client and server,
pinned by a protocol-document clarification that lands first so the
code can align with the new wording. Adjacent client and server
readiness primitives close doc/code gaps that the README had
referenced ahead of implementation.

Subscription lifecycle

Three commits address the request_id-sharing contract end-to-end.

  • docs(protocol): clarify subscription request_id lifetime and demux
    closes two gaps in NANORPC_PROTOCOL.md §5.4 and §6.1: the
    reservation window for a subscription's request_id is named
    explicitly (TYPE_SUBSCRIBE until the unsubscribe acknowledgement
    or session end), and the subscribe TYPE_RESPONSE is distinguished
    from the unsubscribe TYPE_RESPONSE that arrives on the same id.
    §6.1 picks up a stateDiagram-v2 of the
    Pending/Active/Unsubscribing/Terminated transitions and a Phases
    subsection naming which message types may arrive in each phase.
    §6.3 and §10.1 cover the in-flight TYPE_UPDATE that may arrive
    between the unsubscribe request and its acknowledgement.

  • feat(client): surface subscribe ACKs and reject typed nils routes
    the subscribe TYPE_RESPONSE acknowledgement to the typed
    SubscribeCallback as either ErrSubscriptionEstablished on
    STATUS_OK or a ResponseError on any other status, via a
    dispatcher split into isSubscribeACK / subscribeACKErr /
    decodeSubscribePayload. callNewOut rejects typed-nil factory
    results through core.IsNil; GetResponse gains the same
    boundary protection on its client and out arguments.
    ErrSubscriptionEstablished and IsSubscriptionEstablished land
    in the root package.

  • fix(client): demux unsubscribe responses and guard Send fixes
    the dispatcher: popRequestCallback now matches on the
    (request_id, response_type) pair via unsafeIndexCallbacks, so
    the TYPE_RESPONSE acknowledging an Unsubscribe no longer reaches
    the still-registered SubscribeCallback. Send rejects an
    unsubscribe-form TYPE_REQUEST whose target subscription is
    missing or still pending its acknowledgement, factored into
    validateSendArgs / isUnsubscribeShape /
    checkUnsubscribeTarget / normaliseRequestID /
    registerCallback. Failure modes documented on Session.Send
    and the three Client.Unsubscribe* variants.
    popRequestCallback's routing matrix and the Send guard are
    covered with data-driven tests.

Client readiness primitives

  • feat(client): add WaitConnected and connection-state accessors
    exposes Connected(), IsConnected(), and
    WaitConnected(ctx) on Client. Connected() returns a channel
    that closes while the client holds an active session and is
    replaced after each disconnect; IsConnected() is a point-in-time
    snapshot; WaitConnected(ctx) wraps Connected() with the
    caller's context. Closes the doc/code gap left by the
    client/README.md "Connection Management" example, which had
    referenced both accessors before either existed.

Server ergonomic improvements

  • feat(server): accept a handler argument in NewDefaultServer
    allows callers to pass an existing *DefaultMessageHandler — and
    register paths against it — before Server.Serve starts. A nil
    handler preserves the existing behaviour (a fresh
    DefaultMessageHandler with an internal HashCache is built
    in-place). Integration test drives an echo handler through a real
    TCP request.

  • feat(server): expose a Ready channel for the accept loop adds a
    Ready() <-chan struct{} accessor that closes once the accept
    loop has started taking connections, via an idempotent
    signalReady guarded by mu. The channel stays closed across
    shutdown so late observers never block. The test harness drops
    its time.Sleep(50 * time.Millisecond) heuristic for a
    waitServerReady helper with a 1s ceiling; a new
    TestServer_Ready confirms the open-before-Serve /
    closed-after-loop / stays-closed-across-Shutdown sequence.

Test plan

  • make is green — tidy across all four modules, cspell,
    shellcheck, build.
  • make test is green across generator, nanopb, and
    nanorpc.
  • make race is green; new TestServer_Ready does not flake.
  • Examples in pkg/nanorpc/client/README.md and
    pkg/nanorpc/server/{README.md,doc.go} compile against the
    updated API.

Summary by CodeRabbit

  • Documentation
    • Updated NanoRPC protocol docs to clarify subscription lifecycle, routing, and shutdown guarantees.
    • Refreshed client/server README examples to match current APIs and add connection/readiness guidance.
  • New Features
    • Added client connection readiness APIs: Connected(), IsConnected(), and WaitConnected(ctx).
    • Added server readiness signaling via Ready().
    • Introduced ErrSubscriptionEstablished (and IsSubscriptionEstablished) for subscription acknowledgements.
  • Bug Fixes
    • Improved subscription callback/error semantics and stricter validation for invalid callback factories.
  • Tests
    • Expanded coverage for subscription callbacks, session routing, and connection readiness behavior.

@amery amery added enhancement New feature or request Review effort 4/5 labels May 20, 2026
@amery amery self-assigned this May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR clarifies NanoRPC subscription lifecycle semantics and implements corresponding client and server infrastructure. It introduces connection-readiness signaling for client-server coordination, refactors subscription callback dispatch to distinguish acknowledgements from updates, extends the server with readiness APIs, and upgrades the Go toolchain to 1.24 with code hygiene improvements. The protocol documentation, error types, and queue routing logic align to ensure correct request_id-based routing without misrouting between subscription establishment and update delivery phases.

Changes

Subscription routing and readiness coordination

Layer / File(s) Summary
Protocol specification for subscription lifecycle
NANORPC_PROTOCOL.md
Request_id is reserved from TYPE_SUBSCRIBE transmission until unsubscribe TYPE_RESPONSE, and updates may arrive during the Unsubscribing phase; protocol now defines four client-side phases (Pending/Active/Unsubscribing/Terminated) with explicit message ordering and delivery guarantees.
Subscription acknowledgement error type
pkg/nanorpc/errors.go
New sentinel ErrSubscriptionEstablished and IsSubscriptionEstablished() helper represent non-failure subscription acknowledgements surfaced via callbacks.
Client connection readiness API and signaling
pkg/nanorpc/client/client.go, pkg/nanorpc/client/reconnect.go, pkg/nanorpc/client/connected_test.go, pkg/nanorpc/client/README.md
Adds Connected(), IsConnected(), and WaitConnected(ctx) APIs with internal connected channel that closes on session attachment and reinitializes on session end, enabling client-side readiness-edge synchronization.
Subscription callback dispatch and error mapping
pkg/nanorpc/client/request.go, pkg/nanorpc/client/request_test.go, pkg/nanorpc/client/subscribe_callback_test.go, pkg/nanorpc/client/request_counter.go
Subscribe refactored to distinguish TYPE_RESPONSE (acknowledgement) from TYPE_UPDATE (delivery), allocates fresh outputs per callback via callNewOut, validates non-nil newOut, maps ack status to either ErrSubscriptionEstablished or ResponseError, and includes comprehensive callback error/output semantics tests.
Session queue routing by request type and ID
pkg/nanorpc/client/session.go, pkg/nanorpc/client/session_test.go
clientRequestQueue extended with RequestType and Acknowledged flag; popRequestCallback rewritten to split matches by SUBSCRIBE vs non-SUBSCRIBE entries and route TYPE_UPDATE separately; Send validates unsubscribe targets and normalizes request IDs with dedicated helpers; includes routing matrix tests.
Server readiness signaling and NewDefaultServer refactoring
pkg/nanorpc/server/server.go, pkg/nanorpc/server/server_test.go, pkg/nanorpc/server/README.md, pkg/nanorpc/server/doc.go, pkg/nanorpc/server/handler_test.go
Adds ready channel to Server with Ready() API that signals from acceptLoop; extends NewDefaultServer to accept optional *DefaultMessageHandler parameter; updates tests to use core assertion helpers and wait on server readiness instead of fixed sleep.

Go version upgrade and dependency updates

Layer / File(s) Summary
Go 1.24 requirement update
go.mod, pkg/nanopb/go.mod, README.md, AGENTS.md
Go toolchain requirement bumped from 1.23 to 1.24 across all modules and build documentation.
Darvaza dependency version bumps
pkg/nanorpc/go.mod, pkg/generator/go.mod
Darvaza.org/core updated to v0.19.1 and darvaza.org/x/sync to v0.4.1.
Coverage documentation for Go 1.24
internal/build/README-coverage.md
Updated prerequisites and documented stdout capture artifacts from go test runs.

Code hygiene and tooling improvements

Layer / File(s) Summary
Loop modernization to range syntax
pkg/nanorpc/testutils_test.go, pkg/nanorpc/utils/slices_test.go, pkg/nanorpc/server/handler_test.go
Replaces indexed for loops with range loops across test files and uses fmt.Appendf instead of fmt.Sprintf for byte slice construction.
Standard library maps.Copy usage
pkg/nanorpc/utils/testutils/mockfieldlogger.go, pkg/nanorpc/utils/testutils/mockfieldlogger_test.go
Replaces manual field-map copying in MockFieldLogger with stdlib maps.Copy function.
Linter configuration updates
.golangci.yml, .gitignore
Quotes golangci-lint version field, enables modernize linter, removes *.test from gitignore as test artifacts are no longer generated.
GitHub Actions workflows
.github/workflows/claude-code-review.yml, .github/workflows/claude.yml
Adds Claude Code Review workflow triggered on PR events and Claude Code workflow triggered on comments/reviews for code assistance.
DeepSource configuration
.deepsource.toml
Adds static analysis configuration with global exclusions and enables shell and Go analyzers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • protomcp/nanorpc#34: Adds full unsubscribe protocol implementation on client and server, while this PR refines subscription acknowledgement vs update routing and request_id lifecycle semantics.
  • protomcp/nanorpc#21: Fixes Subscribe to send TYPE_SUBSCRIBE, while this PR refactors subscription ack vs update routing with typed request dispatch.
  • protomcp/nanorpc#22: Introduces modular server architecture with DefaultMessageHandler, which this PR extends by making it an optional constructor parameter.

Poem

🐰 In the land of request_id streams,
Where updates flow through subscription dreams,
We split the ACK from the UPDATE dance,
And signal readiness with channel glance,
So client and server may waltz in sync!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: hardening subscription lifecycle semantics and adding readiness accessor methods to the client and server APIs.
Docstring Coverage ✅ Passed Docstring coverage is 81.33% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-amery-fixes

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="Running error: context loading failed: no go files to analyze: running go mod tidy may solve the problem"


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/nanorpc/server/README.md (1)

107-112: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the DI example to the new server constructor signatures

The “Custom Server with Dependency Injection” snippet still uses outdated call forms (NewDefaultMessageHandler(), NewDefaultSessionManager(handler), NewServer(listener, sessionManager, handler)). This example should be updated to match current APIs so readers can copy-paste successfully.

🤖 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 `@pkg/nanorpc/server/README.md` around lines 107 - 112, The DI example uses
outdated constructors (NewDefaultMessageHandler, NewDefaultSessionManager,
NewServer); update the snippet to call the current constructor APIs used in the
package (replace NewDefaultMessageHandler and NewDefaultSessionManager(handler)
with the package's current message handler and session manager constructors and
update the NewServer call to the new server constructor signature), ensuring the
new function names and required parameters/options match the current server
package exports so the example compiles and can be copy-pasted.
🧹 Nitpick comments (4)
pkg/nanorpc/client/connected_test.go (1)

25-123: ⚡ Quick win

Consider converting these readiness tests to table-driven form.

The scenarios are related and would align better with repository test style (and be easier to extend) as a table-driven suite.

As per coding guidelines, "**/*_test.go: Use table-driven tests for comprehensive test coverage in Go."

🤖 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 `@pkg/nanorpc/client/connected_test.go` around lines 25 - 123, Convert the
multiple readiness tests into a single table-driven test that iterates over
scenarios (initially disconnected, opens then closes, swaps after endSession,
already connected fast-path, blocks then succeeds, context cancellation) to
match repo style; create a test table with a name, setup steps (using
newClientForTest, optional calls to setSession or endSession), expected behavior
(IsConnected value, whether Connected channel is closed, WaitConnected result or
blocking), and any timeouts, then implement a loop that runs each case as
t.Run(name, func(t *testing.T){...}) using the existing helpers (Connected,
setSession, endSession, WaitConnected) and assertions, preserving the current
assertions and timeouts for each scenario.
NANORPC_PROTOCOL.md (1)

283-289: ⚡ Quick win

Wrap the new prose lines to the 80-column markdownlint limit.

Multiple added lines exceed 80 chars and will trip the markdownlint configuration.

As per coding guidelines, "**/*.md: Markdown files should follow markdownlint rules with 80-character line limits as configured in internal/build/markdownlint.json."

Also applies to: 323-330, 345-346

🤖 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 `@NANORPC_PROTOCOL.md` around lines 283 - 289, The added paragraph about
subscription lifetimes and routing exceeds the 80-column markdownlint limit;
reflow the prose in NANORPC_PROTOCOL.md so each line is ≤80 chars while
preserving wording about TYPE_SUBSCRIBE, TYPE_RESPONSE, TYPE_REQUEST and
request_id semantics (first TYPE_RESPONSE = subscription ack, later
TYPE_RESPONSE after TYPE_REQUEST with empty data = unsubscribe ack). Apply the
same 80-column wrapping to the other affected blocks noted (around the additions
at the ranges corresponding to lines 323-330 and 345-346) so the file conforms
to the project's markdownlint configuration.
pkg/nanorpc/client/subscribe_callback_test.go (1)

41-133: ⚡ Quick win

Refactor these callback-path tests into a table-driven suite.

The cases are tightly related and fit naturally into a table-driven matrix (ACK OK, ACK error, update data/no-data/bad-data), which also matches project testing conventions.

As per coding guidelines, "**/*_test.go: Use table-driven tests for comprehensive test coverage in Go."

🤖 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 `@pkg/nanorpc/client/subscribe_callback_test.go` around lines 41 - 133, Combine
the individual TestSubscribeCallback_* tests into a single table-driven test
(e.g., TestSubscribeCallback_Table) that iterates over cases describing inputs
and expected outcomes; each case should provide a name, the
nanorpc.NanoRPCResponse (use the existing patterns: TYPE_RESPONSE with
STATUS_OK, TYPE_RESPONSE with non-OK, TYPE_UPDATE with data, TYPE_UPDATE without
data, TYPE_UPDATE with bad data), the expected error predicate
(IsSubscriptionEstablished, IsNotFound, IsNoResponse, nil, or generic decode
error) and expected out assertions, and call invokeSubscribeCallback for each
row using t.Run; preserve existing assertions (core.AssertNoError,
core.AssertErrorIs, core.AssertTrue/False, core.AssertNotNil, core.AssertEqual)
but execute them per case so behavior for invokeSubscribeCallback,
nanorpc.NanoRPCResponse, and the various status checks remains identical while
removing the separate TestSubscribeCallback_ACKSurfacesEstablished,
TestSubscribeCallback_ACKErrorStatusSurfacesRealError,
TestSubscribeCallback_UpdateWithDataIsDelivered,
TestSubscribeCallback_UpdateWithoutDataIsErrNoResponse, and
TestSubscribeCallback_UpdateWithBadDataSurfacesDecodeError functions.
pkg/nanorpc/client/session_test.go (1)

89-116: ⚡ Quick win

Add a regression row for “SUBSCRIBE-only + non-RESPONSE/non-UPDATE”

Please add one case where a SUBSCRIBE-only queue receives TYPE_PONG (or unknown type) and assert no callback plus unchanged residue. This will lock in the demux contract and prevent accidental re-ack behavior regressions.

🤖 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 `@pkg/nanorpc/client/session_test.go` around lines 89 - 116, Add a regression
row in routingTestCases that covers a SUBSCRIBE-only queue receiving a
non-RESPONSE/non-UPDATE (e.g. TYPE_PONG): call newRoutingTestCase with name like
"subscribe_only_nonresponse_nonupdate", initial state core.S(sub(9)), id 9,
response respPong, expected callback index -1, and expected residue
core.S(sub(9)); place this new test alongside the other routingTestCase entries
to ensure newRoutingTestCase, routingTestCases, core.S, sub, and respPong are
exercised and the subscribe-only residue remains unchanged.
🤖 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 `@pkg/nanorpc/client/session.go`:
- Around line 130-134: When matching a SUBSCRIBE callback (subIdx) you currently
set cs.cb[subIdx].Acknowledged = true for any response that isn't TYPE_UPDATE,
which can erroneously acknowledge on other types (e.g., TYPE_PONG); change the
logic so you only set Acknowledged and return cs.cb[subIdx].Callback when the
incoming message's Type equals TYPE_RESPONSE—otherwise do not acknowledge and
return nil. Ensure you reference and check the message type constant
(TYPE_RESPONSE) before modifying cs.cb[subIdx].Acknowledged or returning
cs.cb[subIdx].Callback so only true response messages activate the subscription.

---

Outside diff comments:
In `@pkg/nanorpc/server/README.md`:
- Around line 107-112: The DI example uses outdated constructors
(NewDefaultMessageHandler, NewDefaultSessionManager, NewServer); update the
snippet to call the current constructor APIs used in the package (replace
NewDefaultMessageHandler and NewDefaultSessionManager(handler) with the
package's current message handler and session manager constructors and update
the NewServer call to the new server constructor signature), ensuring the new
function names and required parameters/options match the current server package
exports so the example compiles and can be copy-pasted.

---

Nitpick comments:
In `@NANORPC_PROTOCOL.md`:
- Around line 283-289: The added paragraph about subscription lifetimes and
routing exceeds the 80-column markdownlint limit; reflow the prose in
NANORPC_PROTOCOL.md so each line is ≤80 chars while preserving wording about
TYPE_SUBSCRIBE, TYPE_RESPONSE, TYPE_REQUEST and request_id semantics (first
TYPE_RESPONSE = subscription ack, later TYPE_RESPONSE after TYPE_REQUEST with
empty data = unsubscribe ack). Apply the same 80-column wrapping to the other
affected blocks noted (around the additions at the ranges corresponding to lines
323-330 and 345-346) so the file conforms to the project's markdownlint
configuration.

In `@pkg/nanorpc/client/connected_test.go`:
- Around line 25-123: Convert the multiple readiness tests into a single
table-driven test that iterates over scenarios (initially disconnected, opens
then closes, swaps after endSession, already connected fast-path, blocks then
succeeds, context cancellation) to match repo style; create a test table with a
name, setup steps (using newClientForTest, optional calls to setSession or
endSession), expected behavior (IsConnected value, whether Connected channel is
closed, WaitConnected result or blocking), and any timeouts, then implement a
loop that runs each case as t.Run(name, func(t *testing.T){...}) using the
existing helpers (Connected, setSession, endSession, WaitConnected) and
assertions, preserving the current assertions and timeouts for each scenario.

In `@pkg/nanorpc/client/session_test.go`:
- Around line 89-116: Add a regression row in routingTestCases that covers a
SUBSCRIBE-only queue receiving a non-RESPONSE/non-UPDATE (e.g. TYPE_PONG): call
newRoutingTestCase with name like "subscribe_only_nonresponse_nonupdate",
initial state core.S(sub(9)), id 9, response respPong, expected callback index
-1, and expected residue core.S(sub(9)); place this new test alongside the other
routingTestCase entries to ensure newRoutingTestCase, routingTestCases, core.S,
sub, and respPong are exercised and the subscribe-only residue remains
unchanged.

In `@pkg/nanorpc/client/subscribe_callback_test.go`:
- Around line 41-133: Combine the individual TestSubscribeCallback_* tests into
a single table-driven test (e.g., TestSubscribeCallback_Table) that iterates
over cases describing inputs and expected outcomes; each case should provide a
name, the nanorpc.NanoRPCResponse (use the existing patterns: TYPE_RESPONSE with
STATUS_OK, TYPE_RESPONSE with non-OK, TYPE_UPDATE with data, TYPE_UPDATE without
data, TYPE_UPDATE with bad data), the expected error predicate
(IsSubscriptionEstablished, IsNotFound, IsNoResponse, nil, or generic decode
error) and expected out assertions, and call invokeSubscribeCallback for each
row using t.Run; preserve existing assertions (core.AssertNoError,
core.AssertErrorIs, core.AssertTrue/False, core.AssertNotNil, core.AssertEqual)
but execute them per case so behavior for invokeSubscribeCallback,
nanorpc.NanoRPCResponse, and the various status checks remains identical while
removing the separate TestSubscribeCallback_ACKSurfacesEstablished,
TestSubscribeCallback_ACKErrorStatusSurfacesRealError,
TestSubscribeCallback_UpdateWithDataIsDelivered,
TestSubscribeCallback_UpdateWithoutDataIsErrNoResponse, and
TestSubscribeCallback_UpdateWithBadDataSurfacesDecodeError functions.
🪄 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: c8cf6dae-54c2-406c-946b-e7d60f3984d2

📥 Commits

Reviewing files that changed from the base of the PR and between 27b5f8a and 427295a.

📒 Files selected for processing (14)
  • NANORPC_PROTOCOL.md
  • pkg/nanorpc/client/README.md
  • pkg/nanorpc/client/client.go
  • pkg/nanorpc/client/connected_test.go
  • pkg/nanorpc/client/reconnect.go
  • pkg/nanorpc/client/request.go
  • pkg/nanorpc/client/session.go
  • pkg/nanorpc/client/session_test.go
  • pkg/nanorpc/client/subscribe_callback_test.go
  • pkg/nanorpc/errors.go
  • pkg/nanorpc/server/README.md
  • pkg/nanorpc/server/doc.go
  • pkg/nanorpc/server/server.go
  • pkg/nanorpc/server/server_test.go

Comment thread pkg/nanorpc/client/session.go Outdated
Comment on lines +130 to +134
if subIdx < 0 {
return nil
}
cs.cb[subIdx].Acknowledged = true
return cs.cb[subIdx].Callback

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Acknowledge subscriptions only on TYPE_RESPONSE

Acknowledged is currently set for any non-TYPE_UPDATE response when only a SUBSCRIBE entry matches. That can incorrectly activate a subscription on unexpected response types (for example TYPE_PONG with a colliding request_id).

Suggested fix
-	if subIdx < 0 {
+	if subIdx < 0 {
 		return nil
 	}
+	if respType != nanorpc.NanoRPCResponse_TYPE_RESPONSE {
+		return nil
+	}
 	cs.cb[subIdx].Acknowledged = true
 	return cs.cb[subIdx].Callback
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if subIdx < 0 {
return nil
}
cs.cb[subIdx].Acknowledged = true
return cs.cb[subIdx].Callback
if subIdx < 0 {
return nil
}
if respType != nanorpc.NanoRPCResponse_TYPE_RESPONSE {
return nil
}
cs.cb[subIdx].Acknowledged = true
return cs.cb[subIdx].Callback
🤖 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 `@pkg/nanorpc/client/session.go` around lines 130 - 134, When matching a
SUBSCRIBE callback (subIdx) you currently set cs.cb[subIdx].Acknowledged = true
for any response that isn't TYPE_UPDATE, which can erroneously acknowledge on
other types (e.g., TYPE_PONG); change the logic so you only set Acknowledged and
return cs.cb[subIdx].Callback when the incoming message's Type equals
TYPE_RESPONSE—otherwise do not acknowledge and return nil. Ensure you reference
and check the message type constant (TYPE_RESPONSE) before modifying
cs.cb[subIdx].Acknowledged or returning cs.cb[subIdx].Callback so only true
response messages activate the subscription.

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.95238% with 32 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/nanorpc/client/session.go 77.21% 17 Missing and 1 partial ⚠️
pkg/nanorpc/client/request.go 78.00% 11 Missing ⚠️
pkg/nanorpc/errors.go 0.00% 2 Missing ⚠️
pkg/nanorpc/server/server.go 92.85% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

The subscribe and unsubscribe acknowledgements share a single
request_id, and the original wording in §5.4 and §6.1 left two
gaps: it did not say how long the request_id is reserved, and it
did not distinguish the subscribe TYPE_RESPONSE from the
unsubscribe TYPE_RESPONSE that arrives on the same id.

§5.4 now states the reservation window explicitly — from
TYPE_SUBSCRIBE until the unsubscribe acknowledgement or session
end — and warns that routing keyed only on request_id will
misroute one of the two TYPE_RESPONSEs.

§6.1 expands the termination bullets, adds a stateDiagram-v2 of
the Pending/Active/Unsubscribing/Terminated transitions, and
introduces a Phases subsection naming each phase and which
message types may arrive in it. §6.3 adds a Termination bullet
covering the in-flight TYPE_UPDATE that may still arrive between
the unsubscribe request and its acknowledgement. §10.1's ASCII
sequence shows the same in-flight TYPE_UPDATE alongside the
unsubscribe acknowledgement.

§2.2's request-response ASCII diagram picks up a one-space
column alignment fix.

Signed-off-by: Alejandro Mery <amery@apptly.co>
@amery amery force-pushed the pr-amery-fixes branch from 427295a to 47e3aa2 Compare May 20, 2026 16:12
amery added 10 commits June 13, 2026 16:04
Replace older loop and formatting patterns with their current
stdlib equivalents, in preparation for enabling golangci-lint's
modernize analyser:

- max() for the zero-skipping clamp in the request counter
- range-over-int for C-style index loops in the server and the
  shared test helpers
- maps.Copy for hand-written map-copy loops in the mock field
  logger
- fmt.Appendf in place of []byte(fmt.Sprintf(...))

Every construct is available on the current go 1.23.0 floor
(range-over-int since 1.22), so this stands independently of the
upcoming Go 1.24 bump.

Signed-off-by: Alejandro Mery <amery@apptly.co>
Finish the 1.24 floor that the core v0.19 bump (c3d4153) started: the
generator and nanorpc modules already declared go 1.24.0 through that
dependency, leaving the root and nanopb modules behind at 1.23.0. Raise
both, and align the build configuration with the darvaza.org siblings.

- Raise the root and nanopb modules to go 1.24.0, and update the prose
  that quotes the minimum (README, AGENTS, coverage docs).
- Enable the modernize analyser in .golangci.yml, the code having been
  prepared for it in the preceding refactor, and quote the version
  field to match core/slog/x.
- Add .deepsource.toml, mirroring the siblings with the nanorpc import
  root.
- Drop the duplicate *.test entry from .gitignore and sync the .stdout
  coverage docs from core.

Signed-off-by: Alejandro Mery <amery@apptly.co>
- `claude.yml`: respond to `@claude` mentions in issues and
  pull requests, gated to the `amery` / `karasz` actors. The
  `pull_request_review*` branches add a fork-PR guard
  (`pull_request.head.repo.full_name == github.repository`)
  as belt-and-braces over the actor gate.
- `claude-code-review.yml`: automatic review on non-draft
  pull requests from the base repo, using the `code-review`
  plugin from the anthropics/claude-code-plugins marketplace.
  Renovate bot excluded; fork PRs gated out via the same
  `head.repo.full_name` guard.

Both require the `CLAUDE_CODE_OAUTH_TOKEN` secret on the repo
or inherited from the protomcp org — fork PRs can't access it,
hence the guards.

Signed-off-by: Alejandro Mery <amery@apptly.co>
Move the generator and nanorpc modules from core v0.19.0 to the
v0.19.1 patch release, which adds the MustNoError error-precondition
helpers. No API used here changes.

Signed-off-by: Alejandro Mery <amery@apptly.co>
Expose three readiness primitives on the [Client]:

- Connected() returns a channel that closes whilst the client holds an
  active session. The channel is replaced after each disconnect, so it
  signals the next readiness edge and callers waiting across a reconnect
  cycle must re-fetch.
- IsConnected() reports the current session state as a point-in-time
  snapshot.
- WaitConnected(ctx) wraps Connected with the caller's context, so
  consumers can ride out a brief reconnect blip without polling Pong().

Closes the doc/code gap left by the README's "Connection Management"
example, which had referenced Connected() and IsConnected() before
either existed.

The readiness channel is initialised in init(), closed inside
setSession after the session pointer is attached, and swapped for a
fresh open channel inside endSession when a live session ends.
endSession leaves the channel untouched when already disconnected, so
waiters holding it are not orphaned. Both transitions happen under the
existing mu guarding c.cs.

Signed-off-by: Alejandro Mery <amery@apptly.co>
Allow callers to pass an existing *DefaultMessageHandler — and so
register paths against it — before [Server.Serve] starts. When the
handler is nil, the existing behaviour is preserved: a fresh
DefaultMessageHandler with an internal HashCache is built in-place.

Update the doc.go and README examples so the call shape matches the
new signature, and add an integration test that registers an echo
handler, drives a real TCP request through the server, and verifies
the payload round-trips. Migrate the rest of server_test.go from
t.Fatalf to the darvaza.org/core assertion helpers while touching it.

BREAKING CHANGE: NewDefaultServer gains a handler parameter between
listener and logger; existing callers must pass nil to retain the
previous behaviour.

Signed-off-by: Alejandro Mery <amery@apptly.co>
x/sync v0.4.1 closes the workgroup.doCancel race where enrolling
the OnCancel handler could Add(1) after the inner WaitGroup had
drained, racing a concurrent Wait() return — an ordering
Server.Shutdown depends on.

Signed-off-by: Alejandro Mery <amery@apptly.co>
Add a `ready chan struct{}` field to [Server] and a public
`Ready() <-chan struct{}` accessor that closes once the accept loop
has started taking connections. The close is performed by an
idempotent `signalReady` helper guarded by `mu`, so repeated Serve
calls do not panic on a double close, and the channel stays closed
across shutdown so late observers never block.

Switch the test harness off the `time.Sleep(50 * time.Millisecond)`
heuristic: a `waitServerReady` helper now blocks on `Ready()` with a
generous 1s ceiling, and a new `TestServer_Ready` confirms the
channel is open before Serve, closes after the loop is reached, and
stays closed across Shutdown.

Signed-off-by: Alejandro Mery <amery@apptly.co>
The server's TYPE_RESPONSE acknowledgement now reaches the typed
SubscribeCallback as either ErrSubscriptionEstablished on STATUS_OK
or a ResponseError on any other status, via a dispatcher split into
isSubscribeACK, subscribeACKErr and decodeSubscribePayload, plus a
callNewOut helper that rejects typed-nil factory results through
core.IsNil. The newOut factory is now required at the public
Subscribe boundary.

GetResponse gains the same boundary protection: typed-nil client
and out arguments are rejected with core.ErrInvalid, and the wait
arm is extracted into waitGetResponse so the function stays under
the cognitive-complexity floor.

Add ErrSubscriptionEstablished and IsSubscriptionEstablished in
the root package so callers can recognise the acknowledgement.

Cover the new boundary guards with tests: the callNewOut typed-nil
and factory-error paths, and the Subscribe/GetResponse nil rejections.

BREAKING CHANGE: Subscribe now rejects a nil newOut factory with
core.ErrInvalid instead of substituting a zero-value factory, and
GetResponse rejects typed-nil client or out arguments. Callers that
relied on the previous nil-tolerant behaviour must supply a factory
and non-nil arguments.

Signed-off-by: Alejandro Mery <amery@apptly.co>
The dispatcher previously keyed callbacks on request_id alone, so
the TYPE_RESPONSE acknowledging an Unsubscribe — which reuses the
subscription's request_id — reached the still-registered
SubscribeCallback. With subscribe-ACK surfacing in place, this
showed up as ErrSubscriptionEstablished firing after the caller
had already unsubscribed.

popRequestCallback now matches on the (request_id, response_type)
pair: unsafeIndexCallbacks indexes the queue by request_id and
splits the matches into the SUBSCRIBE entry and the first
non-SUBSCRIBE entry, then popRequestCallback routes by
response_type. TYPE_UPDATE routes to the SUBSCRIBE entry without
removing it; any other response prefers the non-SUBSCRIBE entry and
drops both entries when a SUBSCRIBE entry shadowed it; a non-update
response that resolves only a SUBSCRIBE entry is steered by the
subscription lifecycle in unsafeResolveSubscribeResponse. A Pending
subscription takes its one acknowledgement — STATUS_OK moves it to
Active (Acknowledged flips, the entry stays queued for updates) and a
non-OK ack moves it to Terminated (the entry is dropped so a rejected
subscription cannot linger and later satisfy the unsubscribe guard).
An Active subscription has no acknowledgement left to take, so a
further TYPE_RESPONSE is anomalous and is ignored rather than tearing
down the live subscription.

Send mirrors the protocol shape by rejecting an unsubscribe-form
TYPE_REQUEST (positive request_id on a non-subscribe) whose
target subscription is missing or still pending its
acknowledgement. The function is factored into validateSendArgs,
isUnsubscribeShape, checkUnsubscribeTarget, normaliseRequestID
and registerCallback so the pipeline reads in order.

Document the new os.ErrInvalid failure modes on Session.Send and
on Client.Unsubscribe, Client.UnsubscribeByHash and
Client.UnsubscribeWithHash, each cross-linked to its matching
Subscribe variant.

Cover popRequestCallback's routing matrix and the Send guard
with data-driven tests under package client.

Signed-off-by: Alejandro Mery <amery@apptly.co>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
pkg/nanorpc/client/subscribe_callback_test.go (1)

52-190: 🏗️ Heavy lift

Consider converting the scenario set to one table-driven test matrix.

The scenario coverage is solid, but consolidating these cases into table rows (name/input/expected/wantErr) will make future lifecycle additions cheaper and keep style consistent across test files.

As per coding guidelines, **/*_test.go: "Use table-driven tests for comprehensive test coverage with test cases defined as structs containing name, input, expected, and wantErr fields."

🤖 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 `@pkg/nanorpc/client/subscribe_callback_test.go` around lines 52 - 190, Convert
the multiple individual test functions
(TestSubscribeCallback_ACKSurfacesEstablished,
TestSubscribeCallback_ACKErrorStatusSurfacesRealError,
TestSubscribeCallback_UpdateWithDataIsDelivered,
TestSubscribeCallback_UpdateWithoutDataIsErrNoResponse,
TestSubscribeCallback_UpdateWithBadDataSurfacesDecodeError,
TestSubscribeCallback_NewOutTypedNilRejected,
TestSubscribeCallback_NewOutErrorSurfaces,
TestSubscribeCallback_UpdateNewOutErrorSurfaces) into a single table-driven
test. Define a test case struct containing name, input (NanoRPCResponse),
expected output, and wantErr fields, then create a loop that iterates through
all test cases and executes the same invokeSubscribeCallback logic with
appropriate assertions for each scenario. This consolidation will follow the
coding guidelines for comprehensive test coverage while maintaining consistency.

Source: Coding guidelines

pkg/nanorpc/client/request_test.go (1)

199-229: ⚡ Quick win

Prefer a single table-driven boundary test for these nil-guard cases.

These three tests exercise the same API contract shape; consolidating into a table with name/input/expected/wantErr will reduce duplication and keep this file aligned with repo test conventions.

As per coding guidelines, **/*_test.go: "Use table-driven tests for comprehensive test coverage with test cases defined as structs containing name, input, expected, and wantErr fields."

🤖 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 `@pkg/nanorpc/client/request_test.go` around lines 199 - 229, Consolidate the
three separate test functions TestGetResponse_NilClientRejected,
TestGetResponse_NilOutRejected, and TestSubscribe_NilNewOutRejected into a
single table-driven test. Replace these three functions with one test function
that defines a slice of test case structs with fields for test name, input
parameters, and expected error, then iterate through the table executing the
appropriate GetResponse or Subscribe call for each case and verifying the error
matches core.ErrInvalid using core.AssertErrorIs. This reduces duplication and
aligns with the repository's test conventions.

Source: Coding guidelines

🤖 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 @.github/workflows/claude-code-review.yml:
- Around line 21-33: Replace mutable version tags with immutable commit SHAs for
supply-chain security. For the "Checkout repository" step using
actions/checkout, replace the `@v6` tag with a specific commit SHA. For the "Run
Claude Code Review" step using anthropics/claude-code-action, replace the `@v1`
tag with a specific commit SHA. Additionally, add persist-credentials: false to
the checkout step's with configuration to prevent repository tokens from
persisting in the runner environment.

In @.github/workflows/claude.yml:
- Around line 31-42: The workflow uses mutable action version tags that could be
retargeted to malicious code, and the checkout step does not disable credential
persistence. Replace the mutable tag `actions/checkout@v6` with a pinned SHA
hash, replace `anthropics/claude-code-action@v1` with a pinned SHA hash, and add
`persist-credentials: false` to the checkout step configuration to prevent the
GITHUB_TOKEN from being exposed to subsequent steps.

In `@pkg/nanorpc/client/session.go`:
- Around line 230-231: The validateSendArgs function dereferences
req.RequestType unconditionally without first checking if req is nil, which
causes a panic when Send is called with a nil request. Add a nil guard at the
beginning of the validateSendArgs function that checks if req is nil and returns
an appropriate error (such as an invalid-argument error) before attempting to
access req.RequestType.

---

Nitpick comments:
In `@pkg/nanorpc/client/request_test.go`:
- Around line 199-229: Consolidate the three separate test functions
TestGetResponse_NilClientRejected, TestGetResponse_NilOutRejected, and
TestSubscribe_NilNewOutRejected into a single table-driven test. Replace these
three functions with one test function that defines a slice of test case structs
with fields for test name, input parameters, and expected error, then iterate
through the table executing the appropriate GetResponse or Subscribe call for
each case and verifying the error matches core.ErrInvalid using
core.AssertErrorIs. This reduces duplication and aligns with the repository's
test conventions.

In `@pkg/nanorpc/client/subscribe_callback_test.go`:
- Around line 52-190: Convert the multiple individual test functions
(TestSubscribeCallback_ACKSurfacesEstablished,
TestSubscribeCallback_ACKErrorStatusSurfacesRealError,
TestSubscribeCallback_UpdateWithDataIsDelivered,
TestSubscribeCallback_UpdateWithoutDataIsErrNoResponse,
TestSubscribeCallback_UpdateWithBadDataSurfacesDecodeError,
TestSubscribeCallback_NewOutTypedNilRejected,
TestSubscribeCallback_NewOutErrorSurfaces,
TestSubscribeCallback_UpdateNewOutErrorSurfaces) into a single table-driven
test. Define a test case struct containing name, input (NanoRPCResponse),
expected output, and wantErr fields, then create a loop that iterates through
all test cases and executes the same invokeSubscribeCallback logic with
appropriate assertions for each scenario. This consolidation will follow the
coding guidelines for comprehensive test coverage while maintaining consistency.
🪄 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: bd66ae5f-eb21-4e5c-a1a1-8897b5073d03

📥 Commits

Reviewing files that changed from the base of the PR and between 427295a and da2f635.

⛔ Files ignored due to path filters (2)
  • pkg/generator/go.sum is excluded by !**/*.sum
  • pkg/nanorpc/go.sum is excluded by !**/*.sum
📒 Files selected for processing (33)
  • .deepsource.toml
  • .github/workflows/claude-code-review.yml
  • .github/workflows/claude.yml
  • .gitignore
  • .golangci.yml
  • AGENTS.md
  • NANORPC_PROTOCOL.md
  • README.md
  • go.mod
  • internal/build/README-coverage.md
  • pkg/generator/go.mod
  • pkg/nanopb/go.mod
  • pkg/nanorpc/client/README.md
  • pkg/nanorpc/client/client.go
  • pkg/nanorpc/client/connected_test.go
  • pkg/nanorpc/client/reconnect.go
  • pkg/nanorpc/client/request.go
  • pkg/nanorpc/client/request_counter.go
  • pkg/nanorpc/client/request_test.go
  • pkg/nanorpc/client/session.go
  • pkg/nanorpc/client/session_test.go
  • pkg/nanorpc/client/subscribe_callback_test.go
  • pkg/nanorpc/errors.go
  • pkg/nanorpc/go.mod
  • pkg/nanorpc/server/README.md
  • pkg/nanorpc/server/doc.go
  • pkg/nanorpc/server/handler_test.go
  • pkg/nanorpc/server/server.go
  • pkg/nanorpc/server/server_test.go
  • pkg/nanorpc/testutils_test.go
  • pkg/nanorpc/utils/slices_test.go
  • pkg/nanorpc/utils/testutils/mockfieldlogger.go
  • pkg/nanorpc/utils/testutils/mockfieldlogger_test.go
💤 Files with no reviewable changes (1)
  • .gitignore
✅ Files skipped from review due to trivial changes (11)
  • go.mod
  • pkg/generator/go.mod
  • pkg/nanopb/go.mod
  • README.md
  • AGENTS.md
  • pkg/nanorpc/utils/testutils/mockfieldlogger_test.go
  • .golangci.yml
  • internal/build/README-coverage.md
  • pkg/nanorpc/server/README.md
  • pkg/nanorpc/server/doc.go
  • pkg/nanorpc/server/handler_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • pkg/nanorpc/client/README.md
  • pkg/nanorpc/client/connected_test.go
  • pkg/nanorpc/client/request.go
  • pkg/nanorpc/client/reconnect.go
  • pkg/nanorpc/client/client.go
  • pkg/nanorpc/errors.go

Comment on lines +21 to +33
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1

- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/claude-code-review.yml

Repository: protomcp/nanorpc

Length of output: 1287


🏁 Script executed:

cd .github/workflows && pwd && ls -la

Repository: protomcp/nanorpc

Length of output: 639


🌐 Web query:

GitHub Actions security best practices pin commit SHA instead of tag versions

💡 Result:

Pinning GitHub Actions to a full-length commit SHA is a primary security best practice because it provides an immutable reference to the action's code [1][2][3]. Unlike tags or branch names (e.g., v4, main), which are mutable and can be force-updated to point to different, potentially malicious commits, a full-length commit SHA is cryptographically tied to a specific version of the code [4][5][6]. Key security and operational considerations include: 1. Mitigation of Supply Chain Attacks: If an action's repository is compromised, an attacker can modify existing tags to include malicious code [2][7][8]. Workflows referencing these tags will automatically pull the updated, compromised code [7]. Pinning to a SHA prevents this, as the workflow will continue to run the specific, verified version it was configured to use, regardless of what the tag points to later [2][7]. 2. Policy Enforcement: Organizations can enforce this practice at the repository or organization level through GitHub's policy settings [1][9][5]. When enabled, workflows attempting to use tags or branch names will fail, mandating the use of full commit SHAs [5][2][10]. 3. Maintenance with Automation: While SHA pinning is more secure, it requires manual maintenance to update to newer versions. This is best managed using automation tools like Dependabot or Renovate [4][5][2][8]. These tools can open pull requests to update the pinned SHA, ensuring that users receive security patches and feature updates while maintaining the security benefits of immutable pinning [5][2]. 4. Implementation Best Practice: To maintain readability and ease of maintenance, it is common to include the version tag as a comment alongside the commit SHA in the workflow file [2]: - uses: actions/checkout@3df4ab1 # v4.0.0 5. Important Warnings: - Always use the full-length commit SHA (40 characters). Shortened SHAs are insecure because they can be subject to collision attacks in repository networks [3]. - SHA pinning protects against tag-retargeting attacks, but it does not protect against malicious code that is already present in a commit [8]. You should still only use actions from trusted sources [1][3].

Citations:


🌐 Web query:

actions/checkout persist-credentials security implications GitHub Actions

💡 Result:

The persist-credentials input in actions/checkout determines whether the authentication token used to clone the repository is stored in the Git configuration for subsequent use within the job [1][2]. When set to its default value of true, the action configures the runner's Git environment with credentials, enabling scripts in later steps to execute authenticated Git commands (such as fetch or push) without additional configuration [1][3][4]. Security Implications The primary security risk arises because this configuration persists secrets within the runner environment, which can lead to accidental exposure: 1. Credential Leakage via Artifacts: A well-documented risk known as "Artipacked" occurs when a workflow uploads the workspace (containing the Git configuration) as an artifact using actions/upload-artifact [5][6]. If the credentials are persisted, they are bundled into the artifact and potentially exposed to unauthorized users who have access to those artifacts [7][6]. 2. Exposure to Malicious Steps: Persisting credentials makes them accessible to every subsequent step in the job [8][4]. If a workflow runs untrusted third-party actions, those actions could potentially access and misuse the persisted credentials to perform unauthorized API calls or modify repository contents [9][4]. 3. Mitigation in Newer Versions: In actions/checkout v6 and later, credential storage was improved by moving the persisted information from the repository's.git/config file to a separate, isolated file under $RUNNER_TEMP [1][10][5]. While this offers better isolation, the credentials remain present in the runner environment during the job execution [5]. Recommendation Security best practices mandate setting persist-credentials: false unless your workflow explicitly requires the ability to perform authenticated Git operations (e.g., pushing commits back to the repository) [9][4]. Example of secure usage: - uses: actions/checkout@v4 with: persist-credentials: false By disabling this option, you ensure that the checkout step does not introduce persistent credentials into the job's environment, thereby minimizing the risk of accidental leakage [7][4].

Citations:


Pin all third-party actions to immutable commit SHAs and disable checkout credential persistence.

Line 22 uses actions/checkout@v6 and line 28 uses anthropics/claude-code-action@v1 with mutable version tags. Mutable tags can be force-updated to point to malicious code, creating a supply-chain risk. Additionally, the checkout step at line 24 does not disable credential persistence (persist-credentials: false), leaving the repository token in the runner environment for the duration of the job. With this workflow's write permissions, both issues warrant hardening.

🔒 Suggested hardening patch
       - name: Checkout repository
-        uses: actions/checkout@v6
+        uses: actions/checkout@<full-commit-sha>
         with:
           fetch-depth: 1
+          persist-credentials: false

       - name: Run Claude Code Review
         id: claude-review
-        uses: anthropics/claude-code-action@v1
+        uses: anthropics/claude-code-action@<full-commit-sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 21-24: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 22-22: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 28-28: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/claude-code-review.yml around lines 21 - 33, Replace
mutable version tags with immutable commit SHAs for supply-chain security. For
the "Checkout repository" step using actions/checkout, replace the `@v6` tag with
a specific commit SHA. For the "Run Claude Code Review" step using
anthropics/claude-code-action, replace the `@v1` tag with a specific commit SHA.
Additionally, add persist-credentials: false to the checkout step's with
configuration to prevent repository tokens from persisting in the runner
environment.

Source: Linters/SAST tools

Comment on lines +31 to +42
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
actions: read

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the Claude workflow file
cat -n .github/workflows/claude.yml | head -50

Repository: protomcp/nanorpc

Length of output: 1776


🌐 Web query:

GitHub Actions security best practices pin action version commit SHA vs tag persist-credentials

💡 Result:

To secure GitHub Actions workflows, follow these established best practices regarding action versioning and credential persistence: Pinning Actions: Commit SHA vs. Tag Pinning actions to a full-length commit SHA is the recommended method to ensure immutability and protect against supply chain attacks [1][2][3]. - Commit SHA (Recommended): Because commit SHAs are immutable, pinning an action to a specific SHA guarantees that the exact code you vetted will execute every time [4][3]. This mitigates the risk of "retargeting" attacks, where a malicious actor updates a tag or branch to point to compromised code [5][2]. GitHub allows administrators to enforce this policy at the organization or repository level to mandate full-length commit SHA usage [1][4]. - Tags (Use with Caution): While tags (e.g., @v4) are convenient, they are mutable references that can be moved or deleted by repository maintainers [1][2]. If you choose to use tags, ensure you only trust the action's creator [1]. - Best Practice for Maintenance: To balance security and ease of maintenance, pin to a full-length commit SHA and use automated dependency update tools like Dependabot or Renovate. These tools can automatically create pull requests to update your SHAs, often including the corresponding human-readable version tag in a comment to make the update process transparent [5][2]. Persist-Credentials The persist-credentials parameter in actions/checkout controls whether the Git credential helper is configured with the repository's credentials (typically the GITHUB_TOKEN) [6][7]. - Default Risk: By default, persist-credentials is set to true, which stores the token in the runner's Git configuration [6][7]. This makes the credentials accessible to all subsequent steps and potentially exposes them if the runner is compromised or if the workspace is inadvertently uploaded as an artifact (a vulnerability known as "Artipacked") [8][6][7]. - Security Recommendation: Unless your workflow explicitly requires Git operations that need these credentials (such as pushing commits back to the repository), you should always explicitly set persist-credentials: false [9][6][7]. Summary of Security Configuration For optimal security, use the following patterns: 1. Pin all actions (including sub-actions, if you are an action maintainer) to full-length commit SHAs [1][4]. 2. Set persist-credentials: false for all actions/checkout steps that do not require repository write access [9][6]. 3. Apply the principle of least privilege by configuring minimal permissions for the GITHUB_TOKEN [10][8][6].

Citations:


Pin action SHAs and set persist-credentials: false in checkout configuration.

Line 32 uses actions/checkout@v6 and line 38 uses anthropics/claude-code-action@v1—both are mutable tags that can be retargeted to malicious code. Additionally, the checkout step does not disable credential persistence, exposing the GITHUB_TOKEN to all subsequent steps by default.

🔒 Suggested hardening patch
       - name: Checkout repository
-        uses: actions/checkout@v6
+        uses: actions/checkout@<full-commit-sha>
         with:
           fetch-depth: 1
+          persist-credentials: false

       - name: Run Claude Code
         id: claude
-        uses: anthropics/claude-code-action@v1
+        uses: anthropics/claude-code-action@<full-commit-sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 31-34: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 32-32: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 38-38: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/claude.yml around lines 31 - 42, The workflow uses mutable
action version tags that could be retargeted to malicious code, and the checkout
step does not disable credential persistence. Replace the mutable tag
`actions/checkout@v6` with a pinned SHA hash, replace
`anthropics/claude-code-action@v1` with a pinned SHA hash, and add
`persist-credentials: false` to the checkout step configuration to prevent the
GITHUB_TOKEN from being exposed to subsequent steps.

Source: Linters/SAST tools

Comment on lines +230 to 231
func validateSendArgs(req *nanorpc.NanoRPCRequest, cb RequestCallback) error {
switch req.RequestType {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard Send against a nil request before reading RequestType.

validateSendArgs dereferences req.RequestType unconditionally, so Send(nil, ...) panics instead of returning a typed invalid-argument error.

Suggested fix
 func validateSendArgs(req *nanorpc.NanoRPCRequest, cb RequestCallback) error {
+	if req == nil {
+		return core.QuietWrap(os.ErrInvalid, "missing request")
+	}
 	switch req.RequestType {
 	case nanorpc.NanoRPCRequest_TYPE_PING:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func validateSendArgs(req *nanorpc.NanoRPCRequest, cb RequestCallback) error {
switch req.RequestType {
func validateSendArgs(req *nanorpc.NanoRPCRequest, cb RequestCallback) error {
if req == nil {
return core.QuietWrap(os.ErrInvalid, "missing request")
}
switch req.RequestType {
🤖 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 `@pkg/nanorpc/client/session.go` around lines 230 - 231, The validateSendArgs
function dereferences req.RequestType unconditionally without first checking if
req is nil, which causes a panic when Send is called with a nil request. Add a
nil guard at the beginning of the validateSendArgs function that checks if req
is nil and returns an appropriate error (such as an invalid-argument error)
before attempting to access req.RequestType.

@amery amery closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Review effort 4/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant