Skip to content

refactor(device-set): group RPCs + remove generic collection membership (3/4)#466

Merged
flesher merged 7 commits into
mainfrom
refactor/group-rpcs-cleanup-3of4
Jun 16, 2026
Merged

refactor(device-set): group RPCs + remove generic collection membership (3/4)#466
flesher merged 7 commits into
mainfrom
refactor/group-rpcs-cleanup-3of4

Conversation

@flesher

@flesher flesher commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Part 3 of 4 — Group RPCs + Generic Collection-Membership Removal

1. Summary

Replaces the legacy generic device-membership surface (AddDevicesToDeviceSet / RemoveDevicesFromDeviceSet in device_set/v1, plus AddDevicesToCollection / RemoveDevicesFromCollection in collection/v1) with type-specific RPCs: AddDevicesToGroup / RemoveDevicesFromGroup for many-to-many group membership, and the existing atomic AssignDevicesToRack for racks. After this PR, every device-membership operation in the fleet hierarchy is type-checked at the proto layer, atomic where atomicity matters (racks), and additive where the data model allows it (groups). No generic backdoor that bypasses the orphan-window guarantees the earlier PRs in this series introduced.

Stack

PR Status
#463 — atomic reparent foundation (1/4) merged
#464 — atomic cross-site reparent (2/4) merged
#466 — group RPCs + generic membership removal (3/4) this PR
#468 — bulk SQL + concurrency hardening (4/4) parallel-mergeable

Diff is relative to main (no longer stacked on #464 after #464 merged).

Required context from upstream:

Out of scope, lands elsewhere:

2. How it works

Pre-PR device-membership shape (generic, runtime-checked):

  • AddDevicesToDeviceSet(device_set_id, device_selector) and RemoveDevicesFromDeviceSet(device_set_id, device_selector) accept either a rack OR a group. The service inspects the collection type at runtime and branches.
  • A duplicate generic pair lives on collection/v1 (AddDevicesToCollection / RemoveDevicesFromCollection) for legacy reasons — the wire surface predates the device_set renaming.
  • AssignDevicesToRack(target_rack_id, repeated string device_identifiers) — added in refactor(reparent): atomic reparent foundation (1/4) #463 — handles rack membership atomically but the legacy generic surface still allows a caller to bypass it.

Post-PR device-membership shape (type-specific):

  • AddDevicesToGroup(target_group_id, DeviceSelector) — additive (many-to-many between miner and group). Service does an org-scoped GetCollection inside the tx, rejects non-COLLECTION_TYPE_GROUP targets with InvalidArgument, then calls the existing store-level AddDevicesToCollection (kept as shared infrastructure).
  • RemoveDevicesFromGroup(target_group_id, DeviceSelector) — same type guard, calls store-level RemoveDevicesFromCollection.
  • AssignDevicesToRack — request shape now DeviceSelector device_selector instead of repeated string device_identifiers. Handler accepts only the device_list variant; all_devices is rejected at the translate layer with InvalidArgument. Identifier validation is enforced by a shared ValidateDeviceIdentifiers helper (caps: 10k items, 256 chars per identifier, no empty strings).
  • Legacy AddDevicesToDeviceSet / RemoveDevicesFromDeviceSet (device_set/v1) — removed from the proto, removed from the wire-permissions map, removed from the handler.
  • Legacy AddDevicesToCollection / RemoveDevicesFromCollection (collection/v1) — removed from the proto, removed from the wire-permissions map, removed from the handler. Store-level methods stay as shared infrastructure used by the new group RPCs and by AssignDevicesToRack internally.
  • Service.AddDevicesToCollection / Service.RemoveDevicesFromCollection (the in-process Go-facing methods that the gRPC handlers used to call, and that fleetimport called directly) — also removed. fleetimport migrates to the type-specific Service methods.

fleetimport migration:

  • Group imports (existing-group adds + group create-race adds) → Service.AddDevicesToGroup.
  • Rack imports (existing-rack adds + rack create-race adds) → Service.AssignDevicesToRack. Importer accumulates devicesAssigned from NewlyAssignedCount (Go-only field added to AssignDevicesToRackResult) rather than AssignedCount, so idempotent re-imports report only newly-added rows — matches the old AddDevicesToCollection.AddedCount semantics.

Client UI migration:

  • RackOverviewPage drag flow → assignDevicesToRack (was addDevicesToDeviceSet). Atomic-replace semantics correct: a single-miner drag from one rack to another now atomically clears the prior rack row + adds the new one.
  • MinerActionModalStack group-add flow → addDevicesToGroup (was addDevicesToDeviceSet).
  • useDeviceSets hook drops addDevicesToDeviceSet / removeDevicesFromDeviceSet, adds addDevicesToGroup / removeDevicesFromGroup.

gRPC reflection:

DeviceSetService is now in reflectEnabledServices (server/cmd/fleetd/main.go). Agents using grpcurl -list can discover AssignDevicesToRack, AddDevicesToGroup, RemoveDevicesFromGroup. The auth interceptor chain remains the security boundary; reflection only exposes service shape.

Auth posture: unchanged from pre-PR. The new group RPCs and AssignDevicesToRack are all gated on PermRackManage (no separate PermGroupManage exists in the catalog; the legacy pair was on the same permission). Catalog split for groups is filed as a future-work follow-up.

3. Diagrams

Device-membership surface — before and after

flowchart TB
    subgraph Pre["Pre-PR: generic + runtime type-check"]
        A1["AssignDevicesToRack<br/>(rack-specific, atomic)<br/>device_set/v1"] -.->|"can also be used<br/>but caller may bypass"| A4
        A2["AddDevicesToDeviceSet<br/>generic; runtime branches on type<br/>device_set/v1"] --> A4["store.AddDevicesToCollection"]
        A3["AddDevicesToCollection<br/>duplicate generic surface<br/>collection/v1"] --> A4
        A5["RemoveDevicesFromDeviceSet<br/>device_set/v1"] --> A6["store.RemoveDevicesFromCollection"]
        A7["RemoveDevicesFromCollection<br/>collection/v1"] --> A6
    end
    subgraph Post["Post-PR: type-specific at the proto layer"]
        B1["AssignDevicesToRack<br/>rack-only; DeviceSelector<br/>device_set/v1"] --> B4["store.AddDevicesToCollection<br/>+ store.RemoveDevicesFromAnyRack<br/>(shared infrastructure)"]
        B2["AddDevicesToGroup<br/>group-only; type-checked<br/>device_set/v1"] --> B4
        B3["RemoveDevicesFromGroup<br/>group-only; type-checked<br/>device_set/v1"] --> B5["store.RemoveDevicesFromCollection"]
    end
Loading

AddDevicesToGroup request flow

sequenceDiagram
    participant Client
    participant Handler as deviceset.Handler
    participant Auth as middleware.RequirePermission
    participant Svc as collection.Service
    participant Resolver as deviceresolver
    participant Store as collectionStore
    participant DB

    Client->>Handler: AddDevicesToGroup target_group_id, DeviceSelector
    Handler->>Auth: PermRackManage
    Auth-->>Handler: ok
    Handler->>Resolver: resolveExplicitDevices selector
    Resolver->>Resolver: ValidateDeviceIdentifiers caps + non-empty
    Resolver-->>Handler: identifiers
    Handler->>Svc: AddDevicesToGroup orgID, target_group_id, identifiers
    Svc->>DB: BEGIN
    Svc->>Store: GetCollection orgID, target_group_id
    alt collection is not GROUP
        Svc-->>Handler: InvalidArgument
        Svc->>DB: ROLLBACK
    else collection is GROUP
        Svc->>Store: AddDevicesToCollection
        Store->>DB: INSERT ... ON CONFLICT DO NOTHING
        Svc->>DB: COMMIT
        Svc-->>Handler: added_count
    end
    Handler-->>Client: response
Loading

Removed wire surface

flowchart LR
    subgraph Removed["Removed wire RPCs"]
        D1["AddDevicesToDeviceSet"]
        D2["RemoveDevicesFromDeviceSet"]
        D3["AddDevicesToCollection"]
        D4["RemoveDevicesFromCollection"]
    end
    subgraph Replacements["Replacement RPCs"]
        R1["AddDevicesToGroup"]
        R2["RemoveDevicesFromGroup"]
        R3["AssignDevicesToRack"]
        R4["AssignDevicesToRack<br/>target_rack_id unset"]
    end
    D1 -->|group target| R1
    D1 -->|rack target| R3
    D2 -->|group target| R2
    D2 -->|rack clear| R4
    D3 -->|group target| R1
    D3 -->|rack target| R3
    D4 -->|group target| R2
    D4 -->|rack clear| R4
Loading

4. Areas of the code involved

Area What changed Why it matters for review
proto/device_set/v1/device_set.proto (modified) Removed AddDevicesToDeviceSet / RemoveDevicesFromDeviceSet RPCs + messages. Added AddDevicesToGroup / RemoveDevicesFromGroup RPCs + messages. Swapped AssignDevicesToRack.device_identifiers (repeated string, tag 2) → device_selector (common.v1.DeviceSelector, tag 2). Breaking wire change. Tag 2 reuse is intentional — these RPCs aren't live for external consumers and client+server deploy atomically; cached-bundle window is the only blast radius.
proto/collection/v1/collection.proto (modified) Removed AddDevicesToCollection / RemoveDevicesFromCollection RPCs + messages. collection/v1 keeps Create/Get/Update/Delete/List/Stats/Slot/Zone/Type/SaveRack — only the membership-mutation pair is gone. Wire-only removal. Store-level methods of the same name are retained as shared infrastructure.
server/generated/grpc/** (modified) Proto regen. Generated — skip.
client/src/protoFleet/api/generated/** (modified) Proto regen. Generated — skip.
server/cmd/fleetd/main.go (modified) Added device_setv1connect.DeviceSetServiceName to reflectEnabledServices. Agents using gRPC reflection can now discover AssignDevicesToRack, AddDevicesToGroup, RemoveDevicesFromGroup. Auth boundary unchanged.
server/internal/handlers/middleware/rpc_permissions.go (modified) Removed legacy entries (AddDevicesToCollection, RemoveDevicesFromCollection, AddDevicesToDeviceSet, RemoveDevicesFromDeviceSet). Added entries for AddDevicesToGroup, RemoveDevicesFromGroup, both on PermRackManage. Security boundary. Reviewer should confirm the new entries match sibling RPCs' permission and that no legacy entry was missed.
server/internal/handlers/deviceset/handler.go (modified) Removed legacy handlers. Added AddDevicesToGroup / RemoveDevicesFromGroup handlers (auth-gated, then service call). Updated AssignDevicesToRack handler to plumb the selector through and surface the translate-layer error for non-device_list variants. Thin shim; the work is in the translate layer + service.
server/internal/handlers/deviceset/translate.go (modified) toAssignDevicesToRackParams now extracts identifiers from DeviceSelector.device_list, rejects all_devices and unset variants, and calls ValidateDeviceIdentifiers. Added toAddDevicesToGroupParams / toRemoveDevicesFromGroupParams. Input validation boundary. Reviewer should confirm caps + empty-string rejection.
server/internal/handlers/deviceset/handler_test.go (modified) Tests for the new group RPCs (happy path, rack-target rejection, permission gate), AssignDevicesToRack selector-variant rejection, ValidateDeviceIdentifiers boundary cases (empty string, over-length, over-cap list), cross-org isolation for AddDevicesToGroup. Locks the wire-visible status codes for each rejection path.
server/internal/handlers/collection/handler.go (modified) Removed AddDevicesToCollection / RemoveDevicesFromCollection handler funcs. Handler now matches the trimmed-down collection/v1 interface. Wire-handler removal; verify the interface is satisfied.
server/internal/domain/collection/service.go (modified) Removed Service.AddDevicesToCollection / Service.RemoveDevicesFromCollection. Added Service.AddDevicesToGroup / Service.RemoveDevicesFromGroup with inline GetCollection + type guard (rejects non-GROUP). Added NewlyAssignedCount to AssignDevicesToRackResult (Go-only, no proto change) so re-import callers can read newly-inserted count instead of final-membership count. Core domain logic. Reviewer should focus on (a) the type guard inside the tx, (b) NewlyAssignedCount semantics matching pre-PR AddedCount.
server/internal/domain/collection/service_test.go (modified) Tests for the new Service methods + NewlyAssignedCount exposure. Service-level coverage.
server/internal/domain/deviceresolver/resolver.go (modified) Added shared ValidateDeviceIdentifiers helper with MaxDeviceIdentifiers=10000 and MaxDeviceIdentifierLen=256. Wired into resolveExplicitDevices so every consumer of the device-selector resolver inherits the validation. Validation surface. Previously enforced by buf.validate on the old repeated string proto rules; now lives in code since DeviceSelector.device_list doesn't carry those constraints.
server/internal/domain/fleetimport/importer.go (modified) CollectionManager interface swapped from generic AddDevicesToCollection to AddDevicesToGroup + AssignDevicesToRack. Four call sites migrated (existing-group, group create-race, existing-rack, rack create-race). devicesAssigned accumulator now uses NewlyAssignedCount for rack adds so idempotent re-imports don't overstate. Behavior parity. Re-import counts must match pre-PR semantics; locked by a new test.
server/internal/domain/fleetimport/importer_rack_assign_test.go (new) TestImporter_RackReimport_UsesNewlyAssignedCount — fake CollectionManager returns AssignedCount=3, NewlyAssignedCount=1; assertion confirms only the newly-assigned count flows to devicesAssigned. Regression pin on the re-import semantic.
server/internal/domain/foremanimport/service_test.go (modified) Updated fakeCollectionManager to match the new CollectionManager interface (AddDevicesToGroup, AssignDevicesToRack). Mirror update; no production behavior.
client/src/protoFleet/api/useDeviceSets.ts (modified) Removed addDevicesToDeviceSet / removeDevicesFromDeviceSet hooks. Added addDevicesToGroup / removeDevicesFromGroup hooks mirroring assignDevicesToRack (signal support, error handling, bigint counts). assignDevicesToRack hook now constructs DeviceSelector { deviceList: { identifiers: [...] } } internally so callers still pass string[]. API-surface change; verify caller migration is complete.
client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.tsx (modified) Drag-flow single-miner add → assignDevicesToRack({ targetRackId, deviceIdentifiers: [minerId] }). Atomic-replace semantics correct for single-miner add.
client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.test.tsx (modified) Mock updated from addDevicesToDeviceSet to assignDevicesToRack. Mirror update.
client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerActionModalStack.tsx (modified) Group-add flow → addDevicesToGroup({ targetGroupId, deviceIdentifiers, allDevices }). Caller migration to new group RPC.

5. Key technical decisions & trade-offs

  • Type-specific RPCs over generic with runtime branch. Forces the type check to the proto layer where wrong-target attempts return InvalidArgument at the wire boundary, not after the server has set up a transaction. Alternative — keep the generic surface and add a feature flag — was rejected because it preserves the bypass path that PRs refactor(reparent): atomic reparent foundation (1/4) #463/feat(sites): atomic cross-site reparent (2/4) #464 worked to close.
  • Tag 2 reuse on AssignDevicesToRack over reserving the old tag and adding device_selector at tag 3. Accepted because the RPC isn't live for external consumers and client+server deploy atomically; the blast radius is bounded to the deploy-window stale-bundle case. If this becomes a public API later, mark reserved 2; and migrate.
  • NewlyAssignedCount added as a Go-only field on the service result struct over extending the proto response. Importer's the only caller that cares about the distinction today; adding a wire field would also require client work that no UI surfaces consume. If a second caller needs it, promote to proto.
  • Group RPCs additive (no replace semantics) because groups are many-to-many between miner and group — a miner can belong to multiple groups simultaneously. Rack RPCs are replace-semantics (AssignDevicesToRack) because rack membership is one-per-miner. The atomic replace doesn't apply when there's no prior-state conflict to close.
  • Store-level AddDevicesToCollection / RemoveDevicesFromCollection retained even though the wire and service methods are gone. These are shared infrastructure: Service.AddDevicesToGroup, Service.AssignDevicesToRack, and fleetimport all use them. Renaming for cosmetic alignment was rejected as out-of-scope churn.
  • Validation moved from proto buf.validate rules to a Go helper. DeviceSelector.device_list doesn't carry the per-item rules the old repeated string device_identifiers had (min_len, max_len, max_items). ValidateDeviceIdentifiers is called at the resolver layer so every device-selector consumer inherits the caps. Alternative — add the rules to common.v1.DeviceIdentifierList — would touch a shared message and require coordinating with other consumers; deferred.
  • all_devices selector variant rejected on AssignDevicesToRack only, not on the new group RPCs. Bulk-adding every paired device to a single rack is never the intended operation; bulk-adding to a group is. Per the security review, this is bounded by the org-scoped resolver — all_devices on AddDevicesToGroup enumerates only the caller's own org devices.
  • PermRackManage reused for group RPCs, not a new PermGroupManage. The catalog doesn't currently distinguish the two; matching the legacy pair's permission preserves the pre-PR posture. A separate PermGroupManage is filed as a follow-up if groups gain reach beyond a rack-manager's intended blast radius.

6. Testing & validation

Handler-layer tests (server/internal/handlers/deviceset/handler_test.go):

  • TestAddDevicesToGroup_HappyPath, _RejectsRackTarget, _PermissionRequired, _RejectsCrossOrgTarget — new group RPC coverage.
  • TestRemoveDevicesFromGroup_HappyPath, _RejectsRackTarget, _PermissionRequired — mirror.
  • TestAssignDevicesToRack_RejectsAllDevicesSelector, _RejectsEmptyIdentifier, _RejectsOverlongIdentifier, _RejectsOversizedList — selector + validation boundary cases.
  • Existing TestAssignDevicesToRack_HappyPathAssigns / _UnassignBranch updated to use the new DeviceSelector shape.

Service-layer tests (server/internal/domain/collection/service_test.go):

  • AddDevicesToGroup / RemoveDevicesFromGroup happy path + type guard + collection-not-found.
  • AssignDevicesToRack updated to verify NewlyAssignedCount reflects only newly-inserted rows.

fleetimport tests (server/internal/domain/fleetimport/importer_rack_assign_test.go):

  • TestImporter_RackReimport_UsesNewlyAssignedCount — fake CollectionManager returns mismatched AssignedCount vs NewlyAssignedCount; asserts the importer uses the latter.

Client tests:

  • RackOverviewPage.test.tsx updated mock from addDevicesToDeviceSet to assignDevicesToRack.

Verification:

  • go build ./... clean.
  • go test ./server/internal/handlers/deviceset/... ./server/internal/domain/collection/... ./server/internal/domain/fleetimport/... ./server/internal/domain/deviceresolver/... ./server/internal/handlers/middleware/... — all pass.
  • npx tsc --noEmit (client) — clean.
  • npm run lint — clean.
  • golangci-lint run -c server/.golangci.yaml ./internal/domain/fleetimport/... — clean.

Explicitly NOT covered:

  • Real-Postgres integration test for the new group RPCs — only mock-driven service + handler tests. A focused integration test confirming cross-org isolation against a real DB would close the security-review-surfaced gap on the GetCollection(orgID, ...) org-scoped predicate end-to-end.
  • addDevicesToCollection / removeDevicesFromCollection removal regression is verified by the build (no remaining Go callers; client grep clean for the four removed identifiers). No explicit "wire-RPC is gone" test, since GitHub Actions' build + targeted tests catch missed references.

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

@flesher flesher requested a review from a team as a code owner June 15, 2026 21:02
@github-actions github-actions Bot added javascript Pull requests that update javascript code client server shared labels Jun 15, 2026
@github-actions

github-actions Bot commented Jun 15, 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 (7324718fb6540c92ccd9c49e5f15b1168cae9614...6616fad5294dab3c52a911eda271d17f789560e8, exact PR three-dot diff)
  • Model: gpt-5.5

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


Review Summary

Overall Risk: MEDIUM

Findings

[MEDIUM] RPC Rename And Removal Break Existing Clients

  • Category: Protobuf
  • Location: proto/device_set/v1/device_set.proto:32
  • Description: The PR removes AddDevicesToDeviceSet / RemoveDevicesFromDeviceSet and replaces them with AddDevicesToGroup / RemoveDevicesFromGroup. It also removes the legacy DeviceCollectionService add/remove RPCs. Connect/gRPC method names are part of the API path, so existing clients calling the old methods will receive unimplemented/404-style failures.
  • Impact: Existing SDKs, automations, or a rolling deploy with old frontend/new backend or new frontend/old backend can no longer mutate group/rack membership. That is a reliability and compatibility regression for a public protobuf API.
  • Recommendation: Keep the old RPCs as deprecated aliases for at least one compatibility window, forwarding group operations to the new group paths and preserving auth checks. If this is intentionally breaking, move it behind an explicit versioned API change and document deployment ordering.

[MEDIUM] Rack Assignment Audit Can Report Unrelated Site Changes

  • Category: Reliability
  • Location: server/internal/domain/collection/service.go:990
  • Description: AssignDevicesToRack now builds cascade audit metadata by calling GetDeviceSiteIDsByMembership for every current member of the target rack, then buildDeviceSiteChanges over that full set. The actual cascade update immediately after is limited to params.DeviceIdentifiers, so the audit metadata can include unrelated rack members that were not touched by this assignment.
  • Impact: Activity logs can falsely claim unrelated devices had implicit site changes, or set site_cascade metadata based on stale rack members even when the requested assignment did not cascade those devices. That weakens the audit trail for site/rack moves.
  • Recommendation: Filter priors to the requested identifiers before calling buildDeviceSiteChanges, or add a store query that returns site IDs only for params.DeviceIdentifiers in the target rack. Base total_affected and device_site_changes on the same device set passed to CascadeAddedDeviceSites.

Notes

No changed shell-out, Nmap/mDNS, plugin, Docker, Rust, frontend XSS, or pool/wallet rewrite logic stood out in the reviewed diff. I did not run tests; this was a read-only diff review.


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

@flesher flesher force-pushed the feat/atomic-cross-site-reparent-2of4 branch from ac47c99 to c73d160 Compare June 15, 2026 21:41
@flesher flesher force-pushed the refactor/group-rpcs-cleanup-3of4 branch from 7acd632 to 9266fb5 Compare June 15, 2026 21:42

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

ℹ️ 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/internal/handlers/deviceset/translate.go Outdated
@flesher flesher force-pushed the feat/atomic-cross-site-reparent-2of4 branch from c73d160 to 3931213 Compare June 15, 2026 21:50
@flesher flesher force-pushed the refactor/group-rpcs-cleanup-3of4 branch from 9266fb5 to fe970da Compare June 15, 2026 21:50
@flesher flesher force-pushed the feat/atomic-cross-site-reparent-2of4 branch from 3931213 to 44a1f0c Compare June 15, 2026 22:01
@flesher flesher force-pushed the refactor/group-rpcs-cleanup-3of4 branch from fe970da to 17fa0d1 Compare June 15, 2026 22:01
@flesher flesher force-pushed the feat/atomic-cross-site-reparent-2of4 branch from 44a1f0c to 184614e Compare June 15, 2026 22:21
@flesher flesher force-pushed the refactor/group-rpcs-cleanup-3of4 branch from 17fa0d1 to d75d80e Compare June 15, 2026 22:21
@flesher flesher force-pushed the feat/atomic-cross-site-reparent-2of4 branch from 184614e to b53a01f Compare June 16, 2026 00:02
@flesher flesher force-pushed the refactor/group-rpcs-cleanup-3of4 branch 2 times, most recently from 354b458 to 24720f1 Compare June 16, 2026 01: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: 24720f12db

ℹ️ 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/device_set/v1/device_set.proto
Base automatically changed from feat/atomic-cross-site-reparent-2of4 to main June 16, 2026 18:55
Adds AddDevicesToGroup / RemoveDevicesFromGroup as type-specific group
membership procedures, swaps AssignDevicesToRack.device_identifiers to
common.v1.DeviceSelector (device_list-only; all_devices rejected with
InvalidArgument), and removes the legacy generic procedures:
- device_set.v1: AddDevicesToDeviceSet, RemoveDevicesFromDeviceSet
- collection.v1: AddDevicesToCollection, RemoveDevicesFromCollection

Server changes:
- Group RPCs reject rack targets with InvalidArgument
- Domain Service methods replaced; store-level collection helpers
  retained as shared infrastructure for the new RPCs
- fleetimport migrates to AddDevicesToGroup + AssignDevicesToRack (the
  atomic prior-rack-clear is a no-op for fresh imports)
- DeviceSetService added to gRPC reflection so tooling can discover the
  new RPCs via grpcurl -list
- Handler permission map updated (collection legacy entries dropped;
  device_set entries swapped)

Client changes:
- RackOverviewPage drag flow uses assignDevicesToRack (atomic single
  miner move clears any prior rack membership, the desired behavior)
- MinerActionModalStack group operations use addDevicesToGroup
- useDeviceSets exposes addDevicesToGroup / removeDevicesFromGroup with
  signal support; legacy addDevicesToDeviceSet / removeDevicesFromDeviceSet
  hooks removed
- assignDevicesToRack hook constructs DeviceSelector(device_list)
  internally so callers still pass deviceIdentifiers: string[]

Tests:
- Service: AddDevicesToGroup / RemoveDevicesFromGroup happy path +
  rack-target rejection; existing add-to-collection cascade tests
  removed (rack adds flow through AssignDevicesToRack)
- Handler: AddDevicesToGroup / RemoveDevicesFromGroup happy path,
  rack-target rejection, permission gate; AssignDevicesToRack
  all_devices selector rejection; existing assign tests updated to the
  new selector shape

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@flesher flesher force-pushed the refactor/group-rpcs-cleanup-3of4 branch from 24720f1 to 9fdfa4a Compare June 16, 2026 18:56

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

ℹ️ 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/internal/domain/fleetimport/importer.go Outdated
flesher added 3 commits June 16, 2026 13:04
common.v1.DeviceSelector.device_list replaced the deprecated
`repeated string device_identifiers` field but did not carry over its
buf.validate constraints (min_items: 1, max_items: 10000,
items.string.{min_len: 1, max_len: 256}). Empty strings + oversized
lists flowed through to the store layer where they either silently
matched no rows (while AssignedCount still reported based on requested
count) or amplified ANY()-expansion load.

Enforce the same caps centrally in deviceresolver.ValidateDeviceIdentifiers
and call it from both entry points that unwrap the device_list variant:
deviceresolver.resolveExplicitDevices (used by the group RPCs) and
deviceset.identifiersFromAssignSelector (AssignDevicesToRack's direct
unwrap). Handler tests pin the InvalidArgument code for empty,
over-length, and oversized cases.
AssignDevicesToRackResult.AssignedCount returns final-membership count
(devices whose membership now points at the target rack, including
devices that were already there before the call). The bulk importer
previously used it to accumulate devices_assigned, which overstated the
count on re-imports that re-targeted devices already in the rack — the
old AddDevicesToCollection.AddedCount returned only newly-inserted rows.

Expose AddDevicesToCollection's row count separately as
NewlyAssignedCount on the service result (no proto change — internal
Go-only) and switch the importer's devices_assigned accumulator to use
it. The activity log + handler response continue to use the
final-membership semantics on AssignedCount.

Adds importer_rack_assign_test.go covering the re-import path with a
fake CollectionManager that returns AssignedCount=3 + NewlyAssignedCount=1
to confirm only the newly-inserted count flows into the result.
Adds TestAddDevicesToGroup_RejectsCrossOrgTarget to pin the wire-visible
NotFound code when target_group_id belongs to a different org. The
service layer's GetCollection(orgID, id) already enforces the org-scoped
predicate, but this test exercises the end-to-end path so a future
refactor cannot accidentally leak the existence of cross-org collections.

The all_devices rejection (T1) and rack-target rejection on group RPCs
(T2) are already covered by the existing TestAssignDevicesToRack_RejectsAllDevicesSelector,
TestAddDevicesToGroup_RejectsRackTarget, and
TestRemoveDevicesFromGroup_RejectsRackTarget tests; no new coverage
needed there.
Copilot AI review requested due to automatic review settings June 16, 2026 20:06
gocritic unlambda: replace `func(s string) string { return networking.NormalizeMAC(s) }`
with the direct function reference.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

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 (3/4) continues the device-set refactor by replacing the legacy “generic collection membership” RPC surface with type-specific DeviceSetService RPCs, migrating remaining server and client call sites, and tightening rack assignment to use common.v1.DeviceSelector (explicit list only).

Changes:

  • Introduces type-specific group membership RPCs (AddDevicesToGroup, RemoveDevicesFromGroup) and removes legacy add/remove RPCs from collection.v1 and device_set.v1.
  • Refactors rack assignment (AssignDevicesToRack) to accept common.v1.DeviceSelector and rejects all_devices at the handler boundary; adds server-side identifier list validation.
  • Migrates client flows (rack drag/slot flow + miner group actions) and importer paths to the new RPCs, plus updates reflection and permissions wiring.

Reviewed changes

Copilot reviewed 18 out of 24 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/internal/handlers/middleware/rpc_permissions.go Removes legacy RPC permission entries; adds permissions for new group RPC procedures.
server/internal/handlers/deviceset/translate.go Translates DeviceSelector → identifier list for rack assignment; rejects unsupported selector variants.
server/internal/handlers/deviceset/handler.go Switches handler endpoints to AddDevicesToGroup/RemoveDevicesFromGroup; threads selector-based rack assignment translation errors.
server/internal/handlers/deviceset/handler_test.go Adds handler tests for selector rejection/validation and new group RPC behavior + permission checks.
server/internal/handlers/collection/handler.go Deletes legacy collection membership RPC handlers.
server/internal/domain/foremanimport/service_test.go Updates fakes to match new type-specific collection manager interface.
server/internal/domain/fleetimport/importer.go Migrates importer group adds and rack assignment to new type-specific methods; uses NewlyAssignedCount.
server/internal/domain/fleetimport/importer_rack_assign_test.go Adds coverage ensuring rack re-import uses NewlyAssignedCount semantics.
server/internal/domain/deviceresolver/resolver.go Centralizes identifier-list validation (count + per-item bounds) for explicit selectors.
server/internal/domain/collection/service.go Adds group-specific add/remove APIs; extends rack assignment result with NewlyAssignedCount.
server/internal/domain/collection/service_test.go Updates tests to cover group add/remove flows and rack-target rejection.
server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go Generated — updates connect stubs/procedure constants for new RPCs.
server/generated/grpc/collection/v1/collectionv1connect/collection.connect.go Generated — removes connect stubs/procedure constants for legacy RPCs.
server/cmd/fleetd/main.go Adds DeviceSetService to reflection-enabled services list.
proto/device_set/v1/device_set.proto Defines new group RPCs; updates rack assignment request to DeviceSelector (explicit list only).
proto/collection/v1/collection.proto Removes legacy generic add/remove membership RPCs/messages.
client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.tsx Migrates rack drag/slot flow to assignDevicesToRack.
client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.test.tsx Updates mocks for the new hook surface.
client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerActionModalStack.tsx Migrates group operation flow to addDevicesToGroup.
client/src/protoFleet/api/useDeviceSets.ts Adds new group hooks + selector-based rack assignment; integrates AbortSignal handling.
client/src/protoFleet/api/generated/device_set/v1/device_set_pb.ts Generated — reflects proto changes for new RPCs and selector field changes.
client/src/protoFleet/api/generated/collection/v1/collection_pb.ts Generated — reflects removal of legacy membership RPCs.

Comment thread client/src/protoFleet/api/useDeviceSets.ts
Comment thread client/src/protoFleet/api/useDeviceSets.ts
Comment thread client/src/protoFleet/api/useDeviceSets.ts 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: 5eac3bf611

ℹ️ 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".

flesher and others added 2 commits June 16, 2026 13:36
… rack selector comment

addDevicesToGroup and removeDevicesFromGroup now short-circuit with an
"No devices selected." error when no allDevices flag is set and the
identifier list is empty, instead of issuing an RPC that the server
rejects with InvalidArgument. Also rewords the assignDevicesToRack
selector comment to accurately describe why we always emit device_list
(the server rejects all_devices for that RPC) rather than implying the
construction avoids InvalidArgument on empty input.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ToRack

The atomic rack-reassign path lost the cross-site cascade audit trail
that the old AddDevicesToCollection flow emitted: site_id on the
activity event, plus Metadata keys (site_cascade, final_site_id,
site_reassigned_count, device_site_changes, total_affected, truncated).
Slot-search rack moves and any reparent that crosses sites silently
dropped this audit data.

Capture per-device priors via GetDeviceSiteIDsByMembership before
CascadeAddedDeviceSites runs (same ordering as the CreateCollection
cascade path), feed buildDeviceSiteChanges to produce the metadata
shape audit consumers already understand, and emit them on the
assign_devices_to_rack activity event. The no-op same-site case keeps
the event Metadata-free so consumers can distinguish "implicit move"
from "membership-only change".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@flesher flesher merged commit fa17796 into main Jun 16, 2026
78 checks passed
@flesher flesher deleted the refactor/group-rpcs-cleanup-3of4 branch June 16, 2026 21:44
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.

3 participants