Skip to content

Commit 167a5f2

Browse files
author
ralphstodomingo
committed
fix: mask filesystem paths in telemetry error text
maskString deliberately covers API keys, bearer tokens, emails, internal hosts and quoted strings — but had no rule for filesystem paths, so UNQUOTED paths in error messages reached App Insights raw. Home-directory paths carry the OS username and project-rooted paths leak client repo structure (a live 32-machine core_failure/file_not_found cluster carried full /Users/<name>/... paths). Quoted paths were coincidentally destroyed by the quote rule, which is why the asymmetry went unnoticed. Adds three ordered rules (Windows drive/UNC, POSIX absolute with 2+ segments, home-relative ~/) that replace the whole path with <path> — matching the chain's existing "over-masking is the correct failure mode" doctrine. Ordered after the URL rule (a public URL's path segment is never word-anchored, so it cannot match) and before quote masking. Bare single-segment tokens like "/mcp", MIME types, dates and version specs are proven untouched by the test matrix. Side effect, intended: hashError operates on the masked message, so grouping keys for affected messages change once — and path-variant messages that previously split now collapse into one group.
1 parent e27aeac commit 167a5f2

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

packages/opencode/src/altimate/telemetry/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,22 @@ export namespace Telemetry {
14051405
/\bhttps?:\/\/(?:[^\/\s@]+@)?(?:localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|169\.254\.\d+\.\d+|0\.0\.0\.0|\[(?:::1|fc[0-9a-f]{2}:[^\]]*|fd[0-9a-f]{2}:[^\]]*|fe80:[^\]]*)\]|[A-Za-z0-9.-]+\.(?:local|internal|localhost))(?::\d+)?[\w/.?=&%+#,;~!*'()@:-]*/gi,
14061406
"<internal-host>",
14071407
)
1408+
// altimate_change start — mask filesystem paths in error text
1409+
// Tool failures embed the path that failed ("File not found: /Users/…");
1410+
// home-dir paths carry the OS username, and project-rooted absolute paths
1411+
// leak client repo structure. QUOTED paths were already destroyed by the
1412+
// quote rule below — unquoted ones reached telemetry raw (live 32-machine
1413+
// core_failure/file_not_found cluster). Same philosophy as the rest of
1414+
// this chain: over-masking is the correct failure mode. Ordered after the
1415+
// URL rule (a public URL's path segment is never word-anchored, so it
1416+
// cannot match here) and before quote masking. Three shapes:
1417+
// Windows drive / UNC: C:\Users\… \\server\share
1418+
// POSIX absolute (2+ segments — bare "/mcp" style tokens survive)
1419+
// Home-relative: ~/…
1420+
.replace(/(^|[\s"'`=(,])(?:[A-Za-z]:[\\\/]|\\\\)[^\s'"`]+/g, "$1<path>")
1421+
.replace(/(^|[\s"'`=(,])\/(?:[\w.@+-]+\/)+[^\s'"`)\]},]*/g, "$1<path>")
1422+
.replace(/(^|[\s"'`=(,])~\/[^\s'"`)\]},]*/g, "$1<path>")
1423+
// altimate_change end
14081424
.replace(/'(?:[^'\\]|\\.)*'/g, "?")
14091425
.replace(/"(?:[^"\\]|\\.)*"/g, "?")
14101426
.replace(/\s+/g, " ")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* maskString file-path masking.
3+
*
4+
* The masking chain deliberately covered keys, bearers, emails, internal hosts
5+
* and quoted strings — but had no rule for filesystem paths, so UNQUOTED paths
6+
* in error text reached App Insights raw (a live 32-machine
7+
* core_failure/file_not_found cluster carried full /Users/<name>/… paths:
8+
* OS username + client repo structure). Quoted paths were coincidentally
9+
* destroyed by the quote rule, which is why the gap went unnoticed.
10+
*/
11+
import { describe, expect, it } from "bun:test"
12+
import { Telemetry } from "../../src/altimate/telemetry"
13+
14+
const mask = Telemetry.maskString
15+
16+
describe("maskString file paths", () => {
17+
it("masks home-directory paths (the PII case: username + repo layout)", () => {
18+
expect(mask("File not found: /Users/jdoe/Documents/client-repos/Platform_Project/models/05_da.sql"))
19+
.toBe("File not found: <path>")
20+
expect(mask("ENOENT: no such file or directory, open /home/jdoe/dbt/profiles.yml"))
21+
.toBe("ENOENT: no such file or directory, open <path>")
22+
expect(mask("could not read ~/dbt/profiles.yml")).toBe("could not read <path>")
23+
})
24+
25+
it("masks Windows drive and UNC paths", () => {
26+
expect(mask("No such file: C:\\Users\\jdoe\\project\\models\\a.sql")).toBe("No such file: <path>")
27+
expect(mask("read failed for \\\\fileserver\\share\\models\\x.sql")).toBe("read failed for <path>")
28+
})
29+
30+
it("masks project-rooted absolute paths (client repo structure)", () => {
31+
expect(mask("File not found: /app/models/stg_quickbooks__estimate.sql"))
32+
.toBe("File not found: <path>")
33+
expect(mask("Manifest path for deferral does not exist: /data/warehouse/target/manifest.json"))
34+
.toBe("Manifest path for deferral does not exist: <path>")
35+
})
36+
37+
it("masks paths at string start and inside punctuation", () => {
38+
expect(mask("/Users/jdoe/x/y.sql was deleted")).toBe("<path> was deleted")
39+
expect(mask("failed (from /opt/dbt/bin/dbt)")).toBe("failed (from <path>)")
40+
expect(mask("--project-dir=/srv/analytics/dbt")).toBe("--project-dir=<path>")
41+
})
42+
43+
it("does NOT mask non-path slashes", () => {
44+
expect(mask("content-type application/json rejected")).toBe("content-type application/json rejected")
45+
expect(mask("requirement dbt-core~=1.11.0 not satisfied")).toBe("requirement dbt-core~=1.11.0 not satisfied")
46+
expect(mask("on 8/17/2026 at 3:2 ratio 1/2")).toBe("on 8/17/2026 at 3:2 ratio 1/2")
47+
expect(mask("endpoint /mcp returned 404")).toBe("endpoint /mcp returned 404")
48+
})
49+
50+
it("leaves public URL paths to the URL rules (never word-anchored)", () => {
51+
expect(mask("POST https://api.openai.com/v1/chat/completions failed"))
52+
.toBe("POST https://api.openai.com/v1/chat/completions failed")
53+
// internal hosts keep their existing, stronger treatment
54+
expect(mask("GET http://localhost:8080/api/x timed out")).toContain("<internal-host>")
55+
})
56+
57+
it("composes with the earlier rules in the chain", () => {
58+
// credential first, then the path
59+
expect(mask("sk-abcdefghijklmnopqrstuvwx leaked into /Users/jdoe/log.txt"))
60+
.toBe("sk-*** leaked into <path>")
61+
// quoted path still collapses via the quote rule — no leak either way
62+
expect(mask("open '/Users/jdoe/x.sql' failed")).not.toContain("jdoe")
63+
})
64+
})

0 commit comments

Comments
 (0)