@gotgenes/pi-permission-system 16.0.1 → 16.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/access-intent/access-path.ts +88 -0
  4. package/src/access-intent/bash/command-enumeration.ts +154 -0
  5. package/src/access-intent/bash/cwd-projection.ts +498 -0
  6. package/src/access-intent/bash/node-text.ts +75 -0
  7. package/src/access-intent/bash/parser.ts +42 -0
  8. package/src/access-intent/bash/program.ts +102 -0
  9. package/src/{handlers/gates/bash-token-classification.ts → access-intent/bash/token-classification.ts} +1 -1
  10. package/src/access-intent/bash/token-collection.ts +351 -0
  11. package/src/handlers/gates/bash-command.ts +1 -1
  12. package/src/handlers/gates/bash-external-directory.ts +19 -33
  13. package/src/handlers/gates/bash-path-extractor.ts +10 -4
  14. package/src/handlers/gates/bash-path.ts +2 -2
  15. package/src/handlers/gates/external-directory-policy.ts +65 -0
  16. package/src/handlers/gates/external-directory.ts +8 -11
  17. package/src/handlers/gates/path.ts +1 -3
  18. package/src/handlers/gates/skill-read.ts +0 -4
  19. package/src/handlers/gates/tool-call-gate-pipeline.ts +2 -2
  20. package/src/handlers/gates/tool.ts +1 -1
  21. package/src/handlers/gates/types.ts +1 -1
  22. package/src/path-utils.ts +0 -20
  23. package/test/access-intent/access-path.test.ts +107 -0
  24. package/test/access-intent/bash/node-text.test.ts +147 -0
  25. package/test/access-intent/bash/parser.test.ts +19 -0
  26. package/test/{handlers/gates/bash-program.test.ts → access-intent/bash/program.test.ts} +114 -73
  27. package/test/{handlers/gates/bash-token-classification.test.ts → access-intent/bash/token-classification.test.ts} +1 -1
  28. package/test/access-intent/bash/token-collection.test.ts +300 -0
  29. package/test/handlers/external-directory-symlink-acceptance.test.ts +2 -3
  30. package/test/handlers/gates/bash-command-metamorphic.test.ts +2 -3
  31. package/test/handlers/gates/bash-external-directory.test.ts +2 -10
  32. package/test/handlers/gates/bash-path.test.ts +2 -2
  33. package/test/handlers/gates/external-directory-policy.test.ts +112 -0
  34. package/test/handlers/gates/external-directory.test.ts +0 -5
  35. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +7 -3
  36. package/test/path-utils.test.ts +0 -34
  37. package/src/handlers/gates/bash-program.ts +0 -1143
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
+ ## [16.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v16.0.2...pi-permission-system-v16.1.0) (2026-06-26)
9
+
10
+
11
+ ### Features
12
+
13
+ * **pi-permission-system:** introduce AccessPath value object ([c00d5c5](https://github.com/gotgenes/pi-packages/commit/c00d5c580a828234fafd7946be4e81313665d25d)), closes [#476](https://github.com/gotgenes/pi-packages/issues/476)
14
+
15
+ ## [16.0.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v16.0.1...pi-permission-system-v16.0.2) (2026-06-25)
16
+
17
+
18
+ ### Documentation
19
+
20
+ * **pi-permission-system:** record parser/node-text extraction in architecture and skill ([#473](https://github.com/gotgenes/pi-packages/issues/473)) ([7626425](https://github.com/gotgenes/pi-packages/commit/7626425f587dd62ab54f39390841a6ca152b32e1))
21
+
8
22
  ## [16.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v16.0.0...pi-permission-system-v16.0.1) (2026-06-21)
9
23
 
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "16.0.1",
3
+ "version": "16.1.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,88 @@
1
+ import {
2
+ canonicalNormalizePathForComparison,
3
+ getPathPolicyValues,
4
+ normalizePathForComparison,
5
+ } from "#src/path-utils";
6
+
7
+ /**
8
+ * A path's two representations held behind type-distinct accessors.
9
+ *
10
+ * A single `string` carrying both meanings was the root cause of [#418]:
11
+ * both external-directory gates matched config patterns against the
12
+ * symlink-resolved (canonical) path instead of the typed (lexical) path,
13
+ * defeating a configured `/tmp/*` allow.
14
+ *
15
+ * `AccessPath` makes the misuse a compile error:
16
+ * - {@link matchValues} returns `string[]` — the lexical alias union ∪ canonical,
17
+ * for `external_directory` pattern matching.
18
+ * - {@link boundaryValue} returns `string` — the canonical form, for
19
+ * outside-CWD containment and infra-read checks.
20
+ * - {@link value} returns `string` — the lexical absolute form, for display,
21
+ * approval patterns, decision values, and logs.
22
+ *
23
+ * Construct via {@link forExternalDirectory}; the constructor is private.
24
+ */
25
+ export class AccessPath {
26
+ private constructor(
27
+ private readonly lexical: string,
28
+ private readonly matchAliases: readonly string[],
29
+ private readonly canonical: string,
30
+ ) {}
31
+
32
+ /**
33
+ * Pattern-match values for the `external_directory` surface: the lexical
34
+ * alias union plus the canonical alias, so a config pattern on either the
35
+ * typed form (`/tmp/*`) or the symlink-resolved form (`/private/tmp/*`)
36
+ * matches (#418).
37
+ *
38
+ * Collapses to the lexical aliases when the canonical equals one of them
39
+ * (e.g. when the path is not a symlink).
40
+ */
41
+ matchValues(): string[] {
42
+ return this.canonical
43
+ ? [...new Set([...this.matchAliases, this.canonical])]
44
+ : [...this.matchAliases];
45
+ }
46
+
47
+ /**
48
+ * Canonical (symlink-resolved, win32-lowercased) form, for the outside-CWD
49
+ * boundary decision and Pi infrastructure-read containment checks.
50
+ *
51
+ * Returns `""` when the path could not be resolved (empty input).
52
+ */
53
+ boundaryValue(): string {
54
+ return this.canonical;
55
+ }
56
+
57
+ /**
58
+ * Lexical (as-typed, normalized but not symlink-resolved) form, for display,
59
+ * approval patterns, decision values, and log messages.
60
+ *
61
+ * Returns `""` for empty input.
62
+ */
63
+ value(): string {
64
+ return this.lexical;
65
+ }
66
+
67
+ /**
68
+ * Build an `AccessPath` for a tool-input or bash-token path resolved against
69
+ * `cwd`.
70
+ *
71
+ * - `matchValues()` returns the same set as the former
72
+ * `getExternalDirectoryPolicyValues(pathValue, cwd)` — the lexical alias
73
+ * union from `getPathPolicyValues` plus the canonical alias from
74
+ * `canonicalNormalizePathForComparison` (#418).
75
+ * - `boundaryValue()` returns `canonicalNormalizePathForComparison(pathValue, cwd)`,
76
+ * which is win32-lowercased (#382) — do not substitute a raw
77
+ * `canonicalizePath` output here.
78
+ * - `value()` returns `normalizePathForComparison(pathValue, cwd)`, the
79
+ * absolute lexical form.
80
+ */
81
+ static forExternalDirectory(pathValue: string, cwd: string): AccessPath {
82
+ return new AccessPath(
83
+ normalizePathForComparison(pathValue, cwd),
84
+ getPathPolicyValues(pathValue, { cwd }),
85
+ canonicalNormalizePathForComparison(pathValue, cwd),
86
+ );
87
+ }
88
+ }
@@ -0,0 +1,154 @@
1
+ import type { TSNode } from "#src/access-intent/bash/parser";
2
+ import type { BashCommandContext } from "#src/types";
3
+
4
+ // ── Command type ─────────────────────────────────────────────────────────────
5
+
6
+ /**
7
+ * One command-pattern unit of a parsed bash program.
8
+ *
9
+ * Minimal by design — `text` is the simple-command (or whole compound
10
+ * statement) string matched against the bash rules.
11
+ * The type is the stable extension point: #306 adds an execution `context`,
12
+ * #307 adds per-command path candidates and an effective working directory.
13
+ */
14
+ export interface BashCommand {
15
+ readonly text: string;
16
+ /**
17
+ * Execution context for a nested command (substitution or subshell); absent
18
+ * for a current-shell (top-level) command.
19
+ */
20
+ readonly context?: BashCommandContext;
21
+ }
22
+
23
+ // ── Command enumeration ──────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Container node types descended into when enumerating command units.
27
+ */
28
+ const COMMAND_ENUM_DESCEND = new Set([
29
+ "program",
30
+ "list",
31
+ "pipeline",
32
+ "redirected_statement",
33
+ ]);
34
+
35
+ /**
36
+ * Named node types skipped during command enumeration: redirect targets,
37
+ * comments, and heredoc bodies — none is a command to evaluate.
38
+ * Anonymous tokens (chain operators `&&`/`;`/`|`, substitution and subshell
39
+ * delimiters `$(`/`)`/`` ` ``/`(`) are filtered by the `isNamed` guard, not
40
+ * listed here.
41
+ */
42
+ const COMMAND_ENUM_SKIP = new Set([
43
+ "file_redirect",
44
+ "heredoc_redirect",
45
+ "herestring_redirect",
46
+ "comment",
47
+ "heredoc_body",
48
+ "heredoc_end",
49
+ ]);
50
+
51
+ /**
52
+ * Nested execution contexts whose interior commands really execute and must be
53
+ * evaluated too: command substitution (`$(…)`, backticks) and process
54
+ * substitution (`<(…)`/`>(…)`).
55
+ * Subshells (`( … )`) are handled separately because they are also emitted
56
+ * whole.
57
+ */
58
+ const NESTED_EXECUTION_CONTEXTS = new Map<string, BashCommandContext>([
59
+ ["command_substitution", "command_substitution"],
60
+ ["process_substitution", "process_substitution"],
61
+ ]);
62
+
63
+ /**
64
+ * Enumerate the command units of a bash program, in source order.
65
+ *
66
+ * Descends container nodes (`program`, `list`, `pipeline`,
67
+ * `redirected_statement`) and emits each `command` node whole.
68
+ * Additionally descends into the three nested execution contexts — command
69
+ * substitution (`$(…)`, backticks), process substitution (`<(…)`/`>(…)`), and
70
+ * subshells (`( … )`) — emitting each inner command as its own unit *in
71
+ * addition to* the enclosing command, since those inner commands really execute
72
+ * (#306).
73
+ * Control-flow bodies and `{ … }` brace groups are emitted whole without
74
+ * descending (deferred).
75
+ *
76
+ * The enclosing command/subshell is always still emitted whole, so adding the
77
+ * nested units can only ever produce a more-restrictive decision, never weaker.
78
+ */
79
+ export function collectCommands(node: TSNode): BashCommand[] {
80
+ const out: BashCommand[] = [];
81
+ collectCommandsInto(node, undefined, out);
82
+ return out;
83
+ }
84
+
85
+ function collectCommandsInto(
86
+ node: TSNode,
87
+ context: BashCommandContext | undefined,
88
+ out: BashCommand[],
89
+ ): void {
90
+ // Anonymous tokens (operators `&&`/`;`/`|`, delimiters `$(`/`)`/`` ` ``/`(`)
91
+ // carry no command.
92
+ if (!node.isNamed) return;
93
+ if (COMMAND_ENUM_SKIP.has(node.type)) return;
94
+
95
+ if (node.type === "command") {
96
+ out.push(makeUnit(node.text, context));
97
+ // A command's text already contains any substitution; descend its subtree
98
+ // to ALSO emit the inner commands of command/process substitutions.
99
+ collectSubstitutionCommands(node, out);
100
+ return;
101
+ }
102
+
103
+ if (node.type === "subshell") {
104
+ out.push(makeUnit(node.text, context)); // never-weaker whole emit
105
+ descendCommandChildren(node, "subshell", out);
106
+ return;
107
+ }
108
+
109
+ if (COMMAND_ENUM_DESCEND.has(node.type)) {
110
+ descendCommandChildren(node, context, out);
111
+ return;
112
+ }
113
+
114
+ // Any other named statement (compound_statement `{ … }`, if/while/for/case,
115
+ // function_definition): emit whole, do not descend — deferred (#306).
116
+ out.push(makeUnit(node.text, context));
117
+ }
118
+
119
+ function makeUnit(
120
+ text: string,
121
+ context: BashCommandContext | undefined,
122
+ ): BashCommand {
123
+ return context ? { text, context } : { text };
124
+ }
125
+
126
+ function descendCommandChildren(
127
+ node: TSNode,
128
+ context: BashCommandContext | undefined,
129
+ out: BashCommand[],
130
+ ): void {
131
+ for (let i = 0; i < node.childCount; i++) {
132
+ const child = node.child(i);
133
+ if (child) collectCommandsInto(child, context, out);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Search a command's subtree for command/process substitutions and enumerate
139
+ * the commands inside them, tagged with the substitution's execution context.
140
+ * A substitution can nest under `command_name` (when the whole command is
141
+ * `$(…)`) or under an argument, so the entire subtree is searched.
142
+ */
143
+ function collectSubstitutionCommands(node: TSNode, out: BashCommand[]): void {
144
+ for (let i = 0; i < node.childCount; i++) {
145
+ const child = node.child(i);
146
+ if (!child) continue;
147
+ const nestedContext = NESTED_EXECUTION_CONTEXTS.get(child.type);
148
+ if (nestedContext) {
149
+ descendCommandChildren(child, nestedContext, out);
150
+ } else {
151
+ collectSubstitutionCommands(child, out);
152
+ }
153
+ }
154
+ }