@mxalbert/context-mode 2.0.3 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/cli.js +2 -2
- package/build/security.d.ts +44 -10
- package/build/security.js +147 -32
- package/build/server.js +3 -4
- package/cli.bundle.mjs +196 -196
- package/configs/antigravity-cli/plugin.json +1 -1
- package/configs/copilot-cli/.github/plugin/plugin.json +1 -1
- package/hooks/core/routing.mjs +1 -1
- package/hooks/security.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +137 -137
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "2.0.
|
|
9
|
+
"version": "2.0.4"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "2.0.
|
|
16
|
+
"version": "2.0.4",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "2.0.
|
|
6
|
+
"version": "2.0.4",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mxalbert/context-mode",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/build/cli.js
CHANGED
|
@@ -452,7 +452,7 @@ function defaultSourceForPath(absPath) {
|
|
|
452
452
|
}
|
|
453
453
|
function assertReadAllowed(path, projectDir) {
|
|
454
454
|
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
455
|
-
const denied = evaluateFilePath(path, denyGlobs,
|
|
455
|
+
const denied = evaluateFilePath(path, denyGlobs, undefined, projectDir);
|
|
456
456
|
if (denied.denied) {
|
|
457
457
|
throw new Error(`Read denied by policy: ${path}`);
|
|
458
458
|
}
|
|
@@ -488,7 +488,7 @@ async function indexCommand(argv) {
|
|
|
488
488
|
followSymlinks: boolFlag(parsed.flags, "follow-symlinks"),
|
|
489
489
|
perFileDeny: (filePath) => {
|
|
490
490
|
try {
|
|
491
|
-
return evaluateFilePath(filePath, denyGlobs,
|
|
491
|
+
return evaluateFilePath(filePath, denyGlobs, undefined, projectDir).denied;
|
|
492
492
|
}
|
|
493
493
|
catch {
|
|
494
494
|
return false;
|
package/build/security.d.ts
CHANGED
|
@@ -87,6 +87,18 @@ export declare function readToolDenyPatterns(toolName: string, projectDir?: stri
|
|
|
87
87
|
* needs an out-of-project read expresses it once, in the host config, e.g.
|
|
88
88
|
* `"permissions": { "allow": ["Read(/var/log/**)"] }`, and both the host and
|
|
89
89
|
* context-mode honor it.
|
|
90
|
+
*
|
|
91
|
+
* Settings-source anchor (`/{path}`): per Claude Code's documented permission
|
|
92
|
+
* semantics, a rule glob with a SINGLE leading slash is NOT rooted at the
|
|
93
|
+
* filesystem root — it is anchored at the directory of the settings file that
|
|
94
|
+
* declared it. Project/local settings (`.claude/settings.json`,
|
|
95
|
+
* `.claude/settings.local.json`) therefore anchor at the primary working
|
|
96
|
+
* directory (`projectDir`), while user settings (`~/.claude/settings.json`),
|
|
97
|
+
* adapter-global settings, and a custom `--settings <file>` anchor at that
|
|
98
|
+
* settings file's own directory. Each such glob is emitted as a settings-source
|
|
99
|
+
* RESOLVED twin in addition to the literal glob — the change is additive: no
|
|
100
|
+
* existing literal match is removed, so the README's `Read(/var/log/**)`
|
|
101
|
+
* filesystem-root escape hatch keeps working as a literal absolute rule.
|
|
90
102
|
*/
|
|
91
103
|
export declare function readToolPermissionPatterns(toolName: string, kind: "deny" | "allow", projectDir?: string, globalSettingsPath?: string): string[][];
|
|
92
104
|
interface CommandDecision {
|
|
@@ -123,16 +135,36 @@ export declare function evaluateCommandDenyOnly(command: string, policies: Secur
|
|
|
123
135
|
* Normalizes backslashes to forward slashes before matching so that
|
|
124
136
|
* Windows paths work with Unix-style glob patterns.
|
|
125
137
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
138
|
+
* Rule and file path anchors are expanded before matching:
|
|
139
|
+
* - A leading `~` / `~/` in EITHER the rule glob or the file path is
|
|
140
|
+
* expanded to the user's home directory (Claude Code's `~/path`
|
|
141
|
+
* anchor). `~user/...` is left untouched.
|
|
142
|
+
* - A rule glob beginning with `//` is treated as "absolute from the
|
|
143
|
+
* filesystem root" and has the leading double slash collapsed to one,
|
|
144
|
+
* matching the absolute candidates this module produces. The literal
|
|
145
|
+
* double-slash form is preserved as an additional variant so Windows UNC
|
|
146
|
+
* rules (e.g. `//server/share/**`) still match UNC candidates, which
|
|
147
|
+
* normalize to `//server/share/...`.
|
|
148
|
+
*
|
|
149
|
+
* Symlink resolution is mirrored on both sides. For each rule glob the
|
|
150
|
+
* matcher tests its literal variant AND a best-effort `realpathSync`'d
|
|
151
|
+
* variant (only the static prefix, before the first glob segment, is
|
|
152
|
+
* canonicalized). Against those it tests every file candidate: the raw
|
|
153
|
+
* input, the tilde-expanded input, the path resolved against `projectRoot`
|
|
154
|
+
* (or `process.cwd()` when no project root is supplied), and — when the
|
|
155
|
+
* file exists — its canonical form. A deny match therefore fires if ANY
|
|
156
|
+
* literal/canonical rule variant matches ANY literal/lexical/canonical file
|
|
157
|
+
* candidate, closing the CVE-2025-59829 symlink deny-bypass class.
|
|
158
|
+
*
|
|
159
|
+
* Anchoring the resolved candidate to `projectRoot` (falling back to cwd)
|
|
160
|
+
* means absolute deny rules still match relative `..` traversal even when
|
|
161
|
+
* the caller does not know the project root.
|
|
162
|
+
*
|
|
163
|
+
* Conversely, a RELATIVE rule glob (no leading `/`, `//`, or `~`) is anchored
|
|
164
|
+
* to the current directory at match time — `projectRoot` when supplied, else
|
|
165
|
+
* `process.cwd()` — per the host's "relative to current directory" semantics.
|
|
166
|
+
* The literal glob is kept as well, so both a relative access path and an
|
|
167
|
+
* absolute one are matched.
|
|
136
168
|
*
|
|
137
169
|
* realpath is best-effort: if the file does not exist yet (ENOENT)
|
|
138
170
|
* or the syscall fails for any reason, the lexical resolved form is
|
|
@@ -163,6 +195,8 @@ export declare function evaluateFilePath(filePath: string, denyGlobs: string[][]
|
|
|
163
195
|
*
|
|
164
196
|
* A path equal to the project root itself counts as inside. Comparison is
|
|
165
197
|
* case-insensitive on Windows/macOS to match those filesystems' semantics.
|
|
198
|
+
* A leading `~` in `filePath` is expanded to the user's home directory for
|
|
199
|
+
* consistency with `evaluateFilePath`.
|
|
166
200
|
*
|
|
167
201
|
* Returns `true` when `projectRoot` is falsy (no boundary to enforce) so the
|
|
168
202
|
* caller's fail-open posture is preserved when the root cannot be resolved.
|
package/build/security.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { resolveAdapterGlobalSettingsPaths } from "./util/claude-config.js";
|
|
4
5
|
// ==============================================================================
|
|
5
6
|
// Pattern Parsing
|
|
@@ -380,10 +381,22 @@ export function readToolDenyPatterns(toolName, projectDir, globalSettingsPath) {
|
|
|
380
381
|
* needs an out-of-project read expresses it once, in the host config, e.g.
|
|
381
382
|
* `"permissions": { "allow": ["Read(/var/log/**)"] }`, and both the host and
|
|
382
383
|
* context-mode honor it.
|
|
384
|
+
*
|
|
385
|
+
* Settings-source anchor (`/{path}`): per Claude Code's documented permission
|
|
386
|
+
* semantics, a rule glob with a SINGLE leading slash is NOT rooted at the
|
|
387
|
+
* filesystem root — it is anchored at the directory of the settings file that
|
|
388
|
+
* declared it. Project/local settings (`.claude/settings.json`,
|
|
389
|
+
* `.claude/settings.local.json`) therefore anchor at the primary working
|
|
390
|
+
* directory (`projectDir`), while user settings (`~/.claude/settings.json`),
|
|
391
|
+
* adapter-global settings, and a custom `--settings <file>` anchor at that
|
|
392
|
+
* settings file's own directory. Each such glob is emitted as a settings-source
|
|
393
|
+
* RESOLVED twin in addition to the literal glob — the change is additive: no
|
|
394
|
+
* existing literal match is removed, so the README's `Read(/var/log/**)`
|
|
395
|
+
* filesystem-root escape hatch keeps working as a literal absolute rule.
|
|
383
396
|
*/
|
|
384
397
|
export function readToolPermissionPatterns(toolName, kind, projectDir, globalSettingsPath) {
|
|
385
398
|
const result = [];
|
|
386
|
-
const extractGlobs = (path) => {
|
|
399
|
+
const extractGlobs = (path, baseDir) => {
|
|
387
400
|
let raw;
|
|
388
401
|
try {
|
|
389
402
|
raw = readFileSync(path, "utf-8");
|
|
@@ -408,15 +421,22 @@ export function readToolPermissionPatterns(toolName, kind, projectDir, globalSet
|
|
|
408
421
|
const tp = parseToolPattern(entry);
|
|
409
422
|
if (tp && tp.tool === toolName) {
|
|
410
423
|
globs.push(tp.glob);
|
|
424
|
+
// `/{path}` settings-source anchor: emit the settings-file-relative
|
|
425
|
+
// resolved twin alongside the literal glob (additive — the literal
|
|
426
|
+
// stays so filesystem-root rules like `Read(/var/log/**)` keep
|
|
427
|
+
// working). `//path` is the filesystem-root anchor and gets no twin.
|
|
428
|
+
if (baseDir && tp.glob.startsWith("/") && !tp.glob.startsWith("//")) {
|
|
429
|
+
globs.push(resolve(baseDir, "." + tp.glob));
|
|
430
|
+
}
|
|
411
431
|
}
|
|
412
432
|
}
|
|
413
433
|
return globs;
|
|
414
434
|
};
|
|
415
435
|
if (projectDir) {
|
|
416
|
-
const localGlobs = extractGlobs(resolve(projectDir, ".claude", "settings.local.json"));
|
|
436
|
+
const localGlobs = extractGlobs(resolve(projectDir, ".claude", "settings.local.json"), projectDir);
|
|
417
437
|
if (localGlobs !== null)
|
|
418
438
|
result.push(localGlobs);
|
|
419
|
-
const sharedGlobs = extractGlobs(resolve(projectDir, ".claude", "settings.json"));
|
|
439
|
+
const sharedGlobs = extractGlobs(resolve(projectDir, ".claude", "settings.json"), projectDir);
|
|
420
440
|
if (sharedGlobs !== null)
|
|
421
441
|
result.push(sharedGlobs);
|
|
422
442
|
}
|
|
@@ -428,7 +448,7 @@ export function readToolPermissionPatterns(toolName, kind, projectDir, globalSet
|
|
|
428
448
|
? [globalSettingsPath]
|
|
429
449
|
: resolveAdapterGlobalSettingsPaths();
|
|
430
450
|
for (const globalPath of globalPaths) {
|
|
431
|
-
const globalGlobs = extractGlobs(globalPath);
|
|
451
|
+
const globalGlobs = extractGlobs(globalPath, dirname(globalPath));
|
|
432
452
|
if (globalGlobs !== null)
|
|
433
453
|
result.push(globalGlobs);
|
|
434
454
|
}
|
|
@@ -510,22 +530,88 @@ export function evaluateCommandDenyOnly(command, policies, caseInsensitive = pro
|
|
|
510
530
|
// ==============================================================================
|
|
511
531
|
// File Path Evaluation
|
|
512
532
|
// ==============================================================================
|
|
533
|
+
/**
|
|
534
|
+
* Expand a leading `~` / `~/` (or `~\` on Windows) to the user's home
|
|
535
|
+
* directory, mirroring Claude Code's documented `~/path` rule anchor.
|
|
536
|
+
* `~user/...` is intentionally left unchanged (needs a passwd lookup that is
|
|
537
|
+
* not portable). Returns the input unchanged when there is nothing to expand.
|
|
538
|
+
*/
|
|
539
|
+
function expandHomeTilde(path) {
|
|
540
|
+
if (path === "~")
|
|
541
|
+
return homedir();
|
|
542
|
+
if (path.startsWith("~/") || path.startsWith("~\\"))
|
|
543
|
+
return homedir() + path.slice(1);
|
|
544
|
+
return path;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Claude Code's `//path` rule anchor means "absolute from the filesystem
|
|
548
|
+
* root". Collapse the leading double slash to one so the pattern can match
|
|
549
|
+
* the absolute candidates this module produces (`path.resolve` never emits a
|
|
550
|
+
* leading `//`). Patterns without the anchor are returned unchanged.
|
|
551
|
+
*/
|
|
552
|
+
function normalizeRuleAnchor(glob) {
|
|
553
|
+
return glob.startsWith("//") ? glob.slice(1) : glob;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Best-effort symlink-resolved twin of a permission glob. Only the longest
|
|
557
|
+
* static prefix (path segments before the first `*`/`?` segment) is passed
|
|
558
|
+
* through `realpathSync`; glob segments are re-appended untouched. Returns
|
|
559
|
+
* null when there is no static prefix or it does not exist, so callers can
|
|
560
|
+
* simply fall back to the literal pattern.
|
|
561
|
+
*/
|
|
562
|
+
function canonicalizeGlob(glob) {
|
|
563
|
+
const segments = glob.split(/[\\/]/);
|
|
564
|
+
let cut = segments.findIndex((s) => s.includes("*") || s.includes("?"));
|
|
565
|
+
if (cut === -1)
|
|
566
|
+
cut = segments.length;
|
|
567
|
+
if (cut === 0)
|
|
568
|
+
return null;
|
|
569
|
+
const prefix = segments.slice(0, cut).join(sep);
|
|
570
|
+
if (prefix.length === 0)
|
|
571
|
+
return null;
|
|
572
|
+
try {
|
|
573
|
+
return [realpathSync(prefix), ...segments.slice(cut)].join(sep);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
513
579
|
/**
|
|
514
580
|
* Check if a file path should be denied based on deny globs.
|
|
515
581
|
*
|
|
516
582
|
* Normalizes backslashes to forward slashes before matching so that
|
|
517
583
|
* Windows paths work with Unix-style glob patterns.
|
|
518
584
|
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
585
|
+
* Rule and file path anchors are expanded before matching:
|
|
586
|
+
* - A leading `~` / `~/` in EITHER the rule glob or the file path is
|
|
587
|
+
* expanded to the user's home directory (Claude Code's `~/path`
|
|
588
|
+
* anchor). `~user/...` is left untouched.
|
|
589
|
+
* - A rule glob beginning with `//` is treated as "absolute from the
|
|
590
|
+
* filesystem root" and has the leading double slash collapsed to one,
|
|
591
|
+
* matching the absolute candidates this module produces. The literal
|
|
592
|
+
* double-slash form is preserved as an additional variant so Windows UNC
|
|
593
|
+
* rules (e.g. `//server/share/**`) still match UNC candidates, which
|
|
594
|
+
* normalize to `//server/share/...`.
|
|
595
|
+
*
|
|
596
|
+
* Symlink resolution is mirrored on both sides. For each rule glob the
|
|
597
|
+
* matcher tests its literal variant AND a best-effort `realpathSync`'d
|
|
598
|
+
* variant (only the static prefix, before the first glob segment, is
|
|
599
|
+
* canonicalized). Against those it tests every file candidate: the raw
|
|
600
|
+
* input, the tilde-expanded input, the path resolved against `projectRoot`
|
|
601
|
+
* (or `process.cwd()` when no project root is supplied), and — when the
|
|
602
|
+
* file exists — its canonical form. A deny match therefore fires if ANY
|
|
603
|
+
* literal/canonical rule variant matches ANY literal/lexical/canonical file
|
|
604
|
+
* candidate, closing the CVE-2025-59829 symlink deny-bypass class.
|
|
605
|
+
*
|
|
606
|
+
* Anchoring the resolved candidate to `projectRoot` (falling back to cwd)
|
|
607
|
+
* means absolute deny rules still match relative `..` traversal even when
|
|
608
|
+
* the caller does not know the project root.
|
|
523
609
|
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
610
|
+
* Conversely, a RELATIVE rule glob (no leading `/`, `//`, or `~`) is anchored
|
|
611
|
+
* to the current directory at match time — `projectRoot` when supplied, else
|
|
612
|
+
* `process.cwd()` — per the host's "relative to current directory" semantics.
|
|
613
|
+
* The literal glob is kept as well, so both a relative access path and an
|
|
614
|
+
* absolute one are matched.
|
|
529
615
|
*
|
|
530
616
|
* realpath is best-effort: if the file does not exist yet (ENOENT)
|
|
531
617
|
* or the syscall fails for any reason, the lexical resolved form is
|
|
@@ -534,32 +620,59 @@ export function evaluateCommandDenyOnly(command, policies, caseInsensitive = pro
|
|
|
534
620
|
*/
|
|
535
621
|
export function evaluateFilePath(filePath, denyGlobs, caseInsensitive = process.platform === "win32" || process.platform === "darwin", projectRoot) {
|
|
536
622
|
const toForward = (path) => path.replace(/\\/g, "/");
|
|
537
|
-
//
|
|
538
|
-
// and the canonical (symlink-resolved) path when the file exists.
|
|
539
|
-
// Deduplicated so absolute inputs and paths that don't cross symlinks
|
|
540
|
-
// don't pay the matching cost multiple times.
|
|
623
|
+
// File-side candidates: raw, tilde-expanded, anchored-resolved, canonical.
|
|
541
624
|
const candidates = new Set();
|
|
542
625
|
candidates.add(toForward(filePath));
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
candidates.add(toForward(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
626
|
+
const expandedFilePath = expandHomeTilde(filePath);
|
|
627
|
+
if (expandedFilePath !== filePath) {
|
|
628
|
+
candidates.add(toForward(expandedFilePath));
|
|
629
|
+
}
|
|
630
|
+
const lexical = resolve(projectRoot ?? process.cwd(), expandedFilePath);
|
|
631
|
+
candidates.add(toForward(lexical));
|
|
632
|
+
try {
|
|
633
|
+
candidates.add(toForward(realpathSync(lexical)));
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// File does not exist yet, or realpath failed — rely on lexical form.
|
|
552
637
|
}
|
|
553
638
|
for (const globs of denyGlobs) {
|
|
554
639
|
for (const glob of globs) {
|
|
640
|
+
// Rule-side variants: the literal glob (`//` intact, so Windows UNC
|
|
641
|
+
// rules still match UNC candidates), its `//`-collapsed root-absolute
|
|
642
|
+
// anchor form, the `~`-expanded form of each, and a best-effort
|
|
643
|
+
// symlink-resolved twin of every variant. Matching all of them against
|
|
644
|
+
// every candidate keeps both sides symmetric.
|
|
645
|
+
const anchors = new Set();
|
|
646
|
+
anchors.add(glob);
|
|
647
|
+
anchors.add(normalizeRuleAnchor(glob));
|
|
648
|
+
const expandedGlob = expandHomeTilde(glob);
|
|
649
|
+
if (expandedGlob !== glob) {
|
|
650
|
+
anchors.add(expandedGlob);
|
|
651
|
+
anchors.add(normalizeRuleAnchor(expandedGlob));
|
|
652
|
+
}
|
|
653
|
+
// Match-time cwd anchor for relative rules (host: "relative to current
|
|
654
|
+
// directory"). `**/...` rules already reached absolute candidates; bare
|
|
655
|
+
// `secret/**`, `.env`, `credentials*` did not without this twin.
|
|
656
|
+
if (!isAbsolute(glob) && !glob.startsWith("~")) {
|
|
657
|
+
anchors.add(resolve(projectRoot ?? process.cwd(), glob));
|
|
658
|
+
}
|
|
659
|
+
const variants = new Set();
|
|
660
|
+
for (const variant of anchors) {
|
|
661
|
+
variants.add(variant);
|
|
662
|
+
const canonical = canonicalizeGlob(variant);
|
|
663
|
+
if (canonical !== null)
|
|
664
|
+
variants.add(canonical);
|
|
665
|
+
}
|
|
555
666
|
// Normalize the glob's path separators the same way candidates were
|
|
556
667
|
// normalized — otherwise a Windows absolute deny rule like
|
|
557
668
|
// `Read(C:\Users\...\secret.env)` parses with literal backslashes that
|
|
558
669
|
// never match a forward-slash candidate.
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
670
|
+
for (const variant of variants) {
|
|
671
|
+
const regex = fileGlobToRegex(toForward(variant), caseInsensitive);
|
|
672
|
+
for (const candidate of candidates) {
|
|
673
|
+
if (regex.test(candidate)) {
|
|
674
|
+
return { denied: true, matchedPattern: glob };
|
|
675
|
+
}
|
|
563
676
|
}
|
|
564
677
|
}
|
|
565
678
|
}
|
|
@@ -589,6 +702,8 @@ export function evaluateFilePath(filePath, denyGlobs, caseInsensitive = process.
|
|
|
589
702
|
*
|
|
590
703
|
* A path equal to the project root itself counts as inside. Comparison is
|
|
591
704
|
* case-insensitive on Windows/macOS to match those filesystems' semantics.
|
|
705
|
+
* A leading `~` in `filePath` is expanded to the user's home directory for
|
|
706
|
+
* consistency with `evaluateFilePath`.
|
|
592
707
|
*
|
|
593
708
|
* Returns `true` when `projectRoot` is falsy (no boundary to enforce) so the
|
|
594
709
|
* caller's fail-open posture is preserved when the root cannot be resolved.
|
|
@@ -597,7 +712,7 @@ export function isPathInsideProject(filePath, projectRoot, caseInsensitive = pro
|
|
|
597
712
|
if (!projectRoot)
|
|
598
713
|
return true;
|
|
599
714
|
const root = resolve(projectRoot);
|
|
600
|
-
const lexical = resolve(projectRoot, filePath);
|
|
715
|
+
const lexical = resolve(projectRoot, expandHomeTilde(filePath));
|
|
601
716
|
const within = (root, candidate) => {
|
|
602
717
|
let a = root;
|
|
603
718
|
let b = candidate;
|
package/build/server.js
CHANGED
|
@@ -670,7 +670,7 @@ function getStore() {
|
|
|
670
670
|
store.setDenyChecker((filePath) => {
|
|
671
671
|
try {
|
|
672
672
|
const denyGlobs = readToolDenyPatterns("Read", storeProjectDir);
|
|
673
|
-
const r = evaluateFilePath(filePath, denyGlobs,
|
|
673
|
+
const r = evaluateFilePath(filePath, denyGlobs, undefined, storeProjectDir);
|
|
674
674
|
return r.denied;
|
|
675
675
|
}
|
|
676
676
|
catch {
|
|
@@ -1139,7 +1139,7 @@ function checkFilePathDenyPolicy(filePath, toolName) {
|
|
|
1139
1139
|
try {
|
|
1140
1140
|
const projectDir = getProjectDir();
|
|
1141
1141
|
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
1142
|
-
const result = evaluateFilePath(filePath, denyGlobs,
|
|
1142
|
+
const result = evaluateFilePath(filePath, denyGlobs, undefined, projectDir);
|
|
1143
1143
|
if (result.denied) {
|
|
1144
1144
|
return trackResponse(toolName, {
|
|
1145
1145
|
content: [{
|
|
@@ -2127,10 +2127,9 @@ EXAMPLE: ctx_index(path: "/path/to/large-spec.md", source: "openapi-v2-spec")`,
|
|
|
2127
2127
|
const store = getStore();
|
|
2128
2128
|
const projectDir = getProjectDir();
|
|
2129
2129
|
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
2130
|
-
const isWin32 = process.platform === "win32";
|
|
2131
2130
|
const perFileDeny = (absPath) => {
|
|
2132
2131
|
try {
|
|
2133
|
-
return evaluateFilePath(absPath, denyGlobs,
|
|
2132
|
+
return evaluateFilePath(absPath, denyGlobs, undefined, projectDir).denied;
|
|
2134
2133
|
}
|
|
2135
2134
|
catch {
|
|
2136
2135
|
return false; // fail-open consistent with checkFilePathDenyPolicy
|