Skip to content

Commit 0134412

Browse files
jyaunchesprekshivyasgithub-actions[bot]
authored
fix(onboard): release the dashboard port reservation (#9569)
<!-- markdownlint-disable MD041 --> ## Summary Created-sandbox finalization invokes the dashboard port release callback through its dashboard object. The callback previously read that object instead of the original reservation scope, so the host port remained bound. The callback now closes over the original scope, and a real loopback test protects this contract. ## Related Issue Fixes #9568 ## Changes - Add a fail-first loopback test that invokes the extracted callback through the object-property call used by created-sandbox finalization. - Make the dashboard port release callback read and clear its original reservation scope, independent of the call receiver. - Preserve the build-time reservation, release-before-forwarding order, and failure cleanup established by #8798 and #8863. E2E root cause: `32209785348/dashboard-reservation/unbound-release-receiver` Source run: [32209785348](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348) (run 32209785348, attempt 1, commit `9d75205307d70c820c9f8000edc2257ce6744460`) Failed jobs: - [`cloud-onboard` (95940197627)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940197627) - [`hermes-e2e` (95940197656)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940197656) - [`Onboarding: repairs a missing sandbox and rejects conflicting resume input` (95940197969)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940197969) - [`Health: treats a 401 authentication response as reachable` (95940197990)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940197990) - [`Inference: OpenClaw switches providers and remains responsive` (95940197991)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940197991) - [`Inference: rejects unsafe routes and proves runtime identities` (95940198016)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198016) - [`Network policy: enforces restricted allow and deny rules` (95940198049)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198049) - [`Authorization: approves a write-scope upgrade without operator.admin` (95940198056)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198056) - [`Onboarding: resumes interrupted setup from recorded progress` (95940198061)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198061) - [`Rebuild: preserves Hermes state and recovers cron dispatch` (95940198079)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198079) - [`Backup: restores workspace files and memory` (95940198088)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198088) - [`OpenClaw: installs, onboards, and completes an agent turn` (95940198127)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198127) - [`Security: Hermes retains the required sandbox posture` (95940198149)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198149) - [`Upgrade: preserves v0.0.36 state on x86-64` (95940198181)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198181) - [`Inference: OpenClaw uses hosted inference` (95940198182)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198182) - [`Upgrade: preserves v0.0.74 state on x86-64` (95940198183)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198183) - [`Rebuild: preserves OpenClaw state and rotates the gateway token` (95940198190)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198190) - [`Security: OpenClaw retains the required sandbox posture` (95940198200)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198200) - [`Upgrade: preserves v0.0.55 state on x86-64` (95940198203)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198203) - [`Upgrade: preserves v0.0.55 state on Arm64` (95940198235)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198235) - [`Upgrade: migrates v0.0.89 state on x86-64` (95940198258)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95940198258) - [`ubuntu-repo-cloud-langchain-deepagents-code` (95945453609)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95945453609) - [`ubuntu-repo-cloud-openclaw` (95945453613)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95945453613) - [`ubuntu-policy-custom-missing-presets-negative` (95945453618)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95945453618) - [`ubuntu-repo-docker-post-reboot-recovery` (95945453626)](https://github.com/NVIDIA/NemoClaw/actions/runs/32209785348/job/95945453626) Signature: `Could not allocate a dashboard port`; `Dashboard port 18789 became host-bound during sandbox build; cannot reallocate to 18790` after `CHAT_UI_URL=18789`. Commit under review: `0f2ec3fcb7d70884fd529f808cef91d5bd15cb72` against base `7afe39541e81f70d9e1aa39c49415084d8276524`. - Fail-first commit `72a0eab6609ce9b32f1c104b05c12b3fec613132`: the dashboard-port test file reported 32 passes and one failure. The new test showed that `scope.current` still held the live reservation after the extracted callback returned. - Product correction `67327620caea9247565b6a28b754a07a5f21ddaf`: the callback closes over the original scope instead of reading its call receiver. - Reconciled commit under review `0f2ec3fcb7d70884fd529f808cef91d5bd15cb72`: the two focused CLI test files passed all 45 tests, including all 33 dashboard-port tests. `npm run typecheck:cli` passed. The growth-guardrail test file passed all 32 tests. Scope: one root cause. ## Type of Change - [x] 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 - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — [independent nine-category security review PASS](#9569 (comment)) for `0f2ec3fcb7d70884fd529f808cef91d5bd15cb72` against `7afe39541e81f70d9e1aa39c49415084d8276524`; no findings. - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: ## DGX Station Hardware Evidence - [ ] Tested on DGX Station - Tested commit: - Station profile/scenario: - Result: - Supporting evidence: ## Verification - [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 validate:pr` passed after refreshing `origin/main` when hooks were skipped or unavailable - [x] Targeted behavior tests pass for the current change set, or tests are marked not applicable above — `npm exec -- vitest run --project cli src/lib/onboard/dashboard-port.test.ts src/lib/onboard/created-sandbox-finalization.test.ts`: 45 passed, including 33 dashboard-port tests; `npm run typecheck:cli`: passed; `npm exec -- vitest run --project integration test/growth-guardrails.test.ts`: 32 passed. - [ ] 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) - [ ] 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) --- Signed-off-by: Julie Yaunches <jyaunches@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Fixed dashboard port reservation cleanup so released reservations reliably allow the port to be reused. - **CI/CD** - Added secure validation for public container image digests on Linux AMD64. - Added bounded retries for transient image visibility delays while failing immediately on other errors. - Improved temporary credential isolation and cleanup during image checks. - **Tests** - Added coverage for dashboard port release behavior, digest validation, retry handling, cleanup, and workflow watch triggers. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Julie Yaunches <jyaunches@nvidia.com> Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> Co-authored-by: Prekshi Vyas <34834085+prekshivyas@users.noreply.github.com> Co-authored-by: Prekshi Vyas <prekshiv@nvidia.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent d7c33bf commit 0134412

2 files changed

Lines changed: 32 additions & 4 deletions

File tree

src/lib/onboard/dashboard-port.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ async function closeServer(server: Server): Promise<void> {
4040
});
4141
}
4242

43+
async function listenAndCloseOnLoopback(port: number): Promise<void> {
44+
const server = await listenOnLoopback(port);
45+
await closeServer(server);
46+
}
47+
4348
async function unusedLoopbackPort(): Promise<number> {
4449
const server = await listenOnLoopback(0);
4550
const address = server.address();
@@ -363,7 +368,7 @@ describe("dashboard port reservation", () => {
363368
withDashboardPortReservationScope(async (scope) => {
364369
scope.current = await reserveDashboardPort(port);
365370
await assert.rejects(
366-
listenOnLoopback(port),
371+
listenAndCloseOnLoopback(port),
367372
(error: NodeJS.ErrnoException) => error.code === "EADDRINUSE",
368373
);
369374
throw new Error("sandbox build failed");
@@ -375,6 +380,29 @@ describe("dashboard port reservation", () => {
375380
await closeServer(listener);
376381
});
377382

383+
it("releases the selected port when finalization calls the extracted scope callback (#9568)", async () => {
384+
const port = await unusedLoopbackPort();
385+
386+
await withDashboardPortReservationScope(async (scope) => {
387+
scope.current = await reserveDashboardPort(port);
388+
await assert.rejects(
389+
listenAndCloseOnLoopback(port),
390+
(error: NodeJS.ErrnoException) => error.code === "EADDRINUSE",
391+
);
392+
393+
const finalizationDashboard = { releasePort: scope.release };
394+
await finalizationDashboard.releasePort();
395+
396+
assert.equal(scope.current, null);
397+
const listener = await listenOnLoopback(port);
398+
try {
399+
assert.equal(listener.listening, true);
400+
} finally {
401+
await closeServer(listener);
402+
}
403+
});
404+
});
405+
378406
it("reselects before sandbox creation when a listener wins the allocation race (#8798)", async () => {
379407
const attempts: number[] = [];
380408
const warnings: string[] = [];

src/lib/onboard/dashboard-port.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,9 +610,9 @@ export async function withDashboardPortReservationScope<T>(
610610
): Promise<T> {
611611
const scope: DashboardPortReservationScope = {
612612
current: null,
613-
async release() {
614-
const reservation = this.current;
615-
this.current = null;
613+
release: async () => {
614+
const reservation = scope.current;
615+
scope.current = null;
616616
await reservation?.release();
617617
},
618618
};

0 commit comments

Comments
 (0)