Skip to content

fix(cli): stop silently ignoring fleet enrollments behind a project pin - #1439

Merged
khaliqgant merged 5 commits into
mainfrom
fix/1432-fleet-enrollment-pin-warnings
Aug 6, 2026
Merged

fix(cli): stop silently ignoring fleet enrollments behind a project pin#1439
khaliqgant merged 5 commits into
mainfrom
fix/1432-fleet-enrollment-pin-warnings

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 6, 2026

Copy link
Copy Markdown
Member

Fixes the top three items in #1432. The remaining four are deferred to follow-up issues (listed below) rather than piled into one PR.

The bug

A project pin at <repo>/.agentworkforce/relay/workspace-key.json without an enrolledNodeId made relay node up skip the fleet enrollment store entirely — packages/cli/src/cli/commands/node.ts:125 was a bare if (session) return undefined; with no output, while the sibling branch at :147-149 warned. No credentials, no heartbeat, no message. The Cloud dashboard showed the node, fleet nodes run from that repo showed a different roster, and nothing said the two were different workspaces. One machine was enrolled but invisible for five days.

What changed

1. node up warns before returning (commands/node.ts) — highest value, lowest risk. The warning names how many stored enrollments are being ignored and how to recover. It is gated on the store actually holding enrollments, so a plain pinned project with no fleet enrollment stays quiet; a store that cannot be read still warns (without a count) and still starts, because nothing on this path needs it.

2. cloud enroll records the node on the pin (commands/cloud.ts + new lib/enrollment-pin.ts) — previously it wrote only fleet-enrollments.json, so "enrolled into A while pinned to B" was silently reachable. A pin that already names a different node is reported and left untouched: the one-time enrollment token is already redeemed by the time this runs, so silently repointing a pin would trade one invisible mismatch for another. Every pin failure is reported and swallowed — a redeemed enrollment is never failed by a pin problem, and --json stdout stays parseable.

3. workspace switch|join preserves enrolledNodeId (lib/workspace-session.ts) — it called writeProjectWorkspaceKey with no options, manufacturing exactly the pin state fix 1 warns about.

Deferred, with reasons

Scoped out to keep this reviewable; all four are real and none are regressions introduced here:

Test bar

Every regression test here was run against the unfixed source and fails there. Mutations applied one at a time, each reverting exactly one fix:

Mutation Result
node.ts reverted to origin/main 2 failed / 19 passed
workspace-session.ts reverted to origin/main 1 failed / 7 passed
cloud.ts reverted to origin/main 4 failed / 63 passed
enrollment-pin.ts body reduced to pre-fix no-op 3 failed / 2 passed
all restored 101 passed / 101

The silent case is asserted specifically: warns that a project pin without an enrolled node id is shadowing stored enrollments asserts the warning text is emitted (toHaveBeenCalledTimes(1) plus content), not merely that behavior changed. Against the unfixed blob it fails with expected "vi.fn()" to be called 1 times, but got 0 times.

The tests that pass in both directions are guards, not regression tests — stays quiet when a project pin shadows nothing, does not warn ... when the pin names an enrolled node, does not invent an enrolled node id, leaves an unpinned project alone.

Gates

  • npm run typecheck — exit 0
  • npx vitest run1754 passed, 16 skipped, 5 failed
  • npm run lint — 0 errors (76 pre-existing warnings; none in the files touched here)
  • npm run format:check — clean

The 5 failures are pre-existing on origin/main and unrelated (telemetry machine-id / auth headers). Verified by stashing this branch and re-running the same three files on a clean tree: identical 5 failed | 71 passed. Files: telemetry/client.test.ts (2), agent-relay-mcp.startup.test.ts (2), packages/cloud/src/auth.test.ts (1).

Not covered

No live re-enrollment against Cloud was performed — that needs a one-time enrollment token and would burn it. The behavior is covered at the unit level on both the pin lib (real temp-dir filesystem) and the command wiring.

Refs #1432

🤖 Generated with Claude Code

Review in cubic

A project workspace pin without an `enrolledNodeId` made `relay node up`
skip the fleet enrollment store entirely — no warning, no credentials, no
heartbeat. The Cloud dashboard showed the node, `fleet nodes` showed a
different roster, and nothing said the two were different workspaces
(#1432). One machine was enrolled but invisible for five days.

Three fixes, smallest first:

1. `node.ts` warns before returning when a pin shadows stored enrollments,
   naming how many are being ignored and how to recover. The sibling branch
   already warned; this one did not.
2. `cloud enroll` records the enrolled node on the project pin so `node up`
   in that repo serves it. A pin naming a different node is reported and
   left untouched — the one-time token is already redeemed by then, so
   repointing it would trade one invisible mismatch for another. Pin
   failures never fail a completed enrollment.
3. `workspace switch|join` preserves the pin's `enrolledNodeId` instead of
   dropping it, which is what manufactured the broken state in fix 1.

Refs #1432

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

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI reconciles enrolled Fleet nodes with project workspace pins. node up warns about shadowed enrollments. Workspace switching preserves or clears enrolled node IDs according to the workspace key. Tests cover linking, conflicts, failures, JSON output, and startup behavior.

Changes

Enrollment pin consistency

Layer / File(s) Summary
Preserve enrolled node IDs
packages/cli/src/cli/lib/workspace-session.ts, packages/cli/src/cli/lib/workspace-session.test.ts
Session persistence retains an enrolled node ID for the same workspace and clears it when switching workspaces.
Report cleared enrollment
packages/cli/src/cli/commands/workspace.ts, packages/cli/src/cli/commands/workspace.test.ts, packages/cli/src/cli/agent-relay-mcp.ts, packages/cli/src/cli/agent-relay-mcp.startup.test.ts
Workspace commands and MCP tools report the cleared node ID and re-enrollment command when applicable.
Link enrolled nodes to project pins
packages/cli/src/cli/lib/enrollment-pin.ts, packages/cli/src/cli/lib/enrollment-pin.test.ts
The helper links unassigned pins, preserves existing assignments, reports conflicts, and maintains the workspace key.
Reconcile pins after cloud enrollment
packages/cli/src/cli/commands/cloud.ts, packages/cli/src/cli/commands/cloud.test.ts, CHANGELOG.md
cloud enroll reconciles the enrolled node with the project pin. Conflicts and write failures produce warnings without failing enrollment. JSON output remains parseable.
Warn during node startup
packages/cli/src/cli/commands/node.ts, packages/cli/src/cli/commands/node.test.ts
node up warns when stored enrollments are shadowed by a pin without an enrolled node ID. Store read failures do not block startup.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant CloudEnroll
  participant FleetEnrollmentStore
  participant ProjectWorkspacePin
  Operator->>CloudEnroll: enroll node
  CloudEnroll->>FleetEnrollmentStore: persist enrolled node
  CloudEnroll->>ProjectWorkspacePin: reconcile project pin
  ProjectWorkspacePin-->>CloudEnroll: link, unchanged, conflict, or error
  CloudEnroll-->>Operator: success output and applicable warning
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: willwashburn

Poem

A rabbit linked a node to a pin,
Kept old assignments safely in.
When workspaces changed, it cleared with care,
And warned when stored nodes hid there.
“Hop onward,” said the rabbit.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary fix for fleet enrollments hidden by project pins.
Description check ✅ Passed The description thoroughly explains the changes, testing, deferred scope, and known failures, although it does not use the template headings or checkboxes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1432-fleet-enrollment-pin-warnings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: fe20d6ea9b

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +52
const enrolledNodeId = readProjectWorkspaceSession(projectDataDir)?.enrolledNodeId;
writeProjectWorkspaceKey(projectDataDir, workspaceKey, {
...(enrolledNodeId ? { enrolledNodeId } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear the enrollment when switching workspaces

When a project enrolled as node A in workspace A runs workspace switch or workspace join for workspace B, this preserves A's enrolledNodeId beside B's key. On the next node up, resolveEnrollmentForProject in commands/node.ts resolves solely by that node ID, and applyResolvedNodeSession applies the enrollment without applying the newly pinned key, so the broker reconnects to A while SDK commands use B. This recreates the split-workspace behavior the change is meant to prevent; the association must be cleared unless the selected workspace is verified to match the enrollment.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, and fixed in d40a3f013. You and the other reviewer landed this independently and you were right — preserving the id unconditionally recreated the split the PR exists to remove, because applyResolvedNodeSession returns applyEnrollment(record) without ever calling resumeProjectWorkspace, so the pinned key is never applied to the broker.

Fixed by comparing what can be compared locally. The enrollment store holds workspace ids and the pin holds a key, so "verify the enrollment belongs to the new workspace" is not answerable offline — but key-against-key is:

  • same workspace key re-selected → keep enrolledNodeId (the original bug: dropping it manufactured the pin node up now warns about)
  • different workspace key → clear it, and workspace switch|join prints which node association was dropped and how to re-enroll, rather than dropping it silently

persistWorkspaceSession now returns { clearedEnrolledNodeId? } so the command layer can report it. Regression test: clears the enrolled Fleet node id when moving to a different workspace — fails against the unconditional-preserve version, passes now.

return { status: 'conflict', nodeId, pinnedNodeId: session.enrolledNodeId, pinPath };
}

writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not link enrollments to an unrelated workspace pin

If cloud enroll --workspace B (or a token minted for B) is run from a project whose unassociated pin contains workspace A's key, this writes B's node ID alongside the unchanged A key without checking their workspaces. node up then resolves B's credentials by node ID and ignores the pinned key, while other project commands continue resolving A from the same file; the command nevertheless reports that the pin was successfully linked. Leave the pin unlinked with a warning unless the enrollment workspace can be verified against it, or update both sides together.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly accepted, and the overclaim is fixed in d40a3f013.

The diagnosis is right: nothing here verifies that the enrollment's workspace matches the pinned key, and it cannot — the enrollment store records workspace ids while the pin records a key, with no local mapping between them. That unreconciled gap is called out as the root cause in #1432 and is what #1440/#1442 depend on closing.

Where I did not follow the recommendation: "leave the pin unlinked with a warning unless verifiable" would mean never linking, since it is never locally verifiable — and that removes the fix for the case actually reported. In #1432 the repo was pinned to an auto-provisioned throwaway workspace (204337648549896192) and the user enrolled into rw_7ccfea89 specifically so node up would serve the enrolled node. Refusing to link leaves that user exactly where they started. (The throwaway pin itself is the separate bug in #1440.)

What is fixed is the part I agree was wrong — the command claiming a link it had not verified. The message now names the workspace that will actually be served and states plainly what was not checked:

Linked this project's workspace pin (…) to node …, so 'relay node up' here serves this enrollment in workspace rw_123. The pinned workspace key was not verified against that workspace — if they differ, agent commands in this project keep using the pinned one.

Test cloud enroll links the enrolled node to this project workspace pin now asserts both the served workspace id and the caveat, and fails against the previous message.

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/cli/lib/enrollment-pin.ts">

<violation number="1" location="packages/cli/src/cli/lib/enrollment-pin.ts:66">
P2: Concurrent enrollments can still silently repoint a project pin: the conflict check and this write are not atomic. Protect the read/check/write with a file lock or add an atomic compare-and-set so a second enrollment reports a conflict instead of overwriting the first link.</violation>

<violation number="2" location="packages/cli/src/cli/lib/enrollment-pin.ts:66">
P2: linkEnrolledNodeToProjectPin links the freshly enrolled nodeId to the existing pin (`writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId })`) without verifying that the node's enrollment workspace actually matches `session.workspaceKey`. If the project pin currently holds a different workspace's key (e.g. workspace A) and `cloud enroll` redeems a token for workspace B, this silently links B's node id onto A's key. Subsequent `node up` runs then resolve B's node credentials by id while other project commands keep resolving workspace A from the same pin file — yet the command reports the link as successful (`status: 'linked'`). Consider validating the enrollment's workspace against the pinned workspace key before linking, or reporting a mismatch instead of linking.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

return { status: 'conflict', nodeId, pinnedNodeId: session.enrolledNodeId, pinPath };
}

writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId });

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.

P2: Concurrent enrollments can still silently repoint a project pin: the conflict check and this write are not atomic. Protect the read/check/write with a file lock or add an atomic compare-and-set so a second enrollment reports a conflict instead of overwriting the first link.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/lib/enrollment-pin.ts, line 66:

<comment>Concurrent enrollments can still silently repoint a project pin: the conflict check and this write are not atomic. Protect the read/check/write with a file lock or add an atomic compare-and-set so a second enrollment reports a conflict instead of overwriting the first link.</comment>

<file context>
@@ -0,0 +1,68 @@
+    return { status: 'conflict', nodeId, pinnedNodeId: session.enrolledNodeId, pinPath };
+  }
+
+  writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId });
+  return { status: 'linked', nodeId, pinPath };
+}
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declined, with reasoning.

The read/check/write is genuinely not atomic, so the description is accurate. What I dispute is the consequence:

  • writeProjectWorkspaceKey is already write-to-temp-then-rename, with a per-write nonce and wx exclusive creation. The pin file itself cannot be torn or partially written by a concurrent writer.
  • The only thing the race can lose is a conflict report. Interleaved as read(A)/read(A)/write(B)/write(C), both runs see an unlinked pin, both link, last writer wins — the file is valid and names one real node. No corruption, no partial state.
  • Reaching that interleaving requires two cloud enroll runs in the same project within the same few milliseconds, each holding its own one-time enrollment token, since a token is consumed on redemption. This is an interactive, operator-initiated command, not something a daemon or a loop drives.

A lockfile for that is disproportionate, and it adds a real failure mode this command must not have: enrollment runs after the one-time token is already burned, so a stale lock left by a killed process would block the pin update on every subsequent enroll, in a code path whose whole design constraint is that nothing after redemption may fail. The current code already treats every pin failure as report-and-continue for that reason.

If concurrent enrollment ever becomes a real workflow, the right fix is a compare-and-set inside writeProjectWorkspaceKey — one atomic primitive shared by every writer of that file, including workspace switch and the broker's own pin write at broker-lifecycle.ts:1583 — rather than a lock around one caller. Noted on #1440, which already has to revisit that write path.

Comment thread packages/cli/src/cli/lib/workspace-session.ts Outdated
return { status: 'conflict', nodeId, pinnedNodeId: session.enrolledNodeId, pinPath };
}

writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId });

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.

P2: linkEnrolledNodeToProjectPin links the freshly enrolled nodeId to the existing pin (writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId })) without verifying that the node's enrollment workspace actually matches session.workspaceKey. If the project pin currently holds a different workspace's key (e.g. workspace A) and cloud enroll redeems a token for workspace B, this silently links B's node id onto A's key. Subsequent node up runs then resolve B's node credentials by id while other project commands keep resolving workspace A from the same pin file — yet the command reports the link as successful (status: 'linked'). Consider validating the enrollment's workspace against the pinned workspace key before linking, or reporting a mismatch instead of linking.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/lib/enrollment-pin.ts, line 66:

<comment>linkEnrolledNodeToProjectPin links the freshly enrolled nodeId to the existing pin (`writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId })`) without verifying that the node's enrollment workspace actually matches `session.workspaceKey`. If the project pin currently holds a different workspace's key (e.g. workspace A) and `cloud enroll` redeems a token for workspace B, this silently links B's node id onto A's key. Subsequent `node up` runs then resolve B's node credentials by id while other project commands keep resolving workspace A from the same pin file — yet the command reports the link as successful (`status: 'linked'`). Consider validating the enrollment's workspace against the pinned workspace key before linking, or reporting a mismatch instead of linking.</comment>

<file context>
@@ -0,0 +1,68 @@
+    return { status: 'conflict', nodeId, pinnedNodeId: session.enrolledNodeId, pinPath };
+  }
+
+  writeProjectWorkspaceKey(dataDir, session.workspaceKey, { enrolledNodeId: nodeId });
+  return { status: 'linked', nodeId, pinPath };
+}
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly accepted, and the overclaim is fixed in d40a3f013.

The diagnosis is right: nothing here verifies that the enrollment's workspace matches the pinned key, and it cannot — the enrollment store records workspace ids while the pin records a key, with no local mapping between them. That unreconciled gap is called out as the root cause in #1432 and is what #1440/#1442 depend on closing.

Where I did not follow the recommendation: "leave the pin unlinked with a warning unless verifiable" would mean never linking, since it is never locally verifiable — and that removes the fix for the case actually reported. In #1432 the repo was pinned to an auto-provisioned throwaway workspace (204337648549896192) and the user enrolled into rw_7ccfea89 specifically so node up would serve the enrolled node. Refusing to link leaves that user exactly where they started. (The throwaway pin itself is the separate bug in #1440.)

What is fixed is the part I agree was wrong — the command claiming a link it had not verified. The message now names the workspace that will actually be served and states plainly what was not checked:

Linked this project's workspace pin (…) to node …, so 'relay node up' here serves this enrollment in workspace rw_123. The pinned workspace key was not verified against that workspace — if they differ, agent commands in this project keep using the pinned one.

Test cloud enroll links the enrolled node to this project workspace pin now asserts both the served workspace id and the caveat, and fails against the previous message.

Proactive Runtime Bot and others added 2 commits August 6, 2026 10:19
Codex and cubic both landed the same finding on the first cut, and they
were right: preserving `enrolledNodeId` unconditionally in
`persistWorkspaceSession` recreated the split it was meant to remove.
`node up` resolves an enrollment by node id alone and applies its
credentials *without* applying the pinned key, so a project that switched
from workspace A to B would run the broker as A's node while every other
command read B.

The enrollment store holds workspace ids and the pin holds a key, so the
two cannot be reconciled locally. What can be compared is key against key:

- Re-selecting the same workspace keeps the enrolled node (the original
  fix — dropping it manufactured the pin `node up` warns about).
- Moving to a different workspace clears it, and `workspace switch|join`
  now says so instead of dropping it silently.
- `cloud enroll` still links the pin, but no longer implies the link was
  verified: it names the workspace that will actually be served and states
  that the pinned key was not checked against it.

Declined cubic's P2 on read/check/write atomicity in
`linkEnrolledNodeToProjectPin`: `writeProjectWorkspaceKey` is already
write-then-rename, two concurrent `cloud enroll` runs in one project would
each need their own one-time token, and the race can only lose a conflict
*report*, never corrupt the pin. A lockfile is disproportionate here.

Refs #1432

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes CodeRabbit's docstring-coverage pre-merge warning on the one
undocumented function in a file this branch already touches.

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/cli/commands/workspace.ts`:
- Around line 20-32: Update the workspace create flow around
persistWorkspaceSession to retain its PersistWorkspaceSessionResult and pass it
through the command’s structured JSON output. When clearedEnrolledNodeId is
present, include that node ID and a relay cloud enroll recovery instruction in
JSON-safe result fields, while preserving valid JSON for all workspace create
responses.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1023c1e6-20cb-4833-9fa2-5f14588b2d3e

📥 Commits

Reviewing files that changed from the base of the PR and between fe20d6e and d40a3f0.

📒 Files selected for processing (6)
  • packages/cli/src/cli/commands/cloud.test.ts
  • packages/cli/src/cli/commands/cloud.ts
  • packages/cli/src/cli/commands/workspace.test.ts
  • packages/cli/src/cli/commands/workspace.ts
  • packages/cli/src/cli/lib/workspace-session.test.ts
  • packages/cli/src/cli/lib/workspace-session.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/cli/src/cli/commands/cloud.ts
  • packages/cli/src/cli/commands/cloud.test.ts

Comment thread packages/cli/src/cli/commands/workspace.ts

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/cli/lib/workspace-session.ts">

<violation number="1" location="packages/cli/src/cli/lib/workspace-session.ts:80">
P2: Changing to a different project key clears `enrolledNodeId`, but MCP `set_workspace_key`/`create_workspace` and CLI `workspace create` discard the returned `clearedEnrolledNodeId`; these flows report success while the old fleet enrollment is shadowed until a later `node up` warning. Consuming this result in every pin-changing caller, with a structured warning where JSON/MCP output is required, would keep the clearing behavior visible.

(Based on your team's feedback about Reconcile Node IDs by Workspace Key (2026-08-06).)</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

switchWorkspace(name, options.env);
}

return existing?.enrolledNodeId && !enrolledNodeId

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.

P2: Changing to a different project key clears enrolledNodeId, but MCP set_workspace_key/create_workspace and CLI workspace create discard the returned clearedEnrolledNodeId; these flows report success while the old fleet enrollment is shadowed until a later node up warning. Consuming this result in every pin-changing caller, with a structured warning where JSON/MCP output is required, would keep the clearing behavior visible.

(Based on your team's feedback about Reconcile Node IDs by Workspace Key (2026-08-06).)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/lib/workspace-session.ts, line 80:

<comment>Changing to a different project key clears `enrolledNodeId`, but MCP `set_workspace_key`/`create_workspace` and CLI `workspace create` discard the returned `clearedEnrolledNodeId`; these flows report success while the old fleet enrollment is shadowed until a later `node up` warning. Consuming this result in every pin-changing caller, with a structured warning where JSON/MCP output is required, would keep the clearing behavior visible.

(Based on your team's feedback about Reconcile Node IDs by Workspace Key (2026-08-06).) </comment>

<file context>
@@ -56,4 +76,8 @@ export function persistWorkspaceSession(options: PersistWorkspaceSessionOptions)
     switchWorkspace(name, options.env);
   }
+
+  return existing?.enrolledNodeId && !enrolledNodeId
+    ? { clearedEnrolledNodeId: existing.enrolledNodeId }
+    : {};
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, fixed in 9ab642809. CodeRabbit landed the workspace create half of this independently; your version named the MCP callers too, and all three are now covered.

  • workspace createclearedEnrolledNodeId + warning inside its JSON payload, so the report cannot break a parsing caller.
  • MCP create_workspace and set_workspace_key → returned through the warning field both tools already carried for persistence failures, which is the structured path you asked for.

One shared describeClearedEnrollment supplies the wording to all of them, so the CLI and the MCP tools cannot describe the same event differently. Both test files bind the real implementation via importOriginal instead of a stand-in.

New regression tests fail against the discard-the-result version: workspace create reports a dropped enrolled node inside its JSON output and, on the MCP side, reports an enrolled fleet node dropped by joining another workspace.

CodeRabbit and cubic caught the same gap in the previous commit: only
`workspace switch|join` consumed the new `clearedEnrolledNodeId`. The
other three writers of the pin discarded it and reported success while the
project's fleet enrollment was dropped, leaving the next `node up` warning
as the first mention — the same silence this branch exists to remove.

- `workspace create` carries it in its JSON output (`clearedEnrolledNodeId`
  plus a `warning`), so the report cannot break a parsing caller.
- MCP `create_workspace` and `set_workspace_key` return it through the
  `warning` field they already had for persistence failures.
- `describeClearedEnrollment` is now the single wording shared by all of
  them, and both test suites bind the real implementation via
  `importOriginal` rather than a stand-in, so output cannot drift past the
  assertions.

`workspace create` is the sharpest case: a freshly minted key never matches
an existing pin, so it always clears.

Refs #1432

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

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/cli/src/cli/agent-relay-mcp.ts
The previous commit made `create_workspace` and `set_workspace_key` return
a cleared-enrollment message through their existing `warning` field, but
left the tool descriptions saying `warning` appears only when persistence
failed. An MCP consumer reading that would take a successful create that
dropped an enrolled node for a failed save — and a fresh key never matches
an existing pin, so that case fires on every create over one.

Both descriptions now name both cases and say the text distinguishes them.

Refs #1432

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/cli/agent-relay-mcp.ts`:
- Around line 504-506: Track persistence failure independently in the
set_workspace_key flow around persistWorkspaceSession and
describeClearedEnrollment: use persistedMessage after a successful write,
appending the cleared-enrollment warning, while retaining the failure message
only when persistence fails. Update
packages/cli/src/cli/agent-relay-mcp.startup.test.ts lines 582-596 to assert the
cleared-enrollment response states that the key persisted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7df30b64-f237-43d5-a84c-1743b5e68f17

📥 Commits

Reviewing files that changed from the base of the PR and between ccff796 and f8c6448.

📒 Files selected for processing (5)
  • packages/cli/src/cli/agent-relay-mcp.startup.test.ts
  • packages/cli/src/cli/agent-relay-mcp.ts
  • packages/cli/src/cli/commands/workspace.test.ts
  • packages/cli/src/cli/commands/workspace.ts
  • packages/cli/src/cli/lib/workspace-session.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli/commands/workspace.test.ts

Comment on lines +504 to +506
// Joining a different workspace drops this project's enrolled fleet
// node; surface that here instead of at the next `node up`.
persistenceWarning = describeClearedEnrollment(persistWorkspaceSession({ workspaceKey: key }));

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep cleared enrollment separate from persistence failure.

A cleared enrollment means persistWorkspaceSession succeeded. The current persistenceWarning branch selects activeMessage, so set_workspace_key does not state that the key persisted despite the contract documented at Line 468.

  • packages/cli/src/cli/agent-relay-mcp.ts#L504-L506: Track persistence failure separately. Use persistedMessage plus the cleared-enrollment warning after a successful write.
  • packages/cli/src/cli/agent-relay-mcp.startup.test.ts#L582-L596: Assert that the cleared-enrollment response also says the key persisted.
Proposed fix
+      let persistenceFailed = false;
       let persistenceWarning: string | undefined;
       try {
         persistenceWarning = describeClearedEnrollment(persistWorkspaceSession({ workspaceKey: key }));
       } catch (error) {
+        persistenceFailed = true;
         // existing error message assignment
       }

-      const message = persistenceWarning ? `${activeMessage} ${persistenceWarning}` : persistedMessage;
+      const message = persistenceWarning
+        ? `${persistenceFailed ? activeMessage : persistedMessage} ${persistenceWarning}`
+        : persistedMessage;

Based on PR objectives: MCP tools must report cleared enrollment while preserving their output contract.

📍 Affects 2 files
  • packages/cli/src/cli/agent-relay-mcp.ts#L504-L506 (this comment)
  • packages/cli/src/cli/agent-relay-mcp.startup.test.ts#L582-L596
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/agent-relay-mcp.ts` around lines 504 - 506, Track
persistence failure independently in the set_workspace_key flow around
persistWorkspaceSession and describeClearedEnrollment: use persistedMessage
after a successful write, appending the cleared-enrollment warning, while
retaining the failure message only when persistence fails. Update
packages/cli/src/cli/agent-relay-mcp.startup.test.ts lines 582-596 to assert the
cleared-enrollment response states that the key persisted.

@khaliqgant
khaliqgant merged commit be073c5 into main Aug 6, 2026
41 checks passed
@khaliqgant
khaliqgant deleted the fix/1432-fleet-enrollment-pin-warnings branch August 6, 2026 09:02
khaliqgant added a commit that referenced this pull request Aug 6, 2026
CHANGELOG only. Kept both sides: main's four Fixed entries from #1429/#1439
and this branch's restart-reclaim Fixed entry plus its Security section.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant