Skip to content

fix(sandbox): allocate the snapshot clone its own dashboard port#6749

Merged
cv merged 9 commits into
mainfrom
fix/6746-clone-dashboard-port
Jul 13, 2026
Merged

fix(sandbox): allocate the snapshot clone its own dashboard port#6749
cv merged 9 commits into
mainfrom
fix/6746-clone-dashboard-port

Conversation

@Dongni-Yang

@Dongni-Yang Dongni-Yang commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

snapshot restore --to <dst> auto-creates the destination by spreading the source's registry entry, so the clone was registered with the source's dashboardPort. Because the host forward for that port is owned by the source, the clone's dashboard-url pointed at the source's dashboard and every rebuild of the clone was rejected by the rebuild preflight (Dashboard port NNNNN belongs to sandbox '<src>'.). The clone now gets its own dashboard port allocated at registration, so dashboard-url reports the clone's own port and rebuild proceeds.

Related Issue

Fixes #6746

Changes

  • src/lib/actions/sandbox/snapshot.ts: allocate a destination-owned dashboard port before any destructive --force action, then hold a host-wide reservation from selection through durable registration. Onboard and snapshot restore now share the same lock order: sandbox mutation → host dashboard reservation → gateway route mutation.
  • Clone startup explicitly sets CHAT_UI_URL and NEMOCLAW_DASHBOARD_PORT to the allocated port, so the in-sandbox listener, host forward, and registry all use the same destination port immediately after restore.
  • Enabled Hermes clones persist the allocated public port while preserving the source image's internal relay port and TUI setting. The internal port is excluded from public-port allocation, and the runtime Hermes environment is validated before destination deletion or creation.
  • Sources without a managed dashboard keep the clone dashboard field unset. Allocation or Hermes validation failures abort before destination deletion, and failed operations release the shared reservation for the next allocator.
  • Regression coverage exercises cross-gateway serialization, failure release, OpenClaw and Hermes create argv, Hermes rebuild consistency, port exhaustion before deletion, missing dashboard ports, lifecycle mock compatibility, and shuffled test order.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Docs updated for user-facing behavior changes
  • Docs not applicable — justification: internal registry-registration fix; no doc page documents snapshot-clone dashboard-port behavior.
  • Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging)
  • Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: pending — sensitive-path review requested from maintainers as part of this PR's review.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Verification

  • PR description includes the DCO sign-off declaration and every commit appears as Verified in GitHub
  • Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run check:diff passed when hooks were skipped or unavailable
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — command/result: focused lock/clone suite 36/36; shuffled clone suite 4/4; onboard handler suite 203/203; npm run typecheck:cli passed; full sandbox action suite 1,719 passed / 3 skipped with one unrelated timeout passing immediately in isolation; normal pre-commit, commit-msg, and pre-push hooks passed.
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result:
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Dongni Yang dongniy@nvidia.com

Summary by CodeRabbit

  • Bug Fixes

    • Snapshot-restore clones now reserve and assign a destination-owned dashboard port, ensuring registration uses the correct port and Hermes dashboard settings propagate correctly.
    • If dashboard-port allocation fails during restore, the restore exits cleanly and avoids subsequent destructive or registration actions.
    • When the source sandbox has no dashboard port, the clone is registered with a null dashboard port and allocation is skipped.
  • Tests

    • Expanded snapshot-restore/clone coverage for dashboard-port, Hermes behavior, and failure scenarios.
    • Added/updated dashboard-port reservation locking tests to verify serialization across gateways and proper release on failure.

`snapshot restore --to <dst>` auto-creates the destination by spreading
the source's registry entry, which carried the source's `dashboardPort`
into the clone. The host forward for that port is owned by the source,
so the clone's dashboard URL pointed at the source's dashboard, and the
rebuild preflight — correctly refusing a port owned by another sandbox —
rejected every rebuild of the clone permanently.

Clearing the field instead of allocating would not help: rebuild of a
dashboard-managed sandbox hard-fails without a persisted port
(rebuild-gpu-opt-out.ts), and no connect/recovery path allocates one
for an existing entry outside onboard. So the clone now gets its own
port at registration, via the same `findAvailableDashboardPort` +
cross-gateway registry occupancy view that onboard's
`ensureDashboardForward` uses. Allocation runs before `sandbox create`
so port-range exhaustion aborts without leaving an unregistered
sandbox behind. Sources without a dashboard port (non-dashboard-managed
agents) keep the field unset on the clone.

The Hermes port fields flagged in the issue were audited and left
inherited: `hermesDashboardInternalPort` is baked into the shared
image (inheriting is correct), and changing the Hermes host-port
behavior needs a Hermes-specific repro per the repo's Hermes policy.

Fixes #6746

Signed-off-by: Dongni Yang <dongniy@nvidia.com>
@Dongni-Yang Dongni-Yang self-assigned this Jul 13, 2026
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7aa5f68d-7545-4144-acb0-c91bdfa5c4be

📥 Commits

Reviewing files that changed from the base of the PR and between 4a44df0 and 835196e.

📒 Files selected for processing (1)
  • test/e2e/live/upgrade-stale-sandbox-helpers.ts

📝 Walkthrough

Walkthrough

Snapshot restore now allocates destination-owned dashboard ports for cross-sandbox clones, propagates Hermes dashboard settings, and performs allocation before destructive actions. Dashboard reservation locking is integrated into sandbox creation, with related tests and shared test-helper reuse.

Changes

Sandbox dashboard port lifecycle

Layer / File(s) Summary
Serialize dashboard port reservations
src/lib/onboard/dashboard-port.ts, src/lib/onboard/machine/handlers/..., src/lib/onboard/dashboard-port.test.ts
Adds a durable host-wide reservation lock and nests it with gateway route mutation during managed sandbox creation, with ordering and failure-release tests.
Allocate and register clone dashboard ports
src/lib/actions/sandbox/snapshot.ts
Snapshot restore derives destination dashboard and Hermes environment settings before deletion, passes them into clone startup, and registers destination-specific dashboard fields.
Validate restore dashboard behavior
src/lib/actions/sandbox/snapshot-restore-clone-ports.test.ts, src/lib/actions/sandbox/snapshot-restore-test-fixture.ts, src/lib/actions/sandbox/snapshot-auto-create-failure.test.ts
Tests cover allocated ports, Hermes propagation, allocation failure without destructive actions, absent source ports, and updated fixtures.
Reuse shared stale-build fixture helper
test/e2e/live/upgrade-stale-sandbox-helpers.ts
Uses the shared old-base build-context helper and removes its duplicate local implementation and constants.

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

Sequence Diagram(s)

sequenceDiagram
  participant SnapshotRestore
  participant Openshell
  participant PortAllocator
  participant DashboardReservationLock
  participant SandboxRegistry
  SnapshotRestore->>Openshell: Query forward-list and registry occupancy
  SnapshotRestore->>PortAllocator: Allocate destination dashboard port
  PortAllocator-->>SnapshotRestore: Return port and dashboard environment
  SnapshotRestore->>DashboardReservationLock: Serialize allocation and registration
  DashboardReservationLock->>SandboxRegistry: Register clone with destination dashboard fields
Loading

Possibly related PRs

  • NVIDIA/NemoClaw#5379: Adds related registry-occupancy and dashboard-port allocation machinery used by this change.

Suggested labels: area: sandbox, bug-fix, integration: hermes

Suggested reviewers: cv, ericksoa

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Most edits support the fix, but the helper extraction in test/e2e/live/upgrade-stale-sandbox-helpers.ts is unrelated to the dashboard-port issue. Move that refactor into a separate PR or justify it as necessary for this fix.
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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: snapshot clones now get their own dashboard port.
Linked Issues check ✅ Passed The PR assigns a destination-owned dashboard port on clone creation and avoids inheriting the source port, matching #6746.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/6746-clone-dashboard-port

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

@github-code-quality

github-code-quality Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage remains at 96%, unchanged from the branch.


Updated July 13, 2026 23:49 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

E2E Advisor Recommendation

Required E2E: onboard-resume, onboard-repair, state-backup-restore, upgrade-stale-sandbox
Optional E2E: snapshot-commands

Dispatch hint: onboard-resume,onboard-repair,state-backup-restore,upgrade-stale-sandbox,snapshot-commands

Workflow run

Full advisor summary

E2E Recommendation Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E

  • onboard-resume (45 minutes): Required by the deterministic lifecycle-state risk plan: validate persisted state and live runtime convergence after lifecycle mutations.
  • onboard-repair (75 minutes): Required by the deterministic lifecycle-state risk plan: validate repair convergence after partial or failed sandbox lifecycle operations.
  • state-backup-restore (60 minutes): Required by the deterministic upgrade-rebuild risk plan; exercises real state backup/restore across sandbox destruction and recreation.
  • upgrade-stale-sandbox (55 minutes): Required by the deterministic upgrade-rebuild risk plan; validates replacement of stale runtime state while preserving expected sandbox state.

Optional E2E

  • snapshot-commands: Directly exercises the real snapshot create/list/restore CLI lifecycle; useful adjacent coverage, though its current flow does not establish clone-specific dashboard-port allocation.

New E2E recommendations

  • backup-restore (high): No existing live test was found that restores a snapshot to a distinct clone and verifies the clone receives a different dashboard forward, or that port exhaustion aborts before a forced destination is deleted.
    • Suggested test: Extend the snapshot-commands live E2E flow with snapshot restore --to: assert source and clone have distinct dashboard ports/forwards, and assert an exhausted port range leaves a --force destination intact.

Dispatch hint

  • Workflow: .github/workflows/e2e.yaml
  • jobs input: onboard-resume,onboard-repair,state-backup-restore,upgrade-stale-sandbox,snapshot-commands

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Informational

Advisor assessment: Informational / low confidence
Primary next action: No advisor follow-up required beyond maintainer review.
Findings: 0 blockers · 0 warnings · 0 optional suggestions
Status: No actionable findings remain in the canonical review ledger.

E2E guidance

Advisory only: coverage and selector recommendations are non-authoritative. E2E / PR Gate independently computes and dispatches trusted jobs without consuming this output.

Recommended coverage: cloud-onboard, credential-sanitization, security-posture, onboard-repair, onboard-resume, state-backup-restore, upgrade-stale-sandbox
Recommended selectors: cloud-onboard, credential-sanitization, security-posture, onboard-repair, onboard-resume, state-backup-restore, upgrade-stale-sandbox

  • cloud-onboard — Selected from the trusted checked-in E2E coverage inventory.

  • credential-sanitization — Selected from the trusted checked-in E2E coverage inventory.

  • security-posture — Selected from the trusted checked-in E2E coverage inventory.

  • onboard-repair — Selected from the trusted checked-in E2E coverage inventory.

  • onboard-resume — Selected from the trusted checked-in E2E coverage inventory.

  • state-backup-restore — Selected from the trusted checked-in E2E coverage inventory.

  • upgrade-stale-sandbox — Selected from the trusted checked-in E2E coverage inventory.

  • cloud-onboard — Selected as a trusted checked-in E2E job.

  • credential-sanitization — Selected as a trusted checked-in E2E job.

  • security-posture — Selected as a trusted checked-in E2E job.

  • onboard-repair — Selected as a trusted checked-in E2E job.

  • onboard-resume — Selected as a trusted checked-in E2E job.

  • state-backup-restore — Selected as a trusted checked-in E2E job.

  • upgrade-stale-sandbox — Selected as a trusted checked-in E2E job.

4 optional coverage items · 4 optional selectors · 0 new-test recommendations
  • Optional coverage snapshot-commands — Selected from the trusted checked-in E2E coverage inventory.
  • Optional coverage sandbox-rebuild — Selected from the trusted checked-in E2E coverage inventory.
  • Optional coverage hermes-dashboard — Selected from the trusted checked-in E2E coverage inventory.
  • Optional coverage rebuild-hermes — Selected from the trusted checked-in E2E coverage inventory.
  • Optional selector snapshot-commands — Selected as a trusted checked-in E2E job.
  • Optional selector sandbox-rebuild — Selected as a trusted checked-in E2E job.
  • Optional selector hermes-dashboard — Selected as a trusted checked-in E2E job.
  • Optional selector rebuild-hermes — Selected as a trusted checked-in E2E job.

Workflow run details

This is an automated, non-authoritative review. Findings are inputs to maintainer adjudication. Warnings and optional suggestions do not require a response or follow-up. A human maintainer makes the final merge decision.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/actions/sandbox/snapshot.ts (1)

216-227: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Port allocation happens after the destructive delete in the --force rebuild path, risking sandbox loss.

allocateCloneDashboardPort (called here inside autoCreateSandboxFromSource) can throw and call snapshotExit(1) on port-range exhaustion. In runSnapshotRestoreUnlocked, when targetExists (i.e. --force recreate), deleteSandboxForRestore(targetSandbox) runs before autoCreateSandboxFromSource is invoked (Lines 959-975, unchanged). If the dashboard-port range is exhausted, the destination sandbox is deleted and the recreate then aborts, leaving no sandbox at that name at all.

This is exactly the class of bug the existing #3756 P1 fix in this same file guards against for image resolution and gateway compatibility — both are resolved/validated before the destructive delete (Lines 889-896, 949-958). The new dashboard-port check should follow the same pattern: allocate/validate the port before deleteSandboxForRestore, then pass the pre-computed port into autoCreateSandboxFromSource instead of recomputing it after the destination is already gone.

As per path instructions, src/lib/{sandbox/**,actions/sandbox/**,state/sandbox.ts}: "Destructive lifecycle operations must validate before mutation, preserve state/backup invariants, and cover failure, recovery, rebuild, and resume behavior without bypassing the public action boundary."

🛡️ Proposed fix: hoist allocation before the destructive delete
       const compatibility = checkGatewayRouteCompatibility({
         gatewayName: sourceGatewayName,
         sandboxName: targetSandbox,
         route: lockedSourceEntry,
         sandboxes: registry.listSandboxes().sandboxes,
       });
       if (!compatibility.ok) {
         console.error(`  Error: ${formatGatewayRouteConflict(compatibility)}`);
         snapshotExit(1);
       }
+      // Validate dashboard-port availability before any destructive action,
+      // matching the `#3756` P1 pattern used for image/gateway checks above.
+      const dstDashboardPort = allocateCloneDashboardPort(targetSandbox, lockedSourceEntry);
       if (targetExists) {
         if (targetEntry) {
           verifyRestoreDestinationOnOwnGateway(targetSandbox);
         }
         deleteSandboxForRestore(targetSandbox);
         requireLiveSandboxesOnSandboxGateway(
           sandboxName,
           "  Failed to re-select source sandbox gateway after deleting destination.",
         );
       }
       await autoCreateSandboxFromSource(
         sandboxName,
         targetSandbox,
         lockedSourceEntry,
         lockedFromImage,
+        dstDashboardPort,
       );
// autoCreateSandboxFromSource signature updated to accept the pre-allocated port
async function autoCreateSandboxFromSource(
  srcName: string,
  dstName: string,
  srcEntry: SandboxEntry | { name: string },
  fromImage: string,
  dstDashboardPort: number | null,
): Promise<void> {
  // remove the internal allocateCloneDashboardPort call here
  ...

Once fixed, consider adding a regression test covering the --force rebuild path with port exhaustion to lock in the ordering.

🤖 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 `@src/lib/actions/sandbox/snapshot.ts` around lines 216 - 227, Move the
allocateCloneDashboardPort call out of autoCreateSandboxFromSource and perform
it in runSnapshotRestoreUnlocked before deleteSandboxForRestore when rebuilding
an existing target. Pass the precomputed dashboard port into
autoCreateSandboxFromSource, updating its signature and removing the internal
allocation, so port exhaustion occurs before destructive deletion while normal
creation behavior remains unchanged.

Source: Path instructions

🧹 Nitpick comments (1)
src/lib/actions/sandbox/snapshot.ts (1)

192-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate port-validity predicate.

The typeof port !== "number" || !Number.isInteger(port) || port <= 0 check here duplicates the identical predicate in getRegistryOccupiedDashboardPorts (src/lib/onboard/dashboard-port.ts). Consider extracting a shared isValidDashboardPort(port) helper to avoid the two copies drifting apart.

🤖 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 `@src/lib/actions/sandbox/snapshot.ts` around lines 192 - 197, The
dashboard-port validity predicate is duplicated between
allocateCloneDashboardPort and getRegistryOccupiedDashboardPorts. Extract a
shared isValidDashboardPort helper in the dashboard-port utility module, then
reuse it in both locations while preserving the existing positive-integer
validation behavior.
🤖 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.

Outside diff comments:
In `@src/lib/actions/sandbox/snapshot.ts`:
- Around line 216-227: Move the allocateCloneDashboardPort call out of
autoCreateSandboxFromSource and perform it in runSnapshotRestoreUnlocked before
deleteSandboxForRestore when rebuilding an existing target. Pass the precomputed
dashboard port into autoCreateSandboxFromSource, updating its signature and
removing the internal allocation, so port exhaustion occurs before destructive
deletion while normal creation behavior remains unchanged.

---

Nitpick comments:
In `@src/lib/actions/sandbox/snapshot.ts`:
- Around line 192-197: The dashboard-port validity predicate is duplicated
between allocateCloneDashboardPort and getRegistryOccupiedDashboardPorts.
Extract a shared isValidDashboardPort helper in the dashboard-port utility
module, then reuse it in both locations while preserving the existing
positive-integer validation behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2cb0827e-4aba-4d40-8b3e-775dc0216c0c

📥 Commits

Reviewing files that changed from the base of the PR and between deed1aa and e115108.

📒 Files selected for processing (3)
  • src/lib/actions/sandbox/snapshot-restore-clone-ports.test.ts
  • src/lib/actions/sandbox/snapshot-restore-test-fixture.ts
  • src/lib/actions/sandbox/snapshot.ts

@Dongni-Yang Dongni-Yang added the v0.0.82 Release target label Jul 13, 2026
@Dongni-Yang

Copy link
Copy Markdown
Contributor Author

Thanks — re PRA-1 (establish the destination dashboard forward for the allocated port).

The observation is accurate: after this change dashboard-url resolves the clone's freshly-allocated port, and no forward is listening on it until the clone's first rebuild. But the prescribed action — establish and verify the forward in the restore path — can't produce a working dashboard here, because of the shared-image constraint this path is built on:

  1. The clone is auto-created with openshell sandbox create --from <source image> (the snapshot.ts auto-create path). It reuses the source's baked image, whose in-sandbox gateway listens on the port baked at the source's onboard time via patchStagedDockerfile (src/lib/core/ports.ts:41-49).
  2. OpenShell forwards are same-port on both sidesforwardTarget is 0.0.0.0:${port} / String(port) with a single port (src/lib/dashboard/contract.ts:120-121). There is no host:Q → sandbox:P mapping.

So a host forward on the newly-allocated port Q would map host:Q → sandbox:Q, where nothing is listening (the gateway is on the source's baked port P). The only thing that makes the clone's dashboard live is a rebuild, which re-bakes the clone's own image with Q and forwards host:Q → sandbox:Q. That is exactly why this fix allocates Q at registration: it unblocks that rebuild, which is the issue's stated acceptance criterion ("Rebuilding a clone should not be permanently blocked by its own registration").

Why not the alternative the finding suggests (leave the field unset until first forward setup allocates it): for a dashboard-managed agent (OpenClaw, Hermes) rebuild hard-fails without a persisted port — rebuild-gpu-opt-out.ts:160-164, "Cannot recreate a dashboard-managed sandbox without its persisted dashboard port." No connect/recovery path allocates-and-persists a port for an existing entry outside onboard (resolveSandboxDashboardPort only falls back to the static default). So leaving it unset would re-break the very rebuild this fix is unblocking.

On regression risk: snapshot restore --to has never established a destination dashboard forward — a working clone dashboard has always required a subsequent connect/rebuild. Pre-fix the clone inherited the source's port, so dashboard-url pointed at the source's live dashboard (misleading) and every rebuild was rejected forever. This change is strictly better on both axes (own port, rebuild unblocked); the dead-until-rebuild link is a pre-existing property of the restore path, not new behavior introduced here.

Fully wiring a live dashboard into the restore path would mean triggering a re-bake inside restore --to (touching the sensitive rebuild/onboard lifecycle), which is beyond this bug's scope and its acceptance criterion. Happy to file a follow-up for "restore --to leaves the clone dashboard dead until first rebuild" if maintainers want the restore path to auto-rebuild, but I'd keep that out of this fix.

…lete

The dashboard-port allocation for a snapshot clone ran inside
`autoCreateSandboxFromSource`, which in the `--force` recreate path is
called after `deleteSandboxForRestore` has already removed the existing
destination. On dashboard-port-range exhaustion the allocator calls
`snapshotExit(1)`, so the destination was deleted and the recreate then
aborted — leaving no sandbox at that name.

Hoist the allocation into `runSnapshotRestoreUnlocked` before the
destructive delete and pass the pre-computed port into
`autoCreateSandboxFromSource`, matching the validate-before-mutation
pattern the image-resolution and gateway-route checks already follow
(#3756). Adds a regression test asserting that a `--force` restore aborts
without deleting the destination when no dashboard port is free.

Refs #6746

Signed-off-by: Dongni Yang <dongniy@nvidia.com>
@Dongni-Yang

Copy link
Copy Markdown
Contributor Author

Good catch on the --force ordering — fixed in 0341175.

allocateCloneDashboardPort is now hoisted into runSnapshotRestoreUnlocked and runs before deleteSandboxForRestore, with the pre-computed port passed into autoCreateSandboxFromSource — matching the validate-before-mutation pattern the image-resolution and gateway-route checks already follow (#3756). So on dashboard-port-range exhaustion in the --force recreate path, the restore now aborts while the existing destination is still intact, instead of deleting it and then failing the recreate.

Added a regression test (aborts before deleting a --force destination when no dashboard port is free) that drives the --force path with the allocator throwing and asserts delete never runs and nothing is registered. Confirmed red→green: it fails against the pre-hoist ordering and passes after.

@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 `@src/lib/actions/sandbox/snapshot.ts`:
- Around line 956-960: Update the restore flow around allocateCloneDashboardPort
and withGatewayRouteMutationLock so the selected dashboard port remains reserved
until the cloned sandbox registers it. Use a host-wide reservation spanning
concurrent restores from different gateways, or extend an existing
synchronization scope through registration, and release it on failure.
🪄 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: Enterprise

Run ID: 4a8f4750-69e1-4bc0-9b06-4e378878f331

📥 Commits

Reviewing files that changed from the base of the PR and between e115108 and 0341175.

📒 Files selected for processing (2)
  • src/lib/actions/sandbox/snapshot-restore-clone-ports.test.ts
  • src/lib/actions/sandbox/snapshot.ts

Comment thread src/lib/actions/sandbox/snapshot.ts
@Dongni-Yang

Copy link
Copy Markdown
Contributor Author

Re the --force ordering finding — good catch, fixed in 0341175. allocateCloneDashboardPort is now hoisted into runSnapshotRestoreUnlocked and runs before deleteSandboxForRestore, with the pre-computed port passed into autoCreateSandboxFromSource — matching the validate-before-mutation pattern the image-resolution and gateway-route checks already follow (#3756). On dashboard-port-range exhaustion in the --force recreate path, the restore now aborts while the existing destination is still intact. Added a regression test (aborts before deleting a --force destination when no dashboard port is free) asserting delete never runs and nothing is registered; confirmed red→green against the pre-hoist ordering.

Re the cross-gateway allocation race — accurate observation, but it's a pre-existing property of the whole dashboard-port subsystem, not introduced here, so I'm scoping it out of this fix:

  • Onboard's canonical allocator, ensureDashboardForward (src/lib/onboard/dashboard.ts:280), does the exact same read-then-allocate: forward list + getRegistryOccupiedDashboardPorts + allocate, with registration happening later and no host-wide reservation. Two concurrent onboards on different gateways have the identical window today. This PR deliberately reuses that allocator with the same occupancy inputs, so it doesn't widen the window relative to the established pattern.
  • There is no host-wide reservation primitive in the codebase to extend — withGatewayRouteMutationLock is per-gateway by design, and no other port-allocation site holds anything broader. Introducing a host-wide port-reservation lock would be a new cross-cutting mechanism that ought to cover onboard/connect/rebuild too, which is well beyond this bug's scope.
  • The window is also narrow in practice: findAvailableDashboardPort merges the live per-gateway forward list, the cross-gateway registry occupancy map (every gateway's sandboxes.json entries), and an actual host bind probe (lsofsudo -n lsof → Node net bind), so a collision requires two allocations racing between probe and registration.

Happy to file a follow-up issue for "dashboard-port allocation lacks host-wide reservation across concurrent CLI invocations" covering all allocation sites (onboard, connect, restore) if maintainers want it addressed uniformly.

@wscurran wscurran added area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery bug-fix PR fixes a bug or regression labels Jul 13, 2026

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

Reviewed exact head 3ac93bf. I agree the Advisor's prescribed dead-forward fix is not viable: the reused image listens on source port P and OpenShell forwards are same-port, so a Q forward cannot become live before rebuild. Issue #6746 permits allocate-at-registration. Two other blockers remain:

  1. High — Hermes WebUI clones stay unrebuildable. snapshot.ts:294-307 spreads srcEntry, inherits hermesDashboardPort=P, then writes dashboardPort=Q. rebuild-durable-config.ts:95-109 requires the Hermes public port to equal Q, and rebuild-target-staging.ts:63-77 fails the mismatch. When Hermes WebUI is enabled, set hermesDashboardPort to dstDashboardPort while retaining image-baked hermesDashboardInternalPort, and add a Hermes-specific clone/rebuild acceptance test (or explicitly exclude Hermes pending a repro).

  2. High — uphold CodeRabbit's cross-gateway TOCTOU finding. snapshot.ts:194-208,956-977 selects a free host port under a source-gateway-keyed lock, then only registers it after sandbox creation. Two restores on different gateways—or restore racing onboard—can select and durably register the same port. A similar pre-existing onboard gap does not make a new allocation site safe. Introduce a host-global cross-process reservation shared with canonical onboard allocation, held from occupancy selection until the port is durably registered/owned and released on every failure, with consistent lock ordering.

Add deterministic different-gateway/restore-vs-onboard/failure-release tests. After fixes, run exact-head onboard-resume, onboard-repair, state-backup-restore, and upgrade-stale-sandbox plus a clone-specific restore/rebuild live proof.

@cjagwani cjagwani self-assigned this Jul 13, 2026
@cjagwani cjagwani dismissed their stale review July 13, 2026 22:57

dismissing requested changes

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

Maintainer review complete. The dashboard-port reservation and runtime propagation are covered across OpenClaw and Hermes clone paths, and the stale-upgrade E2E fixture now derives its complete Dockerfile.base context. Approved pending required CI/E2E completion.

@cv cv merged commit 9d22fe6 into main Jul 13, 2026
80 of 82 checks passed
@cv cv deleted the fix/6746-clone-dashboard-port branch July 13, 2026 23:53
cv pushed a commit that referenced this pull request Jul 14, 2026
<!-- markdownlint-disable MD041 -->
## Summary

Release-prep documentation for v0.0.82 now summarizes user-facing
changes merged since v0.0.81.
It also closes stale wording in the stopped-sandbox backup,
snapshot-clone, Ollama selection, and custom-policy authoring guidance.

## Changes

- Add the `v0.0.82` section to `docs/about/release-notes.mdx` with links
to the focused user guides.
- Document that snapshot clones receive a destination-owned dashboard
port before destructive replacement begins.
- Align `backup-all` guidance with eligible stopped Docker-driver
sandboxes that NemoClaw starts temporarily.
- Describe the running and stopped Ollama menu states without claiming
one fixed label.
- Document runtime rejection of catch-all hosts in custom policy files.

### Source summary

- [#6748](#6748) ->
`docs/about/release-notes.mdx`, `docs/manage-sandboxes/lifecycle.mdx`,
and `docs/reference/commands.mdx`: Summarize non-destructive sandbox
`stop` and `start` commands.
- [#6723](#6723) ->
`docs/about/release-notes.mdx`,
`docs/manage-sandboxes/backup-restore.mdx`, and
`docs/reference/commands.mdx`: Record temporary startup and cleanup for
eligible stopped-sandbox backups.
- [#6749](#6749) ->
`docs/about/release-notes.mdx` and
`docs/manage-sandboxes/backup-restore.mdx`: Document destination-owned
dashboard ports for snapshot clones.
- [#6764](#6764) ->
`docs/about/release-notes.mdx`: Summarize installer handling of
route-only onboarding placeholders.
- [#6771](#6771) ->
`docs/about/release-notes.mdx`, `docs/inference/set-up-vllm.mdx`,
`docs/inference/choose-inference-provider.mdx`,
`docs/reference/commands.mdx`, and
`docs/reference/platform-support.mdx`: Summarize managed-vLLM storage
gates, immutable image digests, and the explicit override boundary.
- [#6759](#6759) ->
`docs/about/release-notes.mdx`: Record early, actionable OpenShell
gateway-port conflict diagnostics.
- [#6753](#6753) ->
`docs/about/release-notes.mdx` and `docs/inference/set-up-ollama.mdx`:
Document truthful running and stopped Ollama menu states.
- [#6776](#6776) ->
`docs/about/release-notes.mdx`: Summarize proxy-independent loopback
readiness checks.
- [#6769](#6769) ->
`docs/about/release-notes.mdx`: Record compatible endpoint and agent
guidance when Chat Completions is unavailable.
- [#6730](#6730) ->
`docs/about/release-notes.mdx`: Summarize bounded reuse of an eligible
successful Chat Completions check.
- [#6768](#6768) ->
`docs/about/release-notes.mdx`: Record route-reservation repair during
resumed onboarding.
- [#6742](#6742) ->
`docs/about/release-notes.mdx`: Summarize pre-mutation resolution of
secret-free sandbox create intent.
- [#6721](#6721) ->
`docs/about/release-notes.mdx` and
`docs/get-started/quickstart-langchain-deepagents-code.mdx`: Record
bounded cleanup of completed managed Deep Agents headless sessions.
- [#6731](#6731) ->
`docs/about/release-notes.mdx` and
`docs/network-policy/customize-network-policy.mdx`: Document runtime
rejection of catch-all custom-policy destinations.
- [#6729](#6729) ->
`docs/about/release-notes.mdx` and `docs/get-started/prerequisites.mdx`:
Record the Node.js 22.19 minimum.
- [#6735](#6735) ->
`docs/about/release-notes.mdx` and
`docs/reference/platform-support.mdx`: Summarize the Ubuntu 26.04
userspace contract without claiming pending host or live validation.
- [#6775](#6775) ->
`docs/about/release-notes.mdx` and
`docs/resources/community-contributions.mdx`: Route independent
solutions outside canonical supported-product documentation.
- [#6740](#6740) ->
`docs/about/release-notes.mdx`: Summarize the semantic
dependency-upgrade contributor workflow.
- [#6777](#6777) ->
`docs/about/release-notes.mdx` and `docs/CONTRIBUTING.md`: Summarize the
route-safe documentation-refactor workflow.
- [#6741](#6741) ->
`docs/about/release-notes.mdx` and
`docs/security/openclaw-2026.6.10-dependency-review.md`: Summarize
reviewed npm archive verification and audit enforcement.
- [#6739](#6739) ->
`docs/about/release-notes.mdx` and
`docs/security/openclaw-2026.6.10-dependency-review.md`: Record the
locked offline dependency graph for the managed OpenClaw WeChat runtime.
- [#6737](#6737) ->
`docs/about/release-notes.mdx`: Record removal of the messaging build
plan from final OpenClaw and Hermes image environments.
- [#6733](#6733) ->
`docs/about/release-notes.mdx`: Summarize cached plugin dependency
layers for source and blueprint rebuilds.

### Skipped from docs-skip

- None. No commit or changed path in `v0.0.81..origin/main` matched
`openclaw-sandbox-permissive.yaml` or `config-show`, and the drafted
content contains none of the configured skip terms.

## Type of Change

- [ ] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [x] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Quality Gates

- [ ] Tests added or updated for changed behavior
- [ ] Existing tests cover changed behavior — justification:
- [x] Tests not applicable — justification: This is a documentation-only
release-prep update; behavior is protected by the merged source PRs, and
the documentation build validates the changed routes and agent variants.
- [x] Docs updated for user-facing behavior changes
- [ ] Docs not applicable — justification:
- [ ] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [ ] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification:
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Verification

- [x] PR description includes the DCO sign-off declaration and every
commit appears as `Verified` in GitHub
- [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or
`npm run check:diff` passed when hooks were skipped or unavailable
- [x] Targeted behavior tests pass for the current change set, or tests
are marked not applicable above — tests are not applicable for this
documentation-only change.
- [ ] Applicable broad gate passed — `npm test` for broad
runtime/test-harness changes; `npm run check` for repo-wide
validation/coverage changes — command/result: not run for this
documentation-only change.
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [ ] `npm run docs` builds without warnings (doc changes only) — 0
errors; two pre-existing Fern warnings remain.
- [x] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)
— no new pages.

---
Signed-off-by: Charan Jagwani <cjagwani@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Updated release notes with improvements to sandbox recovery,
onboarding, session management, policy validation, storage checks, and
system requirements.
  * Clarified Ollama setup instructions and status labels.
* Documented safer snapshot restoration, including dedicated ports and
protection against destructive failures.
* Expanded `backup-all` coverage to include eligible stopped sandboxes.
* Added guidance rejecting broad or catch-all network destinations in
custom policies.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Charan Jagwani <cjagwani@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery bug-fix PR fixes a bug or regression v0.0.82 Release target

Projects

None yet

4 participants