From 6172ecdc9fed70a0331977b1b55ab72fae05f24b Mon Sep 17 00:00:00 2001 From: Udaya Tejas Date: Sun, 23 Aug 2026 16:52:30 -0500 Subject: [PATCH 1/4] fix(diagnostics): redact URL credentials from the debug support bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `nemoclaw debug` redacts every collected artifact with `redactFull` alone. That mode has no URL-userinfo pattern, so a credential carried inside a URL — for example `HTTPS_PROXY=http://svc:password@proxy.corp:3128` — is written to the bundle and echoed to the terminal verbatim. The repository already applies URL redaction ahead of `redactFull` at its other full-replacement sinks: `src/lib/actions/sandbox/gateway-restart.ts` and `src/lib/policy/preset-scope-render.ts`. Reuse that order in `src/lib/diagnostics/debug.ts`. Signed-off-by: Udaya Tejas --- src/lib/diagnostics/debug.ts | 15 ++++++++++++-- test/secret-redaction.test.ts | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/lib/diagnostics/debug.ts b/src/lib/diagnostics/debug.ts index bd4f88220c8..59f2aeadf8d 100644 --- a/src/lib/diagnostics/debug.ts +++ b/src/lib/diagnostics/debug.ts @@ -56,9 +56,20 @@ function section(title: string): void { // Secret redaction — delegates to unified redact module (#2381). // --------------------------------------------------------------------------- -import { redactFull as redact } from "../security/redact"; +import { redactFull, redactUrl } from "../security/redact"; +import { URL_TOKEN_PATTERN } from "../security/redact-url"; -export { redact }; +/** + * Redact collected diagnostics before they are written to the bundle or + * echoed to the terminal. `redactFull` covers known secret shapes but leaves + * URL credentials such as `http://user:password@proxy:3128` intact, so URL + * tokens are redacted first. This is the order the other full-replacement + * sinks already use — see `actions/sandbox/gateway-restart.ts` and + * `policy/preset-scope-render.ts`. + */ +export function redact(text: string): string { + return redactFull(text.replace(URL_TOKEN_PATTERN, (url) => redactUrl(url) ?? "")); +} // --------------------------------------------------------------------------- // Command runner diff --git a/test/secret-redaction.test.ts b/test/secret-redaction.test.ts index 76958fc2ab4..dbdbd37994f 100644 --- a/test/secret-redaction.test.ts +++ b/test/secret-redaction.test.ts @@ -82,6 +82,44 @@ describe("secret redaction consistency (#1736)", () => { ); }); + describe("debug redactor removes URL credentials", () => { + const URL_CREDENTIAL_CASES = [ + { + name: "proxy environment assignment", + secret: "example-not-a-real-value-1", + text: "HTTPS_PROXY=http://svc:example-not-a-real-value-1@proxy.corp:3128", + }, + { + name: "git remote in process output", + secret: "example-not-a-real-value-2", + text: " 4321 git remote-https https://octocat:example-not-a-real-value-2@github.com/o/r.git", + }, + { + name: "registry URL with userinfo only", + secret: "example-not-a-real-value-3", + text: "resolved registry https://example-not-a-real-value-3@registry.internal/v2/", + }, + ]; + + it.each(URL_CREDENTIAL_CASES)("redacts a $name", ({ secret, text }) => { + const redacted = debugRedact(text); + expect(redacted).not.toContain(secret); + // The userinfo separator itself must not survive either. + expect(redacted).not.toContain("@"); + }); + + it("still replaces credential-shaped environment assignments outright", () => { + const text = "NVIDIA_INFERENCE_API_KEY=nvapi-" + "a".repeat(30); + expect(debugRedact(text)).toBe("NVIDIA_INFERENCE_API_KEY="); + }); + + it("leaves a credential-free URL readable", () => { + expect(debugRedact("probing https://integrate.api.nvidia.com/v1/models")).toContain( + "integrate.api.nvidia.com", + ); + }); + }); + describe("debug.sh delegates to node when available (#2381)", () => { it("redacts diagnostic command output with the compiled redactor", () => { const tmp = mkdtempSync(join(tmpdir(), "nemoclaw-debug-redact-")); From 4a2a6bffdd128ebd8c8132b0724fe2997d958532 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Sun, 23 Aug 2026 17:46:12 -0700 Subject: [PATCH 2/4] fix(security): centralize full URL redaction Signed-off-by: Apurv Kumaria --- src/lib/actions/sandbox/gateway-restart.ts | 9 ++------ src/lib/diagnostics/debug.ts | 11 +++------ src/lib/policy/preset-scope-render.ts | 6 ++--- src/lib/security/redact.test.ts | 26 +++++++++++++++++++++- src/lib/security/redact.ts | 9 +++++++- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/lib/actions/sandbox/gateway-restart.ts b/src/lib/actions/sandbox/gateway-restart.ts index 2e703a08639..f3fe9817f64 100644 --- a/src/lib/actions/sandbox/gateway-restart.ts +++ b/src/lib/actions/sandbox/gateway-restart.ts @@ -4,8 +4,7 @@ import { GATEWAY_RESTART_MARKERS as MARKERS } from "../../agent/gateway-restart-markers"; import * as agentRuntime from "../../agent/runtime"; import { G, R } from "../../cli/terminal-style"; -import { redactFull, redactUrl } from "../../security/redact"; -import { URL_TOKEN_PATTERN } from "../../security/redact-url"; +import { redactFullWithUrls } from "../../security/redact"; import { hermesMcpReconciliationRemediationLines } from "./mcp-bridge-hermes-reconciliation"; import { inspectHermesMcpReconciliationRefusal } from "./mcp-bridge-recovery"; import { assertHermesPortableCommandUnavailable } from "../../onboard/experimental/portable-agent-lifecycle"; @@ -173,11 +172,7 @@ const ANSI_CONTROL_RE = function sanitizeGatewayRestartFailureLine(line: string): string { const withoutControls = line.replace(ANSI_CONTROL_RE, ""); - const withRedactedUrls = withoutControls.replace( - URL_TOKEN_PATTERN, - (url) => redactUrl(url) ?? "", - ); - return redactFull(withRedactedUrls); + return redactFullWithUrls(withoutControls); } function sanitizeGatewayRestartFailureDetail(detail: string): string { diff --git a/src/lib/diagnostics/debug.ts b/src/lib/diagnostics/debug.ts index 59f2aeadf8d..659db826f79 100644 --- a/src/lib/diagnostics/debug.ts +++ b/src/lib/diagnostics/debug.ts @@ -56,19 +56,14 @@ function section(title: string): void { // Secret redaction — delegates to unified redact module (#2381). // --------------------------------------------------------------------------- -import { redactFull, redactUrl } from "../security/redact"; -import { URL_TOKEN_PATTERN } from "../security/redact-url"; +import { redactFullWithUrls } from "../security/redact"; /** * Redact collected diagnostics before they are written to the bundle or - * echoed to the terminal. `redactFull` covers known secret shapes but leaves - * URL credentials such as `http://user:password@proxy:3128` intact, so URL - * tokens are redacted first. This is the order the other full-replacement - * sinks already use — see `actions/sandbox/gateway-restart.ts` and - * `policy/preset-scope-render.ts`. + * echoed to the terminal. */ export function redact(text: string): string { - return redactFull(text.replace(URL_TOKEN_PATTERN, (url) => redactUrl(url) ?? "")); + return redactFullWithUrls(text); } // --------------------------------------------------------------------------- diff --git a/src/lib/policy/preset-scope-render.ts b/src/lib/policy/preset-scope-render.ts index ae3fbf8928c..f9d23d2b98e 100644 --- a/src/lib/policy/preset-scope-render.ts +++ b/src/lib/policy/preset-scope-render.ts @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { isObjectRecord } from "../core/json-types"; -import { redactFull, redactUrl } from "../security/redact"; -import { URL_TOKEN_PATTERN } from "../security/redact-url"; +import { redactFullWithUrls } from "../security/redact"; import { type PolicyValue, parseNetworkPolicies } from "./preset-parsing"; type RuleScope = { @@ -58,8 +57,7 @@ export function escapeTerminalText(value: string): string { /** Redact credential-shaped content before rendering untrusted YAML scalars. */ function renderTerminalText(value: string): string { - const redactedUrls = value.replace(URL_TOKEN_PATTERN, (url) => redactUrl(url) ?? ""); - return escapeTerminalText(redactFull(redactedUrls)); + return escapeTerminalText(redactFullWithUrls(value)); } function toStringOrUndefined(value: PolicyValue | undefined): string | undefined { diff --git a/src/lib/security/redact.test.ts b/src/lib/security/redact.test.ts index 89d21c73f59..14b566d4cf7 100644 --- a/src/lib/security/redact.test.ts +++ b/src/lib/security/redact.test.ts @@ -3,7 +3,31 @@ import { describe, expect, it } from "vitest"; -import { redactForLog, redactFull, redactLogSequence, redactSensitiveText } from "./redact.js"; +import { + redactForLog, + redactFull, + redactFullWithUrls, + redactLogSequence, + redactSensitiveText, +} from "./redact.js"; + +describe("redactFullWithUrls", () => { + it.each([ + [ + "username and password", + "https://service-user:service-password@example.com/path", + "https://example.com/path", + ], + ["userinfo only", "https://service-token@example.com/path", "https://example.com/path"], + [ + "malformed URL fallback", + "https://fallback-user:fallback-password@[not-an-ip/path", + "https://[not-an-ip/path", + ], + ])("fully redacts URL credentials for %s", (_case, value, expected) => { + expect(redactFullWithUrls(value)).toBe(expected); + }); +}); describe("redactForLog", () => { it("redacts pass aliases in structured keys", () => { diff --git a/src/lib/security/redact.ts b/src/lib/security/redact.ts index 1523585b1a0..59b5e4a829d 100644 --- a/src/lib/security/redact.ts +++ b/src/lib/security/redact.ts @@ -12,7 +12,8 @@ import type { StdioOptions } from "node:child_process"; * * Two modes: * - `redact()` — partial (keep first 4 chars). Used by runner.ts for CLI output. - * - `redactFull()` — full replacement. Used by debug.ts for diagnostic dumps. + * - `redactFull()` — full replacement for known secret patterns. + * - `redactFullWithUrls()` — full replacement for known patterns and URL credentials. * - `redactSensitiveText()` — full replacement + truncation. Used by onboard-session.ts. * * Ref: https://github.com/NVIDIA/NemoClaw/issues/2381 @@ -194,6 +195,12 @@ export function redactFull(text: string): string { return result; } +/** Fully redact secret patterns and credentials embedded in URL tokens. */ +export function redactFullWithUrls(text: string): string { + const redactedUrls = text.replace(URL_TOKEN_PATTERN, (url) => redactUrl(url) ?? ""); + return redactFull(redactedUrls); +} + function redactStandaloneSecrets(text: string, replacement: string): string { let result = text; for (const pattern of [ From 73f0ca97738f0c9a9eafc643412a668691138105 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 24 Aug 2026 12:37:02 -0700 Subject: [PATCH 3/4] test(diagnostics): keep URL cases with their owner Signed-off-by: Apurv Kumaria --- src/lib/diagnostics/debug.test.ts | 11 +++++++++ test/secret-redaction.test.ts | 38 ------------------------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/src/lib/diagnostics/debug.test.ts b/src/lib/diagnostics/debug.test.ts index 14bf1724c55..240c7d20f87 100644 --- a/src/lib/diagnostics/debug.test.ts +++ b/src/lib/diagnostics/debug.test.ts @@ -60,6 +60,17 @@ describe("redact", () => { const clean = "Hello world, no secrets here"; expect(redact(clean)).toBe(clean); }); + + it("removes credentials from a URL", () => { + expect(redact("proxy: https://service-user:service-password@example.com/path")).toBe( + "proxy: https://example.com/path", + ); + }); + + it("leaves a credential-free URL readable", () => { + const url = "https://integrate.api.nvidia.com/v1/models"; + expect(redact(`probing ${url}`)).toBe(`probing ${url}`); + }); }); describe("createTarball", () => { diff --git a/test/secret-redaction.test.ts b/test/secret-redaction.test.ts index dbdbd37994f..76958fc2ab4 100644 --- a/test/secret-redaction.test.ts +++ b/test/secret-redaction.test.ts @@ -82,44 +82,6 @@ describe("secret redaction consistency (#1736)", () => { ); }); - describe("debug redactor removes URL credentials", () => { - const URL_CREDENTIAL_CASES = [ - { - name: "proxy environment assignment", - secret: "example-not-a-real-value-1", - text: "HTTPS_PROXY=http://svc:example-not-a-real-value-1@proxy.corp:3128", - }, - { - name: "git remote in process output", - secret: "example-not-a-real-value-2", - text: " 4321 git remote-https https://octocat:example-not-a-real-value-2@github.com/o/r.git", - }, - { - name: "registry URL with userinfo only", - secret: "example-not-a-real-value-3", - text: "resolved registry https://example-not-a-real-value-3@registry.internal/v2/", - }, - ]; - - it.each(URL_CREDENTIAL_CASES)("redacts a $name", ({ secret, text }) => { - const redacted = debugRedact(text); - expect(redacted).not.toContain(secret); - // The userinfo separator itself must not survive either. - expect(redacted).not.toContain("@"); - }); - - it("still replaces credential-shaped environment assignments outright", () => { - const text = "NVIDIA_INFERENCE_API_KEY=nvapi-" + "a".repeat(30); - expect(debugRedact(text)).toBe("NVIDIA_INFERENCE_API_KEY="); - }); - - it("leaves a credential-free URL readable", () => { - expect(debugRedact("probing https://integrate.api.nvidia.com/v1/models")).toContain( - "integrate.api.nvidia.com", - ); - }); - }); - describe("debug.sh delegates to node when available (#2381)", () => { it("redacts diagnostic command output with the compiled redactor", () => { const tmp = mkdtempSync(join(tmpdir(), "nemoclaw-debug-redact-")); From dda4f0e42f2d2fcf59187f1e120f97dab497a47f Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 24 Aug 2026 13:32:13 -0700 Subject: [PATCH 4/4] fix(ci): recalibrate integration shard ownership Signed-off-by: Apurv Kumaria --- test/cli-coverage-sequencer.test.ts | 4 ++-- test/helpers/cli-coverage-sequencer.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/cli-coverage-sequencer.test.ts b/test/cli-coverage-sequencer.test.ts index 18f13705d62..ce7b169aa5f 100644 --- a/test/cli-coverage-sequencer.test.ts +++ b/test/cli-coverage-sequencer.test.ts @@ -117,8 +117,8 @@ describe("stable CLI coverage sharding", () => { expect(Object.fromEntries(owners)).toEqual({ "cli:src/lib/example.test.ts": 6, "e2e-support:test/e2e/support/example.test.ts": 8, - "integration:test/hermes-restart-config-seal-write-lock.test.ts": 4, - "integration:test/local-credential-helper-fields.test.ts": 7, + "integration:test/hermes-restart-config-seal-write-lock.test.ts": 8, + "integration:test/local-credential-helper-fields.test.ts": 3, "integration:test/regular-0.test.ts": 4, }); }); diff --git a/test/helpers/cli-coverage-sequencer.ts b/test/helpers/cli-coverage-sequencer.ts index 835d9e97a95..11fd15d02e1 100644 --- a/test/helpers/cli-coverage-sequencer.ts +++ b/test/helpers/cli-coverage-sequencer.ts @@ -42,7 +42,7 @@ const cliCoverageProjects = new Set(["cli", "integration", "e2e-support"]); // Integration coverage is serialized, so it needs an independent salt instead // of relying on combined weight from the parallel CLI and E2E-support lanes. const stableShardSalt = "7257"; -const integrationShardSalt = "12432"; +const integrationShardSalt = "63269"; const e2eSupportShardSalt = "13930"; // Only measured outliers are stored; new and ordinary files share the // conservative fallback used to estimate each stable shard's load.