Skip to content

fix: match permission rule glob subjects as opaque text - #2747

Open
tarikermis wants to merge 2 commits into
MoonshotAI:mainfrom
tarikermis:fix/2728-permission-glob-subjects-cross-slashes
Open

fix: match permission rule glob subjects as opaque text#2747
tarikermis wants to merge 2 commits into
MoonshotAI:mainfrom
tarikermis:fix/2728-permission-glob-subjects-cross-slashes

Conversation

@tarikermis

Copy link
Copy Markdown

Related Issue

Resolve #2728

Problem

Permission-rule argument patterns were matched with picomatch path semantics, so * stopped at / and refused dot segments. For command-like subjects this made rules silently never match:

matchesGlobRuleSubject('rm -rf*', 'rm -rf /tmp/x')                // false (before)
matchesGlobRuleSubject('git *', 'git commit -m "fix src/a.ts"')   // false (before)

No user-side workaround existed short of ** (which matches everything). This also contradicts the documented example deny = "Bash(rm -rf*)" in the config docs and the globMatch docstring ("the value is not treated as a file path").

What changed

globMatch (in both agent-core and agent-core-v2, which share this matcher) now:

  1. tries the historical path-semantics match first, so every pattern that matched before keeps matching (e.g. a/**/b still matches a/b) — the change is purely additive;
  2. then matches the subject as opaque text: / is rewritten to a NUL placeholder (real NUL bytes are stripped first) and dot: true is set, so * and ** cross slashes and dot segments.

Path subjects (Read/Write/Edit/ReadMediaFile) are untouched: pathGlobMatch now uses the extracted pathSegmentGlobMatch helper, a byte-identical copy of the old matcher, so e.g. Edit(src/*) still does not match src/sub/a.ts.

On the questions raised in the issue: this takes the shared opaque-text fix for glob subjects (the shape the issue verified against all seven cases); per-tool subject semantics stay as they are. Sub-command decomposition (Bash(git *) authorizing pipelines) is explicitly out of scope — it is a separate concern that pulls in the opposite direction and deserves its own decision.

Honest behavior notes:

  • allow rules widen too: a rule like allow Glob(src/*) now also matches pattern subjects containing slashes/traversal segments (e.g. src/../../etc/*), where it previously never fired. That is inherent to making these rules match at all, but calling it out explicitly.
  • Negated patterns (!) invert the widened matcher, so e.g. allow Bash(!git *) no longer auto-allows git commands whose arguments contain slashes — which is what such a rule was written to mean, but it is a behavior change for configs that (unknowingly) relied on the silent non-match.

Verification

Reproduced the issue's exact cases on current main with failing tests first, then fixed. Added regression tests to the existing matcher test files in both packages covering: the issue's seven cases, URL and search-text subjects, literal-slash negative cases, ** patterns, globstar preservation (a/**/b vs a/b), NUL non-forgery, negation, and unchanged path-rule semantics.

Checks run locally (Node 24.15.0, pnpm 10.33.0):

  • pnpm vitest run packages/agent-core packages/agent-core-v2 — 535 files, 9027 tests passed, 0 failed
  • pnpm --filter @moonshot-ai/agent-core --filter @moonshot-ai/agent-core-v2 run typecheck — clean
  • pnpm lint (oxlint --type-aware) — 0 errors; 0 warnings on the changed files

The diff was reviewed with kiro-cli (claude-opus-5); findings from the review rounds (globstar narrowing, NUL collision, missing negative/URL/search coverage) were verified empirically and addressed.

Limitations: matcher-level and policy-level tests only; I did not run an end-to-end CLI session exercising a live permission prompt. The v2 engine currently does not evaluate config rules, so the v2 change is covered by unit tests only.

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. (No doc update: the fix aligns behavior with the already-documented Bash(rm -rf*) example.)

Command-like rule subjects (Bash commands, FetchURL URLs, WebSearch/Grep
queries, Glob patterns, agent/skill/task names) were matched with
picomatch path semantics, so '*' stopped at '/' and refused dot
segments: a rule like Bash(rm -rf*) never matched 'rm -rf /tmp/x', and
no pattern short of '**' could match such subjects at all.

globMatch now first tries the historical path-semantics match (so every
pattern that matched before keeps matching, e.g. 'a/**/b' vs 'a/b'),
then matches the subject as opaque text with '/' rewritten to a NUL
placeholder and dot matching enabled, letting '*' and '**' cross
slashes and dot segments as the function's contract already promised.
Path subjects (Read/Write/Edit) keep pure path semantics via the
extracted pathSegmentGlobMatch helper.

Applied identically to agent-core and agent-core-v2, which share this
matcher.

Refs MoonshotAI#2728
@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ce2d619

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

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +34 to +35
// Try the historical path-semantics match first so rules that matched
// before keep matching (e.g. `a/**/b` still matches `a/b`).

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 Keep implementation comments in the file header

Remove these newly added implementation-narration comments, including the similar comment inside asOpaqueText, or fold only the externally relevant responsibility into the top-of-file header. This package explicitly requires comments to live solely in that header and forbids comments beside functions or statements.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L36-L38

Useful? React with 👍 / 👎.

Comment on lines +54 to +56
function asOpaqueText(value: string): string {
// Strip real NUL bytes first so one cannot be mistaken for a rewritten `/`.
return value.replaceAll(SLASH_PLACEHOLDER, '').replaceAll('/', SLASH_PLACEHOLDER);

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 Preserve NULs when encoding opaque subjects

When a rule subject can contain \u0000—notably WebSearch, whose query schema accepts any string—deleting NULs makes the permission matcher non-injective: an exact rule for ab now also matches the distinct subject a\u0000b. This can let a later tool call reuse an exact allow/session-approval rule that was granted for different input. Encode slash and NUL injectively (or explicitly reject NUL-bearing subjects) instead of removing the character; the duplicated legacy helper needs the same correction.

Useful? React with 👍 / 👎.

@Win-Hao

Win-Hao commented Aug 9, 2026

Copy link
Copy Markdown

Author of #2728 here — I ran this PR against the full probe matrix from that
investigation, independently of the PR's own tests. Results:

Verification: no functional mismatches (54 probes). Covered: the issue's seven
cases, negative controls, brace/?/char-class features, multi-line heredoc commands,
Windows command shapes (C:\ paths, UNC, mixed separators, /c/ mounts),
session-approval literal round-trips, NUL non-forgery (both directions), and
globstar-compat (a/**/b vs a/b). Also confirmed: ! negation is stripped in
matchRuleSubjects before globbing, so the union doesn't leak through negated rules;
globMatch has no consumers besides matchesGlobRuleSubject in either package; the
pathSegmentGlobMatch extraction is byte-identical to the old matcher. The two
modified test files pass locally (202/202).

One question — ./ normalization for command subjects. stripLeadingDotSlash
is kept in both branches, so run.sh* matches ./run.sh --flag and ./run.sh*
matches run.sh --flag. For file paths that's sound, but for commands ./x and x
are different executables (relative exec vs PATH lookup) — this is a small piece of
path semantics surviving into the opaque matcher. Keeping it is defensible under this
PR's purely-additive principle; dropping it would be stricter. Flagging so the choice
is made consciously rather than inherited.

One non-blocking note on backtracking (applies equally to the shape verified in
#2728, not specific to this PR).
Rewriting / to a placeholder removes the /
backtracking barriers, so picomatch's existing pathological-pattern weakness widens:
*a*a*a*a*a*a*a*a*a*b against a 94-char slash-heavy command takes ~12s per call
(old matcher: sub-ms there, but ~2s on slash-free input — the weakness itself is
pre-existing). Realistic 1–2-star patterns stay sub-ms even against 10KB commands,
and session-approval literals escape all wildcards, so exposure requires an unusual
hand-written config pattern. Worth a guard someday; shouldn't block this fix.

With the above noted, this resolves the B half of #2728 in the shape verified there,
and I agree with keeping sub-command decomposition (C) out of scope.

…ching

Address review findings on the previous head:

- Stripping NULs in the opaque-text rewrite made the matcher
  non-injective: an exact rule for 'ab' also matched the distinct
  subject 'a<NUL>b', so a later call could reuse an exact
  allow/session-approval granted for different input. NUL-bearing
  subjects and patterns now skip the opaque phase entirely and match
  only under the historical literal semantics, which compares NUL
  bytes as-is; the slash rewrite stays injective on the remaining
  NUL-free domain.
- Remove inline comments from agent-core-v2 rule-match.ts per the
  package's header-only comment convention, fold the externally
  relevant contract into the file header, and add a lint probe under
  test/lint guarding the convention for this file.
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.

Permission-rule globs use path semantics for command-like subjects, so * never crosses /

2 participants