@aliou/pi-guardrails 0.9.0 → 0.9.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/README.md +1 -1
- package/package.json +1 -1
- package/src/hooks/policies.ts +29 -10
- package/src/utils/matching.ts +27 -39
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ All fields optional. Missing fields use defaults.
|
|
|
93
93
|
Each rule has:
|
|
94
94
|
|
|
95
95
|
- `id`: stable identifier used for overrides across scopes.
|
|
96
|
-
- `patterns`: files to match (glob by default, regex if `regex: true`).
|
|
96
|
+
- `patterns`: files to match (glob by default, regex if `regex: true`). Glob semantics: patterns containing `/` match the full relative path; patterns without `/` match basename only.
|
|
97
97
|
- `allowedPatterns`: exceptions.
|
|
98
98
|
- `protection`:
|
|
99
99
|
- `noAccess`: block `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`
|
package/package.json
CHANGED
package/src/hooks/policies.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { parse } from "@aliou/sh";
|
|
4
4
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import type { PolicyRule, Protection, ResolvedConfig } from "../config";
|
|
6
6
|
import { emitBlocked } from "../utils/events";
|
|
7
7
|
import { expandGlob, hasGlobChars } from "../utils/glob-expander";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type CompiledPattern,
|
|
10
|
+
compileFilePatterns,
|
|
11
|
+
normalizeFilePath,
|
|
12
|
+
} from "../utils/matching";
|
|
9
13
|
import { walkCommands, wordToString } from "../utils/shell-utils";
|
|
10
14
|
import { pendingWarnings } from "../utils/warnings";
|
|
11
15
|
|
|
@@ -113,6 +117,16 @@ function maybePathLike(token: string): boolean {
|
|
|
113
117
|
);
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
function normalizeTargetForPolicy(filePath: string, cwd: string): string {
|
|
121
|
+
const absolute = resolve(cwd, filePath);
|
|
122
|
+
const rel = relative(cwd, absolute);
|
|
123
|
+
|
|
124
|
+
const candidate =
|
|
125
|
+
rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : absolute;
|
|
126
|
+
|
|
127
|
+
return normalizeFilePath(candidate);
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
function matchesAnyPolicyPattern(
|
|
117
131
|
filePath: string,
|
|
118
132
|
rules: CompiledRule[],
|
|
@@ -135,6 +149,7 @@ async function expandCandidate(candidate: string): Promise<string[]> {
|
|
|
135
149
|
async function extractBashFileTargets(
|
|
136
150
|
command: string,
|
|
137
151
|
rules: CompiledRule[],
|
|
152
|
+
cwd: string,
|
|
138
153
|
): Promise<string[]> {
|
|
139
154
|
const targets = new Set<string>();
|
|
140
155
|
|
|
@@ -143,8 +158,9 @@ async function extractBashFileTargets(
|
|
|
143
158
|
|
|
144
159
|
const expanded = await expandCandidate(candidate);
|
|
145
160
|
for (const file of expanded) {
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
const normalized = normalizeTargetForPolicy(file, cwd);
|
|
162
|
+
if (matchesAnyPolicyPattern(normalized, rules)) {
|
|
163
|
+
targets.add(normalized);
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
166
|
};
|
|
@@ -182,8 +198,9 @@ async function extractBashFileTargets(
|
|
|
182
198
|
|
|
183
199
|
const expanded = await expandCandidate(token);
|
|
184
200
|
for (const file of expanded) {
|
|
185
|
-
|
|
186
|
-
|
|
201
|
+
const normalized = normalizeTargetForPolicy(file, cwd);
|
|
202
|
+
if (matchesAnyPolicyPattern(normalized, rules)) {
|
|
203
|
+
targets.add(normalized);
|
|
187
204
|
}
|
|
188
205
|
}
|
|
189
206
|
}
|
|
@@ -259,14 +276,16 @@ export function setupPoliciesHook(pi: ExtensionAPI, config: ResolvedConfig) {
|
|
|
259
276
|
targets = extractPathTarget(event.input);
|
|
260
277
|
} else if (toolName === "bash") {
|
|
261
278
|
const command = String(event.input.command ?? "");
|
|
262
|
-
targets = await extractBashFileTargets(command, compiledRules);
|
|
279
|
+
targets = await extractBashFileTargets(command, compiledRules, ctx.cwd);
|
|
263
280
|
} else {
|
|
264
281
|
return;
|
|
265
282
|
}
|
|
266
283
|
|
|
267
284
|
for (const target of targets) {
|
|
285
|
+
const normalizedTarget = normalizeTargetForPolicy(target, ctx.cwd);
|
|
286
|
+
|
|
268
287
|
const effective = await getEffectiveProtection(
|
|
269
|
-
|
|
288
|
+
normalizedTarget,
|
|
270
289
|
compiledRules,
|
|
271
290
|
ctx.cwd,
|
|
272
291
|
);
|
|
@@ -276,11 +295,11 @@ export function setupPoliciesHook(pi: ExtensionAPI, config: ResolvedConfig) {
|
|
|
276
295
|
if (!blockedTools.has(toolName)) continue;
|
|
277
296
|
|
|
278
297
|
ctx.ui.notify(
|
|
279
|
-
`Blocked ${toolName} on protected file: ${
|
|
298
|
+
`Blocked ${toolName} on protected file: ${normalizedTarget} (${effective.ruleId})`,
|
|
280
299
|
"warning",
|
|
281
300
|
);
|
|
282
301
|
|
|
283
|
-
const reason = effective.blockMessage.replace("{file}",
|
|
302
|
+
const reason = effective.blockMessage.replace("{file}", normalizedTarget);
|
|
284
303
|
|
|
285
304
|
emitBlocked(pi, {
|
|
286
305
|
feature: "policies",
|
package/src/utils/matching.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Both support `regex: true` for full regex matching.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { matchesGlob } from "node:path";
|
|
11
12
|
import type { PatternConfig } from "../config";
|
|
12
13
|
import { pendingWarnings } from "./warnings";
|
|
13
14
|
|
|
@@ -17,51 +18,34 @@ export interface CompiledPattern {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Normalize file paths before matching.
|
|
22
|
+
* - Use forward slashes for cross-platform consistency.
|
|
23
|
+
* - Drop leading "./" segments.
|
|
24
|
+
* - Collapse duplicate slashes.
|
|
23
25
|
*/
|
|
24
|
-
export function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
break;
|
|
31
|
-
case "?":
|
|
32
|
-
regex += "[^/]";
|
|
33
|
-
break;
|
|
34
|
-
case ".":
|
|
35
|
-
case "(":
|
|
36
|
-
case ")":
|
|
37
|
-
case "+":
|
|
38
|
-
case "^":
|
|
39
|
-
case "$":
|
|
40
|
-
case "{":
|
|
41
|
-
case "}":
|
|
42
|
-
case "|":
|
|
43
|
-
case "\\":
|
|
44
|
-
case "[":
|
|
45
|
-
case "]":
|
|
46
|
-
regex += `\\${ch}`;
|
|
47
|
-
break;
|
|
48
|
-
default:
|
|
49
|
-
regex += ch;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return new RegExp(`^${regex}$`, "i");
|
|
26
|
+
export function normalizeFilePath(input: string): string {
|
|
27
|
+
const normalized = input
|
|
28
|
+
.replaceAll("\\", "/")
|
|
29
|
+
.replace(/^(?:\.\/)+/, "")
|
|
30
|
+
.replace(/\/{2,}/g, "/");
|
|
31
|
+
return normalized;
|
|
53
32
|
}
|
|
54
33
|
|
|
55
34
|
/**
|
|
56
35
|
* Compile a single pattern for file-context matching.
|
|
57
|
-
* Default: glob
|
|
58
|
-
*
|
|
36
|
+
* Default: glob matching.
|
|
37
|
+
* - If pattern includes `/`, match full normalized relative path.
|
|
38
|
+
* - Otherwise, match basename only (backward compatible).
|
|
39
|
+
* regex: true -> full regex (case-insensitive) against normalized path.
|
|
59
40
|
*/
|
|
60
41
|
export function compileFilePattern(config: PatternConfig): CompiledPattern {
|
|
61
42
|
if (config.regex) {
|
|
62
43
|
try {
|
|
63
44
|
const re = new RegExp(config.pattern, "i");
|
|
64
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
test: (input) => re.test(normalizeFilePath(input)),
|
|
47
|
+
source: config,
|
|
48
|
+
};
|
|
65
49
|
} catch {
|
|
66
50
|
pendingWarnings.push(
|
|
67
51
|
`Invalid regex in guardrails config: ${config.pattern}`,
|
|
@@ -70,12 +54,16 @@ export function compileFilePattern(config: PatternConfig): CompiledPattern {
|
|
|
70
54
|
}
|
|
71
55
|
}
|
|
72
56
|
|
|
73
|
-
const
|
|
57
|
+
const matchFullPath = config.pattern.includes("/");
|
|
58
|
+
|
|
74
59
|
return {
|
|
75
60
|
test: (input) => {
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
61
|
+
const normalized = normalizeFilePath(input);
|
|
62
|
+
const candidate = matchFullPath
|
|
63
|
+
? normalized
|
|
64
|
+
: (normalized.split("/").pop() ?? normalized);
|
|
65
|
+
|
|
66
|
+
return matchesGlob(candidate, config.pattern);
|
|
79
67
|
},
|
|
80
68
|
source: config,
|
|
81
69
|
};
|