Skip to content

fix(kaos): treat a leading ] in a glob character class as a literal - #2263

Open
LHMQ878 wants to merge 3 commits into
MoonshotAI:mainfrom
LHMQ878:fix/glob-literal-close-bracket
Open

fix(kaos): treat a leading ] in a glob character class as a literal#2263
LHMQ878 wants to merge 3 commits into
MoonshotAI:mainfrom
LHMQ878:fix/glob-literal-close-bracket

Conversation

@LHMQ878

@LHMQ878 LHMQ878 commented Jul 27, 2026

Copy link
Copy Markdown

Related Issue

No existing issue — the problem is described below.

Problem

globPatternToRegex scans for the character-class terminator with pattern.indexOf(']', i + 1), so a ] in the first position of the class body is read as the terminator instead of as a member. []] therefore compiles to the empty JS character class [], which matches nothing at all, and the whole pattern silently stops matching.

POSIX and Python fnmatch both treat that ] as a literal member:

>>> import fnmatch
>>> fnmatch.fnmatch('].txt', '[]].txt')
True

Python parity is the stated contract for this helper — packages/kaos/test/internal.test.ts has a describe('glob semantic compatibility (Python parity)') block, and the existing "treats an unclosed [ as a literal bracket" test comments that it "Mirrors Python fnmatch/glob".

Reproduced end-to-end through the public API, with ].txt and a.txt in a temp dir:

pattern matches
*.txt ].txt, a.txt
[]].txt (nothing)

[]].txt should have matched ].txt. Failure is silent: the caller gets an empty result set, not an error.

The reachable call path is LocalKaos.glob()_globWalk (packages/kaos/src/local.ts:411) and the SSH equivalent (packages/kaos/src/ssh.ts:683), where the compiled regex filters readdir output. Kaos is part of the SDK surface (KimiHarness accepts a kaos option), so an embedder passing a bracket-class pattern hits this.

What changed

Start the terminator scan past an optional negating ! and a leading ], and escape ] inside the class body so a literal member cannot close the class early:

let scanFrom = i + 1;
if (pattern[scanFrom] === '!') scanFrom++;
if (pattern[scanFrom] === ']') scanFrom++;
const end = pattern.indexOf(']', scanFrom);
...
charClass = charClass.replaceAll('\', '\\').replaceAll(']', '\]');

Applied to both copies of the function — the original in packages/kaos/src/internal.ts and the copy vendored into packages/agent-core-v2/src/_base/execEnv/globPattern.ts (its header says "Vendored from @moonshot-ai/kaos internal.ts"), so the two do not drift.

Compiled output, before → after:

pattern before after
[]].txt ^[]\.txt$ (matches nothing) ^[\]]\.txt$
[!]].txt ^[]\.txt$ ^[^\]]\.txt$
[]a].txt ^[]a\]\.txt$ ^[\]a]\.txt$
[a]].txt ^[a]\]\.txt$ ^[a]\]\.txt$ (unchanged — a ] closing a non-empty class is still the terminator)
file[abc\].txt ^file[abc\]\.txt$ ^file[abc\]\.txt$ (byte-identical)
file[, [, [! literal-bracket fallback unchanged

The last two rows are the no-regression checks: the existing test at packages/kaos/test/internal.test.ts:213 (escapes backslashes inside character classes…) and the unclosed-[ test both compile to exactly the same regex as before.

Tests

  • 4 cases added to the existing glob semantic compatibility (Python parity) block in packages/kaos/test/internal.test.ts.
  • packages/agent-core-v2/test/_base/execEnv/globPattern.test.ts is new — the vendored copy had no test file at all. 5 cases, covering the same four plus the unclosed-[ fallback.

Verification run locally:

  • vitest run on both files: 33 passed, 1 skipped.
  • Control experiment — reverting only the scanFrom lines makes 3 of the new tests fail, confirming they actually pin the bug.
  • oxlint --type-aware on all 4 files: 0 warnings, 0 errors. tsc --noEmit clean for both packages.
  • packages/kaos/test/local.test.ts has 5 failing symlink cases (T-C1T-C6) on my Windows box. A git stash control run reproduces the identical 5 failures without this change, so they are a pre-existing local symlink-permission issue, not a regression here.

Not in scope

While reproducing this I found a second, separate deviation in the same function: / is permitted inside a character class, so a[/]b compiles to ^a[/]b$ and one glob segment can cross a path separator — inconsistent with * and ?, which both compile to [^/]. I left it out to keep this fix reviewable; happy to open a follow-up if you'd like it addressed.

Changeset

No changeset. Per .changeset/README.md only @moonshot-ai/kimi-code and @moonshot-ai/kimi-code-sdk are publishable, and the table's "only modifies internal packages, no user-visible change in SDK / CLI" row applies: @moonshot-ai/kaos and @moonshot-ai/agent-core-v2 are both private, and no in-repo product code calls kaos.glob with a bracket-class pattern — the CLI's Glob tool delegates to ripgrep's own glob engine, and the only in-tree glob() callers are AcpKaos's pass-through and the current.ts re-export. Say the word and I'll add a patch changeset for @moonshot-ai/kimi-code instead.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update. (docs/ does not document glob character-class syntax.)

@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f237cb8

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: c08a8806b3

ℹ️ 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".

Comment on lines +27 to +31
// POSIX (and Python fnmatch): a `]` in the first position of the class
// body — after an optional negating `!` — is a literal member rather
// than the terminator, so the scan for the real terminator has to start
// past it. Treating it as the terminator yields an empty JS class
// (`[]`), which matches nothing at all, so the pattern silently stops

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 Move the implementation explanation into the file header

Move this newly added implementation narration into the existing top-of-file module header, or remove it if the code is sufficiently clear. The scoped agent-core-v2 guide explicitly prohibits comments beside functions or statements, but this block is embedded within the character-class parsing logic.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L13-L13

Useful? React with 👍 / 👎.

Comment on lines +7 to +9
// Python: fnmatch.fnmatch('].txt', '[]].txt') is True. Reading the `]` as
// the terminator instead would leave an empty class, which matches nothing,
// so the pattern would stop matching altogether.

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 Move test commentary into a top-of-file scenario header

Move this explanation into a top-of-file scenario header rather than leaving it beside the test statements. Besides making the new test file start with the scenario context, this is required by the scoped agent-core-v2 convention that comments live solely in the file header.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L13-L13

Useful? React with 👍 / 👎.

let scanFrom = i + 1;
if (pattern[scanFrom] === '!') scanFrom++;
if (pattern[scanFrom] === ']') scanFrom++;
const end = pattern.indexOf(']', scanFrom);

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 Handle reversed ranges after a leading ] without throwing

When a caller supplies a class such as []--], the new scan treats the final ] as the terminator and constructs the JavaScript class [\]--]; new RegExp then throws Range out of order in character class. Before this change the pattern compiled to an empty class and simply matched nothing, which is also what Python fnmatch produces, so both LocalKaos.glob and the SSH implementation now throw for an input that previously returned an empty result. Normalize invalid ranges to a never-match expression or otherwise preserve the non-throwing behavior.

Useful? React with 👍 / 👎.

LHMQ878 added a commit to LHMQ878/kimi-code that referenced this pull request Jul 28, 2026
…class

Review on MoonshotAI#2263 caught that `[]--]` regressed: before the leading-`]` fix it
compiled to an empty class and matched nothing, and afterwards it produces
`[\]--]`, which makes `new RegExp` throw `Range out of order in character
class`. `_globWalk` calls `globPatternToRegex` outside its try block, so the
throw aborts the whole directory walk.

Reversed ranges that do not involve a leading `]` — `[a--]`, `[z-a]` — threw
on `main` too, so guard the compile itself rather than just that one input:
fall back to a never-matching pattern, which is what Python `fnmatch` yields
for all three.

Also move the agent-core-v2 comments into the file header per that package's
AGENTS.md, which requires comments to live solely in the top-of-file block.
@LHMQ878

LHMQ878 commented Jul 30, 2026

Copy link
Copy Markdown
Author

Thanks — all three were valid, and all three are addressed in 2fe604b (pushed 2026-07-27, after the reviewed c08a8806). I should have replied here at the time rather than letting the push speak for itself; details below.

P2 — reversed range now throws where it previously matched nothing

This was the important one, and it is a regression my own change introduced. Confirmed before fixing, by building the exact class the new scan produces for []--]:

[]--]    builds "^[\]--]$"  THROWS: Invalid regular expression: /^[\]--]$/: Range out of order in character class
[z-a]    builds "^[z-a]$"    THROWS: Invalid regular expression: /^[z-a]$/: Range out of order in character class
[]]      builds "^[\]]$"    compiles; matches ']': true

Your reading of the mechanism is exact: scanning past the leading ] makes the final ] the terminator, so the body becomes ]-- and the escaped \]-- is a reversed range. Before the change the class was empty and merely matched nothing.

Two things made this worse than a cosmetic difference, and both are why I took the fail-soft route you suggested rather than rejecting the pattern:

  1. _globWalk (packages/kaos/src/local.ts:411) and the SSH equivalent call globPatternToRegex outside a try block, so the throw aborts the entire directory walk — not just the one non-matching pattern.
  2. Python agrees with the old behaviour, and Python parity is this helper's stated contract:
>>> import fnmatch
>>> [fnmatch.fnmatch(s, '[]--]') for s in (']', 'a', '-')]
[False, False, False]
>>> [fnmatch.fnmatch(s, '[z-a]') for s in (']', 'a', '-')]
[False, False, False]

So the fix normalizes to a never-match expression, in both copies of the helper:

  const flags = caseSensitive ? '' : 'i';
  try {
    return new RegExp(regex, flags);
  } catch {
    // A character class can still be invalid as a JS regex even though it is a
    // legal glob — `[a--]` and `[z-a]` are reversed ranges and make `RegExp`
    // throw `Range out of order in character class`. Python `fnmatch` matches
    // nothing for those, and callers such as `_globWalk` invoke this outside a
    // try block, so throwing here would abort the whole walk. Fall back to a
    // pattern that matches nothing.
    return new RegExp('(?!)', flags);
  }

(?!) is the never-match: verified it rejects ']', '' and '-'.

Regression test in both packages, over ['[]--]', '[a--]', '[z-a]']. Proof it pins the behaviour rather than merely passing — reverting just the try/catch back to return new RegExp(regex, ...) and re-running:

 × matches nothing instead of throwing on a reversed range   (packages/kaos)
 × matches nothing instead of throwing on a reversed range   (packages/agent-core-v2)
SyntaxError: Invalid regular expression: /^[\]--]$/: Range out of order in character class
 Tests  2 failed | 33 passed | 1 skipped (36)

Restored: 35 passed | 1 skipped (36).

P1 ×2 — comment placement under the agent-core-v2 convention

Both correct, and I had missed that packages/agent-core-v2/AGENTS.md scopes comments to the top-of-file block ("Comments live solely in the top-of-file /** */ block — never beside functions, methods, or statements"). The inline narration in globPattern.ts is gone; the POSIX/fnmatch divergence it explained is now two paragraphs in the existing module header, alongside a note on the never-match fallback. The test file gained a top-of-file scenario header covering all four cases it exercises, and the per-test comments were removed.

Note that the convention is scoped to agent-core-v2 only, so the equivalent explanation in packages/kaos/src/internal.ts stays beside the code, matching that package's existing style.

Current head is 2fe604b1. packages/kaos/test/internal.test.ts and packages/agent-core-v2/test/_base/execEnv/globPattern.test.ts both pass.

LHMQ878 and others added 3 commits August 3, 2026 03:32
`globPatternToRegex` scanned for the class terminator with
`pattern.indexOf(']', i + 1)`, so `[]]` closed the class on its first
character. That emitted the empty JS class `[]`, which matches nothing,
and the whole pattern silently stopped matching — `kaos.glob(dir, '[]].txt')`
returned no results even with `].txt` on disk.

POSIX and Python `fnmatch` treat a `]` in the first position of the class
body — after an optional negating `!` — as a literal member, and these
tests declare Python parity as the contract. Start the terminator scan
past an optional `!` and a leading `]`, and escape `]` inside the class
body so a literal member cannot close the class early.

Applied to both copies of the function: the original in `packages/kaos`
and the vendored copy in `agent-core-v2/_base/execEnv`.
…class

Review on MoonshotAI#2263 caught that `[]--]` regressed: before the leading-`]` fix it
compiled to an empty class and matched nothing, and afterwards it produces
`[\]--]`, which makes `new RegExp` throw `Range out of order in character
class`. `_globWalk` calls `globPatternToRegex` outside its try block, so the
throw aborts the whole directory walk.

Reversed ranges that do not involve a leading `]` — `[a--]`, `[z-a]` — threw
on `main` too, so guard the compile itself rather than just that one input:
fall back to a never-matching pattern, which is what Python `fnmatch` yields
for all three.

Also move the agent-core-v2 comments into the file header per that package's
AGENTS.md, which requires comments to live solely in the top-of-file block.
@LHMQ878
LHMQ878 force-pushed the fix/glob-literal-close-bracket branch from 2fe604b to f237cb8 Compare August 2, 2026 19:34
@LHMQ878

LHMQ878 commented Aug 2, 2026

Copy link
Copy Markdown
Author

Rebased onto e22479a6 — this had gone into merge conflict, and I've added the missing changeset.

The conflict was comment-only. #2451 (remove the L0-L7 domain layering and clean up comment conventions) rewrote the module header of packages/agent-core-v2/src/_base/execEnv/globPattern.ts, dropping the (L0) marker and the "Vendored from @moonshot-ai/kaos" sentence. My commit had added a paragraph to that same header. Upstream's total diff to the file was 17 lines, all inside the header — no functional overlap, and nothing upstream touched the character-class parsing. I kept upstream's rewritten prose and re-applied my paragraph after it. The functional diff is unchanged at +151 −7 across the same four files.

Worth noting because it isn't obvious from the conflict: #2451 also removed the sentence saying this file is vendored from @moonshot-ai/kaos. The duplication is still real — packages/kaos/src/internal.ts holds the same function, and this PR fixes both copies — but the header no longer says so, so a future reader has less reason to look for the second copy.

Changeset added (f237cb88). The bot has been flagging this since I opened the PR and I'd missed it. Both packages are private: true, so nothing publishes directly, but they reach users through @moonshot-ai/kimi-code: kaos via agent-core/acp-adapter/kimi-code-sdk, agent-core-v2 via kap-server/klient. Checking the last 25 merged commits that touched either src/ tree, the ones with a user-visible effect carry a "@moonshot-ai/kimi-code": patch changeset while pure refactors carry none — a wrong-files glob match is user-visible, so I've followed the former.

Status of the earlier review, all pushed before this rebase and carried through it:

item commit state
P2 — reversed range after a leading ] threw where it used to match nothing 2fe604b fixed, regression was mine
P1 — implementation narration beside the parsing logic 2fe604b moved into the module header
P1 — test commentary beside the statements 2fe604b moved into a scenario header

vitest on both affected test files: 35 passed, 1 skipped. tsc --noEmit on packages/kaos: clean.

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.

1 participant