Skip to content

feat(fleetnode): pairing foundation (proto, node pairer, SQL guards)#388

Merged
ankitgoswami merged 11 commits into
mainfrom
fleetnode-pairing
Jun 5, 2026
Merged

feat(fleetnode): pairing foundation (proto, node pairer, SQL guards)#388
ankitgoswami merged 11 commits into
mainfrom
fleetnode-pairing

Conversation

@ankitgoswami

@ankitgoswami ankitgoswami commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Foundation for pairing miners discovered via fleet nodes. A fleet node can already discover miners (#235), but those discovered_device rows are a dead end: the cloud Pair path refuses fleet-node-reported devices (it has no route to a LAN behind a node), and PairDeviceToFleetNode needs an already-existing device row that these never get. Per RFC 0001 the node owns miner I/O and holds the per-node miner-signing key, so pairing must happen on the node, orchestrated over the existing ControlStream.

This PR lands the safe, tested scaffolding. It is behavior-preserving and inert: discovery keeps working, and nothing new is reachable until the operator RPC lands.

What is in here

  • Proto + codegen. An AgentCommand oneof carried in ControlCommand.payload (discover today, pair next), FleetNodePairRequest/FleetNodePairTarget, the ReportPairedDevices gateway RPC with per-device FleetNodePairResult/PairOutcome, and the operator-facing ListFleetNodeDiscoveredDevices and PairDiscoveredDevicesOnFleetNode admin RPCs. Existing handlers return Unimplemented for the new RPCs.
  • Node batch pairer. handleCommand decodes AgentCommand and dispatches discover or pair. A new pluginPairer authenticates a batch of targets on the node's local plugins: asymmetric drivers (Proto) use the node's own miner-signing key, basic-auth drivers (Antminer) use operator-supplied or plugin-default credentials, and per-device outcomes stream back via ReportPairedDevices with bounded fan-out and PARTIAL semantics, mirroring discovery.
  • Server SQL. ListFleetNodeDiscoveredDevices (inverse of the discovery listing, which excludes fleet-node rows; surfaces AUTHENTICATION_NEEDED for retry) and the cloud-dial guard refinements (see below).

Two things reviewers should know

  1. Discovery wire change (contained). Discovery moves from a bare DiscoverRequest to an AgentCommand envelope inside ControlCommand.payload. The server-wrap (DiscoverOnFleetNode) and node-unwrap ship together here, so there is no skew within a deploy; node and server are the same binary and this is pre-release.
  2. device_pairing model. A fleet-node-paired device will be device_pairing.pairing_status = PAIRED (so it reads as paired in the UI and is command-targetable), with the transport dimension expressed by fleet_node_device membership. "Cloud-dialed" is therefore refined to PAIRED AND NOT EXISTS(fleet_node_device): DeviceHasActiveCloudPairing, the discovery promotion guard (same-node carve-out so a node can still refresh its own devices), and the miner_service dial/credential fetch all exclude node-owned devices. Display and command-selection queries are unchanged. These refinements are dormant until PR4 creates node-paired data.

Coordination

Aligned with the sibling effort "send commands to miners paired via fleet nodes": the AgentCommand envelope is shared (it adds a miner_command arm), and the device_pairing = PAIRED decision resolves that effort's open question about whether AllDevices includes node devices (it does; transport routes via fleet_node_device). The miner_service dial exclusion is the seam that effort fills with node routing.

Testing

Node tests pass with -race; control-registry, admin-handler (including discovery dispatch through the new envelope), and fleetnode domain tests pass against the live test DB. New tests cover the node pair dispatch and outcomes, the miner-signing key cross-check, the listing query, and the refined guards under the new PAIRED-and-node-bound model.

Still to come

Per-device persistence + registry pair-result routing, the streaming operator RPC that makes pairing reachable, and an end-to-end CLI demo plus fake-rig fixtures.

🤖 Generated with Claude Code

@github-actions github-actions Bot added javascript Pull requests that update javascript code client server shared labels Jun 4, 2026
Comment thread server/cmd/fleetnode/pair.go Dismissed
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (cee1acbbb7770364c63fb469cf2876b8de3e85e7...6b310c5c7bc78e245d967ad758b73a2f0be6c86e, exact PR three-dot diff)
  • Model: gpt-5.5

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: HIGH

Findings

[HIGH] New admin RPCs are exposed but not implemented

  • Category: gRPC | Reliability
  • Location: proto/fleetnodeadmin/v1/fleetnodeadmin.proto:22
  • Description: The PR adds ListFleetNodeDiscoveredDevices and PairDiscoveredDevicesOnFleetNode to FleetNodeAdminService, and the generated Connect router exposes them, but server/internal/handlers/fleetnode/admin/handler.go does not add concrete methods. Because the handler embeds UnimplementedFleetNodeAdminServiceHandler, these new endpoints compile but return CodeUnimplemented.
  • Impact: Authenticated operators cannot list discovered fleet-node devices or start the new fleet-node pairing flow. The new SQL/domain listing code and fleetnode pairing command path are effectively unreachable.
  • Recommendation: Implement both admin handler methods. They should enforce the intended permissions, validate confirmed fleet node ownership, map paging correctly, dispatch pair commands through the control registry, stream node results back to the caller, and include mounted-handler integration tests that fail if the embedded unimplemented method is used.

[HIGH] Pair result upload RPC is exposed but handled by the unimplemented fallback

  • Category: gRPC | Reliability
  • Location: proto/fleetnodegateway/v1/fleetnodegateway.proto:21
  • Description: The PR adds ReportPairedDevices, and the fleet node now calls it after running a pair command, but server/internal/handlers/fleetnode/gateway/handler.go does not override the generated UnimplementedFleetNodeGatewayServiceHandler.ReportPairedDevices.
  • Impact: Every pair result upload returns CodeUnimplemented; the fleet node then reports ACK_CODE_REPORT_FAILED, so operators never receive pairing outcomes and successful node-side pair attempts are not persisted or published server-side.
  • Recommendation: Add a gateway handler analogous to ReportDiscoveredDevices: authenticate the fleet-node subject, require/admit the server-issued command_id, enforce report quota/scope, translate outcomes safely, persist accepted pairings as needed, and publish only accepted results to the waiting admin stream.

Notes

Review was limited to .git/codex-review.diff. I did not find SQL injection, command injection, cryptostealing/pool hijack, or protobuf wire-format issues in the changed hunks beyond the functional gaps above.


Generated by Codex Security Review |
Triggered by: @ankitgoswami |
Review workflow run

@ankitgoswami ankitgoswami marked this pull request as ready for review June 4, 2026 20:10
@ankitgoswami ankitgoswami requested a review from a team as a code owner June 4, 2026 20:10
Copilot AI review requested due to automatic review settings June 4, 2026 20:10

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread proto/fleetnodeadmin/v1/fleetnodeadmin.proto
Comment thread proto/fleetnodegateway/v1/fleetnodegateway.proto

Copilot AI 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.

Pull request overview

This PR adds the foundational scaffolding for pairing miners discovered via fleet nodes: it introduces an AgentCommand envelope for control-stream payloads, adds new proto/RPC shapes for fleet-node pairing and reporting, wires a node-side batch pairer (plugin-based auth + per-device outcomes), and refines server-side SQL guards/listing queries to distinguish cloud-dialed vs node-dialed paired devices.

Changes:

  • Introduces new proto messages/RPCs for fleet-node pairing flows and regenerates server/client codegen surfaces.
  • Updates fleet node control loop to decode an AgentCommand payload and dispatch discovery vs pairing, including bounded fan-out pairing and result reporting.
  • Adds server SQL query + index for listing fleet-node-discovered devices and refines “cloud dial” guards to exclude node-owned devices.

Reviewed changes

Copilot reviewed 22 out of 33 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
server/sqlc/queries/miner_service.sql Excludes fleet-node-owned devices from cloud dial credential fetch queries.
server/sqlc/queries/fleetnodepairing.sql Adds cloud-vs-node pairing guard refinements and a new fleet-node discovered-device listing query.
server/migrations/000074_add_discovered_device_fleet_node_active_index.up.sql Adds a partial index to speed fleet-node discovered-device listing.
server/migrations/000074_add_discovered_device_fleet_node_active_index.down.sql Drops the new index.
server/internal/handlers/middleware/rpc_permissions.go Adds permissions for new fleet-node admin RPCs.
server/internal/handlers/interceptors/config.go Marks new RPCs as session-only and redacts/sensitizes bodies where credentials may appear.
server/internal/handlers/fleetnode/admin/handler_discover_test.go Updates discovery tests for the new AgentCommand control payload envelope.
server/internal/domain/stores/sqlstores/fleetnodepairing.go Adds store method to list fleet-node-discovered devices.
server/internal/domain/fleetnode/pairing/service.go Adds domain service method for listing fleet-node-discovered devices with cursor pagination.
server/internal/domain/fleetnode/pairing/models.go Adds domain model for a fleet-node-discovered (unpaired/auth-needed) device.
server/internal/domain/fleetnode/pairing/discovered_listing_test.go Adds tests for new listing query and refined cloud-vs-node pairing guards.
server/internal/domain/fleetnode/discovery/service.go Wraps discovery requests in the AgentCommand envelope for node control payloads.
server/internal/domain/fleetnode/discovery/service_test.go Updates discovery service tests for AgentCommand payload.
server/cmd/fleetnode/run.go Wires plugin bootstrap to create both discoverer and pairer components.
server/cmd/fleetnode/run_test.go Extends gateway client stub with ReportPairedDevices.
server/cmd/fleetnode/pair.go Implements node-side batch pairing (plugin auth, bounded fan-out, per-device results, report upload).
server/cmd/fleetnode/pair_test.go Adds unit tests for pairing logic, key derivation cross-check, and control-loop pairing behavior.
server/cmd/fleetnode/control.go Decodes AgentCommand, dispatches discover vs pair, and generalizes supervisor wait helper.
server/cmd/fleetnode/control_test.go Updates control-loop tests for AgentCommand envelope and adds pairing-report plumbing to fakes.
proto/pairing/v1/pairing.proto Adds AgentCommand, FleetNodePairRequest, and FleetNodePairTarget.
proto/fleetnodegateway/v1/fleetnodegateway.proto Adds pairing outcome/result messages and ReportPairedDevices RPC.
proto/fleetnodeadmin/v1/fleetnodeadmin.proto Adds admin RPCs for listing discovered devices and initiating pairing on a node.
server/generated/sqlc/miner_service.sql.go Generated sqlc output for miner_service query changes (generated).
server/generated/sqlc/fleetnodepairing.sql.go Generated sqlc output for pairing queries (generated).
server/generated/sqlc/db.go Generated sqlc prepared statement wiring (generated).
server/generated/grpc/pairing/v1/pairing.pb.go Generated Go protobuf output (generated).
server/generated/grpc/fleetnodegateway/v1/fleetnodegatewayv1connect/fleetnodegateway.connect.go Generated Connect bindings for new gateway RPC (generated).
server/generated/grpc/fleetnodeadmin/v1/fleetnodeadminv1connect/fleetnodeadmin.connect.go Generated Connect bindings for new admin RPCs (generated).
client/src/protoFleet/api/generated/pairing/v1/pairing_pb.ts Generated TS protobuf output (generated).
client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts Generated TS protobuf output (generated).
client/src/protoFleet/api/generated/fleetnodeadmin/v1/fleetnodeadmin_pb.ts Generated TS protobuf output (generated).

Comment thread server/sqlc/queries/fleetnodepairing.sql

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/cmd/fleetnode/pair.go Outdated

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/sqlc/queries/miner_service.sql
ankitgoswami and others added 7 commits June 5, 2026 09:19
Wire shapes for pairing fleet-node-discovered miners: an AgentCommand oneof carried in ControlCommand.payload (discover today, pair next), FleetNodePairRequest/Target, the ReportPairedDevices gateway RPC with per-device FleetNodePairResult/PairOutcome, and the operator-facing ListFleetNodeDiscoveredDevices and PairDiscoveredDevicesOnFleetNode admin RPCs. Additive only; existing handlers return Unimplemented for the new RPCs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The node now decodes an AgentCommand from ControlCommand.payload and dispatches discover or pair. Discovery migrates into the envelope (server wraps in DiscoverOnFleetNode, node unwraps) in this commit. A new pluginPairer authenticates a batch of targets on the node's local plugins: asymmetric drivers use the node's own miner-signing key, basic-auth drivers use operator-supplied or plugin-default credentials, and per-device outcomes (PAIRED / AUTH_NEEDED / AUTH_FAILED / ERROR) stream back via ReportPairedDevices with PARTIAL semantics. A cross-check test pins the node-derived public key to token.Service.ExtractPublicKeyFromPrivateKey.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… non-node devices

Adds ListFleetNodeDiscoveredDevices (the inverse of GetActiveUnpairedDiscoveredDevices, which excludes fleet-node rows) so operators can enumerate pair candidates, including AUTHENTICATION_NEEDED rows for retry. Refines the cloud-dial guards to mean PAIRED AND NOT EXISTS(fleet_node_device): a fleet-node-paired device will also be device_pairing=PAIRED (so it reads as paired in the UI) but is node-dialed, so DeviceHasActiveCloudPairing, the discovery promotion guard (same-node carve-out), and the miner_service dial/credential fetch all exclude node-owned devices. Display and command-selection queries are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trim verbose and forward-pointing comments (drop the cross-effort
miner_command pointer, the "PR4" marker, and an over-long guard-test
preamble) and regenerate pairing proto output for the reworded
AgentCommand doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bounds, node ack tests)

Applies code-review findings on the pairing foundation:
- Paginate ListFleetNodeDiscoveredDevices (cursor_id/limit query params, next_cursor
  response field) and DISTINCT ON (dd.id) so one discovered device yields one row;
  add a partial composite index (migration 000074) for the node-filtered listing.
- Harden proto contracts: AgentCommand oneof required, FleetNodePairRequest.targets
  min_items=1, max_len on pairing_status and per-item device_identifiers bounds.
- Node pairer: ParsePort failure now yields PAIR_OUTCOME_ERROR instead of dialing
  port 0; share waitSupervisor[T] and truncateUTF8 across the discovery/pair fan-outs.
- Tests: pair REPORT_FAILED + supervisor-truncated PARTIAL ack paths, empty
  AgentCommand oneof -> BAD_REQUEST, listing pagination; split combined AAA comment.

Note: the listing store/domain signatures gained cursor/limit params; the stacked
orchestration branch must thread them in ResolvePairTargets and the admin handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or cursor

Addresses Copilot review on the pairing-foundation fixes:
- ListFleetNodeDiscoveredDevices excluded bound/PAIRED devices via per-joined-row
  filters, so a discovered device with one bound and one unbound device row still
  surfaced; DISTINCT ON also left the surfaced pairing_status arbitrary. Switch the
  exclusions to NOT EXISTS (judged across all live device rows) and add a d.id DESC
  tie-breaker so one deterministic row per discovered device is returned. Regression
  test covers the multi-row case.
- Add id as the trailing column of idx_discovered_device_fleet_node_active so the
  cursor scan (dd.id > cursor ORDER BY dd.id) seeks from the cursor and returns rows
  pre-ordered instead of scanning and sorting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orting

A pairing-capable plugin returning an identity string (serial/mac/model/
manufacturer/firmware) longer than the FleetNodePairResult caps would fail
ReportPairedDevices validation for the entire chunk, acking REPORT_FAILED and
losing every other device's outcome in that batch. setPaired now truncates each
field to its proto max_len (255, or 64 for mac) so one oversized success can't
sink the batch. Test covers oversized identity input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetAllPairedDeviceIdentifiers fed the telemetry startup loop, which would
schedule cloud polls for node-paired miners that have no direct cloud
route. Adds the same NOT EXISTS(fleet_node_device) guard already on
GetMinerFromDeviceIdentifier/GetDeviceWithCredentialsAndIPByID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ankitgoswami

Copy link
Copy Markdown
Contributor Author

Fixed in 6554aab: added the same NOT EXISTS (fleet_node_device) guard to GetAllPairedDeviceIdentifiers in device.sql (and regenerated sqlc). Node-owned miners are now excluded from the telemetry polling loop at the query level.

@ankitgoswami

Copy link
Copy Markdown
Contributor Author

Addressed in the orchestration PR (#396, commit 91f9f5b): PairDiscoveredDevicesOnFleetNodeProcedure added to RedactedRequestProcedures (request carries credentials), SensitiveBodyProcedures (suppresses request + response bodies at debug level), and SessionOnlyProcedures. ReportPairedDevicesProcedure added to FleetNodeAuthenticatedProcedures and SensitiveBodyProcedures. ListFleetNodeDiscoveredDevicesProcedure added to SessionOnlyProcedures.

@ankitgoswami ankitgoswami force-pushed the fleetnode-pairing branch 2 times, most recently from 4f7f454 to cd54a3a Compare June 5, 2026 16:47
Add the three new procedures introduced by this branch to the required
classification lists so TestRPCContract_EveryRegisteredProcedureIsClassified passes:

- ReportPairedDevicesProcedure -> FleetNodeAuthenticatedProcedures (node-to-cloud)
- ListFleetNodeDiscoveredDevicesProcedure -> ProcedurePermissions (fleetnode:read)
- PairDiscoveredDevicesOnFleetNodeProcedure -> ProcedurePermissions (fleetnode:manage)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ankitgoswami

Copy link
Copy Markdown
Contributor Author

Re: session-only — already addressed in cfb3fb2: ListFleetNodeDiscoveredDevicesProcedure and PairDiscoveredDevicesOnFleetNodeProcedure are both in SessionOnlyProcedures alongside the rest of the FleetNodeAdminService block. Thread resolved.

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/cmd/fleetnode/pair.go Outdated
…te pair payload

Map PairDiscoveredDevicesOnFleetNodeProcedure to PermMinerPair (primary gate,
matching all other miner-onboarding paths); the handler adds an inline
PermFleetnodeManage check so both are required simultaneously.

Call protovalidate.Validate on the FleetNodePairRequest before fan-out so
proto constraints (max 1024 targets, ip:true, port range, field lengths) are
enforced at the agent and invalid payloads ack BAD_REQUEST before any plugin
work starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ankitgoswami

Copy link
Copy Markdown
Contributor Author

Re: validate pair payload — already fixed in a8a854c: protovalidate.Validate(req) is now called in handlePairCommand before fan-out, enforcing the max_items: 1024 cap, ip:true, port CEL range, and all field-length limits. Invalid payloads ack BAD_REQUEST immediately. Thread resolved.

Comment thread proto/fleetnodeadmin/v1/fleetnodeadmin.proto Outdated
Comment thread proto/fleetnodeadmin/v1/fleetnodeadmin.proto Outdated
…iringResult.error

Address review on PairDiscoveredDevicesOnFleetNode's DevicePairingResult:
- pairing_status now uses the existing fleetmanagement.v1.PairingStatus enum
  (PAIRED/AUTHENTICATION_NEEDED/FAILED are a clean subset) instead of a bare
  string, for type safety and consistency with the operator-facing fleet API.
- error gains max_len=4096, matching the gateway FleetNodePairResult.error_message
  cap it echoes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ankitgoswami ankitgoswami enabled auto-merge (squash) June 5, 2026 19:47
@ankitgoswami ankitgoswami merged commit 9ac8a0f into main Jun 5, 2026
74 checks passed
@ankitgoswami ankitgoswami deleted the fleetnode-pairing branch June 5, 2026 19:55
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node (fleet_node_device)
  before marking device_pairing PAIRED so the cloud-paired guard never sees a
  PAIRED-but-unbound row, seeds an ACTIVE status, and stores encrypted credentials
  for password-auth drivers only; AUTH_NEEDED/AUTH_FAILED persist an
  AUTHENTICATION_NEEDED device for retry; ERROR persists nothing.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  bounded discovered-device listing + pair-target resolution.
- pairDeviceLocked factored out of PairDevice so both share binding in a
  caller-owned transaction.

Registry + dispatch:
- PublishPairResults routes per-device results to the operator session.
- Report admission is scoped by command kind so a discovery command_id can't
  admit pair results (or vice versa).
- Shared control.RunCommand/AckFailure dispatch loop used by discovery and pair.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming
  PairDiscoveredDevicesOnFleetNode; results constrained to the dispatched target
  set, with synthesized terminal results for targets a partial batch never
  reported.

Security + reliability:
- Both RPCs are session-only; PairDiscovered requires miner:pair AND
  fleetnode:manage; request/response bodies and credentials are redacted/
  suppressed in logs.
- Credentials are persisted only from the node's authoritative used_* report,
  never for asymmetric-auth devices.
- DevicePairingResult.pairing_status uses the shared fleetmanagement
  PairingStatus enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Resolves the #388 (pairing) <-> #390 (miner commands) overlap on the shared
AgentCommand envelope and control loop:
- AgentCommand: #388 shipped `pair = 2`, so `miner_command` moves to field 3
  (the slot main reserved for it). Pre-release, nothing depends on the old number.
- handleCommand dispatches both the pair (#388) and miner_command (#390) arms.
- run.go wires the pairer, driverGetter, and minerSecrets off the shared plugin manager.
- Regenerated pairing/fleetnodegateway Go + TS; sqlc unchanged-but-verified.
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 5, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 9, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 9, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 9, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 10, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 11, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 11, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 11, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ankitgoswami added a commit that referenced this pull request Jun 11, 2026
Cloud orchestration for pairing fleet-node-discovered miners over the
ControlStream, built on the #388 foundation (AgentCommand pair arm, node pairer,
SQL listing + cloud-dial guards).

Server domain:
- PersistFleetNodePairResult records each device's outcome in its own
  transaction: PAIRED creates the device, binds it to the node before marking
  device_pairing PAIRED (so the cloud-paired guard never sees a PAIRED-but-unbound
  row), seeds an ACTIVE status, and stores encrypted credentials for password-auth
  drivers only; AUTH_NEEDED/AUTH_FAILED persist AUTHENTICATION_NEEDED for retry;
  ERROR persists nothing. It never downgrades an already-PAIRED device (race with
  the cloud or another node) and preserves a learned serial when a report omits it.
- ResolvePairTargets builds the batch from the node's not-yet-paired devices;
  pair-all without credentials excludes AUTHENTICATION_NEEDED rows so a capped
  batch can't starve never-attempted devices on re-issue for large fleets.
- pairDeviceLocked is factored out of PairDevice so both share the binding logic.

Registry + dispatch (authoritative, decoupled from the operator stream):
- The gateway ReportPairedDevices handler is the source of truth: it admits +
  scopes results to the dispatched targets (consuming each to bar replay), rejects
  empty batches, enforces a per-command quota = target count, persists each result
  before acking, and forwards only successfully-persisted results (with the
  persisted status) for live display.
- PublishPairResults / report-kind admission keep discovery and pairing reports
  from cross-admitting.
- The operator command is dispatched on a disconnect-immune context so pairing
  runs to completion server-side and the gateway keeps persisting even if the
  operator/browser disconnects; the admin stream is a best-effort observer.

Operator admin RPCs:
- ListFleetNodeDiscoveredDevices (paged) and streaming PairDiscoveredDevicesOnFleetNode,
  gated by miner:pair + fleetnode:manage, session-only, with credential bodies
  redacted/suppressed in logs.
- DevicePairingResult.pairing_status uses the shared fleetmanagement PairingStatus
  enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client javascript Pull requests that update javascript code server shared

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants