Skip to content

SDKs: node provider clients (TS, Python, Swift)#243

Merged
willwashburn merged 10 commits into
mainfrom
node-provider-sdks
Jul 8, 2026
Merged

SDKs: node provider clients (TS, Python, Swift)#243
willwashburn merged 10 commits into
mainfrom
node-provider-sdks

Conversation

@willwashburn

@willwashburn willwashburn commented Jul 8, 2026

Copy link
Copy Markdown
Member

Stacked on node-providers-engine (#242) — step 2 of the node-providers spec. Its diff is only the SDK client layer plus the two node-WS frames those clients need.

What this adds

A node provider client in each relaycast SDK — TypeScript (@relaycast/sdk), Python (relaycast-sdk), and Swift (Relaycast). Given an explicit { baseUrl, nodeToken, node identity, provider, capabilities, handlers }, it:

  • Connects to /v1/node/ws, sends node.register with the provider identity and capability set, and surfaces per-capability acceptance — hard-failing on a rejected capability or an engine error frame (provider_instance_conflict, action_name_conflict) with a clear error.
  • Runs a provider-scoped heartbeat loop (load, active_agents, handlers_live).
  • Receives action.invoke, runs the registered handler, and replies action.result — a handler throw becomes an error result and an unknown action name becomes an error result; an invocation is never dropped.
  • Reconnects with exponential backoff and full re-registration, minting a new instance_id per connection (the engine's replace semantic).
  • Sends a provider-scoped node.deregister on graceful shutdown.
  • Exposes a handler context with sendMessage and spawnAgent (spec §4).

The constructor takes an explicit token and base URL. Enrollment-file logic (from_enrollment, fleet-enrollments.json) is deliberately out of scope — it layers on top of this client in the relay repo (step 3).

Handler-context surface (ctx.spawnAgent / ctx.sendMessage)

  • ctx.spawnAgent is a node-WS node.spawn request frame answered by the engine, routing the already-implemented dispatchCapacitySpawn — capacity-direct, bypassShadow, always targeting the connection's own node (the frame carries no node target), so a spawn:<harness> shadow handler that delegates cannot re-enter itself and a node credential cannot direct a spawn at another node. This is the only engine frame added here (dispatchCapacitySpawn duplicates no existing surface, so it has no parity problem), with a conformance test.

  • ctx.sendMessage posts through the canonical message route — POST /v1/channels/:name/messages — which now accepts a node token (a new sender auth requirement alongside agent/workspace) with a required from: an agent name resolved strictly within the node token's workspace, so a node credential can never attribute a message to another workspace. Delivery routing, observer events, webhooks, triggers, and idempotency come free because it is the one posting path (spec §5.3: registration, invocation, and events are shared; only the hosting connection differs). There is no parallel node-message posting path. In all three SDKs ctx.sendMessage is an HTTP call to that route with the node token.

Engine changes on this branch (for the engine-branch owner): the node.spawn frame, and the message route's sender auth + from attribution. Conformance covers node-token posting end to end: delivery to node-hosted recipients, required from, unknown-agent and cross-workspace rejection, and from rejected on an agent token.

Parity

The three clients expose the same semantics, named per language (TS camelCase, Python snake_case, Swift Swift-style). Divergences, all intentional:

  • Python: send_message(from_=...) and capability(global_=...) avoid the from/global keyword clash; the wire fields stay from/global.
  • Swift: NodeProvider is an actor (the rest of the SDK uses @unchecked Sendable class + NSLock) — chosen for correctness across the many await points in register/heartbeat/reconnect/invoke bookkeeping. Constructor capabilities is handler-only; per-capability kind/global/queue/metadata go through the capability(...) builder. Adds session/transport params for test injection.
  • All three connect with ?token= (server-side clients could use Authorization: Bearer, but query-param matches the existing SDK WS clients and the engine accepts both).

Tests

  • Engine (nodeProviders.test.ts): node.spawn routes to the connection's own node capacity (never crossing to another node); node-token posting via the canonical route — delivery to a node-hosted recipient, required from, unknown-agent and cross-workspace rejection, and from rejected on an agent token. Full engine suite green (426).
  • TS (11), Python (12), Swift (11) unit tests each against an in-process fake transport — no real sockets or runtimes. Coverage: register + acceptance rejection, invoke → result (success and handler-error), unknown action, reconnect re-register with a new instance_id, heartbeat shape, deregister, spawnAgent, sendMessage.
  • End-to-end interop verified separately: the built TS client registering against the in-process engine, a REST invoke completing through the handler, and ctx.sendMessage posting to a channel.

Follow-ups

  • WS auth header. The three clients authenticate the socket with ?token= (matching the existing SDK WS clients and keeping the TypeScript client isomorphic with the browser WebSocket, which cannot set headers). Server-side transports (Python/Swift, and Node via a header-capable WS) could prefer an Authorization: Bearer header to avoid token-in-URL leakage into intermediary logs; the engine already accepts both. Deferred to keep the clients consistent and the TS build isomorphic.

🤖 Generated with Claude Code

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c2014b83-8fe5-493b-97ae-50be0506cdb4

📥 Commits

Reviewing files that changed from the base of the PR and between a4b483f and 245afcc.

📒 Files selected for processing (5)
  • packages/sdk-python/src/relay_sdk/node.py
  • packages/sdk-python/tests/test_node.py
  • packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
  • packages/sdk-typescript/src/__tests__/node-provider.test.ts
  • packages/sdk-typescript/src/node-provider.ts
📝 Walkthrough

Walkthrough

Adds sender-scoped channel posting, node.spawn routing, and Node Provider client SDKs in Python, Swift, and TypeScript with websocket registration, invoke dispatch, reconnect, heartbeat, and message/spawn helpers.

Changes

Engine Auth and Node Routing

Layer / File(s) Summary
Sender auth contract
packages/engine/src/ports/auth.ts, packages/engine/src/auth/tokenKind.ts, packages/engine/src/middleware/auth.ts
Adds 'sender' to AuthRequire, updates token validation to accept sender-scoped principals, and exports requireSender.
Channel sender attribution
packages/engine/src/routes/message.ts
Switches the channel message route to sender auth, accepts from, resolves sender identity by token type, and threads the resolved sender through message event, idempotency, delivery, and trigger paths.
node.spawn dispatch and conformance
packages/types/src/fleet-wire.ts, packages/engine/src/engine/node.ts, packages/engine/src/__tests__/conformance/nodeProviders.test.ts
Adds the node.spawn wire type, dispatches node.spawn control messages to capacity spawn handling, and covers spawn routing plus node-token posting rules in conformance tests.

Node Provider client SDKs

Layer / File(s) Summary
Python NodeProvider implementation
packages/sdk-python/src/relay_sdk/node.py, packages/sdk-python/src/relay_sdk/__init__.py
Implements the Python node provider client, transport, lifecycle, registration, dispatch, heartbeat, reconnect, channel posting, and spawn helpers; exports the new symbols.
Python NodeProvider tests
packages/sdk-python/tests/test_node.py
Adds fake transport infrastructure and coverage for registration, invoke dispatch, heartbeat, reconnect, deregistration, spawn, messaging, and context helpers.
Swift NodeProvider implementation
packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
Implements the Swift actor-based node provider, transport abstraction, registration, reconnect, heartbeat, dispatch, and handler context APIs.
Swift NodeProvider tests
packages/sdk-swift/Tests/RelaycastTests/NodeProviderTests.swift
Adds in-memory transport and URL protocol test scaffolding plus coverage for the Swift provider lifecycle and context helpers.
TypeScript NodeProvider implementation
packages/sdk-typescript/src/node-provider.ts, packages/sdk-typescript/src/index.ts
Implements the TypeScript node provider client and re-exports its public types and classes from the package barrel.
TypeScript NodeProvider tests
packages/sdk-typescript/src/__tests__/node-provider.test.ts
Adds MockWebSocket-based coverage for registration, invoke dispatch, heartbeat, reconnect, deregistration, spawn, and messaging.

Estimated code review effort: 5 (Critical) | ~150 minutes

Possibly related PRs

Suggested labels: codex

Poem

I hop through webs and tokens bright,
With sender paws and spawn in sight.
A node says “here,” a channel sings,
Three SDKs flap their steady wings.
🐰 Whiskers up, the tests run true,
New routes, new frames, and messages too.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding node provider clients across TS, Python, and Swift SDKs.
Description check ✅ Passed The description is detailed and directly matches the SDK node provider client, node.spawn, and message-posting changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch node-provider-sdks

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0a8f86f2a

ℹ️ 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 thread packages/engine/src/engine/node.ts Outdated
Comment thread packages/engine/src/engine/node.ts Outdated
Comment thread packages/sdk-typescript/src/node-provider.ts Outdated
Comment thread packages/sdk-python/src/relay_sdk/node.py

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread packages/engine/src/engine/node.ts Outdated
Comment thread packages/engine/src/engine/node.ts Outdated

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces language-neutral NodeProvider clients across the Python, Swift, and TypeScript SDKs, allowing processes to attach to a node's capabilities, register them, handle heartbeats, and execute invokes directly via /v1/node/ws without a local broker dependency. It also updates the engine to handle node.spawn and node.message control frames, routing them directly to bypass action dispatch shadows and posting channel messages respectively. Feedback on the TypeScript client points out that onRegisterFailed incorrectly treats temporary transport/connection failures during registration as fatal instead of retrying with backoff like the Python and Swift implementations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/sdk-typescript/src/node-provider.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

Review completed against the latest diff

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread packages/engine/src/engine/node.ts Outdated
Comment thread packages/sdk-typescript/src/node-provider.ts
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift Outdated
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift Outdated
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
Comment thread packages/sdk-python/src/relay_sdk/node.py Outdated
Comment thread packages/sdk-typescript/src/__tests__/node-provider.test.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/sdk-typescript/src/node-provider.ts
Comment thread packages/sdk-typescript/src/node-provider.ts Outdated
Comment thread packages/sdk-python/tests/test_node.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

All reported issues were addressed across 13 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/engine/src/__tests__/conformance/nodeProviders.test.ts Outdated
Comment thread packages/sdk-python/src/relay_sdk/node.py
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift Outdated
Comment thread packages/sdk-typescript/src/node-provider.ts Outdated
Base automatically changed from node-providers-engine to main July 8, 2026 15:39
willwashburn and others added 7 commits July 8, 2026 11:42
…sage frames

Add a node provider client to each relaycast SDK: connect to /v1/node/ws with a
node token, declare provider identity, register capabilities with per-capability
acceptance, run action.invoke handlers, heartbeat, reconnect with a fresh
instance id, and deregister. Handler context exposes sendMessage and spawnAgent.

Wire the ctx helpers to two node-WS request frames: node.spawn (capacity-direct,
routing dispatchCapacitySpawn) and node.message (post + observer fanout).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assert a node token cannot post as an agent from another workspace: the
`from` lookup is workspace-scoped, so a cross-workspace name is rejected
with agent_not_found and no message leaks into the other workspace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop target_node_id from the node.spawn frame: ctx.spawnAgent addresses the
provider's own node capacity executor, and the engine always dispatches to the
connection's node. A node credential cannot direct a spawn at another node —
cross-node placement is workspace-level with agent/workspace credentials.

Conformance asserts the spawn lands on the connection's own node and never
reaches a second node's capacity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TS and Python clients now treat a transport drop during the register
  handshake as a reconnect (only a protocol rejection hard-fails), matching
  Swift. Fixes a Python hang and a TS non-reconnect when the socket closes
  before the node.register reply.
- Remove thread_id from node.message: the field was accepted by the wire
  schema and all three SDKs but silently dropped by the engine (postMessage
  posts top-level only). node.message is a top-level-post surface; thread
  replies are not part of it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- All three clients hard-fail registration when the reply omits a well-formed
  accepted_capabilities list (a missing/corrupt list means acceptance can't be
  confirmed); a malformed acceptance entry counts as rejected (fail-safe). Swift
  previously dropped malformed entries and could register on a corrupt reply.
- Python handlers may be sync or async (awaited only when awaitable), matching
  the TypeScript client.
- Swift spawnAgent takes [String: JSONValue] so callers can't build a node.spawn
  frame the engine's object-typed input schema rejects.
- Strengthen the reconnect test to complete re-registration after the drop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llel frame

Remove the node.message WS frame and its engine handler. Node-attributed posts
now go through the canonical POST /v1/channels/:name/messages route, which
accepts a node token (new `sender` auth requirement) with a required `from` —
an agent resolved strictly within the node token's workspace. Delivery routing,
observer events, webhooks, triggers, and idempotency come free because it is the
one posting path (spec §5.3: only the hosting connection differs).

ctx.sendMessage in all three SDKs becomes an HTTP call to that route with the
node token. node.spawn stays a WS frame (dispatchCapacitySpawn duplicates no
existing surface).

Conformance covers node-token posting: delivery to node-hosted recipients,
required `from`, unknown-agent and cross-workspace rejection, and `from`
rejected on an agent token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e reply

- All three clients reject the registration waiter when reconnects are exhausted
  before registering, so serve()/whenRegistered() no longer hang forever.
- TS onRegistered tolerates a null/undefined reply data (treats it as an invalid
  reply hard-fail) instead of throwing a TypeError that misroutes to reconnect.
- Guard the Python reconnect tests against a race reading the re-register frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Python: reconnect exhaustion no longer overwrites a completed registration, so
  wait_registered() keeps returning success after a later drop.
- ctx.sendMessage normalizes a ws(s):// base to http(s):// (all three SDKs) and
  encodes the channel as a single path segment (Swift no longer leaves `/`
  unescaped). Swift treats a non-HTTP or non-envelope 2xx response as a failure
  instead of returning null.
- Poll for the node-token delivery fanout in conformance instead of a fixed
  sleep, matching the file's other delivery assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 7

🧹 Nitpick comments (1)
packages/sdk-python/tests/test_node.py (1)

259-271: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for partial acceptance replies.

This covers a missing accepted_capabilities field, but not a present list that omits one of the registered capabilities. Add that case to lock in fail-closed registration validation.

🤖 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 `@packages/sdk-python/tests/test_node.py` around lines 259 - 271, Add a test in
test_registration_missing_accepted_capabilities_raises to also cover a reply
where accepted_capabilities is present but only includes a subset of the
capabilities sent in the NodeProvider registration. Use the existing
NodeProvider, FakeNodeServer, conn.push, and node.wait_registered/serve flow to
simulate the partial-acceptance reply, and assert that registration still fails
by raising NodeRegistrationError for both the wait_registered and serve task
paths.
🤖 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 `@packages/sdk-python/src/relay_sdk/node.py`:
- Around line 340-353: Quiesce in-flight invoke tasks during shutdown by
handling outstanding work in NodeProvider.stop(). Before closing the
websocket/HTTP client, drain or cancel any active _invoke_tasks so
_dispatch_invoke does not keep running after shutdown. Update the stop() flow in
NodeProvider to wait for or cancel those tasks, then proceed with
deregistration, _safe_close(conn), and _http.close() only after invoke
processing is complete.
- Around line 488-504: The registration handling in Node._handle_register_reply
only checks the returned accepted_capabilities entries, so an empty list can
incorrectly pass. Update the logic in the Node registration path to compare the
reply against the requested capabilities list, verify every requested capability
name is present in accepted_capabilities before setting self._registered and
calling self._resolve_registered, and raise a NodeRegistrationError if any
requested capability is missing. Use the existing accepted/rejected handling in
Node._handle_register_reply as the place to add this validation.

In `@packages/sdk-swift/Sources/Relaycast/NodeProvider.swift`:
- Around line 324-326: The reconnect delay sanitization in NodeProvider’s
initializer is using the raw reconnectBaseDelayMilliseconds when setting
reconnectMaxDelayMilliseconds, so a negative max can still slip through even
after the base delay is clamped. Update the clamp logic to compute the max delay
from the already-sanitized base value, and apply the same fix anywhere the
reconnect settings are initialized or updated in NodeProvider so
computeBackoffDelayMilliseconds always receives non-negative bounds.
- Around line 628-665: The action result is currently sent through the actor’s
mutable transport, so a delayed handler can reply on a later reconnect instead
of the socket that received the invoke. Update the invoke flow in NodeProvider
by capturing the current transport from the read loop and passing it into
dispatchInvoke, then have sendActionResult/sendFrame use that specific transport
instance for both success and error replies. Keep the behavior localized around
dispatchInvoke, sendActionResult, and sendFrame so replies stay bound to the
original connection epoch.
- Around line 725-728: The channel URL builder is encoding the channel with a
character set that still allows slashes, so
`channelMessageURL(baseURL:channel:)` can treat one channel value as multiple
path segments. Update the `encoded` handling in `channelMessageURL` to
percent-encode `channel` as a single path component so values like `ops/team`
become one segment in the final URL, preserving the existing base URL trimming
and URL construction.

In `@packages/sdk-swift/Tests/RelaycastTests/NodeProviderTests.swift`:
- Around line 530-540: The NodeMockURLProtocol handler in
testSendMessagePostsToTheCanonicalChannelRoute can leak into later tests if the
test exits early, so make sure it is always cleared with defer immediately after
setting it. Apply the same cleanup pattern in the related NodeProviderTests case
referenced by the comment so the static handler does not persist across tests
and create order-dependent failures.

In `@packages/sdk-typescript/src/node-provider.ts`:
- Around line 251-262: The stop flow in NodeProvider.stop only rejects pending
request/reply promises and leaves the registration promise unresolved, so
whenRegistered() can hang forever if stop() happens before registration
completes. Update stop() to also reject the register promise via the same
register-promise handling used by scheduleReconnect/exhaustion (for example,
through rejectRegister or the equivalent helper), and ensure the NodeProvider
state is cleaned up after that path. Add or adjust a test around stop() before
registration completes to verify whenRegistered() is rejected instead of waiting
indefinitely.

---

Nitpick comments:
In `@packages/sdk-python/tests/test_node.py`:
- Around line 259-271: Add a test in
test_registration_missing_accepted_capabilities_raises to also cover a reply
where accepted_capabilities is present but only includes a subset of the
capabilities sent in the NodeProvider registration. Use the existing
NodeProvider, FakeNodeServer, conn.push, and node.wait_registered/serve flow to
simulate the partial-acceptance reply, and assert that registration still fails
by raising NodeRegistrationError for both the wait_registered and serve task
paths.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20cd8fac-361b-4cb0-9afb-93a4aafb6a9a

📥 Commits

Reviewing files that changed from the base of the PR and between 4f8d2eb and df0d7cd.

📒 Files selected for processing (15)
  • packages/engine/src/__tests__/conformance/nodeProviders.test.ts
  • packages/engine/src/auth/tokenKind.ts
  • packages/engine/src/engine/node.ts
  • packages/engine/src/middleware/auth.ts
  • packages/engine/src/ports/auth.ts
  • packages/engine/src/routes/message.ts
  • packages/sdk-python/src/relay_sdk/__init__.py
  • packages/sdk-python/src/relay_sdk/node.py
  • packages/sdk-python/tests/test_node.py
  • packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
  • packages/sdk-swift/Tests/RelaycastTests/NodeProviderTests.swift
  • packages/sdk-typescript/src/__tests__/node-provider.test.ts
  • packages/sdk-typescript/src/index.ts
  • packages/sdk-typescript/src/node-provider.ts
  • packages/types/src/fleet-wire.ts

Comment thread packages/sdk-python/src/relay_sdk/node.py
Comment thread packages/sdk-python/src/relay_sdk/node.py
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift Outdated
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
Comment thread packages/sdk-swift/Tests/RelaycastTests/NodeProviderTests.swift
Comment thread packages/sdk-typescript/src/node-provider.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/sdk-swift/Tests/RelaycastTests/NodeProviderTests.swift Outdated
…egister

- Registration hard-fails unless every requested capability is acknowledged in
  the reply (an empty/partial accepted_capabilities can no longer pass) — all
  three SDKs.
- stop() drains in-flight invoke handlers (bounded) before closing the socket so
  their action.result frames aren't dropped, and rejects a still-pending
  registration waiter so whenRegistered() can't hang when stopped pre-register.
- Swift clamps the reconnect max delay against the sanitized base delay (no
  negative backoff) and its HTTP tests clear the mock URL handler via defer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Caution

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

⚠️ Outside diff range comments (2)
packages/sdk-python/src/relay_sdk/node.py (2)

448-453: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Propagate external cancellation from backoff sleep
stop() intentionally cancels _reconnect_sleep_task, but this except asyncio.CancelledError: pass also swallows a direct cancellation of serve() while it is backing off. Re-raise when _stopped is still false.

🤖 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 `@packages/sdk-python/src/relay_sdk/node.py` around lines 448 - 453, The
backoff wait in `serve()` is swallowing all `asyncio.CancelledError` from
`self._reconnect_sleep_task`, which also hides external cancellation. Update the
`try/except/finally` around `await self._reconnect_sleep_task` in `Node` so that
`stop()`-initiated cancellation is still ignored, but if `_stopped` is still
false the `CancelledError` is re-raised to let `serve()` terminate correctly.

620-627: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Redact the token from connect errors. Some websockets exceptions include the full URI in their message, which can expose node_token from the query string when a connect attempt fails.

🤖 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 `@packages/sdk-python/src/relay_sdk/node.py` around lines 620 - 627, The
websocket URL built by _build_url() includes node_token in the query string,
which can leak into connect-time exception messages from websockets. Update the
connection/error handling around the Node websocket client in relay_sdk.node to
redact or sanitize the URI before logging or rethrowing connect errors, ensuring
any message that references the URL does not include the token. Use the
_build_url() output as the source to locate the connection path and apply
redaction centrally so all websocket connect failures are covered.
🤖 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.

Outside diff comments:
In `@packages/sdk-python/src/relay_sdk/node.py`:
- Around line 448-453: The backoff wait in `serve()` is swallowing all
`asyncio.CancelledError` from `self._reconnect_sleep_task`, which also hides
external cancellation. Update the `try/except/finally` around `await
self._reconnect_sleep_task` in `Node` so that `stop()`-initiated cancellation is
still ignored, but if `_stopped` is still false the `CancelledError` is
re-raised to let `serve()` terminate correctly.
- Around line 620-627: The websocket URL built by _build_url() includes
node_token in the query string, which can leak into connect-time exception
messages from websockets. Update the connection/error handling around the Node
websocket client in relay_sdk.node to redact or sanitize the URI before logging
or rethrowing connect errors, ensuring any message that references the URL does
not include the token. Use the _build_url() output as the source to locate the
connection path and apply redaction centrally so all websocket connect failures
are covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c7d1774-f1e6-4724-b2b0-af84f84b5821

📥 Commits

Reviewing files that changed from the base of the PR and between df0d7cd and a4b483f.

📒 Files selected for processing (7)
  • packages/engine/src/__tests__/conformance/nodeProviders.test.ts
  • packages/sdk-python/src/relay_sdk/node.py
  • packages/sdk-python/tests/test_node.py
  • packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
  • packages/sdk-swift/Tests/RelaycastTests/NodeProviderTests.swift
  • packages/sdk-typescript/src/__tests__/node-provider.test.ts
  • packages/sdk-typescript/src/node-provider.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/sdk-python/tests/test_node.py
  • packages/engine/src/tests/conformance/nodeProviders.test.ts
  • packages/sdk-swift/Tests/RelaycastTests/NodeProviderTests.swift
  • packages/sdk-typescript/src/tests/node-provider.test.ts
  • packages/sdk-typescript/src/node-provider.ts
  • packages/sdk-swift/Sources/Relaycast/NodeProvider.swift

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

6 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/sdk-python/src/relay_sdk/node.py">

<violation number="1" location="packages/sdk-python/src/relay_sdk/node.py:350">
P1: In `NodeProvider.stop()`, the shutdown drain awaits all tasks in `self._invoke_tasks` via `asyncio.gather`. If a capability handler calls `await provider.stop()`, its own task is still in that set, so `gather` deadlocks waiting for itself until the 5-second timeout expires. The timeout can then cancel the current task and prevent `node.deregister` from being sent. Exclude `asyncio.current_task()` from the gathered tasks so a handler-initiated shutdown can complete gracefully.</violation>
</file>

<file name="packages/sdk-swift/Sources/Relaycast/NodeProvider.swift">

<violation number="1" location="packages/sdk-swift/Sources/Relaycast/NodeProvider.swift:413">
P2: Shutdown drains only a snapshot of current invoke tasks while the socket and heartbeat remain active, so new `action.invoke` frames arriving during the drain are dispatched into `invokeTasks` but are not waited on before `node.deregister` closes the transport. This can cause handler results for late-arriving invocations to fail to send.

Additionally, `sendHeartbeat` unconditionally advertises `"handlers_live": true`, and `stopHeartbeat()` is only called after the drain completes, so the engine may route more work to a provider that is already shutting down.

**Recommendation**: Close the transport (or at least stop the heartbeat) immediately after setting `stopped = true`, so no new frames or heartbeats occur during the drain window. Alternatively, guard `handleFrame` so it drops or rejects new `action.invoke` frames once `stopped` is true.</violation>

<violation number="2" location="packages/sdk-swift/Sources/Relaycast/NodeProvider.swift:557">
P2: `sendRegister` validates the registration reply against the live `capabilityOrder` property after an `await`, but Swift actor reentrancy allows concurrent `capability(...)` calls to mutate that array while the register request is in flight. A capability added during that window won't appear in the engine's reply, so the missing-acknowledgement check throws a false `invalid_register_reply` and hard-fails registration. Capture the snapshot of capabilities that were actually sent (e.g., `let sentCapabilities = capabilityOrder` before the `await request(...)`) and validate the reply against `sentCapabilities` instead.</violation>
</file>

<file name="packages/sdk-typescript/src/node-provider.ts">

<violation number="1" location="packages/sdk-typescript/src/node-provider.ts:258">
P1: `stop()` drains only a one-time snapshot of `invokeTasks`, but `handleFrame` continues to accept new `action.invoke` frames while the WebSocket is still open during the drain window. Any handler started during that window but completing after `closeSocket()` gets its `action.result` silently dropped by `sendFrame`, violating the never-drop guarantee. A looping drain that re-snapshots until the set stays empty (or a timeout expires) would close the race.</violation>

<violation number="2" location="packages/sdk-typescript/src/node-provider.ts:261">
P2: The 5-second drain timeout inside this `Promise.race` is never cancelled when invoke tasks finish early, so the timer keeps the Node.js event loop alive unnecessarily for up to 5 seconds after `stop()` resolves. This can delay test process exit and background worker shutdown. Store the timer handle and `clearTimeout` it after the race settles, matching the pattern already used for `reconnectTimer` elsewhere in the class.</violation>

<violation number="3" location="packages/sdk-typescript/src/node-provider.ts:384">
P1: The `onRegistered()` check for missing capabilities reads `this.capabilities.keys()` at reply time, but `sendRegister()` sent a snapshot of the map earlier. Because `capability()` is public and unguarded after `serve()` starts, a caller can add a capability between sending the register frame and receiving its reply. The engine then omits that late capability from the acceptance list, and the reply-time validation treats it as `invalid_register_reply`, permanently stopping the provider. Consider capturing the list of capability names that were actually sent (e.g., into a field at `sendRegister()` time) and validating the reply against that snapshot instead of the live Map.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/sdk-python/src/relay_sdk/node.py Outdated
Comment thread packages/sdk-typescript/src/node-provider.ts Outdated
Comment thread packages/sdk-typescript/src/node-provider.ts
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift Outdated
Comment thread packages/sdk-swift/Sources/Relaycast/NodeProvider.swift
Comment thread packages/sdk-typescript/src/node-provider.ts Outdated
- stop() stops accepting new action.invoke frames once draining, so a late
  invoke can't start a handler whose result would be dropped after close (all
  three SDKs); the engine reschedules the undispatched invocation.
- Registration acceptance is validated against the capability snapshot sent in
  the register frame, not the live map, so a capability added mid-handshake
  can't trigger a false missing-acknowledgement failure (all three SDKs).
- Python stop() excludes the caller's own task from the drain, so a handler that
  calls stop() no longer deadlocks (and gets cancelled, skipping deregister).
- TS clears the drain timeout when tasks finish early; Swift stops heartbeats
  before the drain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@willwashburn willwashburn merged commit b36831e into main Jul 8, 2026
5 checks passed
@willwashburn willwashburn deleted the node-provider-sdks branch July 8, 2026 21:11
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