@gotgenes/pi-permission-system 3.7.0 → 3.9.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/src/index.ts CHANGED
@@ -1,47 +1,7 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- renameSync,
5
- unlinkSync,
6
- writeFileSync,
7
- } from "node:fs";
8
- import { dirname, join, normalize } from "node:path";
9
- import {
10
- type ExtensionAPI,
11
- type ExtensionCommandContext,
12
- type ExtensionContext,
13
- getAgentDir,
14
- } from "@mariozechner/pi-coding-agent";
15
- import {
16
- getActiveAgentName,
17
- getActiveAgentNameFromSystemPrompt,
18
- } from "./active-agent";
19
- import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
2
  import { registerPermissionSystemCommand } from "./config-modal";
21
- import {
22
- DEBUG_LOG_FILENAME,
23
- getGlobalConfigPath,
24
- getGlobalLogsDir,
25
- getLegacyExtensionConfigPath,
26
- getLegacyGlobalPolicyPath,
27
- getLegacyProjectPolicyPath,
28
- getProjectConfigPath,
29
- REVIEW_LOG_FILENAME,
30
- } from "./config-paths";
31
- import { buildResolvedConfigLogEntry } from "./config-reporter";
32
- import {
33
- DEFAULT_EXTENSION_CONFIG,
34
- EXTENSION_ROOT,
35
- ensurePermissionSystemLogsDirectory,
36
- normalizePermissionSystemConfig,
37
- type PermissionSystemExtensionConfig,
38
- } from "./extension-config";
39
- import { setForwardedPermissionLogger } from "./forwarded-permissions/io";
40
- import {
41
- confirmPermission,
42
- type PermissionForwardingDeps,
43
- processForwardedPermissionRequests,
44
- } from "./forwarded-permissions/polling";
3
+ import { getGlobalConfigPath } from "./config-paths";
4
+ import type { PermissionForwardingDeps } from "./forwarded-permissions/polling";
45
5
  import {
46
6
  type HandlerDeps,
47
7
  handleBeforeAgentStart,
@@ -51,406 +11,76 @@ import {
51
11
  handleSessionStart,
52
12
  handleToolCall,
53
13
  } from "./handlers";
54
- import type { PromptPermissionDetails } from "./handlers/types";
55
- import { createPermissionSystemLogger } from "./logging";
14
+ import { requestPermissionDecisionFromUi } from "./permission-dialog";
56
15
  import {
57
- type PermissionPromptDecision,
58
- requestPermissionDecisionFromUi,
59
- } from "./permission-dialog";
60
- import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
61
- import { PermissionManager } from "./permission-manager";
62
- import { SessionApprovalCache } from "./session-approval-cache";
63
- import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
64
- import {
65
- PERMISSION_SYSTEM_STATUS_KEY,
66
- syncPermissionSystemStatus,
67
- } from "./status";
16
+ createExtensionRuntime,
17
+ createPermissionManagerForCwd,
18
+ logResolvedConfigPaths,
19
+ promptPermission,
20
+ refreshExtensionConfig,
21
+ resolveAgentName,
22
+ saveExtensionConfig,
23
+ startForwardedPermissionPolling,
24
+ stopForwardedPermissionPolling,
25
+ } from "./runtime";
68
26
  import { isSubagentExecutionContext } from "./subagent-context";
69
27
  import {
70
28
  canResolveAskPermissionRequest,
71
29
  shouldAutoApprovePermissionState,
72
30
  } from "./yolo-mode";
73
31
 
74
- const PI_AGENT_DIR = getAgentDir();
75
- const SESSIONS_DIR = join(PI_AGENT_DIR, "sessions");
76
- const SUBAGENT_SESSIONS_DIR = join(PI_AGENT_DIR, "subagent-sessions");
77
- const PERMISSION_FORWARDING_DIR = join(SESSIONS_DIR, "permission-forwarding");
78
-
79
- let extensionConfig: PermissionSystemExtensionConfig = {
80
- ...DEFAULT_EXTENSION_CONFIG,
81
- };
82
- const GLOBAL_LOGS_DIR = getGlobalLogsDir(PI_AGENT_DIR);
83
- const extensionLogger = createPermissionSystemLogger({
84
- getConfig: () => extensionConfig,
85
- debugLogPath: join(GLOBAL_LOGS_DIR, DEBUG_LOG_FILENAME),
86
- reviewLogPath: join(GLOBAL_LOGS_DIR, REVIEW_LOG_FILENAME),
87
- ensureLogsDirectory: () =>
88
- ensurePermissionSystemLogsDirectory(GLOBAL_LOGS_DIR),
89
- });
90
- const reportedLoggingWarnings = new Set<string>();
91
- let loggingWarningReporter: ((message: string) => void) | null = null;
92
-
93
- function setExtensionConfig(config: PermissionSystemExtensionConfig): void {
94
- extensionConfig = normalizePermissionSystemConfig(config);
95
- }
96
-
97
- function setLoggingWarningReporter(
98
- reporter: ((message: string) => void) | null,
99
- ): void {
100
- loggingWarningReporter = reporter;
101
- }
102
-
103
- function reportLoggingWarning(message: string): void {
104
- if (!loggingWarningReporter || reportedLoggingWarnings.has(message)) {
105
- return;
106
- }
107
- reportedLoggingWarnings.add(message);
108
- loggingWarningReporter(message);
109
- }
110
-
111
- function writeDebugLog(
112
- event: string,
113
- details: Record<string, unknown> = {},
114
- ): void {
115
- const warning = extensionLogger.debug(event, details);
116
- if (warning) {
117
- reportLoggingWarning(warning);
118
- }
119
- }
120
-
121
- function writeReviewLog(
122
- event: string,
123
- details: Record<string, unknown> = {},
124
- ): void {
125
- const warning = extensionLogger.review(event, details);
126
- if (warning) {
127
- reportLoggingWarning(warning);
128
- }
129
- }
130
-
131
- function derivePiProjectPaths(cwd: string | undefined | null): {
132
- projectGlobalConfigPath: string;
133
- projectAgentsDir: string;
134
- } | null {
135
- if (!cwd) {
136
- return null;
137
- }
138
- return {
139
- projectGlobalConfigPath: getProjectConfigPath(cwd),
140
- projectAgentsDir: join(cwd, ".pi", "agent", "agents"),
141
- };
142
- }
143
-
144
- function createPermissionManagerForCwd(
145
- cwd: string | undefined | null,
146
- ): PermissionManager {
147
- const agentDir = getAgentDir();
148
- const projectPaths = derivePiProjectPaths(cwd);
149
- return new PermissionManager({
150
- globalConfigPath: getGlobalConfigPath(agentDir),
151
- projectGlobalConfigPath: projectPaths?.projectGlobalConfigPath,
152
- projectAgentsDir: projectPaths?.projectAgentsDir,
153
- });
154
- }
155
-
156
32
  export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
157
- let permissionManager = new PermissionManager();
158
- const sessionApprovalCache = new SessionApprovalCache();
159
- let activeSkillEntries: SkillPromptEntry[] = [];
160
- let lastKnownActiveAgentName: string | null = null;
161
- let lastActiveToolsCacheKey: string | null = null;
162
- let lastPromptStateCacheKey: string | null = null;
163
- let permissionForwardingContext: ExtensionContext | null = null;
164
- let permissionForwardingTimer: NodeJS.Timeout | null = null;
165
- let isProcessingForwardedRequests = false;
166
- let runtimeContext: ExtensionContext | null = null;
167
- let lastConfigWarning: string | null = null;
168
-
169
- const notifyWarning = (message: string): void => {
170
- if (!runtimeContext?.hasUI) {
171
- return;
172
- }
173
- runtimeContext.ui.notify(message, "warning");
174
- };
175
-
176
- const refreshExtensionConfig = (ctx?: ExtensionContext): void => {
177
- if (ctx) {
178
- runtimeContext = ctx;
179
- }
180
- const cwd = runtimeContext?.cwd ?? null;
181
- const agentDir = getAgentDir();
182
- const mergeResult = loadAndMergeConfigs(
183
- agentDir,
184
- cwd ?? "",
185
- EXTENSION_ROOT,
186
- );
187
- const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
188
- setExtensionConfig(runtimeConfig);
189
-
190
- if (runtimeContext?.hasUI) {
191
- syncPermissionSystemStatus(runtimeContext, runtimeConfig);
192
- }
193
-
194
- const warning =
195
- mergeResult.issues.length > 0 ? mergeResult.issues.join("\n") : undefined;
196
- if (warning && warning !== lastConfigWarning) {
197
- lastConfigWarning = warning;
198
- notifyWarning(warning);
199
- } else if (!warning) {
200
- lastConfigWarning = null;
201
- }
202
-
203
- writeDebugLog("config.loaded", {
204
- warning: warning ?? null,
205
- debugLog: runtimeConfig.debugLog,
206
- permissionReviewLog: runtimeConfig.permissionReviewLog,
207
- yoloMode: runtimeConfig.yoloMode,
208
- });
209
- };
210
-
211
- const saveExtensionConfig = (
212
- next: PermissionSystemExtensionConfig,
213
- ctx: ExtensionCommandContext,
214
- ): void => {
215
- const normalized = normalizePermissionSystemConfig(next);
216
- const globalPath = getGlobalConfigPath(getAgentDir());
217
-
218
- const existing = loadUnifiedConfig(globalPath);
219
- const merged = {
220
- ...existing.config,
221
- debugLog: normalized.debugLog,
222
- permissionReviewLog: normalized.permissionReviewLog,
223
- yoloMode: normalized.yoloMode,
224
- };
225
-
226
- const tmpPath = `${globalPath}.tmp`;
227
- try {
228
- mkdirSync(dirname(globalPath), { recursive: true });
229
- writeFileSync(tmpPath, `${JSON.stringify(merged, null, 2)}\n`, "utf-8");
230
- renameSync(tmpPath, globalPath);
231
- } catch (error) {
232
- try {
233
- if (existsSync(tmpPath)) {
234
- unlinkSync(tmpPath);
235
- }
236
- } catch {
237
- // Ignore cleanup failures.
238
- }
239
- const message = error instanceof Error ? error.message : String(error);
240
- ctx.ui.notify(
241
- `Failed to save permission-system config at '${globalPath}': ${message}`,
242
- "error",
243
- );
244
- return;
245
- }
246
-
247
- setExtensionConfig(normalized);
248
- syncPermissionSystemStatus(ctx, normalized);
249
- lastConfigWarning = null;
250
-
251
- writeDebugLog("config.saved", {
252
- debugLog: normalized.debugLog,
253
- permissionReviewLog: normalized.permissionReviewLog,
254
- yoloMode: normalized.yoloMode,
255
- });
256
- };
257
-
258
- setLoggingWarningReporter(notifyWarning);
259
- setForwardedPermissionLogger({ writeReviewLog, writeDebugLog });
33
+ const runtime = createExtensionRuntime();
260
34
 
261
35
  const forwardingDeps: PermissionForwardingDeps = {
262
- forwardingDir: PERMISSION_FORWARDING_DIR,
263
- subagentSessionsDir: SUBAGENT_SESSIONS_DIR,
264
- writeReviewLog,
36
+ forwardingDir: runtime.forwardingDir,
37
+ subagentSessionsDir: runtime.subagentSessionsDir,
38
+ logger: {
39
+ writeReviewLog: runtime.writeReviewLog.bind(runtime),
40
+ writeDebugLog: runtime.writeDebugLog.bind(runtime),
41
+ },
42
+ writeReviewLog: runtime.writeReviewLog.bind(runtime),
265
43
  requestPermissionDecisionFromUi,
266
44
  shouldAutoApprove: () =>
267
- shouldAutoApprovePermissionState("ask", extensionConfig),
45
+ shouldAutoApprovePermissionState("ask", runtime.config),
268
46
  };
269
47
 
270
- refreshExtensionConfig();
48
+ refreshExtensionConfig(runtime);
271
49
  registerPermissionSystemCommand(pi, {
272
- getConfig: () => extensionConfig,
273
- setConfig: saveExtensionConfig,
274
- getConfigPath: () => getGlobalConfigPath(getAgentDir()),
50
+ getConfig: () => runtime.config,
51
+ setConfig: (next, ctx) => saveExtensionConfig(runtime, next, ctx),
52
+ getConfigPath: () => getGlobalConfigPath(runtime.agentDir),
275
53
  });
276
54
 
277
- const stopForwardedPermissionPolling = (): void => {
278
- if (permissionForwardingTimer) {
279
- clearInterval(permissionForwardingTimer);
280
- permissionForwardingTimer = null;
281
- }
282
- permissionForwardingContext = null;
283
- isProcessingForwardedRequests = false;
284
- };
285
-
286
- const startForwardedPermissionPolling = (ctx: ExtensionContext): void => {
287
- if (!ctx.hasUI || isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR)) {
288
- stopForwardedPermissionPolling();
289
- return;
290
- }
291
- permissionForwardingContext = ctx;
292
- if (permissionForwardingTimer) {
293
- return;
294
- }
295
- permissionForwardingTimer = setInterval(() => {
296
- if (!permissionForwardingContext || isProcessingForwardedRequests) {
297
- return;
298
- }
299
- isProcessingForwardedRequests = true;
300
- void processForwardedPermissionRequests(
301
- permissionForwardingContext,
302
- forwardingDeps,
303
- ).finally(() => {
304
- isProcessingForwardedRequests = false;
305
- });
306
- }, PERMISSION_FORWARDING_POLL_INTERVAL_MS);
307
- };
308
-
309
55
  const createPermissionRequestId = (prefix: string): string =>
310
56
  `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
311
57
 
312
- const reviewPermissionDecision = (
313
- event: string,
314
- details: PromptPermissionDetails & {
315
- resolution?: string;
316
- denialReason?: string;
317
- },
318
- ): void => {
319
- writeReviewLog(event, {
320
- requestId: details.requestId,
321
- source: details.source,
322
- agentName: details.agentName,
323
- message: details.message,
324
- toolCallId: details.toolCallId ?? null,
325
- toolName: details.toolName ?? null,
326
- skillName: details.skillName ?? null,
327
- path: details.path ?? null,
328
- command: details.command ?? null,
329
- target: details.target ?? null,
330
- toolInputPreview: details.toolInputPreview ?? null,
331
- resolution: details.resolution ?? null,
332
- denialReason: details.denialReason ?? null,
333
- });
334
- };
335
-
336
- const promptPermission = async (
337
- ctx: ExtensionContext,
338
- details: PromptPermissionDetails,
339
- ): Promise<PermissionPromptDecision> => {
340
- if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
341
- reviewPermissionDecision("permission_request.auto_approved", details);
342
- return { approved: true, state: "approved" };
343
- }
344
- reviewPermissionDecision("permission_request.waiting", details);
345
- const decision = await confirmPermission(
346
- ctx,
347
- details.message,
348
- forwardingDeps,
349
- );
350
- reviewPermissionDecision(
351
- decision.approved
352
- ? "permission_request.approved"
353
- : "permission_request.denied",
354
- {
355
- ...details,
356
- resolution: decision.state,
357
- denialReason: decision.denialReason,
358
- },
359
- );
360
- return decision;
361
- };
362
-
363
- const resolveAgentName = (
364
- ctx: ExtensionContext,
365
- systemPrompt?: string,
366
- ): string | null => {
367
- const fromSession = getActiveAgentName(ctx);
368
- if (fromSession) {
369
- lastKnownActiveAgentName = fromSession;
370
- return fromSession;
371
- }
372
- const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
373
- if (fromSystemPrompt) {
374
- lastKnownActiveAgentName = fromSystemPrompt;
375
- return fromSystemPrompt;
376
- }
377
- return lastKnownActiveAgentName;
378
- };
379
-
380
- const logResolvedConfigPaths = (): void => {
381
- const policyPaths = permissionManager.getResolvedPolicyPaths();
382
- const cwd = runtimeContext?.cwd ?? null;
383
- const agentDir = getAgentDir();
384
- const legacyGlobalPolicyDetected = existsSync(
385
- getLegacyGlobalPolicyPath(agentDir),
386
- );
387
- const legacyProjectPolicyDetected = cwd
388
- ? existsSync(getLegacyProjectPolicyPath(cwd))
389
- : false;
390
- const legacyExtConfigPath = getLegacyExtensionConfigPath(EXTENSION_ROOT);
391
- const newGlobalPath = getGlobalConfigPath(agentDir);
392
- const legacyExtensionConfigDetected =
393
- normalize(legacyExtConfigPath) !== normalize(newGlobalPath) &&
394
- existsSync(legacyExtConfigPath);
395
- const entry = buildResolvedConfigLogEntry({
396
- policyPaths,
397
- legacyGlobalPolicyDetected,
398
- legacyProjectPolicyDetected,
399
- legacyExtensionConfigDetected,
400
- });
401
- writeReviewLog(
402
- "config.resolved",
403
- entry as unknown as Record<string, unknown>,
404
- );
405
- writeDebugLog(
406
- "config.resolved",
407
- entry as unknown as Record<string, unknown>,
408
- );
409
- };
410
-
411
58
  const deps: HandlerDeps = {
412
- getPermissionManager: () => permissionManager,
413
- setPermissionManager: (pm) => {
414
- permissionManager = pm;
415
- },
416
- getRuntimeContext: () => runtimeContext,
417
- setRuntimeContext: (ctx) => {
418
- runtimeContext = ctx;
419
- },
420
- getActiveSkillEntries: () => activeSkillEntries,
421
- setActiveSkillEntries: (entries) => {
422
- activeSkillEntries = entries;
423
- },
424
- getLastKnownActiveAgentName: () => lastKnownActiveAgentName,
425
- setLastKnownActiveAgentName: (name) => {
426
- lastKnownActiveAgentName = name;
427
- },
428
- getLastActiveToolsCacheKey: () => lastActiveToolsCacheKey,
429
- setLastActiveToolsCacheKey: (key) => {
430
- lastActiveToolsCacheKey = key;
431
- },
432
- getLastPromptStateCacheKey: () => lastPromptStateCacheKey,
433
- setLastPromptStateCacheKey: (key) => {
434
- lastPromptStateCacheKey = key;
435
- },
436
- sessionApprovalCache,
437
- createPermissionManagerForCwd,
438
- refreshExtensionConfig,
439
- notifyWarning,
440
- logResolvedConfigPaths,
441
- resolveAgentName,
59
+ runtime,
60
+ createPermissionManagerForCwd: (cwd) =>
61
+ createPermissionManagerForCwd(runtime.agentDir, cwd),
62
+ refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
63
+ notifyWarning: (message) =>
64
+ runtime.runtimeContext?.ui.notify(message, "warning"),
65
+ logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
66
+ resolveAgentName: (ctx, systemPrompt) =>
67
+ resolveAgentName(runtime, ctx, systemPrompt),
442
68
  canRequestPermissionConfirmation: (ctx) =>
443
69
  canResolveAskPermissionRequest({
444
- config: extensionConfig,
70
+ config: runtime.config,
445
71
  hasUI: ctx.hasUI,
446
- isSubagent: isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR),
72
+ isSubagent: isSubagentExecutionContext(
73
+ ctx,
74
+ runtime.subagentSessionsDir,
75
+ ),
447
76
  }),
448
- promptPermission,
77
+ promptPermission: (ctx, details) =>
78
+ promptPermission(runtime, forwardingDeps, ctx, details),
449
79
  createPermissionRequestId,
450
- startForwardedPermissionPolling,
451
- stopForwardedPermissionPolling,
452
- writeReviewLog,
453
- writeDebugLog,
80
+ startForwardedPermissionPolling: (ctx) =>
81
+ startForwardedPermissionPolling(runtime, forwardingDeps, ctx),
82
+ stopForwardedPermissionPolling: () =>
83
+ stopForwardedPermissionPolling(runtime),
454
84
  getAllTools: () => pi.getAllTools(),
455
85
  setActiveTools: (names) => pi.setActiveTools(names),
456
86
  };
@@ -0,0 +1,70 @@
1
+ import type { Rule, Ruleset } from "./rule";
2
+ import type { PermissionState } from "./types";
3
+
4
+ /**
5
+ * Subset of UnifiedPermissionConfig covering only policy fields.
6
+ * Used as the input shape for normalizeConfig().
7
+ */
8
+ export interface NormalizableConfig {
9
+ tools?: Record<string, PermissionState>;
10
+ bash?: Record<string, PermissionState>;
11
+ mcp?: Record<string, PermissionState>;
12
+ skills?: Record<string, PermissionState>;
13
+ special?: Record<string, PermissionState>;
14
+ }
15
+
16
+ /**
17
+ * Keys in the `tools` map that serve as fallback defaults for their
18
+ * respective pattern-based surfaces rather than as tool-level rules.
19
+ *
20
+ * `tools.bash` sets the bash default (fallback when no bash pattern matches).
21
+ * `tools.mcp` sets the tool-level MCP fallback.
22
+ *
23
+ * These are NOT normalized into the Ruleset — they are extracted by the
24
+ * caller and handled as separate fallbacks to preserve the semantic that
25
+ * specific bash/mcp patterns always have priority.
26
+ */
27
+ export const TOOL_SURFACE_OVERRIDE_KEYS: ReadonlySet<string> = new Set([
28
+ "bash",
29
+ "mcp",
30
+ ]);
31
+
32
+ /**
33
+ * Convert the on-disk config shape into a flat Ruleset.
34
+ *
35
+ * Ordering within a scope:
36
+ * 1. tools entries (tool-name-as-surface, pattern "*") — excluding bash/mcp
37
+ * 2. bash entries (surface "bash", pattern = command glob)
38
+ * 3. mcp entries (surface "mcp", pattern = target glob)
39
+ * 4. skills entries (surface "skill", pattern = skill glob)
40
+ * 5. special entries (surface "special", pattern = key name)
41
+ *
42
+ * `tools.bash` and `tools.mcp` are excluded — see TOOL_SURFACE_OVERRIDE_KEYS.
43
+ * `defaultPolicy` is NOT included — handled separately by the caller.
44
+ */
45
+ export function normalizeConfig(config: NormalizableConfig): Ruleset {
46
+ const rules: Rule[] = [];
47
+
48
+ for (const [name, action] of Object.entries(config.tools ?? {})) {
49
+ if (TOOL_SURFACE_OVERRIDE_KEYS.has(name)) continue;
50
+ rules.push({ surface: name, pattern: "*", action });
51
+ }
52
+
53
+ for (const [pattern, action] of Object.entries(config.bash ?? {})) {
54
+ rules.push({ surface: "bash", pattern, action });
55
+ }
56
+
57
+ for (const [pattern, action] of Object.entries(config.mcp ?? {})) {
58
+ rules.push({ surface: "mcp", pattern, action });
59
+ }
60
+
61
+ for (const [pattern, action] of Object.entries(config.skills ?? {})) {
62
+ rules.push({ surface: "skill", pattern, action });
63
+ }
64
+
65
+ for (const [name, action] of Object.entries(config.special ?? {})) {
66
+ rules.push({ surface: "special", pattern: name, action });
67
+ }
68
+
69
+ return rules;
70
+ }