Skip to content

fix(agent-core-v2): redact non-ASCII, UNC, and forward-slash paths in telemetry - #2419

Open
LHMQ878 wants to merge 3 commits into
MoonshotAI:mainfrom
LHMQ878:fix/telemetry-path-redaction-non-ascii
Open

fix(agent-core-v2): redact non-ASCII, UNC, and forward-slash paths in telemetry#2419
LHMQ878 wants to merge 3 commits into
MoonshotAI:mainfrom
LHMQ878:fix/telemetry-path-redaction-non-ascii

Conversation

@LHMQ878

@LHMQ878 LHMQ878 commented Jul 30, 2026

Copy link
Copy Markdown

Closes #2418

Problem

cleanTelemetryString is the last step before an event leaves the process — CloudAppender.track cleans properties, buffers the EnrichedCloudEvent, and CloudTransport POSTs it (cloudAppender.ts:115). Both of its path patterns were built from \w:

const POSIX_PATH = /(?:\/[\w.~+-]+){2,}\/?/g;
const WINDOWS_PATH = /\b[A-Za-z]:\\(?:[\w.~ -]+\\?){2,}/g;

\w is [A-Za-z0-9_], and neither pattern carries the u flag, so a match ends at the first non-ASCII byte and everything after it — user name included — goes out verbatim. On a Chinese-locale Windows install, C:\Users\<CJK name> is the ordinary case, not an edge case.

Measured on main:

case input main with this change
POSIX, CJK home dir /home/李明/proj/secret.txt /home/李明<REDACTED: user-file-path> <REDACTED: user-file-path>
Windows, CJK home dir C:\Users\李明\proj\secret.txt <REDACTED: user-file-path>李明\proj\secret.txt <REDACTED: user-file-path>
POSIX, Cyrillic home dir /home/иван/proj/secret.txt /home/иван<REDACTED: user-file-path> <REDACTED: user-file-path>
POSIX, accented home dir /home/josé/proj/secret.txt <REDACTED: user-file-path>é<REDACTED: user-file-path> <REDACTED: user-file-path>
Windows, accented home dir C:\Users\josé\proj\secret.txt <REDACTED: user-file-path>é\proj\secret.txt <REDACTED: user-file-path>
Windows, apostrophe in name C:\Users\O'Brien\proj\secret.txt <REDACTED: user-file-path>'Brien\proj\secret.txt <REDACTED: user-file-path>
UNC share \\fileserver\home\alice.chen\proj\secret.txt (unchanged — whole path emitted) <REDACTED: user-file-path>
Windows, forward slashes C:/Users/alice.chen/proj/secret.txt C:<REDACTED: user-file-path> <REDACTED: user-file-path>
Windows, node_modules tail C:\Users\alice.chen\repo\node_modules\pkg\index.js <REDACTED: user-file-path> node_modules/pkg/index.js
Windows path then prose C:\Program Files\a.txt could not be read <REDACTED: user-file-path> <REDACTED: user-file-path> could not be read

Three root causes for the leaks: \w is ASCII-only; UNC and \\?\ forms have no drive letter so WINDOWS_PATH never matched them; and C:/a/b needs a backslash to match WINDOWS_PATH, so only the tail was redacted.

The last two rows are behaviour bugs rather than leaks. The node_modules/ marker is spelled with a forward slash and the Windows branch never looked for it, so the diagnostic tail the module docstring promises to keep was thrown away on Windows. And WINDOWS_PATH allowed a bare space inside a segment without requiring a separator after it, so trailing message text was swallowed into the placeholder.

None of this was covered — privacy.ts had no test file.

What changed

Replaced both patterns with one alternation. The segment is defined by exclusion — anything that is not a separator, whitespace, or a character no filesystem permits in a name:

const SEGMENT = String.raw`(?:[^\\/\s"<>|:*?]+)`;

That covers every script without enumerating any, and it also covers O'Brien, which an enumerated class would have to remember. Three branches handle drive-letter (either separator, optional \\?\ prefix), UNC, and POSIX; merging them into one pass is deliberate, because running them as separate replace calls let a later pattern re-scan an earlier one's replacement — that is what produced node_modules<REDACTED: user-file-path> in an intermediate version of this fix.

Interior segments may contain spaces (Program Files, alice chen) but only where a separator follows, which is what keeps a match from running into surrounding prose. The final segment uses the space-free form and stops at the first space.

Two notes on the regex source:

  • Each fragment is wrapped in (?:…) so a quantifier applies to the whole fragment. A bare [^…]+ followed by ? reads as a lazy +?, not an optional segment; that cost a debugging round here and the comment says so.
  • redactPath normalizes \ to / before searching for node_modules/, which is what fixes the Windows tail.

Verification

  • vitest run test/app/telemetry/privacy.test.ts20 pass.
  • Control: with privacy.ts reverted to main and the new tests kept — 12 fail / 8 pass. Every claim in the table above is a failing assertion on main.
  • vitest run test/app/telemetry/56 pass across 6 files, unchanged.
  • tsc -p tsconfig.json --noEmit — the only errors are two pre-existing TS2307s for @moonshot-ai/tree-sitter-bash in bashParserService.ts, present on main and unrelated.
  • oxlint on both touched files — 0 warnings, 0 errors.
  • Full agent-core-v2 suite: the set of failing test files is byte-identical before and after (64 files, all pre-existing on this Windows checkout). Test counts move only by the 20 added here.
  • git ls-files --eol reports i/lf for both files, per the repo's .gitattributes.

Tests added

test/app/telemetry/privacy.test.ts, 20 cases in three groups:

  • non-ASCII home directories — CJK, Cyrillic, accented, apostrophe, and space-containing names on both platforms, plus the assertion that a space-tolerant segment stops at the end of the path rather than mid-sentence.
  • Windows path spellings — UNC share, \\?\ long form, forward-slash drive-letter, drive root.
  • behaviour that must not regress — plain ASCII paths, node_modules tails on both platforms, non-paths (3/4, /tmp, moonshotai/kimi-k2-instruct, plain prose, empty string), the labeled patterns, a path following an earlier placeholder, multiple paths in one value, and repeated calls.

The leak cases assert the private identifier is absent, not merely that a placeholder is present — a partial redaction still contains a placeholder.

Scope

privacy.ts and its new test only. agent-core (v1) has no counterpart to this module; I did not touch it.

… telemetry

`cleanTelemetryString` built both path patterns from `\w`, which is ASCII-only,
so a match ended at the first non-ASCII byte and the rest of the path — user
name included — went out in the POSTed event. A home directory named `李明`,
`иван`, or `josé` leaked, as did `O'Brien`, since `'` is not in `\w` either.

Two more spellings were missed outright: UNC paths have no drive letter so
nothing matched them at all, and `C:/a/b` only matched the POSIX pattern, which
left the drive letter behind as `C:<REDACTED: ...>`.

Replace both patterns with one alternation whose segment is defined by
exclusion — anything that is not a separator, whitespace, or a character no
filesystem permits — covering every script without enumerating any. Merging the
branches into a single pass also stops a later pattern from re-scanning an
earlier one's replacement.

Two smaller fixes in the same function, both visible in the new tests:

- Normalize separators before looking for the `node_modules/` marker, so the
  diagnostic tail the module docstring promises to keep survives on Windows too.
- Require a separator after a space-containing segment, so
  `C:\Program Files\a.txt could not be read` no longer swallows the message.

Adds `test/app/telemetry/privacy.test.ts`; the module had no coverage.

Closes MoonshotAI#2418
@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2c6936d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a04e94c4b3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

[
// Drive-letter, either separator, optional `\\?\` / `\\.\` long-path prefix:
// C:\a\b, C:/a/b, \\?\C:\a\b
String.raw`(?:\\\\[?.]\\)?[A-Za-z]:[\\/]${INTERIOR}*${SEGMENT}?`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact final path segments that contain spaces

For Windows drive-letter paths whose filename contains a space, this branch stops the match at the first word because the final component is ${SEGMENT} rather than the space-tolerant segment; for example C:\Users\alice\secret file.txt is cleaned to <REDACTED: user-file-path> file.txt, so the filename tail still leaves the process in telemetry. The previous Windows pattern redacted that whole path, and the v2 telemetry rule relies on CloudAppender redacting absolute paths as the safety net.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L40-L42

Useful? React with 👍 / 👎.

Comment on lines +24 to +25
/**
* One path segment, defined by exclusion: anything that is not a separator,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep implementation comments out of v2 modules

This added block comment is the first of several local comments explaining regex internals, but the agent-core-v2 guide requires comments to live only in the top-of-file /** */ header and not beside statements. Please move any necessary rationale into the module header or tests, and remove the inline implementation narration so this file stays consistent with the package convention.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L11-L15

Useful? React with 👍 / 👎.

…s intact

The path alternation ended its final segment at the first space, so a
filename with a space in it kept everything after that space on the wire:
`C:\Users\alice\secret file.txt` cleaned to
`<REDACTED: user-file-path> file.txt`. The filename is exactly the part
worth redacting, so this is the leak the pattern exists to prevent.

The final segment now accepts interior spaces, but only when what follows
them still ends in a file extension. Trailing prose does not, so
`C:\proj\a.txt could not be read` backtracks to the filename rather than
swallowing the sentence — the boundary the previous design got by
stopping at the first space is now expressed as a condition instead.

Fixes a second over-redaction found while validating that boundary: with
a space-tolerant final segment, `read 1/2 then 3/4 then 5/6` matched as
one path, because `2 then 3` reads as a single spaced segment. The POSIX
branch now refuses to start immediately after an alphanumeric. Requiring
two interior segments would also have blocked the fraction, but it would
have stopped redacting `/tmp/foo.txt` and `/etc/passwd`, which the
pattern this branch replaced did redact.

Comments move into the top-of-file block per the agent-core-v2 comment
convention; the regex rationale is unchanged, only relocated.
@LHMQ878

LHMQ878 commented Jul 30, 2026

Copy link
Copy Markdown
Author

Both findings addressed in 73862fe4f. The P1 is real, and a bit narrower than described; validating the fix also turned up a second over-redaction of mine that nothing flagged.

P1 — confirmed for the drive-letter branch. I ran the old and new patterns side by side rather than reasoning about them:

INPUT : "C:\\Users\\alice\\secret file.txt"
  old : "<REDACTED: user-file-path>"
  new : "<REDACTED: user-file-path> file.txt"     <-- leak
INPUT : "/home/alice/secret file.txt"
  old : "<REDACTED: user-file-path> file.txt"
  new : "<REDACTED: user-file-path> file.txt"     <-- unchanged

One correction to the finding: main's POSIX pattern (/(?:\/[\w.~+-]+){2,}\/?/g) excludes spaces too, so it truncated exactly the same way. This PR regressed the Windows case only; the POSIX case has always leaked the tail of a spaced filename. That makes it worth fixing on its own merits rather than as a regression, and the filename is the highest-value part of the path to redact, so I treated it as such.

Fix. The final segment now accepts interior spaces, but only when what follows still ends in a file extension:

const FINAL_SEGMENT = String.raw`(?:${SEGMENT_WITH_SPACES}\.[A-Za-z][A-Za-z0-9]{0,9}(?![^\s"'<>])|${SEGMENT})`;

Prose after a path has no extension at the end, so the alternation backtracks to the bare segment. The boundary the old design got for free by stopping at the first space is now an explicit condition, and C:\proj\a.txt could not be read still keeps its sentence.

A second over-redaction, mine, not previously reported. With a space-tolerant final segment, read 1/2 then 3/4 then 5/6 matched as one path — 2 then 3 reads as a single spaced segment, so the run from /2 to /6 qualified. main left it alone. Fixed by refusing to start the POSIX branch immediately after an alphanumeric:

String.raw`(?<![A-Za-z0-9])(?:\/${SEGMENT_WITH_SPACES}(?=\/))+\/${FINAL_SEGMENT}\/?`,

Worth recording why not {2,} interior segments, which was my first candidate and also blocks the fraction: it stops redacting /tmp/foo.txt and /etc/passwd, both of which main does redact. That would have traded a cosmetic over-redaction for a privacy regression, so the lookbehind is the right lever. (target is ES2024, so the lookbehind is in-range; Node has supported it since 8.3.)

P2 — correct, and it applied to more than the one block. Every per-constant /** */ and inline // in the file was in violation, not just the flagged one. All the rationale is now in the top-of-file header; nothing was dropped, only relocated, plus a line for the new lookbehind.

Verification. 23 tests pass in privacy.test.ts, up from 19. Four new cases: spaced filename on all three branches (drive-letter, POSIX, UNC), a double-extension name (my report v2.final.docx), the chained-fraction case, and /tmp/... + /etc/passwd to pin the two-segment behaviour the rejected {2,} candidate would have broken. I confirmed the new tests are load-bearing by stashing only the source file and re-running — the two new assertions fail, the other 21 pass.

Full package suite is 88 failed / 3203 passed, against a 87/3201 baseline on a clean tree at the same commit. The one-test delta is sessionIndex.test.ts > list filters by childOf from the read model, which passes 3/3 in isolation and touches no telemetry code — a timing flake in the shared run, not this change. oxlint --type-aware is clean on both files, and tsc reports only the pre-existing @moonshot-ai/tree-sitter-bash resolution errors.

`agent-core-v2` is private, but it is a workspace dependency of the published
`@moonshot-ai/kimi-code`, so the fix reaches users and needs a version bump.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry path redaction fails for non-ASCII home directories, UNC paths, and C:/ spellings

1 participant