Skip to content

node up silently ignores fleet enrollment when a project workspace pin lacks enrolledNodeId #1432

Description

@khaliqgant

Summary

A project workspace pin (<repo>/.agentworkforce/relay/workspace-key.json) that lacks an enrolledNodeId makes relay node up silently ignore the fleet enrollment entirely. relay cloud enroll reports success, the node appears in the Cloud dashboard, and the node never heartbeats. Meanwhile relay fleet nodes run from that repo queries the pinned workspace, so it shows a completely different roster — with no error from either side.

Net user experience: the dashboard shows node A, fleet nodes shows node B, workspace active names a third answer, and nothing anywhere says the three are different workspaces.

Repro

cd <repo-with-a-workspace-key.json-pin>       # pin has no enrolledNodeId
relay cloud enroll --token 'ocl_node_enr_…' --enrollment-url 'https://agentrelay.com/cloud/api/v1/fleet/register' --name 'kjg-laptop'
# → Enrolled node "kjg-laptop" (node_2108…) in workspace rw_7ccfea89.
relay node up --background
# → Broker started. Broker PID: 96942
relay fleet nodes
# → shows node "cloud", never "kjg-laptop"

Roster after the above — kjg-laptop exists and has never heartbeat:

kjg-laptop | node_210851746276208640 | offline | live False | hb None | created 2026-08-05T20:09:33Z

Broker log (~/Library/Logs/agentworkforce/relay/cloud.log.2026-08-05), same run:

20:09:36 INFO relay_broker::runtime::init: reusing cached node token
         node_id=node_72241249b105bfd8b2edc3532abaec75 workspace_id=204337648549896192

workspace_id=204337648549896192 is the pinned auto-provisioned workspace — not rw_7ccfea89 where the enrollment lives.

Root cause

packages/cli/src/cli/commands/node.ts:112-131:

if (session?.enrolledNodeId) {  }
if (session) {
  return undefined;          // node.ts:125  ← pin present, no enrolledNodeId → enrollment store never read
}
return deps.resolveEnrollment({});   // only reached when there is NO pin

fleet-enrollments.json is never consulted, no RELAY_NODE_TOKEN/RELAY_NODE_ID is set, and no warning is printed — the sibling branch at node.ts:147-149 does warn, this one doesn't. Locked in by node.test.ts:239-257.

There is also no workspace-id-vs-key reconciliation anywhere: the enrollment store holds workspace ids, the pin holds a key, so per the comment at node.ts:166-169 a mismatch is bypassed rather than detected. The broker then registers an auto-derived node in the pinned workspace (crates/broker/src/runtime/init.rs:305-330, :743-778), which is why it looks healthy.

Proposed fixes

  1. node.ts:125 should warn() before returning — e.g. "project pinned to workspace <id>; ignoring N fleet enrollment(s). Run …". One line removes the entire class of confusion. Highest value / lowest risk.
  2. relay cloud enroll should update the pin's enrolledNodeId (or refuse loudly when the repo is pinned to a different workspace). Today it writes only fleet-enrollments.json (commands/cloud.ts:920-927), so "enrolled into A while pinned to B" is a silently reachable state.
  3. workspace switch|join should preserve enrolledNodeId — they call writeProjectWorkspaceKey(projectDataDir, workspaceKey) with no options (lib/workspace-session.ts:44-46), manufacturing exactly the broken state in fix fix(truncation): improvements #1.
  4. Stop minting a throwaway workspace on every enrolled start. When an enrollment applies, resumePinnedProjectWorkspace bails because RELAY_NODE_TOKEN is set (lib/broker-lifecycle.ts:1327-1330), the broker auto-provisions a fresh workspace, and then broker-lifecycle.ts:1583-1586 persists that throwaway key over the pin. Observed three separate free-plan workspaces all named relay-6bf605d7 created for one repo: 204337648549896192 (07-18), 210854606762164224, 210855543429939200. The node plane stays correct while the agent plane silently drifts to an empty workspace.
  5. fleet status should match the roster by nodeId, not node name. It reported "no node named \"kjg-laptop\" in the workspace" while holding the correct nodeId in the same payload, and separately matched an unrelated stale node that happened to share the repo-basename name.
  6. --workspace-key + enrollment is an unguarded footgun. It disables enrollment pickup (node.ts:170-175), so the intuitive "just point it at the right workspace" fix mints a duplicate node instead of serving the enrolled one. Warn when both an enrollment and an explicit key are present.
  7. Env-var recovery path needs --broker-name or it loops. With RELAY_WORKSPACE_KEY+RELAY_NODE_TOKEN+RELAY_NODE_ID but no --broker-name, the broker takes the repo basename and tries to rename the enrolled node to it:
    ERROR relay_broker::node_control: engine rejected a node control frame
      code=node_name_conflict Node name "cloud" is already enrolled
    ERROR relay_broker::node_control: … code=node_control_failed
      Failed query: update "nodes" set "name" = ? where …
    
    repeating indefinitely; heartbeats never land. Either derive the broker name from RELAY_NODE_ID or don't attempt the rename.

Working invocation (verified)

RELAY_WORKSPACE_KEY='<workspace key>' \
RELAY_NODE_TOKEN='<nt_live_… from fleet-enrollments.json>' \
RELAY_NODE_ID='node_210851746276208640' \
relay node up --broker-name kjg-laptop --background

Impact beyond one laptop

The same drift is visible across the fleet: nightcto-sf, nightcto-finn, nightcto-barry, and relayauth-finn-0803 all live in the auto-provisioned relay-6bf605d7 workspace rather than the workspace the Cloud dashboard reads, so their activity is invisible to fleet tooling. Diagnosing any of this currently requires reading the Rust broker log and resolving raw rk_live_* keys against /v1/workspace by hand.

Environment

agent-relay 11.4.0 (relay-broker/11.4.0), macOS (Darwin 25.5.0), 2026-08-05.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions