@gotgenes/pi-permission-system 23.0.0 → 23.0.1
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/CHANGELOG.md +12 -0
- package/docs/configuration.md +6 -3
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +4 -12
- package/src/path-normalizer.ts +7 -10
- package/src/wildcard-matcher.ts +39 -14
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [23.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v23.0.0...pi-permission-system-v23.0.1) (2026-07-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** fold separators on both sides of a win32 path match ([50e2ac0](https://github.com/gotgenes/pi-packages/commit/50e2ac0dd66b4b308a676849a09e3fff59e754be)), closes [#653](https://github.com/gotgenes/pi-packages/issues/653)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* **pi-permission-system:** record the symmetric win32 separator fold ([e2eea21](https://github.com/gotgenes/pi-packages/commit/e2eea21e0aac1212f46be9680fb59146b90c68f7)), closes [#653](https://github.com/gotgenes/pi-packages/issues/653)
|
|
19
|
+
|
|
8
20
|
## [23.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v22.0.0...pi-permission-system-v23.0.0) (2026-07-24)
|
|
9
21
|
|
|
10
22
|
|
package/docs/configuration.md
CHANGED
|
@@ -618,15 +618,18 @@ Infrastructure directories include:
|
|
|
618
618
|
Write tools (`write`, `edit`) to infrastructure paths are **not** auto-allowed and still go through the gate.
|
|
619
619
|
|
|
620
620
|
On Windows, path matching for `external_directory`, `path`, and the path-bearing tools is case-insensitive and tolerant of either separator (`\` or `/`), matching the case-insensitive filesystem.
|
|
621
|
-
|
|
622
|
-
|
|
621
|
+
The separator folding applies to the rule pattern **and** to the value it is matched against, so either side may be written with either separator.
|
|
622
|
+
A mixed-case allow override such as `~/AppData/Roaming/npm/node_modules/@earendil-works/pi-coding-agent/*` therefore matches a lowercased, backslash-normalized path value, and a forward-slash rule such as `"/dev/null"` matches a value that is also spelled with forward slashes.
|
|
623
|
+
POSIX matching remains case-sensitive and does not fold separators.
|
|
623
624
|
|
|
624
625
|
#### Git Bash / MSYS paths on Windows
|
|
625
626
|
|
|
626
627
|
On Windows, Pi executes bash commands through Git Bash, so a bash token that looks like a POSIX absolute path carries MSYS mount semantics rather than native `node:path.win32` semantics.
|
|
627
628
|
The `external_directory` and `path` gates interpret bash tokens accordingly (tool-input paths for `read`/`write`/`edit` keep native Windows semantics, since those tools resolve them through Node's filesystem):
|
|
628
629
|
|
|
629
|
-
- The safe device paths (`/dev/null`, `/dev/stdin`, `/dev/stdout`, `/dev/stderr`) are recognized as MSYS devices
|
|
630
|
+
- The safe device paths (`/dev/null`, `/dev/stdin`, `/dev/stdout`, `/dev/stderr`) are recognized as MSYS devices rather than filesystem paths, so they never trigger the `external_directory` gate — the same exclusion that holds on POSIX.
|
|
631
|
+
The cross-cutting `path` surface still governs them on both platforms: if a `path` rule matches the token, it decides.
|
|
632
|
+
A device is therefore allow-listed the way any other path is, written as typed — `path: { "/dev/null": "allow" }`.
|
|
630
633
|
- MSYS drive mounts (`/c/…`, `/d/…`) are translated to their Windows equivalent (`C:\…`), so a project file referenced through a mount is matched against its real Windows path and an in-CWD mount is not flagged.
|
|
631
634
|
- Every other POSIX-absolute token (`/tmp/foo`, `/usr/bin`) has an install-dependent target this extension cannot resolve deterministically (Git Bash mounts `/tmp` to `%TEMP%`, MSYS2 to its own root), so it is treated as an external path matched and displayed exactly as typed, never rewritten to `C:\tmp\foo`.
|
|
632
635
|
|
package/package.json
CHANGED
|
@@ -120,20 +120,12 @@ export class AccessPath {
|
|
|
120
120
|
* unknown (a relative bash token after a non-literal `cd`).
|
|
121
121
|
*
|
|
122
122
|
* Carries no canonical alias and no absolute resolution — `matchValues()` is
|
|
123
|
-
* `[literal
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `matchAliases` supplies extra match-only forms that do not change the
|
|
127
|
-
* display value: a win32 Git Bash POSIX absolute carries a backslash-separated
|
|
128
|
-
* alias so the separator-folding path matcher can match a `/tmp/*` rule (#533).
|
|
123
|
+
* `[literal]` (or `[]` when empty) and `boundaryValue()` is `""` — so no
|
|
124
|
+
* spurious absolute or symlink-resolved rule can match (#393).
|
|
129
125
|
*/
|
|
130
|
-
static forLiteral(
|
|
131
|
-
literal: string,
|
|
132
|
-
matchAliases: readonly string[] = [],
|
|
133
|
-
): AccessPath {
|
|
126
|
+
static forLiteral(literal: string): AccessPath {
|
|
134
127
|
if (!literal) return new AccessPath("", [], "");
|
|
135
|
-
|
|
136
|
-
return new AccessPath(literal, aliases, "");
|
|
128
|
+
return new AccessPath(literal, [literal], "");
|
|
137
129
|
}
|
|
138
130
|
|
|
139
131
|
/**
|
package/src/path-normalizer.ts
CHANGED
|
@@ -62,8 +62,8 @@ export class PathNormalizer {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** Build a literal-only AccessPath (unknown base after a non-literal `cd`). */
|
|
65
|
-
forLiteral(literal: string
|
|
66
|
-
return AccessPath.forLiteral(literal
|
|
65
|
+
forLiteral(literal: string): AccessPath {
|
|
66
|
+
return AccessPath.forLiteral(literal);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -86,17 +86,14 @@ export class PathNormalizer {
|
|
|
86
86
|
return AccessPath.forDevice(token);
|
|
87
87
|
case "drive-mount":
|
|
88
88
|
return this.forPath(shape.windowsPath, options);
|
|
89
|
-
case "posix-absolute":
|
|
89
|
+
case "posix-absolute":
|
|
90
90
|
// A non-mount POSIX absolute (`/tmp`, `/usr`) has an install-dependent
|
|
91
91
|
// Windows target this package cannot know, so it is kept literal: always
|
|
92
92
|
// external, matched and displayed as typed, never fabricated into
|
|
93
|
-
// `c:\tmp` (#533). The win32 path matcher folds
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
const literal = normalizePathPolicyLiteral(token);
|
|
98
|
-
return this.forLiteral(literal, [literal.replaceAll("/", "\\")]);
|
|
99
|
-
}
|
|
93
|
+
// `c:\tmp` (#533). The win32 path matcher folds separators on both the
|
|
94
|
+
// rule and the value (#653), so a natural `/tmp/*` rule matches the
|
|
95
|
+
// as-typed literal directly.
|
|
96
|
+
return this.forLiteral(normalizePathPolicyLiteral(token));
|
|
100
97
|
case "plain":
|
|
101
98
|
return this.forPath(token, options);
|
|
102
99
|
}
|
package/src/wildcard-matcher.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { expandHomePath } from "./expand-home";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* A pattern compiled once for repeated matching.
|
|
5
|
+
*
|
|
6
|
+
* Matching is a method rather than an exposed `RegExp` so that both halves of
|
|
7
|
+
* the {@link WildcardMatchOptions} fold stay together: the compiled regex
|
|
8
|
+
* carries the pattern-side folding, and {@link matches} applies the same
|
|
9
|
+
* folding to the value. A caller holding the raw regex could apply one without
|
|
10
|
+
* the other, which is exactly the asymmetry that made forward-slash path rules
|
|
11
|
+
* inert on Windows (#653).
|
|
12
|
+
*/
|
|
13
|
+
export interface CompiledWildcardPattern<TState> {
|
|
14
|
+
readonly pattern: string;
|
|
15
|
+
readonly state: TState;
|
|
16
|
+
matches(value: string): boolean;
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
export type WildcardPatternMatch<TState> = {
|
|
10
20
|
state: TState;
|
|
@@ -17,8 +27,11 @@ export type WildcardPatternMatch<TState> = {
|
|
|
17
27
|
*
|
|
18
28
|
* - `caseInsensitive` compiles the pattern with the `i` flag so a mixed-case
|
|
19
29
|
* pattern matches a lowercased (canonicalized) path value.
|
|
20
|
-
* - `windowsSeparators` rewrites `/` to `\` in the expanded pattern
|
|
21
|
-
*
|
|
30
|
+
* - `windowsSeparators` rewrites `/` to `\` in both the expanded pattern and
|
|
31
|
+
* the matched value, so two spellings of the same path match regardless of
|
|
32
|
+
* which separator either side was written with. Folding only the pattern
|
|
33
|
+
* leaves every forward-slash value (a Git Bash device, an as-typed literal)
|
|
34
|
+
* unmatchable (#653).
|
|
22
35
|
*/
|
|
23
36
|
export interface WildcardMatchOptions {
|
|
24
37
|
caseInsensitive?: boolean;
|
|
@@ -34,10 +47,7 @@ export function compileWildcardPattern<TState>(
|
|
|
34
47
|
state: TState,
|
|
35
48
|
options?: WildcardMatchOptions,
|
|
36
49
|
): CompiledWildcardPattern<TState> {
|
|
37
|
-
|
|
38
|
-
if (options?.windowsSeparators) {
|
|
39
|
-
expanded = expanded.replaceAll("/", "\\");
|
|
40
|
-
}
|
|
50
|
+
const expanded = foldSeparators(expandHomePath(pattern), options);
|
|
41
51
|
let escaped = expanded
|
|
42
52
|
.split("*")
|
|
43
53
|
.map((part) => escapeRegExp(part).replaceAll("\\?", "."))
|
|
@@ -50,10 +60,15 @@ export function compileWildcardPattern<TState>(
|
|
|
50
60
|
escaped = `${escaped.slice(0, -3)}( .*)?`;
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
const regex = new RegExp(
|
|
64
|
+
`^${escaped}$`,
|
|
65
|
+
options?.caseInsensitive ? "si" : "s",
|
|
66
|
+
);
|
|
67
|
+
|
|
53
68
|
return {
|
|
54
69
|
pattern,
|
|
55
70
|
state,
|
|
56
|
-
|
|
71
|
+
matches: (value) => regex.test(foldSeparators(value, options)),
|
|
57
72
|
};
|
|
58
73
|
}
|
|
59
74
|
|
|
@@ -75,7 +90,7 @@ export function findCompiledWildcardMatch<TState>(
|
|
|
75
90
|
patterns: readonly CompiledWildcardPattern<TState>[],
|
|
76
91
|
name: string,
|
|
77
92
|
): WildcardPatternMatch<TState> | null {
|
|
78
|
-
const match = patterns.findLast((p) => p.
|
|
93
|
+
const match = patterns.findLast((p) => p.matches(name));
|
|
79
94
|
if (match === undefined) return null;
|
|
80
95
|
return {
|
|
81
96
|
state: match.state,
|
|
@@ -95,7 +110,17 @@ export function wildcardMatch(
|
|
|
95
110
|
value: string,
|
|
96
111
|
options?: WildcardMatchOptions,
|
|
97
112
|
): boolean {
|
|
98
|
-
return compileWildcardPattern(pattern, null, options).
|
|
113
|
+
return compileWildcardPattern(pattern, null, options).matches(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Apply the `windowsSeparators` half of the fold to one operand.
|
|
118
|
+
*
|
|
119
|
+
* Called for the pattern at compile time and for the value at match time —
|
|
120
|
+
* the fold is an equivalence relation, so both sides must pass through it.
|
|
121
|
+
*/
|
|
122
|
+
function foldSeparators(value: string, options?: WildcardMatchOptions): string {
|
|
123
|
+
return options?.windowsSeparators ? value.replaceAll("/", "\\") : value;
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
export function findCompiledWildcardMatchForNames<TState>(
|