@davisliu2006/pi-permission-system 0.1.0 → 0.2.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.
@@ -0,0 +1,136 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { generateBashWildcard, generateBashWildcards } from "./ai-wildcard.ts";
4
+ import { STATUS_KEY } from "./constants.ts";
5
+ import { resolveAllowAlwaysMode } from "./policy-bash.ts";
6
+ import { analyzeShellCommand } from "./shell.ts";
7
+ import {
8
+ BROAD_WILDCARD_TITLE,
9
+ formatBroadWildcardConfirm,
10
+ formatBroadWildcardListConfirm,
11
+ STATUS_GENERATING_WILDCARD,
12
+ WILDCARD_EDITOR_TITLE,
13
+ WILDCARD_LIST_EDITOR_TITLE,
14
+ } from "./ui-strings.ts";
15
+ import { INVALID_WILDCARD_PREFIX } from "./warning-strings.ts";
16
+ import {
17
+ validateWildcardSuggestion,
18
+ validateWildcardSuggestionList,
19
+ } from "./wildcard-validation.ts";
20
+ import type {
21
+ AllowAlwaysRule,
22
+ PermissionRequest,
23
+ PolicyResolution,
24
+ } from "./types.ts";
25
+
26
+ /**
27
+ * Single-command wildcard path.
28
+ */
29
+ async function chooseSingleWildcard(
30
+ ctx: ExtensionContext,
31
+ command: string
32
+ ): Promise<AllowAlwaysRule | undefined> {
33
+ ctx.ui.setStatus(STATUS_KEY, STATUS_GENERATING_WILDCARD);
34
+ let suggestion: string | undefined;
35
+ try {
36
+ suggestion = await generateBashWildcard(ctx, command);
37
+ } finally {
38
+ ctx.ui.setStatus(STATUS_KEY, undefined);
39
+ }
40
+
41
+ let current = suggestion ?? command;
42
+ while (true) {
43
+ const edited = await ctx.ui.editor(WILDCARD_EDITOR_TITLE, current);
44
+ if (edited === undefined) {return undefined;}
45
+
46
+ const pattern = edited.trim();
47
+ const validation = validateWildcardSuggestion(pattern, command);
48
+ if (!validation.valid) {
49
+ ctx.ui.notify(`${INVALID_WILDCARD_PREFIX}${validation.reason}`, "warning");
50
+ current = pattern || command;
51
+ continue;
52
+ }
53
+
54
+ if (validation.broad) {
55
+ const confirmed = await ctx.ui.confirm(
56
+ BROAD_WILDCARD_TITLE,
57
+ formatBroadWildcardConfirm(pattern)
58
+ );
59
+ if (!confirmed) {
60
+ current = pattern;
61
+ continue;
62
+ }
63
+ }
64
+
65
+ return { kind: "wildcard", tool: "bash", body: pattern };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Multi-command wildcard path.
71
+ */
72
+ async function chooseWildcardListRules(
73
+ ctx: ExtensionContext,
74
+ commands: readonly string[]
75
+ ): Promise<AllowAlwaysRule[] | undefined> {
76
+ ctx.ui.setStatus(STATUS_KEY, STATUS_GENERATING_WILDCARD);
77
+ let suggestion: string[] | undefined;
78
+ try {
79
+ suggestion = await generateBashWildcards(ctx, commands);
80
+ } finally {
81
+ ctx.ui.setStatus(STATUS_KEY, undefined);
82
+ }
83
+
84
+ let current = (suggestion ?? commands).join("; ");
85
+ while (true) {
86
+ const edited = await ctx.ui.editor(WILDCARD_LIST_EDITOR_TITLE, current);
87
+ if (edited === undefined) {return undefined;}
88
+
89
+ const validation = validateWildcardSuggestionList(edited, commands);
90
+ if (!validation.valid) {
91
+ ctx.ui.notify(`${INVALID_WILDCARD_PREFIX}${validation.reason}`, "warning");
92
+ current = edited.trim() || commands.join("; ");
93
+ continue;
94
+ }
95
+
96
+ if (validation.broadPatterns.length > 0) {
97
+ const confirmed = await ctx.ui.confirm(
98
+ BROAD_WILDCARD_TITLE,
99
+ formatBroadWildcardListConfirm(validation.broadPatterns)
100
+ );
101
+ if (!confirmed) {
102
+ current = validation.patterns.join("; ");
103
+ continue;
104
+ }
105
+ }
106
+
107
+ return validation.patterns.map((pattern) => ({
108
+ kind: "wildcard" as const,
109
+ tool: "bash" as const,
110
+ body: pattern,
111
+ }));
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Selects the "allow always" matching rules for the request.
117
+ */
118
+ export async function chooseAlwaysRules(
119
+ ctx: ExtensionContext,
120
+ request: PermissionRequest,
121
+ policy: PolicyResolution
122
+ ): Promise<AllowAlwaysRule[] | undefined> {
123
+ const mode = resolveAllowAlwaysMode(request, policy);
124
+
125
+ if (mode === "wildcard") {
126
+ const rule = await chooseSingleWildcard(ctx, request.body);
127
+ return rule ? [rule] : undefined;
128
+ }
129
+
130
+ if (mode === "wildcard-list") {
131
+ const analysis = request.bashAnalysis ?? analyzeShellCommand(request.body);
132
+ return chooseWildcardListRules(ctx, analysis.segments);
133
+ }
134
+
135
+ return [{ kind: "exact", tool: request.toolName, body: request.body }];
136
+ }
@@ -3,6 +3,29 @@ import {
3
3
  isValidWildcardPattern,
4
4
  matchesWildcard,
5
5
  } from "./matcher.ts";
6
+ import {
7
+ formatWildcardListCountReason,
8
+ formatWildcardListItemReason,
9
+ WILDCARD_LENGTH_REASON,
10
+ WILDCARD_NO_MATCH_REASON,
11
+ } from "./warning-strings.ts";
12
+
13
+ export type WildcardValidationResult = {
14
+ valid: true;
15
+ broad: boolean;
16
+ } | {
17
+ valid: false;
18
+ reason: string;
19
+ };
20
+
21
+ export type WildcardListValidationResult = {
22
+ valid: true;
23
+ patterns: string[];
24
+ broadPatterns: string[];
25
+ } | {
26
+ valid: false;
27
+ reason: string;
28
+ };
6
29
 
7
30
  /**
8
31
  * Validates a wildcard pattern against a command.
@@ -10,12 +33,54 @@ import {
10
33
  export function validateWildcardSuggestion(
11
34
  pattern: string,
12
35
  command: string
13
- ): { valid: true; broad: boolean } | { valid: false; reason: string } {
36
+ ): WildcardValidationResult {
14
37
  if (!isValidWildcardPattern(pattern)) {
15
- return { valid: false, reason: "Pattern must be between 1 and 500 characters." };
38
+ return { valid: false, reason: WILDCARD_LENGTH_REASON };
16
39
  }
17
40
  if (!matchesWildcard(pattern, command)) {
18
- return { valid: false, reason: "Pattern does not match the current command." };
41
+ return { valid: false, reason: WILDCARD_NO_MATCH_REASON };
19
42
  }
20
43
  return { valid: true, broad: isMatchEverythingPattern(pattern) };
21
44
  }
45
+
46
+ /**
47
+ * Splits a semicolon-separated wildcard list from the model or editor.
48
+ */
49
+ export function splitWildcardList(value: string): string[] {
50
+ return value
51
+ .split(";")
52
+ .map((part) => part.trim())
53
+ .filter((part) => part.length > 0);
54
+ }
55
+
56
+ /**
57
+ * Validates semicolon-separated patterns against a list of commands.
58
+ */
59
+ export function validateWildcardSuggestionList(
60
+ patternsText: string,
61
+ commands: readonly string[]
62
+ ): WildcardListValidationResult {
63
+ const patterns = splitWildcardList(patternsText);
64
+ if (patterns.length !== commands.length) {
65
+ return {
66
+ valid: false,
67
+ reason: formatWildcardListCountReason(commands.length, patterns.length),
68
+ };
69
+ }
70
+
71
+ const broadPatterns: string[] = [];
72
+ for (let index = 0; index < patterns.length; index++) {
73
+ const pattern = patterns[index]!;
74
+ const command = commands[index]!;
75
+ const result = validateWildcardSuggestion(pattern, command);
76
+ if (!result.valid) {
77
+ return {
78
+ valid: false,
79
+ reason: formatWildcardListItemReason(index + 1, pattern, result.reason),
80
+ };
81
+ }
82
+ if (result.broad) {broadPatterns.push(pattern);}
83
+ }
84
+
85
+ return { valid: true, patterns, broadPatterns };
86
+ }