diff --git a/docs/reference/troubleshoot-mcp-servers.mdx b/docs/reference/troubleshoot-mcp-servers.mdx index 05b838027b2..f4080057f61 100644 --- a/docs/reference/troubleshoot-mcp-servers.mdx +++ b/docs/reference/troubleshoot-mcp-servers.mdx @@ -145,13 +145,15 @@ The redacted `error_body` value contains at most 2,048 UTF-8 bytes before JSON e It redacts session identifiers, bearer tokens, structured credentials such as `access_token`, `refresh_token`, and `client_secret`, and known token prefixes. The diagnostic does not include a JSON-RPC operation because this boundary sees only the endpoint. -To find the matching sandbox audit record, read the audit log for the same endpoint around the failure time: +To find the matching sandbox audit record, read the sandbox log for the same endpoint around the failure time. +The `logs` command reads both OpenClaw gateway output and OpenShell audit events, so it needs no separate audit flag. ```bash -$$nemoclaw logs --audit --tail 200 +$$nemoclaw logs --tail 200 ``` Match on `target` and the timestamp. +If NemoClaw cannot enable OpenShell audit logs, it prints a warning and the policy denial events can be missing from the output. `diagnostic_id` is a local identifier created for one line group. It is not a distributed trace identifier and does not appear in OpenShell audit events. Correlate with OpenShell by endpoint and time until OpenShell records a shared identifier. diff --git a/test/cli/logs-documented-invocations.test.ts b/test/cli/logs-documented-invocations.test.ts new file mode 100644 index 00000000000..c3e6abba92d --- /dev/null +++ b/test/cli/logs-documented-invocations.test.ts @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import path from "node:path"; + +import { describe, expect, test as it } from "../helpers/owned-test-resources"; + +import { createLogsTestSetup } from "./helpers"; + +const REPO_ROOT = path.join(import.meta.dirname, "..", ".."); +const DOCS_ROOT = path.join(REPO_ROOT, "docs"); +const SANDBOX_NAME = "alpha"; +const LOGS_INVOCATION = /^\$\$nemoclaw\s+\S+\s+logs\b(?.*)$/; +const PLACEHOLDER_OR_SHELL_SYNTAX = /[[\]|><]/; + +type DocumentedInvocation = { + args: string; + reference: string; +}; + +function walkMdxFiles(dir: string): string[] { + return fs + .readdirSync(dir, { withFileTypes: true }) + .sort((left, right) => left.name.localeCompare(right.name)) + .flatMap((entry) => { + const absolute = path.join(dir, entry.name); + return entry.name === "_build" + ? [] + : entry.isDirectory() + ? walkMdxFiles(absolute) + : entry.isFile() && entry.name.endsWith(".mdx") + ? [absolute] + : []; + }); +} + +function extractInvocation(line: string, index: number, file: string): DocumentedInvocation | null { + const rest = LOGS_INVOCATION.exec(line.trim())?.groups?.rest.trim(); + return rest !== undefined && !PLACEHOLDER_OR_SHELL_SYNTAX.test(rest) + ? { + args: [SANDBOX_NAME, "logs", rest].filter(Boolean).join(" "), + reference: `${path.relative(REPO_ROOT, file)}:${index + 1}`, + } + : null; +} + +function isDocumentedInvocation( + invocation: DocumentedInvocation | null, +): invocation is DocumentedInvocation { + return invocation !== null; +} + +function documentedLogsInvocations(): DocumentedInvocation[] { + return walkMdxFiles(DOCS_ROOT).flatMap((file) => + fs + .readFileSync(file, "utf8") + .split(/\r?\n/) + .map((line, index) => extractInvocation(line, index, file)) + .filter(isDocumentedInvocation), + ); +} + +describe("documented sandbox logs invocations", () => { + const invocations = documentedLogsInvocations(); + + it("collects runnable logs invocations from the published pages", () => { + expect(invocations.length).toBeGreaterThanOrEqual(5); + }); + + for (const { args, reference } of invocations) { + it(`runs the invocation documented at ${reference}`, ({ resources }) => { + const setup = createLogsTestSetup(resources, "nemoclaw-cli-logs-documented-"); + const result = setup.runLogs(`${args} 2>&1`); + + expect(result.out).not.toContain("Nonexistent flag"); + expect(result.code).toBe(0); + }); + } +});