@gotgenes/pi-permission-system 20.3.0 → 20.4.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 +15 -0
- package/docs/cross-extension-api.md +4 -0
- package/docs/subagent-integration.md +2 -2
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +7 -5
- package/src/access-intent/bash/bash-path-resolver.ts +1 -2
- package/src/access-intent/bash/msys-bash-tokens.ts +2 -2
- package/src/access-intent/bash/parser.ts +39 -0
- package/src/access-intent/bash/sync-commands.ts +31 -0
- package/src/access-intent/bash/token-classification.ts +18 -32
- package/src/access-intent/path-normalization.ts +24 -43
- package/src/access-intent/tool-kind.ts +57 -0
- package/src/authority/approval-escalator.ts +3 -3
- package/src/authority/authorizer-selection.ts +1 -1
- package/src/authority/authorizer.ts +2 -2
- package/src/authority/denying-authorizer.ts +1 -1
- package/src/authority/forwarded-request-server.ts +3 -3
- package/src/authority/forwarder-context.ts +1 -1
- package/src/authority/forwarding-io.ts +3 -3
- package/src/{forwarding-manager.ts → authority/forwarding-manager.ts} +2 -2
- package/src/authority/local-user-authorizer.ts +2 -2
- package/src/{permission-forwarding.ts → authority/permission-forwarding.ts} +1 -2
- package/src/authority/permission-prompter.ts +2 -2
- package/src/authority/subagent-context.ts +11 -14
- package/src/authority/subagent-detection.ts +5 -4
- package/src/bash-advisory-check.ts +38 -0
- package/src/denial-messages.ts +6 -9
- package/src/handlers/before-agent-start.ts +8 -0
- package/src/handlers/gates/helpers.ts +12 -4
- package/src/handlers/gates/runner.ts +1 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +3 -2
- package/src/handlers/gates/tool.ts +9 -4
- package/src/index.ts +23 -12
- package/src/input-normalizer.ts +53 -48
- package/src/{canonicalize-path.ts → path/canonicalize-path.ts} +4 -3
- package/src/path/path-containment.ts +24 -0
- package/src/path/path-flavor.ts +114 -0
- package/src/{pi-infrastructure-read.ts → path/pi-infrastructure-read.ts} +12 -17
- package/src/path-normalizer.ts +35 -59
- package/src/permission-gate.ts +1 -1
- package/src/permission-manager.ts +36 -56
- package/src/permission-prompts.ts +4 -3
- package/src/permission-session.ts +6 -5
- package/src/permissions-service.ts +8 -0
- package/src/rule.ts +19 -21
- package/src/session-approval.ts +1 -1
- package/src/tool-input-path.ts +17 -19
- package/src/tool-preview-formatter.ts +2 -5
- package/src/path-containment.ts +0 -56
- /package/src/{permission-dialog.ts → authority/permission-dialog.ts} +0 -0
- /package/src/{subagent-lifecycle-events.ts → authority/subagent-lifecycle-events.ts} +0 -0
- /package/src/{subagent-registry.ts → authority/subagent-registry.ts} +0 -0
package/src/rule.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { PathFlavor } from "#src/path/path-flavor";
|
|
2
|
+
|
|
1
3
|
import { PATH_SURFACES } from "./path-surfaces";
|
|
2
4
|
import type { PermissionState } from "./types";
|
|
3
|
-
import { wildcardMatch } from "./wildcard-matcher";
|
|
5
|
+
import { type WildcardMatchOptions, wildcardMatch } from "./wildcard-matcher";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Provenance of a rule — which source contributed it.
|
|
@@ -74,12 +76,10 @@ export function evaluate(
|
|
|
74
76
|
surface: string,
|
|
75
77
|
pattern: string,
|
|
76
78
|
rules: Ruleset,
|
|
77
|
-
|
|
79
|
+
flavor: PathFlavor,
|
|
78
80
|
defaultAction?: PermissionState,
|
|
79
81
|
): Rule {
|
|
80
|
-
const rule = rules.findLast((r) =>
|
|
81
|
-
ruleMatches(r, surface, pattern, platform),
|
|
82
|
-
);
|
|
82
|
+
const rule = rules.findLast((r) => ruleMatches(r, surface, pattern, flavor));
|
|
83
83
|
if (rule !== undefined) return rule;
|
|
84
84
|
return {
|
|
85
85
|
surface,
|
|
@@ -96,20 +96,18 @@ export function evaluate(
|
|
|
96
96
|
*/
|
|
97
97
|
export function pathMatchOptions(
|
|
98
98
|
surface: string,
|
|
99
|
-
|
|
100
|
-
):
|
|
101
|
-
return
|
|
102
|
-
? { caseInsensitive: true, windowsSeparators: true }
|
|
103
|
-
: undefined;
|
|
99
|
+
flavor: PathFlavor,
|
|
100
|
+
): WildcardMatchOptions | undefined {
|
|
101
|
+
return PATH_SURFACES.has(surface) ? flavor.matchOptions : undefined;
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
function ruleMatches(
|
|
107
105
|
rule: Rule,
|
|
108
106
|
surface: string,
|
|
109
107
|
value: string,
|
|
110
|
-
|
|
108
|
+
flavor: PathFlavor,
|
|
111
109
|
): boolean {
|
|
112
|
-
const matchOptions = pathMatchOptions(surface,
|
|
110
|
+
const matchOptions = pathMatchOptions(surface, flavor);
|
|
113
111
|
return (
|
|
114
112
|
wildcardMatch(rule.surface, surface) &&
|
|
115
113
|
wildcardMatch(rule.pattern, value, matchOptions)
|
|
@@ -144,11 +142,11 @@ export function evaluateMostRestrictive(
|
|
|
144
142
|
surface: string,
|
|
145
143
|
values: string[],
|
|
146
144
|
rules: Ruleset,
|
|
147
|
-
|
|
145
|
+
flavor: PathFlavor,
|
|
148
146
|
): { rule: Rule; value: string } | null {
|
|
149
147
|
let worst: { rule: Rule; value: string } | null = null;
|
|
150
148
|
for (const value of values) {
|
|
151
|
-
const rule = evaluate(surface, value, rules,
|
|
149
|
+
const rule = evaluate(surface, value, rules, flavor);
|
|
152
150
|
if (rule.action === "deny") return { rule, value };
|
|
153
151
|
if (rule.action === "ask" && worst?.rule.action !== "ask") {
|
|
154
152
|
worst = { rule, value };
|
|
@@ -161,10 +159,10 @@ export function evaluateFirst(
|
|
|
161
159
|
surface: string,
|
|
162
160
|
values: string[],
|
|
163
161
|
rules: Ruleset,
|
|
164
|
-
|
|
162
|
+
flavor: PathFlavor,
|
|
165
163
|
): { rule: Rule; value: string } {
|
|
166
164
|
for (const value of values) {
|
|
167
|
-
const rule = evaluate(surface, value, rules,
|
|
165
|
+
const rule = evaluate(surface, value, rules, flavor);
|
|
168
166
|
if (rule.layer !== "default") {
|
|
169
167
|
return { rule, value };
|
|
170
168
|
}
|
|
@@ -172,7 +170,7 @@ export function evaluateFirst(
|
|
|
172
170
|
// All candidates matched only the synthesized default — use the first.
|
|
173
171
|
const fallbackValue = values[0] ?? "*";
|
|
174
172
|
return {
|
|
175
|
-
rule: evaluate(surface, fallbackValue, rules,
|
|
173
|
+
rule: evaluate(surface, fallbackValue, rules, flavor),
|
|
176
174
|
value: fallbackValue,
|
|
177
175
|
};
|
|
178
176
|
}
|
|
@@ -189,22 +187,22 @@ export function evaluateAnyValue(
|
|
|
189
187
|
surface: string,
|
|
190
188
|
values: string[],
|
|
191
189
|
rules: Ruleset,
|
|
192
|
-
|
|
190
|
+
flavor: PathFlavor,
|
|
193
191
|
): { rule: Rule; value: string } {
|
|
194
192
|
const fallbackValue = values[0] ?? "*";
|
|
195
193
|
const rule = rules.findLast((r) =>
|
|
196
|
-
values.some((value) => ruleMatches(r, surface, value,
|
|
194
|
+
values.some((value) => ruleMatches(r, surface, value, flavor)),
|
|
197
195
|
);
|
|
198
196
|
if (rule !== undefined) {
|
|
199
197
|
return {
|
|
200
198
|
rule,
|
|
201
199
|
value:
|
|
202
|
-
values.find((value) => ruleMatches(rule, surface, value,
|
|
200
|
+
values.find((value) => ruleMatches(rule, surface, value, flavor)) ??
|
|
203
201
|
fallbackValue,
|
|
204
202
|
};
|
|
205
203
|
}
|
|
206
204
|
return {
|
|
207
|
-
rule: evaluate(surface, fallbackValue, rules,
|
|
205
|
+
rule: evaluate(surface, fallbackValue, rules, flavor),
|
|
208
206
|
value: fallbackValue,
|
|
209
207
|
};
|
|
210
208
|
}
|
package/src/session-approval.ts
CHANGED
package/src/tool-input-path.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { classifyToolKind } from "./access-intent/tool-kind";
|
|
2
2
|
import type { ToolAccessExtractorLookup } from "./tool-access-extractor-registry";
|
|
3
3
|
import { getNonEmptyString, toRecord } from "./value-guards";
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ export function getPathBearingToolPath(
|
|
|
6
6
|
toolName: string,
|
|
7
7
|
input: unknown,
|
|
8
8
|
): string | null {
|
|
9
|
-
if (
|
|
9
|
+
if (classifyToolKind(toolName) !== "path") {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -31,24 +31,22 @@ export function getToolInputPath(
|
|
|
31
31
|
input: unknown,
|
|
32
32
|
extractors?: ToolAccessExtractorLookup,
|
|
33
33
|
): string | null {
|
|
34
|
-
if (toolName === "bash") {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
const record = toRecord(input);
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
switch (classifyToolKind(toolName)) {
|
|
37
|
+
case "bash":
|
|
38
|
+
return null;
|
|
39
|
+
case "path":
|
|
40
|
+
return getNonEmptyString(record.path);
|
|
41
|
+
case "mcp":
|
|
42
|
+
return getNonEmptyString(toRecord(record.arguments).path);
|
|
43
|
+
case "skill":
|
|
44
|
+
case "extension": {
|
|
45
|
+
const custom = extractors?.get(toolName);
|
|
46
|
+
if (custom) {
|
|
47
|
+
return getNonEmptyString(custom(record));
|
|
48
|
+
}
|
|
49
|
+
return getNonEmptyString(record.path);
|
|
50
|
+
}
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
return getNonEmptyString(record.path);
|
|
54
52
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyToolKind, isMcpCheck } from "./access-intent/tool-kind";
|
|
1
2
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
2
3
|
import type { ToolInputFormatterLookup } from "./tool-input-formatter-registry";
|
|
3
4
|
import {
|
|
@@ -158,11 +159,7 @@ export class ToolPreviewFormatter {
|
|
|
158
159
|
input: unknown,
|
|
159
160
|
pathBearingTools: ReadonlySet<string>,
|
|
160
161
|
): string | undefined {
|
|
161
|
-
if (
|
|
162
|
-
result.toolName === "bash" ||
|
|
163
|
-
result.toolName === "mcp" ||
|
|
164
|
-
result.source === "mcp"
|
|
165
|
-
) {
|
|
162
|
+
if (classifyToolKind(result.toolName) === "bash" || isMcpCheck(result)) {
|
|
166
163
|
return undefined;
|
|
167
164
|
}
|
|
168
165
|
|
package/src/path-containment.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { posix as posixPath, win32 as winPath } from "node:path";
|
|
2
|
-
|
|
3
|
-
import { isSafeSystemPath } from "./safe-system-paths";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Returns true when `pathValue` is `directory` itself or nested inside it.
|
|
7
|
-
*
|
|
8
|
-
* Containment is decided with Node's platform-native `path.relative` rather
|
|
9
|
-
* than a hand-rolled prefix check: on `win32` the comparison folds case (and
|
|
10
|
-
* tolerates either separator), matching the case-insensitive filesystem.
|
|
11
|
-
* `platform` is injected from the composition root so Windows behavior is
|
|
12
|
-
* testable on a POSIX CI.
|
|
13
|
-
*/
|
|
14
|
-
export function isPathWithinDirectory(
|
|
15
|
-
pathValue: string,
|
|
16
|
-
directory: string,
|
|
17
|
-
platform: NodeJS.Platform,
|
|
18
|
-
): boolean {
|
|
19
|
-
if (!pathValue || !directory) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (pathValue === directory) {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const impl = platform === "win32" ? winPath : posixPath;
|
|
28
|
-
const rel = impl.relative(directory, pathValue);
|
|
29
|
-
return (
|
|
30
|
-
rel !== "" &&
|
|
31
|
-
rel !== ".." &&
|
|
32
|
-
!rel.startsWith(`..${impl.sep}`) &&
|
|
33
|
-
!impl.isAbsolute(rel)
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Pure geometry: is `canonicalPath` outside `canonicalCwd`?
|
|
39
|
-
*
|
|
40
|
-
* Both operands must already be canonical (symlink-resolved, win32-lowercased)
|
|
41
|
-
* — the caller prepares them (see {@link PathNormalizer.isOutsideWorkingDirectory}).
|
|
42
|
-
* This predicate touches no filesystem and does no derivation.
|
|
43
|
-
*/
|
|
44
|
-
export function isPathOutsideWorkingDirectory(
|
|
45
|
-
canonicalPath: string,
|
|
46
|
-
canonicalCwd: string,
|
|
47
|
-
platform: NodeJS.Platform,
|
|
48
|
-
): boolean {
|
|
49
|
-
if (!canonicalCwd || !canonicalPath) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
if (isSafeSystemPath(canonicalPath)) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return !isPathWithinDirectory(canonicalPath, canonicalCwd, platform);
|
|
56
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|