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.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/src/lib/diagnostics/debug.ts b/src/lib/diagnostics/debug.ts index bd4f88220c8..659db826f79 100644 --- a/src/lib/diagnostics/debug.ts +++ b/src/lib/diagnostics/debug.ts @@ -56,9 +56,15 @@ function section(title: string): void { // Secret redaction — delegates to unified redact module (#2381). // --------------------------------------------------------------------------- -import { redactFull as redact } from "../security/redact"; +import { redactFullWithUrls } from "../security/redact"; -export { redact }; +/** + * Redact collected diagnostics before they are written to the bundle or + * echoed to the terminal. + */ +export function redact(text: string): string { + return redactFullWithUrls(text); +} // --------------------------------------------------------------------------- // Command runner 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 [