Architecture Finding
Type: coupling / missing package boundary
Affected area: src/pkg/hub (v5 branch, HEAD 99fca74), importers src/pkg/dashboard and src/cmd/hive
pkg/hub (66,543 non-test lines) contains two deployment roles in a single Go package:
- SaaS control-plane server —
saas_*.go (~27,231 lines across 20 files), server.go (4,460), saas_provision.go, cluster_app_key.go, hub_generations*.go, oauth.go, journey/enrollment/clusters registry, etc.
- Spoke-side client helpers — heartbeat push (
StartHeartbeat, LastHeartbeatAttempt/Success, HeartbeatEnabled as package-level atomics), SSO verification (SpokeSSOPublicKeys, VerifySSOTokenAcrossKeys), terminal assertions (MintTerminalAssertion, TerminalSigningKey), SpokeInviteKey, self-upgrade (UpgradeSelfToSHA, RolloutRestartSelf), fd gauge, cluster-health collection.
The spoke consumers (pkg/dashboard/{api,api_contribute,server,session}.go, cmd/hive/{hubwire,configwire,hookwire,main*}.go) use ~60 spoke-side symbols, yet importing them compiles the entire control plane into every spoke build and couples both roles' internal state.
Much of the shared state is package-level mutable globals (heartbeat atomics, contribute queue/fleet snapshot, signing keys), so the boundary between "what the spoke may call" and "what only the hub process may touch" exists only by convention — nothing structural stops spoke code from reaching into control-plane state (e.g. pkg/dashboard tests already drive hub.ReadyQueue, hub.FleetSnapshot, hub.SetHeartbeatStateForTest).
Impact
Recommendation
Phase the split along the deployment-role seam, keeping cmd/hive's dual-mode binary intact:
- Extract spoke-facing client code into
pkg/hub/spoke (or pkg/spokeclient): heartbeat push + status, task-status push, SSO verify, terminal assertion mint/verify, invite key, self-upgrade. This is mostly mechanical (move + import rewrite) since these files barely reference saas internals.
- Move wire/DTO types shared by both sides (HeartbeatPayload, TaskStatusPayload, AgentSummary, LeaderboardEntry, *Wire types) into
pkg/hub/wire with no behavior.
- Leave the control plane in
pkg/hub (or pkg/hub/saas); its only inward dependency becomes the wire package.
After the split, go list makes the spoke↔hub contract explicit and reviewable, and the compiler enforces it.
Coordination: open PRs #6046 (v4→v5 sync: cmd/hive/hubwire.go, main_helpers.go) and #6047–#6049 (v4: pkg/hub heartbeat/health/saas) occupy these files — this refactor should wait until they land.
Filed by architect agent (ACMM L5 — hold-gated mode)
🐝 Hive Agent: architect | Instance: hosted-available-oke-11-placeholder-r05x | SHA: unknown
— hive: agent=architect backend=copilot model=claude-opus-4-6
Architecture Finding
Type: coupling / missing package boundary
Affected area:
src/pkg/hub(v5 branch, HEAD 99fca74), importerssrc/pkg/dashboardandsrc/cmd/hivepkg/hub(66,543 non-test lines) contains two deployment roles in a single Go package:saas_*.go(~27,231 lines across 20 files),server.go(4,460),saas_provision.go,cluster_app_key.go,hub_generations*.go,oauth.go, journey/enrollment/clusters registry, etc.StartHeartbeat,LastHeartbeatAttempt/Success,HeartbeatEnabledas package-level atomics), SSO verification (SpokeSSOPublicKeys,VerifySSOTokenAcrossKeys), terminal assertions (MintTerminalAssertion,TerminalSigningKey),SpokeInviteKey, self-upgrade (UpgradeSelfToSHA,RolloutRestartSelf), fd gauge, cluster-health collection.The spoke consumers (
pkg/dashboard/{api,api_contribute,server,session}.go,cmd/hive/{hubwire,configwire,hookwire,main*}.go) use ~60 spoke-side symbols, yet importing them compiles the entire control plane into every spoke build and couples both roles' internal state.Much of the shared state is package-level mutable globals (heartbeat atomics, contribute queue/fleet snapshot, signing keys), so the boundary between "what the spoke may call" and "what only the hub process may touch" exists only by convention — nothing structural stops spoke code from reaching into control-plane state (e.g.
pkg/dashboardtests already drivehub.ReadyQueue,hub.FleetSnapshot,hub.SetHeartbeatStateForTest).Impact
*ForTestescape hatches across package lines.Recommendation
Phase the split along the deployment-role seam, keeping
cmd/hive's dual-mode binary intact:pkg/hub/spoke(orpkg/spokeclient): heartbeat push + status, task-status push, SSO verify, terminal assertion mint/verify, invite key, self-upgrade. This is mostly mechanical (move + import rewrite) since these files barely reference saas internals.pkg/hub/wirewith no behavior.pkg/hub(orpkg/hub/saas); its only inward dependency becomes the wire package.After the split,
go listmakes the spoke↔hub contract explicit and reviewable, and the compiler enforces it.Coordination: open PRs #6046 (v4→v5 sync: cmd/hive/hubwire.go, main_helpers.go) and #6047–#6049 (v4: pkg/hub heartbeat/health/saas) occupy these files — this refactor should wait until they land.
Filed by architect agent (ACMM L5 — hold-gated mode)
🐝 Hive Agent:
architect| Instance:hosted-available-oke-11-placeholder-r05x| SHA:unknown— hive: agent=architect backend=copilot model=claude-opus-4-6