diff --git a/src/logging/redact.test.ts b/src/logging/redact.test.ts index 0162c3ac0017..8ea35700cf48 100644 --- a/src/logging/redact.test.ts +++ b/src/logging/redact.test.ts @@ -735,6 +735,11 @@ describe("redactSensitiveText", () => { }); it("masks expanded vendor-prefix token corpus", () => { + const fireworksTokens = [ + { token: `fw-${"C".repeat(40)}`, redacted: "fw-CCC…CCCC" }, + { token: `fw_${"A".repeat(40)}`, redacted: "fw_AAA…AAAA" }, + { token: `fpk_${"B".repeat(40)}`, redacted: "fpk_BB…BBBB" }, + ]; const tokens = [ "sk-ant-abcdefghijklmnopqrstuvwxyz", "gho_abcdefghijklmnopqrstuvwxyz", @@ -797,6 +802,7 @@ describe("redactSensitiveText", () => { "mem0_abcdefghijklmnopqrstuvwxyz", "brv_abcdefghijklmnopqrstuvwxyz", "xai-abcdefghijklmnopqrstuvwxyzABCDE", + ...fireworksTokens.map(({ token }) => token), ]; // Redact each fixture alone so every vendor pattern proves it stays reachable through // DEFAULT_REDACT_PREFILTER_RE; a joined corpus would let one trigger unlock all others. @@ -817,15 +823,36 @@ describe("redactSensitiveText", () => { expect(redactSensitiveText("xai-abcdefghijklmnopqrstuvwxyzABCDE", { mode: "tools" })).toBe( "xai-ab…BCDE", ); + for (const { token, redacted } of fireworksTokens) { + expect(redactSensitiveText(token, { mode: "tools" })).toBe(redacted); + expect(redactSensitiveText(redacted, { mode: "tools" })).toBe(redacted); + } }); it("does not redact ordinary identifiers containing short token-prefix substrings", () => { - const input = - "npm_telegram_package_spec ask_openclaw_query_patterns team_management risk_assessment glpat-docs dapi-example sbp_short nfp_site CCIPAT_docs ATATT-example"; + const input = [ + "npm_telegram_package_spec ask_openclaw_query_patterns team_management risk_assessment glpat-docs dapi-example sbp_short nfp_site CCIPAT_docs ATATT-example fw-tooshort fw_tooshort fpk_tooshort", + `fixturefw-${"C".repeat(40)}`, + `fixture_fw_${"A".repeat(40)}`, + `fixture_fpk_${"B".repeat(40)}`, + ].join(" "); const output = redactSensitiveText(input, { mode: "tools" }); expect(output).toBe(input); }); + it("masks Fireworks tokens that cross bounded-replacement chunk boundaries", () => { + const chunkSize = 16_384; + const prefix = `${"x".repeat(chunkSize - 2)} `; + const suffix = "y".repeat(chunkSize); + const tokens = [`fw-${"C".repeat(40)}`, `fw_${"A".repeat(40)}`, `fpk_${"B".repeat(40)}`]; + + for (const token of tokens) { + expect(redactSensitiveText(`${prefix}${token}${suffix}`, { mode: "tools" })).not.toContain( + token, + ); + } + }); + it("does not corrupt base64 blobs that embed token-prefix shapes", () => { // Tiny-PNG base64 contains a gAAAA run from zero-filled IHDR bytes; pure-base64-alphabet // prefixes must not fire mid-blob or media payloads get mangled. diff --git a/src/logging/redact.ts b/src/logging/redact.ts index b41198d43fcd..1a71e02bcb78 100644 --- a/src/logging/redact.ts +++ b/src/logging/redact.ts @@ -122,6 +122,7 @@ const STANDALONE_ASSIGNMENT_REDACT_PATTERN = String.raw`(^|[\s,;])(?:${STANDALON // delimiters like `/` and `=` still qualify) but skip explicit `;base64,` payload spans, so // data-URL media is never corrupted while tokens in URL paths or assignments still redact. const BASE64_SAFE_TOKEN_BOUNDARY = String.raw`(^|[^A-Za-z0-9])(?(); -// Patterns whose left-context assertions (BASE64_SAFE_TOKEN_BOUNDARY) break under chunked -// replacement: a chunk start satisfies `^` and hides the `;base64,` container from the -// lookbehind, so these must always run against the full string. +// Patterns whose left-context assertions or complete token can cross a chunk boundary must run +// against the full string; chunking can invent a `^` boundary or split the secret itself. const chunkUnsafePatterns = new WeakSet(); const DEFAULT_REDACT_PATTERNS: string[] = [ @@ -235,6 +235,9 @@ const DEFAULT_REDACT_PATTERNS: string[] = [ String.raw`(mem0_[A-Za-z0-9]{10,})`, String.raw`(brv_[A-Za-z0-9]{10,})`, String.raw`(xai-[A-Za-z0-9]{30,})`, + String.raw`${IDENTIFIER_SAFE_TOKEN_BOUNDARY}(fw-[A-Za-z0-9]{30,})`, + String.raw`${IDENTIFIER_SAFE_TOKEN_BOUNDARY}(fw_[A-Za-z0-9]{30,})`, + String.raw`${IDENTIFIER_SAFE_TOKEN_BOUNDARY}(fpk_[A-Za-z0-9]{30,})`, // Additional access-key and token-style prefixes. String.raw`${BASE64_SAFE_TOKEN_BOUNDARY}(AKIA[A-Z0-9]{16})`, String.raw`${BASE64_SAFE_TOKEN_BOUNDARY}(ASIA[A-Z0-9]{16})`, @@ -261,7 +264,7 @@ const DEFAULT_REDACT_PREFILTER_SOURCES: string[] = [ // URL userinfo and connection-string password slots (`scheme://user:pass@host`). String.raw`:\/\/[^\/\s:@]*:[^\/\s@]+@`, // Vendor token prefixes and webhook hosts, ordered like DEFAULT_REDACT_PATTERNS. - String.raw`sk-|gh[opsur]_|github_pat_|glpat-|gloas-|xox[baprs]-|xapp-|hooks\.slack\.com|discord|gsk_|AIza|ya29\.|1\/\/0|eyJ|pplx-|fal_|fc-|bb_live_|gAAAA|[sr]k_(?:live|test)_|\bSG\.|npm_|pypi-|do[opr]_v1_|dp\.(?:ct|pt|sa|st|scim|audit)\.|dckr_|bkua_|CCIPAT_|sbp_|dapi[0-9a-f]|dd[pw]_|glsa_|nfp_|CFPAT-|ATCTT3|ATATT|ATBB|BBDC-|HRKU-|pat-(?:eu|na)1-|apify_api_|FlyV1|fio-u-|tvly-|exa_|syt_|retaindb_|mem0_|brv_|xai-`, + String.raw`sk-|gh[opsur]_|github_pat_|glpat-|gloas-|xox[baprs]-|xapp-|hooks\.slack\.com|discord|gsk_|AIza|ya29\.|1\/\/0|eyJ|pplx-|fal_|fc-|bb_live_|gAAAA|[sr]k_(?:live|test)_|\bSG\.|npm_|pypi-|do[opr]_v1_|dp\.(?:ct|pt|sa|st|scim|audit)\.|dckr_|bkua_|CCIPAT_|sbp_|dapi[0-9a-f]|dd[pw]_|glsa_|nfp_|CFPAT-|ATCTT3|ATATT|ATBB|BBDC-|HRKU-|pat-(?:eu|na)1-|apify_api_|FlyV1|fio-u-|tvly-|exa_|syt_|retaindb_|mem0_|brv_|xai-|fw-|fw_|fpk_`, String.raw`(?:^|[^A-Za-z0-9_])(?:am_|sk_)`, String.raw`A[KS]IA[A-Z0-9]|AKID|LTAI|hf_|api_org_|r8_`, String.raw`\bbot\d{6,}:|\b\d{6,}:[A-Za-z0-9_-]{20,}`, @@ -313,7 +316,11 @@ function parsePattern(raw: RedactPattern): RegExp | null { if (pattern && typeof raw === "string" && SHELL_REFERENCE_PRESERVING_PATTERN_SOURCES.has(raw)) { shellReferencePreservingPatterns.add(pattern); } - if (pattern && typeof raw === "string" && raw.startsWith(BASE64_SAFE_TOKEN_BOUNDARY)) { + if ( + pattern && + typeof raw === "string" && + (raw.startsWith(BASE64_SAFE_TOKEN_BOUNDARY) || raw.startsWith(IDENTIFIER_SAFE_TOKEN_BOUNDARY)) + ) { chunkUnsafePatterns.add(pattern); } return pattern; diff --git a/ui/src/ui/browser-redact.test.ts b/ui/src/ui/browser-redact.test.ts index 76d317acf096..b198780bb1cd 100644 --- a/ui/src/ui/browser-redact.test.ts +++ b/ui/src/ui/browser-redact.test.ts @@ -10,6 +10,9 @@ describe("browser tool detail redaction", () => { "curl 'https://example.test?refresh_token=ya29.longOAuthRefreshTokenValue&ok=1'", "client_secret=clientSecretValueThatShouldNotRender", "AIzaSyDUMMYGoogleApiKeyValue1234567890", + `bare Fireworks key fw-${"C".repeat(40)}`, + `https://example.test?debug=fw_${"A".repeat(40)}&ok=1`, + `X-Debug: fpk_${"B".repeat(40)}`, "-----BEGIN PRIVATE KEY-----\nabc123\n-----END PRIVATE KEY-----", 'cookie: "sessionid=verySensitiveCookieValue"', ].join("\n"), @@ -27,8 +30,24 @@ describe("browser tool detail redaction", () => { expect(redacted).not.toContain("longOAuthRefreshTokenValue"); expect(redacted).not.toContain("clientSecretValueThatShouldNotRender"); expect(redacted).not.toContain("DUMMYGoogleApiKeyValue1234567890"); + expect(redacted).toContain("bare Fireworks key fw-CCC...CCCC"); + expect(redacted).toContain("https://example.test?debug=fw_AAA...AAAA&ok=1"); + expect(redacted).toContain("X-Debug: fpk_BB...BBBB"); expect(redacted).not.toContain("abc123"); expect(redacted).not.toContain("verySensitiveCookieValue"); + for (const masked of ["fw-CCC...CCCC", "fw_AAA...AAAA", "fpk_BB...BBBB"]) { + expect(redactToolDetail(masked)).toBe(masked); + } + }); + + it("preserves long non-token identifiers containing Fireworks prefixes", () => { + const input = [ + `fixturefw-${"C".repeat(40)}`, + `fixture_fw_${"A".repeat(40)}`, + `fixture_fpk_${"B".repeat(40)}`, + ].join(" "); + + expect(redactToolDetail(input)).toBe(input); }); it("exposes the tool payload redaction name used by shared display modules", () => { diff --git a/ui/src/ui/browser-redact.ts b/ui/src/ui/browser-redact.ts index c215a00a4bb7..06ff9119cf19 100644 --- a/ui/src/ui/browser-redact.ts +++ b/ui/src/ui/browser-redact.ts @@ -36,6 +36,9 @@ const SECRET_DETAIL_PATTERNS: RegExp[] = [ /\b(1\/\/0[0-9A-Za-z_\-./+=]{10,})\b/g, /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, /\b(pplx-[A-Za-z0-9_-]{10,})\b/g, + /(^|[^A-Za-z0-9_])(fw-[A-Za-z0-9]{30,})/g, + /(^|[^A-Za-z0-9_])(fw_[A-Za-z0-9]{30,})/g, + /(^|[^A-Za-z0-9_])(fpk_[A-Za-z0-9]{30,})/g, /\b(npm_[A-Za-z0-9]{10,})\b/g, /\b(AKID[A-Za-z0-9]{10,})\b/g, /\b(LTAI[A-Za-z0-9]{10,})\b/g,