Skip to content

feat(fleetnode): route miner commands to fleet-node-paired devices#390

Draft
ankitgoswami wants to merge 1 commit into
fleetnode-commandsfrom
fleetnode-commands-p2
Draft

feat(fleetnode): route miner commands to fleet-node-paired devices#390
ankitgoswami wants to merge 1 commit into
fleetnode-commandsfrom
fleetnode-commands-p2

Conversation

@ankitgoswami
Copy link
Copy Markdown
Contributor

Summary

Builds the actual capability: operator commands now reach miners paired via fleet nodes. Stacked on #389 (the ControlStream foundation) — review/merge that first.

A command issued through the existing RPCs/UI against a fleet-node-paired device now routes down that node's ControlStream, the node reconstructs the LAN miner and executes the action, and the result comes back as a ControlAck. The entire command service / queue / status / permissions / curtailment pipeline is unchanged — this is RFC 0001's remote-node Miner adapter slotted in at miner resolution.

What changed

  • proto: pairing.v1.MinerCommand — a oneof over reboot / start / stop / blink-LED / curtail / uncurtail / set-cooling-mode / set-power-target, plus a MinerConnectionDescriptor (the cloud supplies connection coords; the node has no device DB) and an opaque credential field. Wired as AgentCommand.miner_command. Added ACK_CODE_UNAUTHENTICATED / ACK_CODE_UNIMPLEMENTED.
  • server adapter (internal/domain/miner/remotenode): an interfaces.Miner whose methods marshal a MinerCommand, call registry.SendCommand, and map the ControlAck to the method's error contract (auth → evict + Unauthenticated; unsupported → Unimplemented; offline → FailedPrecondition).
  • resolution (internal/domain/miner): new GetActiveFleetNodeForDevice query; GetMinerFromDeviceIdentifier checks fleet-node ownership first and returns the adapter, else falls through to the direct PluginMiner. Wired in fleetd via WithCommandSender.
  • node (cmd/fleetnode): handleMinerCommand builds a control device from the descriptor via the plugin driver and runs the action, behind a credential-provider seam.

Scope / boundary

  • Works end-to-end now: the no-secret virtual driver (the just dev fake miners).
  • Routes correctly, lights up with pairing: authed drivers (proto, antminer) need the node's per-org key to decrypt credentials — that provisioning is the separate pairing effort. Until then those commands reach the miner and fail auth gracefully (UNAUTHENTICATED).
  • Deferred (Phase 3): firmware update, log download, password change, unpair (need out-of-band transfer / pairing semantics); read/telemetry methods. The adapter returns Unimplemented for these.

Test plan

  • go test -race ./cmd/fleetnode/... ./internal/domain/miner/... — green. Adapter: action encoding for all 8 commands + ack→error mapping + offline + unsupported. Node executor (gomock driver/device): execute+ack, enum conversion, error classification, unknown-driver. Resolution (real DB): a fleet-node-paired device resolves to the remote-node miner and a command reaches the sender.
  • Full server build, golangci-lint, pre-push tsc — clean.

Manual end-to-end (virtual driver)

just dev; enroll/confirm a fleet node; discover a virtual device on it; seed device + device_pairing=PAIRED + fleet_node_device (until the pairing flow lands); issue Reboot/Curtail/StartMining and confirm it routes over the ControlStream (node logs control command received, kind=miner_command), executes against the fake miner, and the batch reports SUCCESS. Verify an offline node fails fast and a command during a discovery on the same node still succeeds.

🤖 Generated with Claude Code

Build the command path on the Phase-1 ControlStream foundation: operator commands
for a device paired to a fleet node now dispatch over the node's ControlStream and
execute against the LAN miner, reusing the existing command pipeline unchanged
(RFC 0001's remote-node Miner adapter).

- proto: add pairing.v1.MinerCommand (oneof over reboot/start/stop/blink/curtail/
  uncurtail/set-cooling-mode/set-power-target) plus a connection descriptor and an
  opaque credential field, wired as AgentCommand.miner_command. Add
  ACK_CODE_UNAUTHENTICATED / ACK_CODE_UNIMPLEMENTED.
- server: a remote-node interfaces.Miner adapter marshals each command, calls
  registry.SendCommand, and maps the ControlAck to the method's error contract.
  MinerService resolves a device's active fleet node first (new
  GetActiveFleetNodeForDevice query) and returns the adapter; cloud-dialed devices
  fall through to the direct PluginMiner. Wired in fleetd via WithCommandSender.
- node: handleMinerCommand reconstructs the target miner from the descriptor via the
  plugin driver and runs the action, behind a credential-provider seam (no-secret
  drivers work now; authed-driver credential delivery is a pairing concern).

End-to-end for the no-secret virtual driver; authed drivers route correctly and
light up once pairing provisions the node's per-org key. Heavy commands (firmware,
logs, password, unpair) and read/telemetry methods return Unimplemented for now.

Also fixes a test-only data race the Phase-1 worker pool can trigger: the stub
discoverer now hands out cloned reports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added javascript Pull requests that update javascript code client server shared labels Jun 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

🔐 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 (f1ad3dc175e508e9f088fa83780c123127817de4...432a7274be17add48812f5b4cf4b86a30c44cc58, 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] Fleet-node commands route authenticated miners without credentials

  • Category: Auth
  • Location: server/internal/domain/miner/service.go:193
  • Description: tryFleetNodeMiner now routes any device paired to a confirmed fleet node through the remote-node adapter, but the descriptor is built without Credential and the node-side default nodeSecretProvider always returns an empty sdk.SecretBundle. The comment in nodeSecretProvider explicitly says authenticated drivers will fail until pairing lands, but there is no gate preventing Antminer/Proto or other credentialed drivers from entering this path.
  • Impact: Real fleet-node-paired miners that require basic auth or bearer auth will fail every command with UNAUTHENTICATED, causing retries and eventual command failure. This makes the new remote command path work only for no-secret drivers while appearing enabled for all paired devices.
  • Recommendation: Either implement credential delivery/decryption before enabling this path for authenticated drivers, or gate tryFleetNodeMiner to only no-secret-capable drivers and fail unsupported devices before enqueueing remote commands.

[HIGH] Concurrent commands can overwrite and close each other’s plugin handles

  • Category: Concurrency
  • Location: server/cmd/fleetnode/minercommand.go:72
  • Description: Each remote miner command calls driver.NewDevice with the same device_identifier and defers dev.Close, while the control loop allows up to 16 commands to run concurrently. The SDK gRPC plugin server stores device handles by device_id, so two concurrent commands for the same miner can replace the same map entry and then close a handle another command is still using.
  • Impact: Concurrent commands to one miner can race, fail nondeterministically, execute against a freshly replaced handle, or close the active handle mid-command.
  • Recommendation: Serialize command execution per target device on the fleet node, or introduce a command-scoped/plugin-scoped handle model that does not reuse the logical miner identifier as the transient SDK handle key.

[MEDIUM] Transient offline/busy states are converted into permanent queue failures

  • Category: Reliability
  • Location: server/internal/domain/miner/remotenode/miner.go:166
  • Description: ErrNoActiveStream is mapped to FailedPrecondition, and ACK_CODE_BUSY is also mapped to FailedPrecondition at line 198. The command execution service treats FailedPrecondition as permanently failed, so a fleet node reconnect or temporary command-pool saturation will not be retried.
  • Impact: Commands can fail permanently during normal transient conditions such as node reconnects, stream churn, or temporary agent worker exhaustion.
  • Recommendation: Map offline/busy outcomes to a retryable error class for queued command execution, or add an explicit temporary fleet-node error type that keeps the queue retry behavior while preserving permanent failures for BAD_REQUEST and UNIMPLEMENTED.

Notes

No hardcoded wallet, pool, stratum, or worker-credential rewrite logic appeared in the scoped diff. The protobuf/sqlc generated outputs appear consistent with the source proto/query changes.


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

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.

1 participant