Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6aad973
feat(cli): add structured logging with --debug and NEMOCLAW_LOG_LEVEL
sauravdev Jul 4, 2026
32d8bad
fix(cli): harden structured logging controls
apurvvkumaria Jul 7, 2026
5ac42be
test(cli): strengthen logging boundary assertions
apurvvkumaria Jul 7, 2026
935aa56
fix(cli): close structured logging redaction gaps
apurvvkumaria Jul 7, 2026
4f67f2b
Merge branch 'main' into feat/structured-logging
cv Jul 7, 2026
8cfa2bb
fix(cli): preserve provenance redaction boundaries
apurvvkumaria Jul 7, 2026
5db0ca2
fix(cli): preserve redacted diagnostic context
apurvvkumaria Jul 7, 2026
a3ab263
fix(cli): constrain credential context labels
apurvvkumaria Jul 7, 2026
4d5b64a
merge: sync main into structured logging
apurvvkumaria Jul 7, 2026
da289b0
merge: sync main into structured logging
apurvvkumaria Jul 8, 2026
f201979
Merge branch 'main' into feat/structured-logging
cjagwani Jul 8, 2026
eed7d52
merge: sync main into structured logging
apurvvkumaria Jul 8, 2026
bff0386
fix(logging): avoid redacting benign structured keys
apurvvkumaria Jul 8, 2026
844fcf0
fix(logging): redact combined API key names
apurvvkumaria Jul 8, 2026
5f39fc8
feat(cli): add structured logging with --debug and NEMOCLAW_LOG_LEVEL
sauravdev Jul 4, 2026
bc20a3a
merge: restore reviewed structured logging history
apurvvkumaria Jul 9, 2026
7f9411a
merge: sync main into structured logging
apurvvkumaria Jul 9, 2026
7ff951f
feat(cli): add structured logging with --debug and NEMOCLAW_LOG_LEVEL
sauravdev Jul 4, 2026
a488c8c
merge: restore reviewed structured logging history
apurvvkumaria Jul 9, 2026
a4377c1
merge(main): sync PR #6272 with latest main
cv Jul 9, 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
20 changes: 20 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3019,6 +3019,26 @@ NemoClaw reads the following environment variables to configure service ports, o
Set them before running `$$nemoclaw onboard` or any command that starts services.
All ports must be non-privileged integers between 1024 and 65535.

### CLI Logging

The centralized CLI logger writes its output to `stderr` and uses `info` verbosity by default.
These controls affect leveled logger output; they do not suppress command results or command-specific output that has not migrated to the centralized logger.

| Variable | Accepted values | Effect |
|----------|-----------------|--------|
| `NEMOCLAW_LOG_LEVEL` | `error`, `warn`, `info`, or `debug` (case-insensitive; surrounding whitespace is ignored) | Sets the logging threshold. A valid value takes precedence over `NEMOCLAW_DEBUG`. An invalid, blank, or unset value falls through to `NEMOCLAW_DEBUG`. |
| `NEMOCLAW_DEBUG` | `1`, `true`, `y`, or `yes` (case-insensitive) | Enables `debug` logging when `NEMOCLAW_LOG_LEVEL` does not contain a valid value. |

The environment precedence is `NEMOCLAW_LOG_LEVEL`, then `NEMOCLAW_DEBUG`, followed by the default `info` level.
The `error` level prints errors only, `warn` also prints warnings, `info` also prints informational messages, and `debug` prints all levels with timestamps.
Use these NemoClaw-specific variables instead of the generic `DEBUG` variable. `DEBUG` is not a NemoClaw logger control and can enable dependency diagnostics that include raw command arguments.

Commands whose parser owns the base logging options also accept the hidden long-form `--debug` and `--quiet` flags, even though these options do not appear in command help.
The flags are mutually exclusive.
`--debug` overrides the environment-derived threshold and selects `debug`, while `--quiet` caps verbosity at `warn` without increasing an environment-derived `error` threshold.
There is no global `-q` logging shorthand.
Passthrough commands do not consume flags intended for the downstream command as host logging options, so use the environment variables when you need unambiguous host logging around a passthrough invocation.

| Variable | Default | Service |
|----------|---------|---------|
| `NEMOCLAW_GATEWAY_PORT` | 8080 | OpenShell gateway port |
Expand Down
27 changes: 26 additions & 1 deletion src/commands/sandbox/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ vi.mock("../../lib/actions/sandbox/agent/passthrough", () => ({
runAgentPassthrough: runAgentPassthroughMock,
}));

import { log } from "../../lib/cli/logger";
import SandboxAgentCommand from "./agent";

const rootDir = process.cwd();
Expand All @@ -21,7 +22,7 @@ describe("SandboxAgentCommand oclif parse path", () => {
});

afterEach(() => {
logSpy.mockRestore();
vi.restoreAllMocks();
});

it("forwards the OpenClaw argv verbatim to runAgentPassthrough", async () => {
Expand All @@ -31,6 +32,30 @@ describe("SandboxAgentCommand oclif parse path", () => {
});
});

it("forwards downstream logging flags without changing host logging", async () => {
const configure = vi.spyOn(log, "configure").mockImplementation(() => undefined);

await SandboxAgentCommand.run(["alpha", "--debug", "-m", "hi"], rootDir);

expect(runAgentPassthroughMock).toHaveBeenCalledWith("alpha", {
extraArgs: ["--debug", "-m", "hi"],
});
expect(configure).toHaveBeenCalledWith({ debug: false, quiet: false });
expect(configure).not.toHaveBeenCalledWith({ debug: true, quiet: false });
});

it("preserves the option boundary without treating later flags as host flags", async () => {
const configure = vi.spyOn(log, "configure").mockImplementation(() => undefined);

await SandboxAgentCommand.run(["alpha", "--", "--quiet"], rootDir);

expect(runAgentPassthroughMock).toHaveBeenCalledWith("alpha", {
extraArgs: ["--", "--quiet"],
});
expect(configure).toHaveBeenCalledWith({ debug: false, quiet: false });
expect(configure).not.toHaveBeenCalledWith({ debug: false, quiet: true });
});

it("passes --help after the sandbox name to agent-aware dispatch (#5790)", async () => {
await SandboxAgentCommand.run(["alpha", "--help"], rootDir);
expect(runAgentPassthroughMock).toHaveBeenCalledWith("alpha", {
Expand Down
24 changes: 22 additions & 2 deletions src/commands/sandbox/exec.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const execSandboxMock = vi.hoisted(() => vi.fn(async () => {}));
vi.mock("../../lib/actions/sandbox/exec", () => ({
execSandbox: execSandboxMock,
}));

import { log } from "../../lib/cli/logger";
import SandboxExecCommand from "./exec";

const rootDir = process.cwd();
Expand All @@ -17,6 +18,10 @@ describe("SandboxExecCommand oclif parse path", () => {
execSandboxMock.mockReset();
});

afterEach(() => {
vi.restoreAllMocks();
});

it("forwards everything after -- as the inner command argv", async () => {
await SandboxExecCommand.run(
["alpha", "--", "openclaw", "agent", "--agent", "main", "-m", "hi"],
Expand All @@ -29,6 +34,21 @@ describe("SandboxExecCommand oclif parse path", () => {
);
});

it("does not assign host meaning to logging flags after --", async () => {
const configure = vi.spyOn(log, "configure").mockImplementation(() => undefined);

await SandboxExecCommand.run(["alpha", "--", "agent-cli", "--debug", "--quiet"], rootDir);

expect(execSandboxMock).toHaveBeenCalledWith("alpha", ["agent-cli", "--debug", "--quiet"], {
workdir: undefined,
tty: null,
timeoutSeconds: undefined,
});
expect(configure).toHaveBeenCalledWith({ debug: false, quiet: false });
expect(configure).not.toHaveBeenCalledWith({ debug: true, quiet: false });
expect(configure).not.toHaveBeenCalledWith({ debug: false, quiet: true });
});

it("preserves repeated flag/value pairs after -- in their original order", async () => {
await SandboxExecCommand.run(
[
Expand Down Expand Up @@ -75,7 +95,7 @@ describe("SandboxExecCommand oclif parse path", () => {
"-c",
"pass",
],
{ workdir: undefined, tty: null, timeoutSeconds: undefined },
{ workdir: undefined, tty: null, timeoutSeconds: undefined, stdin: undefined },
);
});

Expand Down
32 changes: 31 additions & 1 deletion src/commands/simple-global-oclif-adapters.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => {
class GatewayTokenCommandError extends Error {
Expand Down Expand Up @@ -87,6 +87,7 @@ vi.mock("../lib/uninstall-command", () => ({
}));
vi.mock("../lib/core/version", () => ({ getVersion: mocks.getVersion }));

import { log } from "../lib/cli/logger";
import DebugCliCommand from "./debug";
import DeployCliCommand from "./deploy";
import RootHelpCommand from "./root/help";
Expand All @@ -110,6 +111,10 @@ describe("simple global oclif adapters", () => {
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

it("maps debug and deploy parser output to actions", async () => {
await DebugCliCommand.run(
["--quick", "--output", "/tmp/debug.tar.gz", "--sandbox", "alpha"],
Expand All @@ -127,6 +132,19 @@ describe("simple global oclif adapters", () => {
expect(mocks.runDeployAction).toHaveBeenCalledWith("gpu-alpha");
});

it("keeps debug -q scoped to quick diagnostics instead of global quiet mode", async () => {
const configure = vi.spyOn(log, "configure").mockImplementation(() => undefined);

await DebugCliCommand.run(["-q"], rootDir);

expect(mocks.runDebugCommandWithOptions).toHaveBeenCalledWith(
{ quick: true },
expect.objectContaining({ runDebug: expect.any(Function) }),
);
expect(configure).toHaveBeenCalledWith({ debug: false, quiet: false });
expect(configure).not.toHaveBeenCalledWith({ debug: false, quiet: true });
});

it("builds debug defaults from the sandbox registry and OpenShell liveness", async () => {
mocks.listSandboxes.mockReturnValue({
defaultSandbox: "alpha",
Expand Down Expand Up @@ -309,4 +327,16 @@ describe("simple global oclif adapters", () => {
}),
);
});

it("forwards uninstall flags without assigning host logging semantics", async () => {
const configure = vi.spyOn(log, "configure").mockImplementation(() => undefined);

await UninstallCliCommand.run(["--yes", "--debug"], rootDir);

expect(mocks.runUninstallCommand).toHaveBeenCalledWith(
expect.objectContaining({ args: ["--yes", "--debug"] }),
);
expect(configure).toHaveBeenCalledWith({ debug: false, quiet: false });
expect(configure).not.toHaveBeenCalledWith({ debug: true, quiet: false });
});
});
Loading
Loading