@gotgenes/pi-permission-system 7.0.1 → 7.1.0

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 CHANGED
@@ -5,6 +5,20 @@ 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
+ ## [7.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.0.1...pi-permission-system-v7.1.0) (2026-05-22)
9
+
10
+
11
+ ### Features
12
+
13
+ * support glob patterns in piInfrastructureReadPaths ([#122](https://github.com/gotgenes/pi-packages/issues/122)) ([7ebce24](https://github.com/gotgenes/pi-packages/commit/7ebce24ff36f573c3165fdf49e4b470f68d79b69))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * document piInfrastructureReadPaths glob support ([#122](https://github.com/gotgenes/pi-packages/issues/122)) ([94fa688](https://github.com/gotgenes/pi-packages/commit/94fa688f1c41a6cf1d7bb49a3934728741dd1001))
19
+ * fix misleading ** examples and clarify * matches all characters ([00563dc](https://github.com/gotgenes/pi-packages/commit/00563dc90e3e800e0fc1b0f3ad0a9ed8c78f86b7))
20
+ * plan glob support for piInfrastructureReadPaths ([#122](https://github.com/gotgenes/pi-packages/issues/122)) ([1450d8b](https://github.com/gotgenes/pi-packages/commit/1450d8b61529d54c21df49d364d8bbcc1c7e55ec))
21
+
8
22
  ## [7.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.0.0...pi-permission-system-v7.0.1) (2026-05-21)
9
23
 
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "7.0.1",
3
+ "version": "7.1.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,8 +30,8 @@
30
30
  "default": false
31
31
  },
32
32
  "piInfrastructureReadPaths": {
33
- "description": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the external_directory gate. Supports ~ expansion. Directory prefixes only (no globs).",
34
- "markdownDescription": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the `external_directory` gate.\n\nThe extension auto-discovers the global node_modules root (walks up from the extension's install path; falls back to `npm root -g` from a dev checkout), `agentDir`, `agentDir/git`, and project-local `.pi/npm/` and `.pi/git/`. Add entries here for edge cases where auto-discovery is insufficient (e.g. custom `npmCommand` pointing to pnpm).\n\nSupports `~` expansion. Directory prefixes onlyno glob patterns.",
33
+ "description": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the external_directory gate. Supports ~ expansion and wildcard patterns (* and ?).",
34
+ "markdownDescription": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the `external_directory` gate.\n\nThe extension auto-discovers the global node_modules root (walks up from the extension's install path; falls back to `npm root -g` from a dev checkout), `agentDir`, `agentDir/git`, and project-local `.pi/npm/` and `.pi/git/`. Add entries here for edge cases where auto-discovery is insufficient (e.g. custom `npmCommand` pointing to pnpm).\n\nSupports `~`/`$HOME` expansion. Entries may be plain directory prefixes or wildcard patterns using `*` (matches any characters, including `/`) and `?` (matches exactly one character). `**` and `*` are equivalent both cross directory boundaries.",
35
35
  "type": "array",
36
36
  "items": {
37
37
  "type": "string",
package/src/path-utils.ts CHANGED
@@ -2,6 +2,8 @@ import { homedir } from "node:os";
2
2
  import { join, normalize, resolve, sep } from "node:path";
3
3
 
4
4
  import { getNonEmptyString, toRecord } from "./common";
5
+ import { expandHomePath } from "./expand-home";
6
+ import { wildcardMatch } from "./wildcard-matcher";
5
7
 
6
8
  export function normalizePathForComparison(
7
9
  pathValue: string,
@@ -111,6 +113,10 @@ export function isPathOutsideWorkingDirectory(
111
113
  return !isPathWithinDirectory(normalizedPath, normalizedCwd);
112
114
  }
113
115
 
116
+ function containsGlobChars(value: string): boolean {
117
+ return value.includes("*") || value.includes("?");
118
+ }
119
+
114
120
  /**
115
121
  * Returns true if the given tool + normalized path combination qualifies for
116
122
  * automatic allow as a Pi infrastructure read.
@@ -121,7 +127,8 @@ export function isPathOutsideWorkingDirectory(
121
127
  * OR within the project-local Pi package directories
122
128
  * (`<cwd>/.pi/npm/` or `<cwd>/.pi/git/`).
123
129
  *
124
- * `infrastructureDirs` should contain pre-expanded absolute paths (no `~`).
130
+ * `infrastructureDirs` entries may be absolute paths or patterns containing
131
+ * `~`/`$HOME` (expanded at call time) or glob characters (`*`, `?`).
125
132
  * Project-local paths are computed fresh from `cwd` on each call so they
126
133
  * follow working-directory changes without a runtime rebuild.
127
134
  */
@@ -136,8 +143,11 @@ export function isPiInfrastructureRead(
136
143
  }
137
144
 
138
145
  for (const dir of infrastructureDirs) {
139
- if (isPathWithinDirectory(normalizedPath, dir)) {
140
- return true;
146
+ if (containsGlobChars(dir)) {
147
+ if (wildcardMatch(dir, normalizedPath)) return true;
148
+ } else {
149
+ if (isPathWithinDirectory(normalizedPath, expandHomePath(dir)))
150
+ return true;
141
151
  }
142
152
  }
143
153
 
@@ -290,4 +290,40 @@ describe("isPiInfrastructureRead", () => {
290
290
  false,
291
291
  );
292
292
  });
293
+
294
+ // ── glob patterns ─────────────────────────────────────────────────
295
+
296
+ test("glob entry matches a versioned path", () => {
297
+ expect(
298
+ isPiInfrastructureRead(
299
+ "read",
300
+ "/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
301
+ ["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
302
+ cwd,
303
+ ),
304
+ ).toBe(true);
305
+ });
306
+
307
+ test("glob entry does not match an unrelated path", () => {
308
+ expect(
309
+ isPiInfrastructureRead(
310
+ "read",
311
+ "/etc/passwd",
312
+ ["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
313
+ cwd,
314
+ ),
315
+ ).toBe(false);
316
+ });
317
+
318
+ test("plain entry with ~ expands to home dir for matching", () => {
319
+ // node:os is mocked: homedir() returns "/mock/home"
320
+ expect(
321
+ isPiInfrastructureRead(
322
+ "read",
323
+ "/mock/home/.pi/agent/config.json",
324
+ ["~/.pi/agent"],
325
+ cwd,
326
+ ),
327
+ ).toBe(true);
328
+ });
293
329
  });
@@ -1,3 +1,4 @@
1
+ import { homedir } from "node:os";
1
2
  import { join } from "node:path";
2
3
  import { beforeEach, describe, expect, test, vi } from "vitest";
3
4
 
@@ -261,3 +262,108 @@ describe("isPiInfrastructureRead", () => {
261
262
  ).toBe(true);
262
263
  });
263
264
  });
265
+
266
+ // ── isPiInfrastructureRead — glob patterns ─────────────────────────────────
267
+
268
+ describe("isPiInfrastructureRead with glob patterns", () => {
269
+ test("glob entry matches a versioned nested path", () => {
270
+ expect(
271
+ isPiInfrastructureRead(
272
+ "read",
273
+ "/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
274
+ ["/opt/homebrew/*/@earendil-works/pi-coding-agent/*"],
275
+ CWD,
276
+ ),
277
+ ).toBe(true);
278
+ });
279
+
280
+ test("** behaves the same as * (matches across path separators)", () => {
281
+ expect(
282
+ isPiInfrastructureRead(
283
+ "read",
284
+ "/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
285
+ ["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
286
+ CWD,
287
+ ),
288
+ ).toBe(true);
289
+ });
290
+
291
+ test("glob entry does not match an unrelated path", () => {
292
+ expect(
293
+ isPiInfrastructureRead(
294
+ "read",
295
+ "/etc/passwd",
296
+ ["/opt/homebrew/*/@earendil-works/pi-coding-agent/*"],
297
+ CWD,
298
+ ),
299
+ ).toBe(false);
300
+ });
301
+
302
+ test("? matches exactly one character", () => {
303
+ expect(
304
+ isPiInfrastructureRead(
305
+ "read",
306
+ "/opt/homebrew/X/file.md",
307
+ ["/opt/homebrew/?/file.md"],
308
+ CWD,
309
+ ),
310
+ ).toBe(true);
311
+ });
312
+
313
+ test("? does not match multiple characters", () => {
314
+ expect(
315
+ isPiInfrastructureRead(
316
+ "read",
317
+ "/opt/homebrew/abc/file.md",
318
+ ["/opt/homebrew/?/file.md"],
319
+ CWD,
320
+ ),
321
+ ).toBe(false);
322
+ });
323
+
324
+ test("mixed array of plain dirs and glob patterns — both branches work", () => {
325
+ const dirs = [
326
+ "/home/user/.pi/agent",
327
+ "/opt/homebrew/*/@earendil-works/pi-coding-agent/*",
328
+ ];
329
+ expect(
330
+ isPiInfrastructureRead(
331
+ "read",
332
+ "/home/user/.pi/agent/config.json",
333
+ dirs,
334
+ CWD,
335
+ ),
336
+ ).toBe(true);
337
+ expect(
338
+ isPiInfrastructureRead(
339
+ "read",
340
+ "/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
341
+ dirs,
342
+ CWD,
343
+ ),
344
+ ).toBe(true);
345
+ });
346
+
347
+ test("plain entry with ~ prefix matches after home expansion", () => {
348
+ const home = homedir();
349
+ expect(
350
+ isPiInfrastructureRead(
351
+ "read",
352
+ `${home}/.pi/agent/config.json`,
353
+ ["~/.pi/agent"],
354
+ CWD,
355
+ ),
356
+ ).toBe(true);
357
+ });
358
+
359
+ test("write tool with a glob-matching path is still rejected", () => {
360
+ expect(
361
+ isPiInfrastructureRead(
362
+ "write",
363
+ "/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
364
+ ["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
365
+ CWD,
366
+ ),
367
+ ).toBe(false);
368
+ });
369
+ });