@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.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +88 -0
- package/src/access-intent/bash/command-enumeration.ts +154 -0
- package/src/access-intent/bash/cwd-projection.ts +498 -0
- package/src/access-intent/bash/node-text.ts +75 -0
- package/src/access-intent/bash/parser.ts +42 -0
- package/src/access-intent/bash/program.ts +102 -0
- package/src/{handlers/gates/bash-token-classification.ts → access-intent/bash/token-classification.ts} +1 -1
- package/src/access-intent/bash/token-collection.ts +351 -0
- package/src/handlers/gates/bash-command.ts +1 -1
- package/src/handlers/gates/bash-external-directory.ts +19 -33
- package/src/handlers/gates/bash-path-extractor.ts +10 -4
- package/src/handlers/gates/bash-path.ts +2 -2
- package/src/handlers/gates/external-directory-policy.ts +65 -0
- package/src/handlers/gates/external-directory.ts +8 -11
- package/src/handlers/gates/path.ts +1 -3
- package/src/handlers/gates/skill-read.ts +0 -4
- package/src/handlers/gates/tool-call-gate-pipeline.ts +2 -2
- package/src/handlers/gates/tool.ts +1 -1
- package/src/handlers/gates/types.ts +1 -1
- package/src/path-utils.ts +0 -20
- package/test/access-intent/access-path.test.ts +107 -0
- package/test/access-intent/bash/node-text.test.ts +147 -0
- package/test/access-intent/bash/parser.test.ts +19 -0
- package/test/{handlers/gates/bash-program.test.ts → access-intent/bash/program.test.ts} +114 -73
- package/test/{handlers/gates/bash-token-classification.test.ts → access-intent/bash/token-classification.test.ts} +1 -1
- package/test/access-intent/bash/token-collection.test.ts +300 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +2 -3
- package/test/handlers/gates/bash-command-metamorphic.test.ts +2 -3
- package/test/handlers/gates/bash-external-directory.test.ts +2 -10
- package/test/handlers/gates/bash-path.test.ts +2 -2
- package/test/handlers/gates/external-directory-policy.test.ts +112 -0
- package/test/handlers/gates/external-directory.test.ts +0 -5
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +7 -3
- package/test/path-utils.test.ts +0 -34
- 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
|
@@ -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
|
+
}
|