Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions src/lib/diagnostics/debug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
// Import from compiled dist/ so coverage is attributed correctly.
import {
buildDmesgRerunCommand,
createTarball,
dmesgRestrictedMessage,
getDebugCompletionMessages,
isDmesgPermissionDeniedOutput,
isDmesgRestrictedForCurrentUser,
Expand Down Expand Up @@ -147,3 +149,67 @@ describe("isDmesgPermissionDeniedOutput", () => {
expect(isDmesgPermissionDeniedOutput("docker: Permission denied")).toBe(false);
});
});

describe("dmesgRestrictedMessage (#4366)", () => {
it("explains why kernel messages were skipped", () => {
const msg = dmesgRestrictedMessage("kernel.dmesg_restrict=1 prevents non-root access");
expect(msg).toContain("kernel messages skipped");
expect(msg).toContain("kernel.dmesg_restrict=1 prevents non-root access");
});

it("includes a 'sudo nemoclaw debug' hint so users can re-run with kernel logs", () => {
const msg = dmesgRestrictedMessage("some-reason");
expect(msg).toMatch(/sudo nemoclaw debug/);
expect(msg.toLowerCase()).toMatch(/re-?run/);
});

it("warns that privileged diagnostics may contain sensitive data", () => {
const msg = dmesgRestrictedMessage("some-reason");
expect(msg.toLowerCase()).toMatch(/sensitive/);
});

it("preserves --quick in the rerun hint when the user invoked debug --quick", () => {
const msg = dmesgRestrictedMessage("some-reason", { quick: true });
expect(msg).toContain("sudo nemoclaw debug --quick");
});

it("preserves --output in the rerun hint when the user supplied an output path", () => {
const msg = dmesgRestrictedMessage("some-reason", { output: "/tmp/out.tgz" });
expect(msg).toContain("sudo nemoclaw debug --output '/tmp/out.tgz'");
});

it("preserves both --quick and --output together", () => {
const msg = dmesgRestrictedMessage("some-reason", {
quick: true,
output: "/tmp/out.tgz",
});
expect(msg).toContain("sudo nemoclaw debug --quick --output '/tmp/out.tgz'");
});

it("falls back to bare 'sudo nemoclaw debug' when no options are supplied", () => {
const msg = dmesgRestrictedMessage("some-reason");
expect(msg).toMatch(/`sudo nemoclaw debug`/);
});
});

describe("buildDmesgRerunCommand (#4366)", () => {
it("returns the bare command when no options are set", () => {
expect(buildDmesgRerunCommand()).toBe("sudo nemoclaw debug");
});

it("appends --quick when opts.quick is true", () => {
expect(buildDmesgRerunCommand({ quick: true })).toBe("sudo nemoclaw debug --quick");
});

it("appends a single-quoted --output path", () => {
expect(buildDmesgRerunCommand({ output: "/tmp/out.tgz" })).toBe(
"sudo nemoclaw debug --output '/tmp/out.tgz'",
);
});

it("escapes single quotes inside the output path", () => {
expect(buildDmesgRerunCommand({ output: "/tmp/o'ut.tgz" })).toBe(
"sudo nemoclaw debug --output '/tmp/o'\\''ut.tgz'",
);
});
});
38 changes: 31 additions & 7 deletions src/lib/diagnostics/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,34 @@ export function isDmesgPermissionDeniedOutput(output: string): boolean {
return /\b(dmesg|kernel buffer|kernel logs?)\b/i.test(output);
}

function dmesgRestrictedMessage(reason: string): string {
return ` (kernel messages skipped: dmesg access is restricted for this user; ${reason})`;
/**
* Build the option-aware re-run command for the dmesg-restricted hint.
*
* Preserves the user's original invocation flags (`--quick`, `--output`) so the
* hint nudges them back into the same scoped diagnostic instead of a broader
* privileged collector. See issue #4366.
*/
export function buildDmesgRerunCommand(opts: DebugOptions = {}): string {
const parts = ["sudo", "nemoclaw", "debug"];
if (opts.quick) parts.push("--quick");
if (opts.output) {
// Single-quote the path and escape embedded single quotes for shell safety.
const escaped = opts.output.replace(/'/g, "'\\''");
parts.push("--output", `'${escaped}'`);
}
return parts.join(" ");
}
Comment on lines +157 to +173
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if sandbox name affects diagnostic collection behavior

# Find where sandboxName is used in debug collection
rg -n -C3 'sandboxName' src/lib/diagnostics/debug.ts

# Check if there are sandbox-specific collection steps
ast-grep --pattern 'function collect$_($$$, sandboxName, $$$) { $$$ }'

Repository: NVIDIA/NemoClaw

Length of output: 3397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where buildDmesgRerunCommand is used (rerun hint path)
rg -n "buildDmesgRerunCommand" -S src/lib/diagnostics/debug.ts src/lib/diagnostics

# 2) Inspect dmesgRestrictedMessage and surrounding logic to see what opts are passed
rg -n "dmesgRestrictedMessage|collectDmesg|debug.*rerun|rerun" -S src/lib/diagnostics/debug.ts

# 3) Check how detectSandboxName determines a default (and whether it depends on registry/home)
rg -n "function detectSandboxName|detectSandboxName\\(" -S src/lib/diagnostics/debug.ts src/lib

# 4) If registry is involved, check where it’s stored/scoped
rg -n "REGISTRY_FILE|sandboxes\\.json|sandbox.*registry|nemoclaw.*sandboxes" -S src/lib

Repository: NVIDIA/NemoClaw

Length of output: 9589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant code blocks in debug.ts
sed -n '140,260p' src/lib/diagnostics/debug.ts
echo "----"
sed -n '200,280p' src/lib/diagnostics/debug.ts
echo "----"
sed -n '220,260p' src/lib/diagnostics/debug.ts
echo "----"
sed -n '560,640p' src/lib/diagnostics/debug.ts
echo "----"

# Show relevant tests for buildDmesgRerunCommand
sed -n '160,250p' src/lib/diagnostics/debug.test.ts
echo "----"
rg -n "4366|dmesg-rerun|buildDmesgRerunCommand" -S src/lib/diagnostics/debug.test.ts

Repository: NVIDIA/NemoClaw

Length of output: 13189


Preserve --sandbox in the dmesg-restricted rerun hint

buildDmesgRerunCommand() only carries --quick and --output and ignores opts.sandboxName, so dmesgRestrictedMessage() generates a re-run command without --sandbox. Since runDebug() uses opts.sandboxName to scope sandbox-specific collection (e.g., OpenShell fetches/logs use sandboxName), re-running via the hint under sudo can target a different sandbox because auto-detection reads the HOME-scoped registry and otherwise falls back to the first entry from openshell sandbox list.

Add --sandbox '<name>' to the hint when opts.sandboxName is set (using the same single-quote escaping as --output) and extend the existing buildDmesgRerunCommand unit tests.

🤖 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/diagnostics/debug.ts` around lines 157 - 173, The dmesg rerun hint
omits sandbox scoping: update buildDmesgRerunCommand to append "--sandbox"
followed by the sandbox name when opts.sandboxName is set (use the same
single-quote escaping logic as for opts.output — replace any "'" with "'\\''"
and wrap the escaped value in single quotes), so the generated command preserves
sandbox scope; also add/extend unit tests for buildDmesgRerunCommand to verify a
sandboxName results in a "--sandbox '<name>'" token and that embedded single
quotes are correctly escaped.


export function dmesgRestrictedMessage(reason: string, opts: DebugOptions = {}): string {
const rerun = buildDmesgRerunCommand(opts);
return [
` (kernel messages skipped: dmesg access is restricted for this user; ${reason}.`,
` Re-run with \`${rerun}\` to include kernel logs in this report.`,
" Note: privileged diagnostics and kernel logs may contain sensitive data; review before sharing.)",
].join("\n");
}

function collectDmesg(collectDir: string): void {
function collectDmesg(collectDir: string, opts: DebugOptions = {}): void {
if (!commandExists("dmesg")) {
writeCollectedMessage(collectDir, "dmesg", " (dmesg not found, skipping)");
return;
Expand All @@ -170,6 +193,7 @@ function collectDmesg(collectDir: string): void {
"dmesg",
dmesgRestrictedMessage(
`${DMESG_RESTRICT_PATH}=1 prevents non-root users from reading kernel logs`,
opts,
),
);
return;
Expand All @@ -186,7 +210,7 @@ function collectDmesg(collectDir: string): void {
writeCollectedMessage(
collectDir,
"dmesg",
dmesgRestrictedMessage("the dmesg command denied access to kernel logs"),
dmesgRestrictedMessage("the dmesg command denied access to kernel logs", opts),
);
return;
}
Expand Down Expand Up @@ -486,7 +510,7 @@ function collectKernel(collectDir: string): void {
}
}

function collectKernelMessages(collectDir: string): void {
function collectKernelMessages(collectDir: string, opts: DebugOptions = {}): void {
section("Kernel Messages");
if (isMacOS) {
collectShell(
Expand All @@ -495,7 +519,7 @@ function collectKernelMessages(collectDir: string): void {
'log show --last 5m --predicate "eventType == logEvent" --style compact 2>/dev/null | tail -100',
);
} else {
collectDmesg(collectDir);
collectDmesg(collectDir, opts);
}
}

Expand Down Expand Up @@ -587,7 +611,7 @@ export function runDebug(opts: DebugOptions = {}): void {
collectKernel(collectDir);
}

collectKernelMessages(collectDir);
collectKernelMessages(collectDir, opts);

let tarballOk = true;
if (output) {
Expand Down
Loading