@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/docs/cross-extension-api.md +4 -0
  3. package/docs/subagent-integration.md +2 -2
  4. package/package.json +1 -1
  5. package/src/access-intent/access-path.ts +7 -5
  6. package/src/access-intent/bash/bash-path-resolver.ts +1 -2
  7. package/src/access-intent/bash/msys-bash-tokens.ts +2 -2
  8. package/src/access-intent/bash/parser.ts +39 -0
  9. package/src/access-intent/bash/sync-commands.ts +31 -0
  10. package/src/access-intent/bash/token-classification.ts +18 -32
  11. package/src/access-intent/path-normalization.ts +24 -43
  12. package/src/access-intent/tool-kind.ts +57 -0
  13. package/src/authority/approval-escalator.ts +3 -3
  14. package/src/authority/authorizer-selection.ts +1 -1
  15. package/src/authority/authorizer.ts +2 -2
  16. package/src/authority/denying-authorizer.ts +1 -1
  17. package/src/authority/forwarded-request-server.ts +3 -3
  18. package/src/authority/forwarder-context.ts +1 -1
  19. package/src/authority/forwarding-io.ts +3 -3
  20. package/src/{forwarding-manager.ts → authority/forwarding-manager.ts} +2 -2
  21. package/src/authority/local-user-authorizer.ts +2 -2
  22. package/src/{permission-forwarding.ts → authority/permission-forwarding.ts} +1 -2
  23. package/src/authority/permission-prompter.ts +2 -2
  24. package/src/authority/subagent-context.ts +11 -14
  25. package/src/authority/subagent-detection.ts +5 -4
  26. package/src/bash-advisory-check.ts +38 -0
  27. package/src/denial-messages.ts +6 -9
  28. package/src/handlers/before-agent-start.ts +8 -0
  29. package/src/handlers/gates/helpers.ts +12 -4
  30. package/src/handlers/gates/runner.ts +1 -1
  31. package/src/handlers/gates/tool-call-gate-pipeline.ts +3 -2
  32. package/src/handlers/gates/tool.ts +9 -4
  33. package/src/index.ts +23 -12
  34. package/src/input-normalizer.ts +53 -48
  35. package/src/{canonicalize-path.ts → path/canonicalize-path.ts} +4 -3
  36. package/src/path/path-containment.ts +24 -0
  37. package/src/path/path-flavor.ts +114 -0
  38. package/src/{pi-infrastructure-read.ts → path/pi-infrastructure-read.ts} +12 -17
  39. package/src/path-normalizer.ts +35 -59
  40. package/src/permission-gate.ts +1 -1
  41. package/src/permission-manager.ts +36 -56
  42. package/src/permission-prompts.ts +4 -3
  43. package/src/permission-session.ts +6 -5
  44. package/src/permissions-service.ts +8 -0
  45. package/src/rule.ts +19 -21
  46. package/src/session-approval.ts +1 -1
  47. package/src/tool-input-path.ts +17 -19
  48. package/src/tool-preview-formatter.ts +2 -5
  49. package/src/path-containment.ts +0 -56
  50. /package/src/{permission-dialog.ts → authority/permission-dialog.ts} +0 -0
  51. /package/src/{subagent-lifecycle-events.ts → authority/subagent-lifecycle-events.ts} +0 -0
  52. /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
- platform: NodeJS.Platform,
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
- platform: NodeJS.Platform,
100
- ): { caseInsensitive: true; windowsSeparators: true } | undefined {
101
- return platform === "win32" && PATH_SURFACES.has(surface)
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
- platform: NodeJS.Platform,
108
+ flavor: PathFlavor,
111
109
  ): boolean {
112
- const matchOptions = pathMatchOptions(surface, platform);
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
- platform: NodeJS.Platform,
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, platform);
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
- platform: NodeJS.Platform,
162
+ flavor: PathFlavor,
165
163
  ): { rule: Rule; value: string } {
166
164
  for (const value of values) {
167
- const rule = evaluate(surface, value, rules, platform);
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, platform),
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
- platform: NodeJS.Platform,
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, platform)),
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, platform)) ??
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, platform),
205
+ rule: evaluate(surface, fallbackValue, rules, flavor),
208
206
  value: fallbackValue,
209
207
  };
210
208
  }
@@ -1,4 +1,4 @@
1
- import type { ForwardedSessionApproval } from "./permission-forwarding";
1
+ import type { ForwardedSessionApproval } from "#src/authority/permission-forwarding";
2
2
 
3
3
  /**
4
4
  * Value object for a session-scoped approval: one surface, one-or-more patterns.
@@ -1,4 +1,4 @@
1
- import { PATH_BEARING_TOOLS } from "./path-surfaces";
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 (!PATH_BEARING_TOOLS.has(toolName)) {
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
- if (PATH_BEARING_TOOLS.has(toolName)) {
41
- return getNonEmptyString(record.path);
42
- }
43
-
44
- if (toolName === "mcp") {
45
- return getNonEmptyString(toRecord(record.arguments).path);
46
- }
47
-
48
- const custom = extractors?.get(toolName);
49
- if (custom) {
50
- return getNonEmptyString(custom(record));
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
 
@@ -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
- }