fix(sandbox): treat an already-active port forward as recovery success - #7092
Conversation
`ensureSandboxPortForwardForPort` trusted the `openshell forward start` exit code: any non-zero status returned false and surfaced as "the dashboard/API host forward could not be restored". But OpenShell exits non-zero when the port is already forwarded, so when recovery's stop -> start ran against a still-active forward (e.g. list drift left the entry stale while the port kept listening), `forward start` reported "already forwarded" and recover failed for a healthy forward — the #7085 symptom on `nemohermes recover`. The exit code is not the authoritative success signal; the live forward list is (as onboard/forward-start.ts already documents). On a non-zero start, re-probe `isSandboxPortForwardHealthy`: accept an already-active, target-owned forward as idempotent success, and still fail when no target-owned forward is active so a genuine start failure is not masked. Adds unit coverage for both the already-forwarded (success) and genuinely-absent (failure) non-zero-start cases. Closes #7085 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Jason Ma <jama@nvidia.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughSandbox forward recovery now tolerates non-authoritative intermediate states and re-probes forward ownership after non-zero background starts. Tests cover immediate, delayed, absent, and conflicting ownership outcomes. ChangesSandbox forward recovery
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Recovery as ensureSandboxPortForwardForPort
participant CLI as runOpenshell
participant Health as forward health probe
Recovery->>CLI: Start forward in background
CLI-->>Recovery: Non-zero status
Recovery->>Health: Re-probe forward ownership
Health-->>Recovery: Target-owned, absent, or different sandbox
Recovery-->>Recovery: Accept, retry, or return false
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Code Coverage OverviewLanguages: TypeScript TypeScript / code-coverage/pluginThe overall coverage remains at 96%, unchanged from the TypeScript / code-coverage/cliThe overall coverage in the Show a code coverage summary of the most impacted files.
Updated |
There was a problem hiding this comment.
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 `@test/process-recovery-forward-failure.test.ts`:
- Around line 15-17: Replace the root-level test’s requireSource-based imports
around ensureSandboxPortForwardForPort with ESM imports, and convert the
associated mocks at the referenced locations to Vitest’s ESM mocking approach.
Remove createRequire usage while preserving the existing mocked behavior and
test coverage.
🪄 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: bc71d9bb-2a7f-4bdf-a675-6ce68f95a6dc
📒 Files selected for processing (2)
src/lib/actions/sandbox/forward-recovery.tstest/process-recovery-forward-failure.test.ts
PR Review Advisor — InformationalAdvisor assessment: Informational / high confidence Model lanes
Nemotron output stays in workflow artifacts and does not change the assessment above. E2E guidanceAdvisory only. E2E / PR Gate selects and runs jobs independently. Recommended E2E: This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge. |
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
apurvvkumaria
left a comment
There was a problem hiding this comment.
Exact-head review for a735921 found one production-path blocker. With the default nonzero recovery wait, a listener that is already reachable enters the pre-start settling branch in forward-recovery.ts; when live ownership metadata remains stale/ownerless, that branch returns false before forward start is attempted. The new post-start already-active tolerance is therefore unreachable for the PR’s stated list-drift scenario. The positive regression test masks this by setting NEMOCLAW_FORWARD_RECOVERY_WAIT_MS=0. Please add a default/nonzero-wait regression with a pre-existing reachable listener and stale metadata, and reconcile it without ever accepting an unowned listener. Current CI, DCO/signatures, advisor review, and protected E2E otherwise look healthy.
Allow the idempotent forward start path to restore authoritative ownership after a nonzero settle wait while retaining fail-closed behavior for unavailable or wrong-owner metadata. Co-authored-by: Jason Ma <jama@nvidia.com> Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
Addressed in verified commit 0bc0e60. The nonzero-wait ownerless-listener path now reaches idempotent start reconciliation, and success still requires authoritative exact target ownership. Wrong-owner, unavailable-metadata, and forced-restart paths remain fail-closed; 37 targeted tests and all focused structural/build checks passed.
Co-authored-by: Jason Ma <jama@nvidia.com> Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
cv
left a comment
There was a problem hiding this comment.
Approved on the refreshed exact head after the deterministic gate passed: 54 current checks green, no conflicts, no unresolved major/critical review findings, DCO present, and all 6 commits verified. The recovery path remains fail-closed for absent, foreign, and unavailable listeners, replaces only when ownership is proven, and treats the same owned active forward as idempotent success. Focused recovery suites passed 37 tests; the nine-category security review is clean.
<!-- markdownlint-disable MD041 --> ## Summary Adds the canonical `docs/changelog/2026-07-18.mdx` release-prep entry with the exact `## v0.0.88` heading. The entry summarizes every user-visible change on `main` since v0.0.87 and links each release theme to the focused user documentation. ## Changes - Add one parser-safe dated changelog entry for v0.0.88 covering DGX Station preparation, inference health, multi-gateway sandbox operations and recovery, onboarding policy defaults, and rebuild credential reuse. - Reconcile the changelog against the merged v0.0.88-labeled PRs and the complete `v0.0.87..origin/main` commit range. - Source mapping: - [#7152](#7152) -> `docs/changelog/2026-07-18.mdx`: Document RDMA-aware OpenIB service remediation during DGX Station preparation. - [#7155](#7155) -> `docs/changelog/2026-07-18.mdx`: Document stopped-container preservation and fail-closed restart-policy boundaries. - [#7158](#7158) -> `docs/changelog/2026-07-18.mdx`: Document bounded packaged CDI refresh for the exact AI Developer Tools Station profile. - [#7074](#7074) -> `docs/changelog/2026-07-18.mdx`: Document authenticated upstream model probes and precise route-reachability claims. - [#7007](#7007) -> `docs/changelog/2026-07-18.mdx`: Document the explicit serving-process health gap in `status` and `doctor`. - [#7113](#7113) -> `docs/changelog/2026-07-18.mdx`: Document owning-gateway selection for sandbox-scoped status and exec operations. - [#7092](#7092) -> `docs/changelog/2026-07-18.mdx`: Document idempotent recovery for target-owned active port forwards. - [#7133](#7133) -> `docs/changelog/2026-07-18.mdx`: Document web-search-aware policy preset defaults during onboarding. - [#7129](#7129) -> `docs/changelog/2026-07-18.mdx`: Document gateway-registered web-search credential reuse during rebuild preflight. ## 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 <!-- Check one tests line and one docs line. Check other lines when applicable. Add every requested justification or approval reference. --> - [ ] Tests added or updated for changed behavior - [x] Existing tests cover changed behavior — justification: `test/changelog-docs.test.ts` validates the dated changelog contract, exact release heading, and parser-safe MDX structure. - [ ] Tests not applicable — justification: - [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 <!-- Check each applicable item only when supported by the requested evidence. Run targeted tests once per relevant change set and rerun after later edits or hook autofixes that can affect the tested behavior. Do not rerun hook-covered checks. --> - [x] PR description includes a `Signed-off-by:` line 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 — command/result or justification: `npx vitest run test/changelog-docs.test.ts` passed 6 tests. - [ ] Applicable broad gate passed — `npm test` for broad runtime/test-harness changes; `npm run check` for repo-wide validation/coverage changes — command/result: - [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) — completed successfully with 0 errors and 2 existing Fern warnings. - [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) — not applicable because native changelog entries use the required parser-safe MDX SPDX comment without frontmatter. --- Signed-off-by: Aaron Erickson <aerickson@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added improved DGX Station preparation workflows. * Enhanced sandbox status and diagnostic reporting for inference health. * Improved state selection and recovery across multiple gateways. * Added safer onboarding defaults for web search policies. * Improved rebuild preflight handling for credential reuse and fail-closed behavior. * **Documentation** * Added release notes for version 0.0.88. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Summary
nemohermes <sandbox> recover(and any dashboard/API forward recovery) could report "the dashboard/API host forward could not be restored" even when the forward was already active and healthy.ensureSandboxPortForwardForPorttrusted theopenshell forward startexit code — any non-zero status returned failure — but OpenShell exits non-zero when the port is already forwarded. When recovery's stop → start ran against a still-active forward (e.g. OpenShell's forward list drifted stale while the port kept listening),forward startreported "already forwarded" and recovery failed for a healthy forward. After this change, a non-zero start with a reachable local listener settles against the authoritative live forward list and accepts an already-active, target-owned forward as idempotent success, including when owner metadata appears after a short delay.Related Issue
Closes #7085
Changes
src/lib/actions/sandbox/forward-recovery.ts: on a non-zeroforward start, fail immediately when no local listener is reachable; otherwise use the existing bounded health-settling path and accept success only when the live forward list confirms a running, reachable forward owned by the target sandbox. This keeps the live list authoritative, handles delayed owner visibility, and preserves fail-closed behavior for absent or occupied forwards. The maintenance comment records the OpenShell 0.0.85 source boundary and removal condition.test/process-recovery-forward-failure.test.ts: add five focused cases for non-zero starts: immediate target ownership succeeds, an absent forward fails, a reachable listener without authoritative ownership times out and fails, delayed target ownership succeeds, and delayed ownership by another sandbox fails. The new setup uses ESM imports and linear Vitest mocks, with no DGX or real sandbox dependency.Type of Change
Quality Gates
recoverpath). The change keeps fail-closed behaviour for occupied/absent forwards and only accepts a target-owned, reachable, running forward. Prepared by an automated fix loop.Verification
Signed-off-by:line and every commit appears asVerifiedin GitHubpre-commit,commit-msg, andpre-pushhooks passed, ornpm run check:diffpassed when hooks were skipped or unavailablenpx vitest run --project integration test/process-recovery.test.ts test/process-recovery-forward-failure.test.ts→ 36 passed;npm run check:diff→ passed.npm testfor broad runtime/test-harness changes;npm run checkfor repo-wide validation/coverage changes — command/result:npm run docsbuilds without warnings (doc changes only)Verification detail
forward startsucceeds for immediate or delayed target ownership, fails fast when no listener remains, rejects a reachable but ownerless listener after the settle deadline, and rejects delayed ownership by another sandbox. The broader recovery regression set remains green.nemohermes recoverDGX Station repro (rebuild a Hermes sandbox with a not-running gateway, runrecover, observe the already-active forward reported as success) needs a DGX/sandbox not available to this loop.Signed-off-by: Jason Ma jama@nvidia.com
Signed-off-by: Julie Yaunches jyaunches@nvidia.com
Summary by CodeRabbit
Bug Fixes
Tests