feat(fleetnode): pairing foundation (proto, node pairer, SQL guards)#388
Conversation
🔐 Codex Security Review
Review SummaryOverall Risk: HIGH Findings[HIGH] New admin RPCs are exposed but not implemented
[HIGH] Pair result upload RPC is exposed but handled by the unimplemented fallback
NotesReview was limited to Generated by Codex Security Review | |
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
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
AgentCommandpayload 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). |
There was a problem hiding this comment.
💡 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".
fc56691 to
56cd24c
Compare
There was a problem hiding this comment.
💡 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".
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>
56cd24c to
09b523d
Compare
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>
|
Fixed in 6554aab: added the same |
|
Addressed in the orchestration PR (#396, commit 91f9f5b): |
4f7f454 to
cd54a3a
Compare
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>
cd54a3a to
cfb3fb2
Compare
|
Re: session-only — already addressed in cfb3fb2: |
There was a problem hiding this comment.
💡 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".
…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>
|
Re: validate pair payload — already fixed in a8a854c: |
…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>
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>
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Summary
Foundation for pairing miners discovered via fleet nodes. A fleet node can already discover miners (#235), but those
discovered_devicerows are a dead end: the cloudPairpath refuses fleet-node-reported devices (it has no route to a LAN behind a node), andPairDeviceToFleetNodeneeds an already-existingdevicerow 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 existingControlStream.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
AgentCommandoneof carried inControlCommand.payload(discover today,pairnext),FleetNodePairRequest/FleetNodePairTarget, theReportPairedDevicesgateway RPC with per-deviceFleetNodePairResult/PairOutcome, and the operator-facingListFleetNodeDiscoveredDevicesandPairDiscoveredDevicesOnFleetNodeadmin RPCs. Existing handlers return Unimplemented for the new RPCs.handleCommanddecodesAgentCommandand dispatches discover or pair. A newpluginPairerauthenticates 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 viaReportPairedDeviceswith bounded fan-out and PARTIAL semantics, mirroring discovery.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
DiscoverRequestto anAgentCommandenvelope insideControlCommand.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.device_pairingmodel. A fleet-node-paired device will bedevice_pairing.pairing_status = PAIRED(so it reads as paired in the UI and is command-targetable), with the transport dimension expressed byfleet_node_devicemembership. "Cloud-dialed" is therefore refined toPAIRED 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 theminer_servicedial/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
AgentCommandenvelope is shared (it adds aminer_commandarm), and thedevice_pairing= PAIRED decision resolves that effort's open question about whetherAllDevicesincludes node devices (it does; transport routes viafleet_node_device). Theminer_servicedial 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