@aliou/pi-guardrails 0.9.0 → 0.9.2

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-guardrails",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -29,7 +29,7 @@
29
29
  "README.md"
30
30
  ],
31
31
  "dependencies": {
32
- "@aliou/pi-utils-settings": "^0.8.0",
32
+ "@aliou/pi-utils-settings": "^0.10.0",
33
33
  "@aliou/sh": "^0.1.0"
34
34
  },
35
35
  "peerDependencies": {
@@ -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 { type CompiledPattern, compileFilePatterns } from "../utils/matching";
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
- if (matchesAnyPolicyPattern(file, rules)) {
147
- targets.add(file);
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
- if (matchesAnyPolicyPattern(file, rules)) {
186
- targets.add(file);
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
- target,
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: ${target} (${effective.ruleId})`,
298
+ `Blocked ${toolName} on protected file: ${normalizedTarget} (${effective.ruleId})`,
280
299
  "warning",
281
300
  );
282
301
 
283
- const reason = effective.blockMessage.replace("{file}", target);
302
+ const reason = effective.blockMessage.replace("{file}", normalizedTarget);
284
303
 
285
304
  emitBlocked(pi, {
286
305
  feature: "policies",
@@ -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
- * Convert a glob pattern to a regex.
21
- * `*` matches any non-`/` chars, `?` matches a single char.
22
- * The rest is escaped.
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 globToRegex(glob: string): RegExp {
25
- let regex = "";
26
- for (const ch of glob) {
27
- switch (ch) {
28
- case "*":
29
- regex += "[^/]*";
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 against the basename of the path.
58
- * regex: true -> full regex (case-insensitive) against the full path.
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 { test: (input) => re.test(input), source: config };
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 re = globToRegex(config.pattern);
57
+ const matchFullPath = config.pattern.includes("/");
58
+
74
59
  return {
75
60
  test: (input) => {
76
- // Match against basename
77
- const basename = input.split("/").pop() ?? input;
78
- return re.test(basename);
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
  };