Skip to content

fix(cli): make workspace activation reversible and broker binding explicit - #1425

Open
khaliqgant wants to merge 1 commit into
mainfrom
fix/workspace-restore-rebind
Open

fix(cli): make workspace activation reversible and broker binding explicit#1425
khaliqgant wants to merge 1 commit into
mainfrom
fix/workspace-restore-rebind

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

  • keep workspace create activating, but record the displaced workspace and emit the required restore warning on stderr
  • add workspace restore with explicit no-record, deleted-record, and already-active errors
  • add workspace rebind <name> to pin only the current project and clear its old enrolled-node association
  • make first-run telemetry notices stderr-only so JSON stdout remains parseable
  • preserve an enrolled node's project workspace on restart, record workspace-binding provenance, and show that provenance in node status
  • make detached startup stop polling when its child exits, surface the redacted child failure, and avoid trying to kill a dead PID
  • document broker binding precedence and recovery; update the Unreleased changelog as Minor

Binding diagnosis

node up does not directly read the machine-global ~/.agentworkforce/relay/workspaces.json active selection. Its effective precedence is:

  1. --workspace-key
  2. AGENT_RELAY_WORKSPACE_KEY / RELAY_WORKSPACE_KEY
  3. the project's .agentworkforce/relay/workspace-key.json pin
  4. the broker's deterministic Relaycast fallback

Fleet enrollment contributes node identity/token, not the messaging workspace key. Workspace commands bridge the global named store into the project pin. The project file also acts as a broker-written cache after startup, which explains why editing it without fixing the startup authority did not stick.

I kept the original and addendum work together because the restore path, project rebind, restart behavior, and provenance all share the same workspace-session authority; splitting them would leave the reported recovery incomplete.

workspace switch decision

Yes: named workspace switch operations record the previous workspace through the same store primitive as create/restore. This is symmetric and makes every genuine active-workspace change reversible. Re-selecting the already-active workspace does not overwrite the restore point.

Validation

  • task-focused tests: Test Files 8 passed (8); Tests 177 passed (177)
  • persistence/restore tests after strengthening the real-store assertion: Test Files 3 passed (3); Tests 28 passed (28)
  • isolated relayfile contract file: Test Files 1 passed (1); Tests 10 passed | 4 skipped (14)
  • npm run typecheck: passed
  • npm run format:check: passed
  • npm run lint: passed with 0 errors (76 existing warning-level findings)
  • source CLI help check lists both restore and rebind
  • git diff --check: passed

Full suite at commit d5db49942, verbatim result:

Test Files  1 failed | 125 passed | 2 skipped (128)
Tests  1 failed | 1704 passed | 16 skipped (1721)

The sole failure is the existing relayfile stale-daemon contract: @relayfile/client does not complete its lsof ownership probe within 1000 ms during the full run. The exact test passes in isolation. On untouched origin/main (f0179c81d), the same isolated-environment sequential full run also failed that relayfile test and additionally timed out in doctor:

Test Files  2 failed | 124 passed | 2 skipped (128)
Tests  2 failed | 1690 passed | 16 skipped (1708)

Accordingly, the definition-of-done item "existing test suite passes" remains baseline-limited; all feature-specific and static gates pass.

Safety

No real workspace create, switch, or restore command was executed. The broker was not stopped or restarted.

Review in cubic

@cursor

cursor Bot commented Aug 3, 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 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI now supports workspace restoration and project rebinding. Workspace stores record prior selections. Broker connections record binding provenance. Node restarts resume project bindings. Detached startup failures and JSON output receive improved reporting.

Changes

Workspace and broker lifecycle

Layer / File(s) Summary
Workspace recovery and project rebinding
packages/cloud/src/workspace-store.*, packages/cli/src/cli/lib/workspace-session.*, packages/cli/src/cli/commands/workspace.*, packages/cli/src/cli/bootstrap.test.ts, packages/cli/README.md
Workspace switching records the previous workspace. New workspace restore and workspace rebind <name> commands manage global and project-local bindings.
Workspace binding and restart propagation
packages/cli/src/cli/lib/broker-lifecycle.ts, packages/cli/src/cli/commands/node.*, packages/cli/src/cli/commands/core.test.ts, packages/cli/README.md, CHANGELOG.md
Broker startup selects binding sources by precedence and persists provenance. Enrolled node restarts resume project-pinned workspace credentials.
Detached startup diagnostics
packages/cli/src/cli/lib/broker-lifecycle.*, packages/cli/src/cli/commands/core.test.ts, packages/cli/README.md
Detached startup records redacted child errors, detects early process exit, and avoids terminating an already-dead process.
Command output integrity
packages/cli/src/cli/telemetry/*
The first-run telemetry notice is written to stderr instead of stdout.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant runUpCommand
  participant DetachedBroker
  participant ReadinessPolling
  participant ErrorFile
  CLI->>runUpCommand: start detached broker
  runUpCommand->>DetachedBroker: spawn child
  runUpCommand->>ReadinessPolling: poll with child PID
  DetachedBroker->>ErrorFile: persist redacted startup error
  ReadinessPolling-->>runUpCommand: report child exit
  runUpCommand->>ErrorFile: read and report child error
Loading

Possibly related PRs

Suggested labels: size:L

Suggested reviewers: willwashburn

Poem

A rabbit restores the workspace trail,
Pins the project key without fail.
Errors hop to stderr’s stream,
Dead child brokers end the dream.
Provenance marks each binding bright—
JSON stays clean and right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 summarizes the main changes: reversible workspace activation and explicit broker binding.
Description check ✅ Passed The description explains the changes, validation results, known baseline failure, and safety considerations in sufficient detail.
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/workspace-restore-rebind

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.

@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 `@CHANGELOG.md`:
- Around line 18-20: Update the changelog by moving the node status
workspace-provenance entry from Fixed to Changed as its own concise,
impact-first bullet. Split the remaining enrolled-node restart binding
correction and detached startup child-failure correction into separate bullets
under Fixed, preserving their distinct user-visible impacts.
🪄 Autofix (Beta)

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: 66e3664b-ffc3-43d9-8ec1-0e311596887e

📥 Commits

Reviewing files that changed from the base of the PR and between f0179c8 and d5db499.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • packages/cli/README.md
  • packages/cli/src/cli/bootstrap.test.ts
  • packages/cli/src/cli/commands/core.test.ts
  • packages/cli/src/cli/commands/node.test.ts
  • packages/cli/src/cli/commands/node.ts
  • packages/cli/src/cli/commands/workspace.test.ts
  • packages/cli/src/cli/commands/workspace.ts
  • packages/cli/src/cli/lib/broker-lifecycle.test.ts
  • packages/cli/src/cli/lib/broker-lifecycle.ts
  • packages/cli/src/cli/lib/workspace-session.test.ts
  • packages/cli/src/cli/lib/workspace-session.ts
  • packages/cli/src/cli/telemetry/client.test.ts
  • packages/cli/src/cli/telemetry/client.ts
  • packages/cloud/src/workspace-store.test.ts
  • packages/cloud/src/workspace-store.ts

Comment thread CHANGELOG.md
Comment on lines +18 to +20
### Fixed

- `node status` reports whether its workspace came from `--workspace-key`, the environment, the project pin, or the broker fallback; enrolled-node restarts keep the project workspace binding, and detached startup surfaces early child failures without trying to kill an already exited process.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the status-output entry to Changed and split the unrelated fixes.

node status provenance is new output behavior. It is not a defect correction. Keep the enrolled-node restart and detached-child failure corrections as separate Fixed entries.

Proposed changelog update
 ### Changed

 - `workspace create` warns on stderr when it changes the active workspace and records the prior name; named switches now record the same restore point, and first-run telemetry notices no longer contaminate JSON stdout.
+- `node status` now reports whether its workspace came from `--workspace-key`, the environment, the project pin, or the broker fallback.

 ### Fixed

-- `node status` reports whether its workspace came from `--workspace-key`, the environment, the project pin, or the broker fallback; enrolled-node restarts keep the project workspace binding, and detached startup surfaces early child failures without trying to kill an already exited process.
+- Enrolled-node restarts keep the project workspace binding.
+- Detached startup surfaces early child failures without trying to kill an already exited process.

As per coding guidelines, use supported sections and concise impact-first bullets. Based on learnings, split a bullet when it bundles genuinely distinct user-visible changes.

🤖 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 `@CHANGELOG.md` around lines 18 - 20, Update the changelog by moving the node
status workspace-provenance entry from Fixed to Changed as its own concise,
impact-first bullet. Split the remaining enrolled-node restart binding
correction and detached startup child-failure correction into separate bullets
under Fixed, preserving their distinct user-visible impacts.

Sources: Coding guidelines, Learnings

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

ℹ️ 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 +770 to +773
const file = deps.env.AGENT_RELAY_BACKGROUND_START_ERROR_FILE?.trim();
if (!file) return;
try {
deps.fs.writeFileSync(file, `${message}\n`, 'utf-8');

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 Constrain the detached-error path to the broker state directory

When an automatically loaded project .env sets AGENT_RELAY_BACKGROUND_START_ERROR_FILE, any foreground broker startup failure reaches recordBackgroundStartError and overwrites that arbitrary path with the current user's permissions. Because bootstrap.ts loads .env before command execution and this writer does not verify that it is actually running as the detached child, an untrusted repository can use a relative path traversal or absolute path to clobber files outside the project; derive or validate this path against the broker state directory instead of trusting the environment value.

Useful? React with 👍 / 👎.

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

4 issues found across 16 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/README.md">

<violation number="1" location="packages/cli/README.md:76">
P2: `node up` also accepts the legacy `RELAY_API_KEY` workspace-key alias, and `RELAY_WORKSPACE_KEY` takes precedence over `AGENT_RELAY_WORKSPACE_KEY`; the documented environment step omits that supported input and leaves conflicting environments ambiguous. Including all aliases in resolver order would keep the binding documentation accurate.</violation>

<violation number="2" location="packages/cli/README.md:84">
P1: Enrolled-node restarts do not currently resume the project pin as documented: `RELAY_NODE_TOKEN` takes the broker-created fallback branch before the project pin is read, so the restarted broker can bind to a different workspace. The restart path should restore the project workspace key alongside the enrolled identity, or this documentation should be corrected to describe the actual precedence.</violation>
</file>

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

<violation number="1" location="packages/cli/src/cli/lib/workspace-session.test.ts:157">
P3: The test name says "without changing ... old enrollment" but the assertion `readProjectWorkspaceSession(projectDataDir)?.enrolledNodeId` expects `toBeUndefined()` — that means the old enrollment **is** changed (cleared). The name should say "clears old enrollment" to match the behavior and the PR description's stated intent.</violation>
</file>

<file name="packages/cli/src/cli/commands/workspace.ts">

<violation number="1" location="packages/cli/src/cli/commands/workspace.ts:85">
P3: `workspace create --json` is a no-op: the command always emits JSON, so this newly advertised flag does not change behavior. Removing the option (to preserve the existing always-JSON contract) or making output conditional on `o.json` would keep the CLI surface consistent with its implementation.</violation>
</file>

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

Re-trigger cubic

Comment thread packages/cli/README.md
directly by `node up`; workspace commands copy a selected key into the project
pin. Likewise, the Fleet enrollment store supplies node identity and a node
token, not a messaging workspace key. On an enrolled-node restart, Relay now
resumes the project pin alongside the enrolled identity so the two cannot drift.

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.

P1: Enrolled-node restarts do not currently resume the project pin as documented: RELAY_NODE_TOKEN takes the broker-created fallback branch before the project pin is read, so the restarted broker can bind to a different workspace. The restart path should restore the project workspace key alongside the enrolled identity, or this documentation should be corrected to describe the actual precedence.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/README.md, line 84:

<comment>Enrolled-node restarts do not currently resume the project pin as documented: `RELAY_NODE_TOKEN` takes the broker-created fallback branch before the project pin is read, so the restarted broker can bind to a different workspace. The restart path should restore the project workspace key alongside the enrolled identity, or this documentation should be corrected to describe the actual precedence.</comment>

<file context>
@@ -47,6 +47,47 @@ agent-relay node agent release <name>
+directly by `node up`; workspace commands copy a selected key into the project
+pin. Likewise, the Fleet enrollment store supplies node identity and a node
+token, not a messaging workspace key. On an enrolled-node restart, Relay now
+resumes the project pin alongside the enrolled identity so the two cannot drift.
+
+For detached startup failures, `node up --background` reports the child error
</file context>

Comment thread packages/cli/README.md
Broker workspace selection on `node up` is, in order:

1. `--workspace-key` / `--wk`;
2. `AGENT_RELAY_WORKSPACE_KEY` or `RELAY_WORKSPACE_KEY` from the environment;

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: node up also accepts the legacy RELAY_API_KEY workspace-key alias, and RELAY_WORKSPACE_KEY takes precedence over AGENT_RELAY_WORKSPACE_KEY; the documented environment step omits that supported input and leaves conflicting environments ambiguous. Including all aliases in resolver order would keep the binding documentation accurate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/README.md, line 76:

<comment>`node up` also accepts the legacy `RELAY_API_KEY` workspace-key alias, and `RELAY_WORKSPACE_KEY` takes precedence over `AGENT_RELAY_WORKSPACE_KEY`; the documented environment step omits that supported input and leaves conflicting environments ambiguous. Including all aliases in resolver order would keep the binding documentation accurate.</comment>

<file context>
@@ -47,6 +47,47 @@ agent-relay node agent release <name>
+Broker workspace selection on `node up` is, in order:
+
+1. `--workspace-key` / `--wk`;
+2. `AGENT_RELAY_WORKSPACE_KEY` or `RELAY_WORKSPACE_KEY` from the environment;
+3. the current project's `.agentworkforce/relay/workspace-key.json` pin;
+4. a deterministic Relaycast workspace created by the broker when no key was supplied.
</file context>
Suggested change
2. `AGENT_RELAY_WORKSPACE_KEY` or `RELAY_WORKSPACE_KEY` from the environment;
2. `RELAY_WORKSPACE_KEY`, `AGENT_RELAY_WORKSPACE_KEY`, or the legacy `RELAY_API_KEY` from the environment (in that order);

setWorkspaceKey('default', 'rk_live_default', env);
writeProjectWorkspaceKey(projectDataDir, 'rk_live_old', { enrolledNodeId: 'node_old' });

pinProjectWorkspaceSession({ workspaceKey: 'rk_live_default', projectDataDir, env });

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.

P3: The test name says "without changing ... old enrollment" but the assertion readProjectWorkspaceSession(projectDataDir)?.enrolledNodeId expects toBeUndefined() — that means the old enrollment is changed (cleared). The name should say "clears old enrollment" to match the behavior and the PR description's stated intent.

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.test.ts, line 157:

<comment>The test name says "without changing ... old enrollment" but the assertion `readProjectWorkspaceSession(projectDataDir)?.enrolledNodeId` expects `toBeUndefined()` — that means the old enrollment **is** changed (cleared). The name should say "clears old enrollment" to match the behavior and the PR description's stated intent.</comment>

<file context>
@@ -122,4 +146,18 @@ describe('workspace session persistence', () => {
+    setWorkspaceKey('default', 'rk_live_default', env);
+    writeProjectWorkspaceKey(projectDataDir, 'rk_live_old', { enrolledNodeId: 'node_old' });
+
+    pinProjectWorkspaceSession({ workspaceKey: 'rk_live_default', projectDataDir, env });
+
+    expect(readProjectWorkspaceKey(projectDataDir)).toBe('rk_live_default');
</file context>

.description('Create a new workspace and store its key')
.argument('<name>', 'Workspace name')
.option('--base-url <url>', 'Override the API base URL')
.option('--json', 'Output the created workspace as JSON')

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.

P3: workspace create --json is a no-op: the command always emits JSON, so this newly advertised flag does not change behavior. Removing the option (to preserve the existing always-JSON contract) or making output conditional on o.json would keep the CLI surface consistent with its implementation.

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

<comment>`workspace create --json` is a no-op: the command always emits JSON, so this newly advertised flag does not change behavior. Removing the option (to preserve the existing always-JSON contract) or making output conditional on `o.json` would keep the CLI surface consistent with its implementation.</comment>

<file context>
@@ -78,13 +82,19 @@ export function registerWorkspaceCommands(
     .description('Create a new workspace and store its key')
     .argument('<name>', 'Workspace name')
     .option('--base-url <url>', 'Override the API base URL')
+    .option('--json', 'Output the created workspace as JSON')
     .option('--reveal-secrets', 'Include the raw workspace key in the output')
     .action(async (name: string, o: Record<string, unknown>) => {
</file context>

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