Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
05db612
fix(health): add verifyDeployment() and fix false 'Health Offline' on…
jyaunches Apr 29, 2026
658a0c5
ci: add cross-repo image rebuild trigger and pin launchable to stable
jyaunches Apr 29, 2026
7b9f547
test(e2e): add device-auth-health regression test for #2342
jyaunches May 6, 2026
e0c18a2
fix(e2e): increase device-auth-health timeout to 30m
jyaunches May 6, 2026
2f47f5e
fix(e2e): use SSH for sandbox exec and increase timeout
jyaunches May 6, 2026
b07cfaf
ci(nightly): migrate E2E jobs to NVIDIA self-hosted runners
jyaunches May 6, 2026
6e6f24d
fix(e2e): fix PATH and install flow in device-auth-health test
jyaunches May 6, 2026
0e17cce
Merge remote-tracking branch 'origin/main' into issue-2342-brev-launc…
jyaunches May 6, 2026
16fa34c
fix(e2e): make sandbox exec probes non-fatal when SSH unavailable
jyaunches May 6, 2026
5ec077e
style(e2e): apply shfmt formatting to device-auth-health test
jyaunches May 6, 2026
7340c70
fix: use named sandbox exec format for health probes
jyaunches May 6, 2026
6ac5434
style: apply shfmt formatting to brev-launchable-ci-cpu.sh
jyaunches May 6, 2026
9228360
style: fix shfmt case-indent (-ci) in brev-launchable-ci-cpu.sh
jyaunches May 6, 2026
dc8ce62
test: update cli.test.ts probe mocks for new curl health pattern
jyaunches May 6, 2026
d39a353
fix(health): treat gateway version as cosmetic, not a health signal
jyaunches May 6, 2026
2a1aa17
refactor: remove out-of-scope CI changes from health fix PR
jyaunches May 6, 2026
674831e
Merge branch 'main' into issue-2342-brev-launchable-version-pin-and-g…
cv May 6, 2026
3075a70
Merge branch 'main' into issue-2342-brev-launchable-version-pin-and-g…
prekshivyas May 6, 2026
24c3ef0
fix: address CodeRabbit review feedback
jyaunches May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion .github/workflows/nightly-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ on:
rebuild-hermes-stale-base-e2e, double-onboard-e2e,
onboard-repair-e2e, onboard-resume-e2e, runtime-overrides-e2e,
credential-sanitization-e2e, telegram-injection-e2e,
overlayfs-autofix-e2e, launchable-smoke-e2e, gpu-e2e, gpu-double-onboard-e2e
overlayfs-autofix-e2e, device-auth-health-e2e,
launchable-smoke-e2e, gpu-e2e, gpu-double-onboard-e2e
required: false
type: string
default: ""
Expand Down Expand Up @@ -1418,6 +1419,42 @@ jobs:
/tmp/nemoclaw-e2e-onboard-negative.log
if-no-files-found: ignore

# ── Device Auth Health Probe (#2342) ────────────────────────────
# Regression test for #2342: verifies health probes work correctly when
# device auth is enabled (the default). Previously `curl -sf` treated
# HTTP 401 as failure, causing false "Health Offline" readings.
# Validates: /health returns 200, / returns 401, status != Offline,
# gateway recovery with device auth, port forward liveness.
device-auth-health-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' &&
(github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',device-auth-health-e2e,'))
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Run device auth health E2E
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-health-auth"
NEMOCLAW_RECREATE_SANDBOX: "1"
GITHUB_TOKEN: ${{ github.token }}
run: bash test/e2e/test-device-auth-health.sh

- name: Upload install log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: device-auth-health-install-log
path: /tmp/nemoclaw-e2e-health-install.log
if-no-files-found: ignore
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ── Launchable Install-Flow Smoke Test ─────────────────────────
# Validates the community install path (brev-launchable-ci-cpu.sh) end-to-end.
# The launchable script has ZERO Brev dependencies — it's a generic Ubuntu
Expand Down Expand Up @@ -1619,6 +1656,7 @@ jobs:
credential-sanitization-e2e,
telegram-injection-e2e,
overlayfs-autofix-e2e,
device-auth-health-e2e,
launchable-smoke-e2e,
gpu-e2e,
gpu-double-onboard-e2e,
Expand Down Expand Up @@ -1696,6 +1734,7 @@ jobs:
rebuild-hermes-e2e,
rebuild-hermes-stale-base-e2e,
overlayfs-autofix-e2e,
device-auth-health-e2e,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
gpu-e2e,
]
if: ${{ always() && github.event_name == 'workflow_dispatch' }}
Expand Down Expand Up @@ -1802,6 +1841,7 @@ jobs:
credential-sanitization-e2e,
telegram-injection-e2e,
overlayfs-autofix-e2e,
device-auth-health-e2e,
gpu-e2e,
gpu-double-onboard-e2e,
]
Expand Down
11 changes: 8 additions & 3 deletions src/lib/actions/sandbox/process-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,19 @@ function parseSandboxGatewayProbe(result: SandboxCommandResult | null): boolean

/**
* Check whether the OpenClaw gateway process is running inside the sandbox.
* Uses the gateway's HTTP endpoint (dashboard port) as the source of truth,
* Uses the gateway's HTTP /health endpoint as the source of truth,
* since the gateway runs as a separate user and pgrep may not see it.
* Returns true (running), false (stopped), or null (cannot determine).
*
* Uses HTTP status code extraction instead of `curl -sf` so that
* 401 (device auth enabled) is correctly treated as "alive".
* Fixes #2342 — previously `curl -sf` failed on 401, causing false
* "Health Offline" readings.
*/
function isSandboxGatewayRunning(sandboxName: string): boolean | null {
const agent = agentRuntime.getSessionAgent(sandboxName);
const probeUrl = agentRuntime.getHealthProbeUrl(agent);
const command = `curl -sf --max-time 3 ${shellQuote(probeUrl)} > /dev/null 2>&1 && echo RUNNING || echo STOPPED`;
const command = `HTTP_CODE=$(curl -so /dev/null -w '%{http_code}' --max-time 3 ${shellQuote(probeUrl)} 2>/dev/null || echo 000); case "$HTTP_CODE" in 200|401) echo RUNNING ;; *) echo STOPPED ;; esac`;
const execProbe = parseSandboxGatewayProbe(executeSandboxExecCommand(sandboxName, command));
if (execProbe !== null) return execProbe;
return parseSandboxGatewayProbe(executeSandboxCommand(sandboxName, command));
Expand All @@ -171,7 +176,7 @@ export async function isSandboxGatewayRunningForStatus(
): Promise<boolean | null> {
const agent = agentRuntime.getSessionAgent(sandboxName);
const probeUrl = agentRuntime.getHealthProbeUrl(agent);
const command = `curl -sf --max-time 3 ${shellQuote(probeUrl)} > /dev/null 2>&1 && echo RUNNING || echo STOPPED`;
const command = `HTTP_CODE=$(curl -so /dev/null -w '%{http_code}' --max-time 3 ${shellQuote(probeUrl)} 2>/dev/null || echo 000); case "$HTTP_CODE" in 200|401) echo RUNNING ;; *) echo STOPPED ;; esac`;
return parseSandboxGatewayProbe(await executeSandboxExecCommandForStatus(sandboxName, command));
}

Expand Down
14 changes: 9 additions & 5 deletions src/lib/agent-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ export function getSessionAgent(sandboxName?: string): AgentDefinition | null {

/**
* Get the health probe URL for the agent.
* Returns the agent's configured probe URL, or the OpenClaw default.
* Returns the agent's configured probe URL, or the OpenClaw /health endpoint.
*
* Uses /health (not /) because /health returns 200 regardless of device auth
* state, while / returns 401 when device auth is enabled. This ensures
* health probes work correctly in all configurations. Fixes #2342.
*/
export function getHealthProbeUrl(agent: AgentDefinition | null): string {
if (!agent) return `http://127.0.0.1:${DASHBOARD_PORT}/`;
return agent.healthProbe?.url || `http://127.0.0.1:${DASHBOARD_PORT}/`;
if (!agent) return `http://127.0.0.1:${DASHBOARD_PORT}/health`;
return agent.healthProbe?.url || `http://127.0.0.1:${DASHBOARD_PORT}/health`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function escapeEre(value: string): string {
Expand Down Expand Up @@ -162,7 +166,7 @@ export function buildOpenClawRecoveryScript(port: number): string {
"if [ -r /tmp/nemoclaw-proxy-env.sh ]; then . /tmp/nemoclaw-proxy-env.sh; _PE_MISSING=0; else _PE_MISSING=1; fi;",
"[ -f ~/.bashrc ] && . ~/.bashrc;",
'if [ "$_PE_MISSING" = "0" ]; then case "${NODE_OPTIONS:-}" in *nemoclaw-sandbox-safety-net*) _SN_MISSING=0 ;; *) _SN_MISSING=1 ;; esac; case "${NODE_OPTIONS:-}" in *nemoclaw-ciao-network-guard*) _CIAO_MISSING=0 ;; *) _CIAO_MISSING=1 ;; esac; if [ "$_SN_MISSING" = "0" ] && [ "$_CIAO_MISSING" = "0" ]; then _GUARDS_MISSING=0; else _GUARDS_MISSING=1; fi; else _GUARDS_MISSING=0; fi;',
`if curl -sf --max-time 3 http://127.0.0.1:${port}/ > /dev/null 2>&1; then echo ALREADY_RUNNING; exit 0; fi;`,
`_GW_CODE=$(curl -so /dev/null -w '%{http_code}' --max-time 3 http://127.0.0.1:${port}/health 2>/dev/null || echo 000); case "$_GW_CODE" in 200|401) echo ALREADY_RUNNING; exit 0 ;; esac;`,
"rm -rf /tmp/openclaw-*/gateway.*.lock 2>/dev/null;",
...buildGatewayLogSetup(true, "gateway"),
buildGatewayLogSelection(),
Expand Down Expand Up @@ -227,7 +231,7 @@ export function buildRecoveryScript(agent: AgentDefinition | null, port: number)
return [
"[ -f ~/.bashrc ] && . ~/.bashrc;",
hermesHome,
`if curl -sf --max-time 3 ${shellQuote(probeUrl)} > /dev/null 2>&1; then echo ALREADY_RUNNING; exit 0; fi;`,
`_GW_CODE=$(curl -so /dev/null -w '%{http_code}' --max-time 3 ${shellQuote(probeUrl)} 2>/dev/null || echo 000); case "$_GW_CODE" in 200|401) echo ALREADY_RUNNING; exit 0 ;; esac;`,
...buildGatewayLogSetup(false),
buildGatewayLogSelection(),
`_GATEWAY_PROC_PATTERN=${shellQuote(staleGatewayPattern)};`,
Expand Down
82 changes: 69 additions & 13 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,32 @@ function runCaptureOpenshell(
return runCapture(openshellArgv(args, opts), opts);
}

/**
* Execute a shell command inside a sandbox for post-deployment verification.
* Returns a structured result with status, stdout, stderr — or null if
* the sandbox is unreachable. Uses `openshell sandbox exec` with sh -c.
*/
function executeSandboxCommandForVerification(
sandboxName: string,
script: string,
): { status: number; stdout: string; stderr: string } | null {
try {
const result = spawnSync(
getOpenshellBinary(),
["sandbox", "exec", "-n", sandboxName, "--", "sh", "-c", script],
{ encoding: "utf-8", timeout: 15000, stdio: ["ignore", "pipe", "pipe"] },
);
if (result.error) return null;
return {
status: result.status ?? 1,
stdout: (result.stdout || "").trim(),
stderr: (result.stderr || "").trim(),
};
} catch {
return null;
}
Comment on lines +862 to +880

@coderabbitai coderabbitai Bot May 6, 2026

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, check if the file and function exist at the cited lines
head -n 880 src/lib/onboard.ts | tail -n 50

Repository: NVIDIA/NemoClaw

Length of output: 1728


🏁 Script executed:

# Search for SSH-based sandbox probe implementations
rg "ssh|SSH" src/lib/onboard.ts -A 3 -B 3

Repository: NVIDIA/NemoClaw

Length of output: 4259


🏁 Script executed:

# Look for device-auth E2E references
fd -type f "*.ts" -path "**e2e**" | head -20

Repository: NVIDIA/NemoClaw

Length of output: 230


🏁 Script executed:

# Search for mentions of openshell sandbox exec reliability issues
rg "sandbox exec|false.*probe|000.*probe" . --type ts -A 2 -B 2

Repository: NVIDIA/NemoClaw

Length of output: 19657


🏁 Script executed:

# Look for verifyDeployment function and related verification logic
rg "verifyDeployment|executeSandboxCommandForVerification" src/lib/onboard.ts -A 5 -B 2

Repository: NVIDIA/NemoClaw

Length of output: 1578


🏁 Script executed:

# Check verify-deployment.ts for SSH fallback implementation
head -n 100 src/lib/verify-deployment.ts

Repository: NVIDIA/NemoClaw

Length of output: 3631


🏁 Script executed:

# Search for the executeSandboxCommand callback usage in verify-deployment.ts
rg "executeSandboxCommand" src/lib/verify-deployment.ts -A 5 -B 2

Repository: NVIDIA/NemoClaw

Length of output: 1710


🏁 Script executed:

# Look for SSH fallback in verify-deployment or related modules
rg "recoveredSsh|SSH|ssh" src/lib/verify-deployment.ts -A 3 -B 3

Repository: NVIDIA/NemoClaw

Length of output: 1248


🏁 Script executed:

# Search for dashboard readiness probe implementation
rg "dashboard.*readiness|readiness.*dashboard" src/lib/onboard.ts -A 5 -B 5

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Look for unbounded probes or dashboard readiness loop
rg "dashboardReady|dashboard.*health|18789|DASHBOARD_PORT" src/lib/onboard.ts -A 3 -B 3 | head -100

Repository: NVIDIA/NemoClaw

Length of output: 4510


🏁 Script executed:

# Look for SSH fallback implementation pattern
rg "execSync|spawnSync.*ssh|ssh.*config" src/lib -A 5 -B 2 | head -80

Repository: NVIDIA/NemoClaw

Length of output: 4782


🏁 Script executed:

# Search for the SSH-based probe mentioned in the review
rg "device-auth|SSH.*fallback|ssh-config" src/lib -A 3 -B 3

Repository: NVIDIA/NemoClaw

Length of output: 5917


🏁 Script executed:

# Look at process-recovery.ts for SSH fallback implementation
cat src/lib/actions/sandbox/process-recovery.ts | head -100

Repository: NVIDIA/NemoClaw

Length of output: 3141


🏁 Script executed:

# Check if there's a dashboard readiness loop in onboard.ts
rg "waitFor|ready|health.*dashboard|dashboard.*health" src/lib/onboard.ts -A 4 -B 2 | head -120

Repository: NVIDIA/NemoClaw

Length of output: 5757


🏁 Script executed:

# Search for E2E tests mentioning device-auth
fd -type f "*.ts" | xargs rg "device-auth" 2>/dev/null | head -20

Repository: NVIDIA/NemoClaw

Length of output: 658


🏁 Script executed:

# Check executeSandboxCommand in process-recovery.ts more completely
head -n 150 src/lib/actions/sandbox/process-recovery.ts | tail -n 80

Repository: NVIDIA/NemoClaw

Length of output: 2628


🏁 Script executed:

# Search for how verify-deployment is actually used and if there's a wrapper
rg "verifyDeployment|executeSandboxCommandForVerification" src/lib/onboard.ts -B 5 -A 15 | head -80

Repository: NVIDIA/NemoClaw

Length of output: 2891


🏁 Script executed:

# Look for comment or issue about sandbox exec being unreliable in CI
rg "false.*probe|000.*probe|sandbox exec.*flaky|sandbox exec.*unreliable" . --type ts -A 2 -B 2

Repository: NVIDIA/NemoClaw

Length of output: 868


🏁 Script executed:

# Check if there's a dashboard readiness loop that's unbounded
rg "dashboard.*ready|waitFor.*dashboard|health.*loop" src/lib/onboard.ts -A 8 -B 3 | head -100

Repository: NVIDIA/NemoClaw

Length of output: 933


Consider using the SSH-based probe pattern that already exists in executeSandboxCommand.

The executeSandboxCommand function in process-recovery.ts already implements a robust SSH fallback for sandbox communication. Rather than keeping executeSandboxCommandForVerification on openshell sandbox exec, reuse the same SSH + sandbox ssh-config pattern to improve reliability when the sandbox is unreachable via the exec transport.

The dashboard readiness loop (lines 5036–5047) is already bounded to 15 attempts, so no change needed there.

🤖 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/onboard.ts` around lines 862 - 880, The current
executeSandboxCommandForVerification function uses openshell sandbox exec which
can fail when the exec transport is unavailable; change it to reuse the
SSH-based probe pattern implemented by executeSandboxCommand in
process-recovery.ts: call openshell sandbox ssh-config to obtain SSH connection
args, build and run an ssh sh -c "<script>" probe (with the same
encoding/timeout/stdio handling and error-to-null behavior), and only fall back
to the original openshell sandbox exec approach if ssh-config or the ssh probe
fails; update the function to return the same {status, stdout, stderr} shape and
preserve the 15s timeout and null-on-error semantics while referencing
executeSandboxCommand and "sandbox ssh-config" in the change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — the SSH fallback is a valid hardening suggestion. However, executeSandboxCommandForVerification is only called during the post-onboard verification step when the sandbox has just been created and the exec transport is known to be working (onboard just used it successfully for the readiness loop). Adding an SSH fallback here would add complexity for a path that's already proven reliable at this point in the flow.

The E2E test already demonstrated that openshell sandbox exec can be unreliable in some CI environments (which is why Phase 2 skips gracefully), but the onboard runtime context is different — the exec transport was just exercised moments earlier. Will revisit if we see failures in production.

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.

@jyaunches Got it — agreed to keep executeSandboxCommandForVerification on openshell sandbox exec only for now given the just-proven exec path during readiness. No change requested.

If you’d like, I can open a low-priority follow-up issue to track a gated SSH fallback (opt-in via env) in case we ever see production flakiness. Otherwise I’ll consider this thread resolved on your decision.


✏️ Learnings added
Learnt from: jyaunches
Repo: NVIDIA/NemoClaw PR: 3128
File: src/lib/onboard.ts:862-880
Timestamp: 2026-05-06T23:11:17.624Z
Learning: NVIDIA/NemoClaw preference: In src/lib/onboard.ts, executeSandboxCommandForVerification should remain exec-only (uses `openshell sandbox exec`) because it runs immediately after onboard readiness where exec was proven reliable. Avoid adding SSH fallback unless production failures are observed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jyaunches
Repo: NVIDIA/NemoClaw PR: 2495
File: src/lib/onboard-inference-probes.ts:135-323
Timestamp: 2026-04-27T13:06:57.456Z
Learning: In NVIDIA/NemoClaw PRs that extract code from `src/lib/onboard.ts` into new behavior-preserving modules (e.g., `src/lib/onboard-providers.ts`, `src/lib/onboard-ollama-proxy.ts`, `src/lib/onboard-inference-probes.ts`), reviewers should avoid requesting refactors/complexity/style improvements in the extracted modules as part of the same PR. Only check for correctness and behavior preservation; defer non-behavior-preserving cleanup to separate follow-up PRs.

Learnt from: ericksoa
Repo: NVIDIA/NemoClaw PR: 2554
File: src/lib/onboard.ts:698-700
Timestamp: 2026-04-28T17:22:19.715Z
Learning: For NVIDIA/NemoClaw credential environment hydration/migration, implement fill-only-if-missing semantics across the onboarding and credentials modules: (1) In src/lib/credentials.ts, stageLegacyCredentialsToEnv() should only consider an allowlisted set of credential keys and should set process.env[key] only when getCredential(key) returns null (never override an explicitly provided env export). Treat blank/whitespace values as unset. (2) In src/lib/onboard.ts, hydrateCredentialEnv(envName) must first read getCredential(envName) and only call stageLegacyCredentialsToEnv() when that specific key is missing; after staging, re-read and then set process.env[envName]. This ensures stale credentials.json values cannot override explicitly provided environment credentials.

}

// URL/string utilities — delegated to src/lib/url-utils.ts
const {
compactText,
Expand Down Expand Up @@ -5007,23 +5033,18 @@ async function createSandbox(
// Wait for the branded dashboard to become fully ready (web server live)
// This prevents port forwards from connecting to a non-existent port
// or seeing 502/503 errors during initial load.
console.log(` Waiting for ${cliDisplayName()} dashboard to become ready...`);
// Probes /health endpoint and accepts 200 or 401 (device auth) as "alive".
// Previously used `curl -sf` which failed on 401, causing false negatives. Fixes #2342.
console.log(" Waiting for NemoClaw dashboard to become ready...");
const openshellBin = getOpenshellBinary();
for (let i = 0; i < 15; i++) {
const readyMatch = runCaptureOpenshell(
[
"sandbox",
"exec",
"-n",
sandboxName,
"--",
"curl",
"-sf",
`http://localhost:${effectiveDashboardPort}/`,
],
const readyOutput = runCaptureOpenshell(
["sandbox", "exec", "-n", sandboxName, "--", "curl", "-so", "/dev/null", "-w", "%{http_code}",
"--max-time", "3", `http://localhost:${effectiveDashboardPort}/health`],
{ ignoreError: true },
);
if (readyMatch) {
const readyCode = parseInt((readyOutput || "").trim(), 10) || 0;
if (readyCode === 200 || readyCode === 401) {
console.log(" ✓ Dashboard is live");
break;
}
Expand Down Expand Up @@ -9485,6 +9506,41 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
`providers/channels enabled to migrate them, then the file is removed automatically.`,
);
}
// Post-deployment verification — confirm the full delivery chain is
// operational before telling the user "YOUR AGENT IS LIVE". Fixes #2342.
const verifyDeploymentModule: typeof import("./verify-deployment") = require("./verify-deployment");
const _verifyChatUiUrl = process.env.CHAT_UI_URL || `http://127.0.0.1:${DASHBOARD_PORT}`;
const verifyChain = buildChain({ chatUiUrl: _verifyChatUiUrl, isWsl: isWsl(), wslHostAddress: getWslHostAddress() });
const verificationResult = verifyDeploymentModule.verifyDeployment(
sandboxName,
verifyChain,
{
executeSandboxCommand: (name: string, script: string) => {
return executeSandboxCommandForVerification(name, script);
},
probeHostPort: (port: number, probePath: string) => {
const result = runCapture(
["curl", "-so", "/dev/null", "-w", "%{http_code}", "--max-time", "3",
`http://127.0.0.1:${port}${probePath}`],
{ ignoreError: true },
);
return parseInt(result.trim(), 10) || 0;
},
captureForwardList: () => {
const output = runCaptureOpenshell(["forward", "list"], { ignoreError: true });
return output || null;
},
getMessagingChannels: (_name: string) => selectedMessagingChannels || [],
providerExistsInGateway: (providerName: string) => providerExistsInGateway(providerName),
},
);

// Print verification diagnostics
const diagLines = verifyDeploymentModule.formatVerificationDiagnostics(verificationResult);
for (const line of diagLines) {
console.log(line);
}
Comment on lines +9509 to +9542

@coderabbitai coderabbitai Bot May 6, 2026

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate onboarding success on verificationResult.healthy.

Because Line 9483 already calls completeSession() and the legacy-credential cleanup runs before this block, a failed verification still leaves onboarding finalized and prints the normal post-onboard dashboard output. That makes the new verifier warning-only, which defeats the “verify before success” goal and leaves a broken deployment non-resumable. Move session completion / cleanup / final success output behind a healthy verification result, and exit non-zero when verification fails.

Suggested flow
-    onboardSession.completeSession(toSessionUpdates({ sandboxName, provider, model }));
-    completed = true;
-    // Onboarding finished successfully. Delete the legacy plaintext
-    // credentials.json only when every staged *value* was actually pushed
-    // to the gateway in this run.
-    const allStagedMigrated =
-      stagedLegacyKeys.length > 0 && stagedLegacyKeys.every((k) => migratedLegacyKeys.has(k));
-    if (allStagedMigrated) {
-      removeLegacyCredentialsFile();
-    } else if (stagedLegacyKeys.length > 0) {
-      ...
-    }
     const verifyDeploymentModule: typeof import("./verify-deployment") = require("./verify-deployment");
     ...
     const verificationResult = verifyDeploymentModule.verifyDeployment(
       sandboxName,
       verifyChain,
       { ... },
     );

     const diagLines = verifyDeploymentModule.formatVerificationDiagnostics(verificationResult);
     for (const line of diagLines) {
       console.log(line);
     }
+
+    if (!verificationResult.healthy) {
+      process.exit(1);
+    }
+
+    onboardSession.completeSession(toSessionUpdates({ sandboxName, provider, model }));
+    completed = true;
+    const allStagedMigrated =
+      stagedLegacyKeys.length > 0 && stagedLegacyKeys.every((k) => migratedLegacyKeys.has(k));
+    if (allStagedMigrated) {
+      removeLegacyCredentialsFile();
+    } else if (stagedLegacyKeys.length > 0) {
+      ...
+    }

     printDashboard(sandboxName, model, provider, nimContainer, agent);
🤖 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/onboard.ts` around lines 9506 - 9539, The post-deployment
verification currently runs after session finalization, so a failed verification
still marks onboarding complete; change control flow so completeSession() and
any legacy-credential cleanup and the final dashboard success output are
executed only when verificationResult.healthy is true, and if
verificationResult.healthy is false log the diagnostics (using
verifyDeploymentModule.formatVerificationDiagnostics) and exit non‑zero (e.g.,
process.exit(1)) to prevent finalizing the onboarding; locate and move the call
to completeSession() (and related cleanup/final success printing) to after the
check of verificationResult.healthy and ensure
verifyDeploymentModule.verifyDeployment(...) and verificationResult are
evaluated before that decision.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional design: verification is informational, not a gate. The rationale is that a partially-working deployment (e.g., inference route temporarily unavailable during cold start) is still better than no deployment — users can see the diagnostics and act on them. Blocking onboard completion on transient probe failures would cause frustrating retries for users whose sandboxes are actually functional.

The completeSession() call is correct here — the sandbox IS created and the gateway IS running. The verification step gives users visibility into what might not be fully ready yet, without forcing them to re-run onboard.

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.

@jyaunches Got it — thanks for clarifying the intent. I’ll withdraw the gating suggestion. Keeping verification informational while finalizing the session makes sense here, given transient cold‑start conditions.

Optional low-friction tweaks you might consider (no behavior change by default):

  • Add a single concluding line when verification isn’t healthy to make the “continuing without gating” explicit for operators:
    console.warn("Continuing despite failed verification — see diagnostics above for next steps.");
  • Provide an opt-in strict mode for CI without changing defaults, e.g.:
    if (process.env.NEMOCLAW_STRICT_VERIFY === "1" && !verificationResult.healthy) { process.exit(1); }

If you want, I can open a follow-up issue proposing the opt-in strict flag and the final warning line.


✏️ Learnings added
Learnt from: jyaunches
Repo: NVIDIA/NemoClaw PR: 3128
File: src/lib/onboard.ts:9509-9542
Timestamp: 2026-05-06T22:33:22.643Z
Learning: In NVIDIA/NemoClaw onboarding (src/lib/onboard.ts), post-deployment verification via verifyDeployment is intentionally informational and must not gate success: completeSession() should remain before/independent of verification, and onboarding must not exit non-zero on verification failures by default.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jyaunches
Repo: NVIDIA/NemoClaw PR: 2495
File: src/lib/onboard-inference-probes.ts:135-323
Timestamp: 2026-04-27T13:06:57.456Z
Learning: In NVIDIA/NemoClaw PRs that extract code from `src/lib/onboard.ts` into new behavior-preserving modules (e.g., `src/lib/onboard-providers.ts`, `src/lib/onboard-ollama-proxy.ts`, `src/lib/onboard-inference-probes.ts`), reviewers should avoid requesting refactors/complexity/style improvements in the extracted modules as part of the same PR. Only check for correctness and behavior preservation; defer non-behavior-preserving cleanup to separate follow-up PRs.

Learnt from: ericksoa
Repo: NVIDIA/NemoClaw PR: 2554
File: src/lib/onboard.ts:698-700
Timestamp: 2026-04-28T17:22:19.715Z
Learning: For NVIDIA/NemoClaw credential environment hydration/migration, implement fill-only-if-missing semantics across the onboarding and credentials modules: (1) In src/lib/credentials.ts, stageLegacyCredentialsToEnv() should only consider an allowlisted set of credential keys and should set process.env[key] only when getCredential(key) returns null (never override an explicitly provided env export). Treat blank/whitespace values as unset. (2) In src/lib/onboard.ts, hydrateCredentialEnv(envName) must first read getCredential(envName) and only call stageLegacyCredentialsToEnv() when that specific key is missing; after staging, re-read and then set process.env[envName]. This ensures stale credentials.json values cannot override explicitly provided environment credentials.


printDashboard(sandboxName, model, provider, nimContainer, agent);
} finally {
releaseOnboardLock();
Expand Down
Loading
Loading