@gotgenes/pi-permission-system 4.0.1 → 4.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 +13 -0
- package/package.json +5 -1
- package/src/external-directory.ts +9 -14
- package/tests/bash-external-directory.test.ts +76 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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
|
+
## [4.1.0](https://github.com/gotgenes/pi-permission-system/compare/v4.0.1...v4.1.0) (2026-05-04)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* replace regex tokenizer with shell-quote ([#72](https://github.com/gotgenes/pi-permission-system/issues/72)) ([1568992](https://github.com/gotgenes/pi-permission-system/commit/1568992fb82b45c84b10e3cc9c50777f42d30dfa))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* plan shell-quote tokenizer migration ([#72](https://github.com/gotgenes/pi-permission-system/issues/72)) ([0390e06](https://github.com/gotgenes/pi-permission-system/commit/0390e06de5ca0e5cc79e438f2a94db610ca90289))
|
|
19
|
+
* **retro:** add retro notes for issue [#68](https://github.com/gotgenes/pi-permission-system/issues/68) ([4775453](https://github.com/gotgenes/pi-permission-system/commit/47754539806fbe15f739ea0eaa4a100a69db82ef))
|
|
20
|
+
|
|
8
21
|
## [4.0.1](https://github.com/gotgenes/pi-permission-system/compare/v4.0.0...v4.0.1) (2026-05-04)
|
|
9
22
|
|
|
10
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-permission-system",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -56,10 +56,14 @@
|
|
|
56
56
|
"@mariozechner/pi-coding-agent": "^0.72.1",
|
|
57
57
|
"@mariozechner/pi-tui": "^0.72.1",
|
|
58
58
|
"@types/node": "^25.6.0",
|
|
59
|
+
"@types/shell-quote": "^1.7.5",
|
|
59
60
|
"markdownlint-cli2": "^0.22.1",
|
|
60
61
|
"typescript": "6.0.3",
|
|
61
62
|
"vitest": "^4.1.5"
|
|
62
63
|
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"shell-quote": "^1.8.3"
|
|
66
|
+
},
|
|
63
67
|
"scripts": {
|
|
64
68
|
"build": "tsc -p tsconfig.json",
|
|
65
69
|
"lint": "biome check .",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, normalize, resolve, sep } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { parse } from "shell-quote";
|
|
5
|
+
|
|
4
6
|
import { getNonEmptyString, toRecord } from "./common";
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -196,27 +198,20 @@ function classifyTokenAsPathCandidate(token: string): string | null {
|
|
|
196
198
|
return null;
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
/**
|
|
200
|
-
* Strips content inside single and double quotes from a command string.
|
|
201
|
-
* Replaces quoted segments with empty string so paths inside quotes are not tokenized.
|
|
202
|
-
* This is a simple regex approach — it cannot handle escaped quotes within strings.
|
|
203
|
-
*/
|
|
204
|
-
function stripQuotedStrings(command: string): string {
|
|
205
|
-
return command.replace(/"[^"]*"/g, "").replace(/'[^']*'/g, "");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
201
|
/**
|
|
209
202
|
* Extracts paths from a bash command string that resolve outside CWD.
|
|
210
|
-
*
|
|
203
|
+
* Uses shell-quote to tokenize so that quoted strings, operators, and comments
|
|
204
|
+
* are handled correctly, eliminating false positives from the old regex approach.
|
|
211
205
|
*/
|
|
212
206
|
export function extractExternalPathsFromBashCommand(
|
|
213
207
|
command: string,
|
|
214
208
|
cwd: string,
|
|
215
209
|
): string[] {
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
210
|
+
// shell-quote parse() returns strings (resolved arguments), operator objects,
|
|
211
|
+
// glob objects, and comment objects. Only string entries are path candidates.
|
|
212
|
+
const tokens = parse(command).filter(
|
|
213
|
+
(entry): entry is string => typeof entry === "string",
|
|
214
|
+
);
|
|
220
215
|
const seen = new Set<string>();
|
|
221
216
|
const externalPaths: string[] = [];
|
|
222
217
|
|
|
@@ -213,8 +213,9 @@ describe("extractExternalPathsFromBashCommand", () => {
|
|
|
213
213
|
expect(result).toContain("/etc/hosts");
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
test
|
|
217
|
-
//
|
|
216
|
+
test("does not flag path when adjacent quoted segments form one word", () => {
|
|
217
|
+
// shell-quote concatenates adjacent quoted/unquoted segments into one word:
|
|
218
|
+
// '"path is "/etc/hosts""' → 'path is /etc/hosts' (one token, not a path candidate).
|
|
218
219
|
const result = extractExternalPathsFromBashCommand(
|
|
219
220
|
'echo "path is "/etc/hosts""',
|
|
220
221
|
cwd,
|
|
@@ -300,6 +301,15 @@ describe("extractExternalPathsFromBashCommand", () => {
|
|
|
300
301
|
expect(result).toHaveLength(0);
|
|
301
302
|
});
|
|
302
303
|
|
|
304
|
+
test("bare-slash guard is still needed: shell-quote emits / as a string token", () => {
|
|
305
|
+
// shell-quote.parse('echo /') returns ['echo', '/'] — the bare slash IS a string
|
|
306
|
+
// token, not an operator. classifyTokenAsPathCandidate must still reject it.
|
|
307
|
+
// This test documents that the /^\/+$/ guard remains a necessary
|
|
308
|
+
// defense-in-depth layer even with shell-quote as the tokenizer.
|
|
309
|
+
const result = extractExternalPathsFromBashCommand("echo /", cwd);
|
|
310
|
+
expect(result).toHaveLength(0);
|
|
311
|
+
});
|
|
312
|
+
|
|
303
313
|
test("still flags real external path alongside //", () => {
|
|
304
314
|
const result = extractExternalPathsFromBashCommand(
|
|
305
315
|
"cat /etc/hosts; echo //",
|
|
@@ -310,6 +320,70 @@ describe("extractExternalPathsFromBashCommand", () => {
|
|
|
310
320
|
});
|
|
311
321
|
});
|
|
312
322
|
|
|
323
|
+
describe("node -e and multi-line commands", () => {
|
|
324
|
+
test("does not flag path inside single-quoted string in node -e argument", () => {
|
|
325
|
+
const result = extractExternalPathsFromBashCommand(
|
|
326
|
+
"node -e \"const p = '/etc/hosts'; console.log(p);\"",
|
|
327
|
+
cwd,
|
|
328
|
+
);
|
|
329
|
+
expect(result).toHaveLength(0);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("does not flag path inside multi-line node -e argument", () => {
|
|
333
|
+
// Actual newlines inside the double-quoted -e argument.
|
|
334
|
+
const cmd =
|
|
335
|
+
"node -e \"\nimport('x').then(() => {\n console.log('/etc/hosts');\n});\n\"";
|
|
336
|
+
const result = extractExternalPathsFromBashCommand(cmd, cwd);
|
|
337
|
+
expect(result).toHaveLength(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("does not flag path that appears after escaped quote in multi-line node -e argument", () => {
|
|
341
|
+
// This is the shape of the command that triggered a prompt during dog-fooding.
|
|
342
|
+
// The outer \"...\" arg contains both actual newlines and \\" escape sequences,
|
|
343
|
+
// with /etc/hosts appearing after a \\" boundary.
|
|
344
|
+
const cmd = [
|
|
345
|
+
'node -e "',
|
|
346
|
+
"import('shell-quote').then(({ parse }) => {",
|
|
347
|
+
" const cmd = \\\"cat << 'EOF'\\n/etc/hosts\\nsome content\\nEOF\\\";",
|
|
348
|
+
" console.log(JSON.stringify(parse(cmd)));",
|
|
349
|
+
"});",
|
|
350
|
+
'"',
|
|
351
|
+
].join("\n");
|
|
352
|
+
const result = extractExternalPathsFromBashCommand(cmd, cwd);
|
|
353
|
+
expect(result).toHaveLength(0);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("shell-quote tokenizer edge cases", () => {
|
|
358
|
+
test("does not flag path inside string when escaped quote is present", () => {
|
|
359
|
+
// stripQuotedStrings regex breaks at \" — content after it leaks into the token stream.
|
|
360
|
+
// shell-quote correctly parses the escaped quote and keeps the path inside the string.
|
|
361
|
+
const result = extractExternalPathsFromBashCommand(
|
|
362
|
+
'git commit -m "fix: update \\"the /etc/hosts\\" handler"',
|
|
363
|
+
cwd,
|
|
364
|
+
);
|
|
365
|
+
expect(result).toHaveLength(0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("does not flag path appearing only in a shell comment", () => {
|
|
369
|
+
const result = extractExternalPathsFromBashCommand(
|
|
370
|
+
"echo hello # /etc/shadow",
|
|
371
|
+
cwd,
|
|
372
|
+
);
|
|
373
|
+
expect(result).toHaveLength(0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("flags real path before comment but not path inside comment", () => {
|
|
377
|
+
const result = extractExternalPathsFromBashCommand(
|
|
378
|
+
"cat /etc/hosts # see also /etc/shadow",
|
|
379
|
+
cwd,
|
|
380
|
+
);
|
|
381
|
+
expect(result).toContain("/etc/hosts");
|
|
382
|
+
expect(result).not.toContain("/etc/shadow");
|
|
383
|
+
expect(result).toHaveLength(1);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
313
387
|
describe("deduplication", () => {
|
|
314
388
|
test("returns deduplicated paths", () => {
|
|
315
389
|
const result = extractExternalPathsFromBashCommand(
|