Skip to content

fix(onboard): persist per-sandbox dashboard ports for multi-sandbox UI access#2008

Open
nvshaxie wants to merge 2 commits intoNVIDIA:mainfrom
nvshaxie:fix/1179-dashboard-port-isolation
Open

fix(onboard): persist per-sandbox dashboard ports for multi-sandbox UI access#2008
nvshaxie wants to merge 2 commits intoNVIDIA:mainfrom
nvshaxie:fix/1179-dashboard-port-isolation

Conversation

@nvshaxie
Copy link
Copy Markdown

@nvshaxie nvshaxie commented Apr 17, 2026

Summary

This change prevents multi-sandbox onboard flows from reusing the same dashboard port and silently repointing the OpenClaw UI at the most recently created sandbox.

It persists a dashboardPort per sandbox in the registry, fails fast when a new sandbox tries to claim a port already owned by another sandbox, and preserves backward compatibility for legacy sandbox entries that do not yet have a dashboardPort field.

Fixes #1179.

What changed

  • persist dashboardPort in sandbox registry entries
  • detect dashboard port conflicts before creating a new sandbox
  • show a clear onboarding error instead of silently reusing an existing dashboard forward
  • use stored per-sandbox dashboard ports for reconnect/recovery flows
  • treat legacy sandbox entries without dashboardPort as the historical default port 18789
  • add regression coverage for:
    • dashboard port conflict detection
    • legacy registry entries without dashboardPort

Test plan

  • npm run build:cli
  • local targeted verification of legacy registry fallback:
    • legacy sandbox entries conflict with 18789
    • legacy sandbox entries do not falsely conflict with 19000
  • Brev validation:
    • confirmed second onboard on default dashboard port is blocked with a clear conflict message
    • confirmed legacy sandbox entries without dashboardPort do not falsely block onboarding on a new port
    • confirmed new sandboxes can be created on distinct ports (19000, 19001)
    • confirmed openshell forward list shows separate forwards for:
      • existing sandbox on 18789
      • new sandbox on 19000
      • new sandbox on 19001
    • confirmed registry persists dashboardPort for newly created sandboxes

Notes

A separate OpenShell policy application issue was observed during Brev validation (openshell policy set ... -> Unimplemented), but it occurs after sandbox creation and forward setup and is not part of this fix.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Per-sandbox dashboard port persisted in the registry so each sandbox can have its own dashboard port.
    • Onboarding enforces dashboard-port uniqueness to prevent conflicts.
    • Recovery and port-forwarding now honor per-sandbox dashboard ports (with a sensible default fallback).
  • Tests

    • Added tests for port-conflict detection, onboarding failure on conflict, and registry compatibility with default ports.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Per-sandbox dashboard ports are introduced: ports are derived, persisted to the registry, validated for conflicts during onboarding, and used during recovery and port-forwarding instead of a single global dashboard port.

Changes

Cohort / File(s) Summary
Port configuration
src/lib/ports.ts
Added exported DEFAULT_DASHBOARD_PORT (18789); DASHBOARD_PORT now falls back to DEFAULT_DASHBOARD_PORT.
Registry data model
src/lib/registry.ts
SandboxEntry extended with optional `dashboardPort?: number
Onboarding logic
src/lib/onboard.ts
Added helpers: getSandboxDashboardPort, findDashboardPortConflict, assertDashboardPortConflictFree; compute and persist dashboardPort during createSandbox; export findDashboardPortConflict.
Runtime operations
src/nemoclaw.ts
recoverSandboxProcesses() and ensureSandboxPortForward() now read per-sandbox dashboardPort from registry (fallback to DEFAULT_DASHBOARD_PORT) instead of using the prior global port.
Tests
test/onboard.test.ts, test/registry.test.ts
Added tests for conflict detection and persistence of dashboardPort; adjusted test harness env to isolate NEMOCLAW_DASHBOARD_PORT.

Sequence Diagram(s)

sequenceDiagram
  participant User as "CLI / User"
  participant Onboard as "onboard.ts"
  participant Registry as "registry"
  participant Agent as "Port Forwarder / Agent"
  participant UI as "OpenClaw UI"

  User->>Onboard: createSandbox(sandboxName, options)
  Onboard->>Registry: getSandbox(sandboxName)
  Registry-->>Onboard: existingEntry? (includes dashboardPort?)
  Onboard->>Onboard: resolve dashboardPort (stored || DEFAULT_DASHBOARD_PORT)
  Onboard->>Onboard: findDashboardPortConflict(sandboxName, dashboardPort)
  alt conflict found
    Onboard->>User: write error & exit(1)
  else no conflict
    Onboard->>Registry: registerSandbox(..., dashboardPort)
    Registry-->>Onboard: ack
    Onboard->>Agent: ensureSandboxPortForward(sandboxName, dashboardPort)
    Agent-->>UI: forward localhost:dashboardPort -> UI endpoint
    Onboard->>User: print access info (tokenized URL via forwarded port)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hop and tap a tiny key,
Ports assigned for you and me,
No clashes now, each sandbox fine,
Registry saves the port divine,
A little hop — UI works just right! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: persisting per-sandbox dashboard ports to enable multi-sandbox UI access.
Linked Issues check ✅ Passed The pull request implements all core requirements from issue #1179: persisting per-sandbox dashboard ports, detecting port conflicts during onboarding, failing fast with clear error messages, using stored ports for recovery, and preserving backward compatibility with legacy entries.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the dashboard port persistence objective. Port configuration constants, registry persistence, onboarding validation, and recovery flows are all essential to resolving the multi-sandbox UI access issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/nemoclaw.ts (1)

244-268: LGTM — per-sandbox port retrieval with legacy fallback is correctly implemented.

The != null check properly handles legacy registry entries missing dashboardPort, and the fallback to DEFAULT_DASHBOARD_PORT preserves backward compatibility.

Optional defensive improvement: Consider validating that the computed port is a finite number to guard against corrupted registry data:

,

🛡️ Optional: Add NaN guard for corrupted registry data
   const sandbox = registry.getSandbox(sandboxName);
-  const dashboardPort =
-    sandbox?.dashboardPort != null ? Number(sandbox.dashboardPort) : DEFAULT_DASHBOARD_PORT;
+  const rawPort = sandbox?.dashboardPort != null ? Number(sandbox.dashboardPort) : DEFAULT_DASHBOARD_PORT;
+  const dashboardPort = Number.isFinite(rawPort) ? rawPort : DEFAULT_DASHBOARD_PORT;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nemoclaw.ts` around lines 244 - 268, The computed dashboardPort may
become NaN if registry.getSandbox(sandboxName)?.dashboardPort contains corrupted
non-numeric data; after the existing ternary that sets dashboardPort (using
Number(sandbox.dashboardPort) or DEFAULT_DASHBOARD_PORT), validate the resulting
numeric value with Number.isFinite (or Number.isNaN) and, if it is not a finite
number, fall back to DEFAULT_DASHBOARD_PORT so downstream uses
(agentRuntime.buildRecoveryScript, the curl liveness check, and nohup gateway
run) always receive a valid port number.
src/lib/onboard.ts (1)

5523-5526: Minor redundancy in fallback chain.

On line 5526, chatUiUrl || \http://127.0.0.1:${storedPort}\`` is redundant because chatUiUrl is guaranteed to be truthy after the assignment on line 5525 (it has its own fallback). The extra fallback is harmless but adds cognitive load.

♻️ Suggested simplification
   const storedPort = getSandboxDashboardPort(sandboxName);
   const chatUiUrl =
     options.chatUiUrl || process.env.CHAT_UI_URL || `http://127.0.0.1:${storedPort}`;
-  const dashboardPort = Number(getDashboardForwardPort(chatUiUrl || `http://127.0.0.1:${storedPort}`));
+  const dashboardPort = Number(getDashboardForwardPort(chatUiUrl));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 5523 - 5526, The code redundantly passes a
second fallback when calling getDashboardForwardPort because chatUiUrl is
already guaranteed to be non-empty after its assignment; remove the extra
fallback and call getDashboardForwardPort with chatUiUrl directly. Update the
dashboardPort assignment (variable: dashboardPort) to use
Number(getDashboardForwardPort(chatUiUrl)) and keep storedPort and chatUiUrl
logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/onboard.test.ts`:
- Around line 2789-2794: The test currently spreads process.env into the spawn
env which allows an external NEMOCLAW_DASHBOARD_PORT to alter behavior; change
the env creation so NEMOCLAW_DASHBOARD_PORT cannot be inherited (e.g., build a
local env object from { ...process.env, HOME: tmpDir, PATH:
`${fakeBin}:${process.env.PATH || ""}`, NEMOCLAW_NON_INTERACTIVE: "1" } and then
delete env.NEMOCLAW_DASHBOARD_PORT or omit that key before passing it), ensuring
the conflict path in this test remains deterministic; update the env usage where
the spawn (or equivalent) is called in test/onboard.test.ts.

---

Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 5523-5526: The code redundantly passes a second fallback when
calling getDashboardForwardPort because chatUiUrl is already guaranteed to be
non-empty after its assignment; remove the extra fallback and call
getDashboardForwardPort with chatUiUrl directly. Update the dashboardPort
assignment (variable: dashboardPort) to use
Number(getDashboardForwardPort(chatUiUrl)) and keep storedPort and chatUiUrl
logic unchanged.

In `@src/nemoclaw.ts`:
- Around line 244-268: The computed dashboardPort may become NaN if
registry.getSandbox(sandboxName)?.dashboardPort contains corrupted non-numeric
data; after the existing ternary that sets dashboardPort (using
Number(sandbox.dashboardPort) or DEFAULT_DASHBOARD_PORT), validate the resulting
numeric value with Number.isFinite (or Number.isNaN) and, if it is not a finite
number, fall back to DEFAULT_DASHBOARD_PORT so downstream uses
(agentRuntime.buildRecoveryScript, the curl liveness check, and nohup gateway
run) always receive a valid port number.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 519118d3-a171-4a1b-ba93-228c2f4805c2

📥 Commits

Reviewing files that changed from the base of the PR and between e7dc62a and 6ecb146.

📒 Files selected for processing (6)
  • src/lib/onboard.ts
  • src/lib/ports.ts
  • src/lib/registry.ts
  • src/nemoclaw.ts
  • test/onboard.test.ts
  • test/registry.test.ts

Comment thread test/onboard.test.ts
@nvshaxie
Copy link
Copy Markdown
Author

Addressed the review feedback in follow-up commit 7d35e615:

  • made the conflict test deterministic by isolating NEMOCLAW_DASHBOARD_PORT
  • simplified the redundant fallback in getDashboardAccessInfo()
  • added finite-number guards for recovered/stored dashboard ports

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
test/onboard.test.ts (1)

2789-2797: ⚠️ Potential issue | 🟠 Major

Make this conflict test deterministic by isolating NEMOCLAW_DASHBOARD_PORT.

On Line 2793, inheriting process.env directly allows host NEMOCLAW_DASHBOARD_PORT to bypass the default-18789 conflict path and make the test flaky.

🔧 Suggested patch
+    // Ensure host shell env cannot change the requested dashboard port in this test.
+    // eslint-disable-next-line `@typescript-eslint/no-unused-vars`
+    const { NEMOCLAW_DASHBOARD_PORT: _ignoredDashboardPort, ...inheritedEnv } = process.env;
     const result = spawnSync(process.execPath, [scriptPath], {
       cwd: repoRoot,
       encoding: "utf-8",
       env: {
-        ...process.env,
+        ...inheritedEnv,
         HOME: tmpDir,
         PATH: `${fakeBin}:${process.env.PATH || ""}`,
         NEMOCLAW_NON_INTERACTIVE: "1",
       },
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/onboard.test.ts` around lines 2789 - 2797, The test's spawnSync
invocation inherits process.env allowing an external NEMOCLAW_DASHBOARD_PORT to
make the conflict branch nondeterministic; in the spawnSync env object (the call
that assigns result via spawnSync in test/onboard.test.ts) explicitly set
NEMOCLAW_DASHBOARD_PORT to the expected default (e.g., "18789") or remove it
(set to undefined/"") so the test always hits the default-port conflict path
instead of being influenced by the host environment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/onboard.test.ts`:
- Around line 2789-2797: The test's spawnSync invocation inherits process.env
allowing an external NEMOCLAW_DASHBOARD_PORT to make the conflict branch
nondeterministic; in the spawnSync env object (the call that assigns result via
spawnSync in test/onboard.test.ts) explicitly set NEMOCLAW_DASHBOARD_PORT to the
expected default (e.g., "18789") or remove it (set to undefined/"") so the test
always hits the default-port conflict path instead of being influenced by the
host environment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2da1f758-eb55-4a7a-919a-074c84b0f359

📥 Commits

Reviewing files that changed from the base of the PR and between 6ecb146 and 7d35e61.

📒 Files selected for processing (3)
  • src/lib/onboard.ts
  • src/nemoclaw.ts
  • test/onboard.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/nemoclaw.ts

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.

[NemoClaw][Brev Launchable] Openclaw UI only shows the most recently onboarded sandbox configuration when two sandboxes exist

1 participant