@gotgenes/pi-permission-system 5.9.0 → 5.11.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,281 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+
3
+ import {
4
+ getActiveAgentName,
5
+ getActiveAgentNameFromSystemPrompt,
6
+ } from "./active-agent";
7
+ import type { PermissionSystemExtensionConfig } from "./extension-config";
8
+ import type { ExtensionPaths } from "./extension-paths";
9
+ import type { ForwardingController } from "./forwarding-manager";
10
+ import type { PermissionPromptDecision } from "./permission-dialog";
11
+ import type { PermissionManager } from "./permission-manager";
12
+ import type { PromptPermissionDetails } from "./permission-prompter";
13
+ import type { Rule } from "./rule";
14
+ import { createPermissionManagerForCwd } from "./runtime";
15
+ import type { SessionLogger } from "./session-logger";
16
+ import { SessionRules } from "./session-rules";
17
+ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
18
+ import type { PermissionCheckResult, PermissionState } from "./types";
19
+
20
+ /**
21
+ * Runtime operations that `PermissionSession` delegates to but does not own.
22
+ *
23
+ * Injected at construction time from the composition root (`index.ts`),
24
+ * where the `ExtensionRuntime` is available.
25
+ */
26
+ export interface PermissionSessionRuntimeDeps {
27
+ /** Reload merged config from disk; optionally update the stored runtime context. */
28
+ refreshExtensionConfig(ctx?: ExtensionContext): void;
29
+ /** Write the resolved config path set to the review and debug logs. */
30
+ logResolvedConfigPaths(): void;
31
+ /** Read current extension config (called at query time). */
32
+ getConfig(): PermissionSystemExtensionConfig;
33
+ /** Whether the current context can show an interactive permission prompt. */
34
+ canRequestPermissionConfirmation(ctx: ExtensionContext): boolean;
35
+ /** Prompt the user for a permission decision, log the outcome, and return it. */
36
+ promptPermission(
37
+ ctx: ExtensionContext,
38
+ details: PromptPermissionDetails,
39
+ ): Promise<PermissionPromptDecision>;
40
+ }
41
+
42
+ /**
43
+ * Encapsulates all mutable session state and exposes operations instead of
44
+ * fields.
45
+ *
46
+ * Replaces the `SessionState` interface + scattered handler field mutations
47
+ * with a single class that owns the `PermissionManager`, `SessionRules`,
48
+ * cache keys, skill entries, and runtime context.
49
+ *
50
+ * Constructor deps:
51
+ * - `ExtensionPaths` — immutable path constants
52
+ * - `SessionLogger` — debug + review + warn
53
+ * - `ForwardingController` — polling lifecycle
54
+ * - `PermissionSessionRuntimeDeps` — config refresh + log delegates
55
+ */
56
+ export class PermissionSession {
57
+ private context: ExtensionContext | null = null;
58
+ private permissionManager: PermissionManager;
59
+ private readonly sessionRules = new SessionRules();
60
+ private skillEntries: SkillPromptEntry[] = [];
61
+ private knownAgentName: string | null = null;
62
+ private toolsCacheKey: string | null = null;
63
+ private promptCacheKey: string | null = null;
64
+
65
+ constructor(
66
+ private readonly paths: ExtensionPaths,
67
+ readonly logger: SessionLogger,
68
+ private readonly forwarding: ForwardingController,
69
+ private readonly runtimeDeps: PermissionSessionRuntimeDeps,
70
+ ) {
71
+ this.permissionManager = createPermissionManagerForCwd(
72
+ paths.agentDir,
73
+ undefined,
74
+ );
75
+ }
76
+
77
+ // ── Context lifecycle ──────────────────────────────────────────────────
78
+
79
+ /** Store the current extension context and start forwarding. */
80
+ activate(ctx: ExtensionContext): void {
81
+ this.context = ctx;
82
+ this.forwarding.start(ctx);
83
+ }
84
+
85
+ /** Clear the context and stop forwarding. */
86
+ deactivate(): void {
87
+ this.context = null;
88
+ this.forwarding.stop();
89
+ }
90
+
91
+ /** Return the current runtime context, or null if not activated. */
92
+ getRuntimeContext(): ExtensionContext | null {
93
+ return this.context;
94
+ }
95
+
96
+ // ── Permission checking (delegates to PermissionManager) ───────────────
97
+
98
+ checkPermission(
99
+ surface: string,
100
+ input: unknown,
101
+ agentName?: string,
102
+ sessionRules?: Rule[],
103
+ ): PermissionCheckResult {
104
+ return this.permissionManager.checkPermission(
105
+ surface,
106
+ input,
107
+ agentName,
108
+ sessionRules,
109
+ );
110
+ }
111
+
112
+ getToolPermission(toolName: string, agentName?: string): PermissionState {
113
+ return this.permissionManager.getToolPermission(toolName, agentName);
114
+ }
115
+
116
+ getConfigIssues(agentName?: string): string[] {
117
+ return this.permissionManager.getConfigIssues(agentName);
118
+ }
119
+
120
+ getPolicyCacheStamp(agentName?: string): string {
121
+ return this.permissionManager.getPolicyCacheStamp(agentName);
122
+ }
123
+
124
+ // ── Session rules (delegates to SessionRules) ──────────────────────────
125
+
126
+ getSessionRuleset(): Rule[] {
127
+ return this.sessionRules.getRuleset();
128
+ }
129
+
130
+ approveSessionRule(surface: string, pattern: string): void {
131
+ this.sessionRules.approve(surface, pattern);
132
+ }
133
+
134
+ // ── Session lifecycle ────────────────────────────────────────────────────
135
+
136
+ /**
137
+ * Reset all mutable state for a new session.
138
+ *
139
+ * Creates a fresh PermissionManager scoped to `ctx.cwd`, clears caches,
140
+ * skill entries, and activates the new context.
141
+ */
142
+ resetForNewSession(ctx: ExtensionContext): void {
143
+ this.permissionManager = createPermissionManagerForCwd(
144
+ this.paths.agentDir,
145
+ ctx.cwd,
146
+ );
147
+ this.skillEntries = [];
148
+ this.toolsCacheKey = null;
149
+ this.promptCacheKey = null;
150
+ this.activate(ctx);
151
+ }
152
+
153
+ /**
154
+ * Shut down the session: clear rules, caches, skill entries, and
155
+ * deactivate context + forwarding.
156
+ */
157
+ shutdown(): void {
158
+ this.sessionRules.clear();
159
+ this.skillEntries = [];
160
+ this.toolsCacheKey = null;
161
+ this.promptCacheKey = null;
162
+ this.deactivate();
163
+ }
164
+
165
+ /**
166
+ * Reload permission manager and clear caches for the current context.
167
+ * Used on config reload (e.g. `resources_discover` with reason "reload").
168
+ */
169
+ reload(): void {
170
+ this.permissionManager = createPermissionManagerForCwd(
171
+ this.paths.agentDir,
172
+ this.context?.cwd,
173
+ );
174
+ this.skillEntries = [];
175
+ this.toolsCacheKey = null;
176
+ this.promptCacheKey = null;
177
+ }
178
+
179
+ // ── Agent-start caching ────────────────────────────────────────────────
180
+
181
+ shouldUpdateActiveTools(cacheKey: string): boolean {
182
+ return this.toolsCacheKey !== cacheKey;
183
+ }
184
+
185
+ commitActiveToolsCacheKey(cacheKey: string): void {
186
+ this.toolsCacheKey = cacheKey;
187
+ }
188
+
189
+ shouldUpdatePromptState(cacheKey: string): boolean {
190
+ return this.promptCacheKey !== cacheKey;
191
+ }
192
+
193
+ commitPromptStateCacheKey(cacheKey: string): void {
194
+ this.promptCacheKey = cacheKey;
195
+ }
196
+
197
+ // ── Skill entries ──────────────────────────────────────────────────────
198
+
199
+ getActiveSkillEntries(): SkillPromptEntry[] {
200
+ return this.skillEntries;
201
+ }
202
+
203
+ setActiveSkillEntries(entries: SkillPromptEntry[]): void {
204
+ this.skillEntries = entries;
205
+ }
206
+
207
+ // ── Agent name ─────────────────────────────────────────────────────────
208
+
209
+ /**
210
+ * Resolve the active agent name from the session context, system prompt,
211
+ * or last known name. Updates lastKnownActiveAgentName as a side effect.
212
+ */
213
+ resolveAgentName(
214
+ ctx: ExtensionContext,
215
+ systemPrompt?: string,
216
+ ): string | null {
217
+ const fromSession = getActiveAgentName(ctx);
218
+ if (fromSession) {
219
+ this.knownAgentName = fromSession;
220
+ return fromSession;
221
+ }
222
+ const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
223
+ if (fromSystemPrompt) {
224
+ this.knownAgentName = fromSystemPrompt;
225
+ return fromSystemPrompt;
226
+ }
227
+ return this.knownAgentName;
228
+ }
229
+
230
+ get lastKnownActiveAgentName(): string | null {
231
+ return this.knownAgentName;
232
+ }
233
+
234
+ // ── Config ─────────────────────────────────────────────────────────────
235
+
236
+ /** Reload merged config from disk; optionally update the stored runtime context. */
237
+ refreshConfig(ctx?: ExtensionContext): void {
238
+ this.runtimeDeps.refreshExtensionConfig(ctx);
239
+ }
240
+
241
+ /** Write the resolved config path set to the review and debug logs. */
242
+ logResolvedConfigPaths(): void {
243
+ this.runtimeDeps.logResolvedConfigPaths();
244
+ }
245
+
246
+ /** Read current extension config. */
247
+ get config(): PermissionSystemExtensionConfig {
248
+ return this.runtimeDeps.getConfig();
249
+ }
250
+
251
+ // ── Infrastructure paths ───────────────────────────────────────────────
252
+
253
+ getInfrastructureDirs(): readonly string[] {
254
+ return this.paths.piInfrastructureDirs;
255
+ }
256
+
257
+ /** Config-derived infrastructure read paths (current at call time). */
258
+ getInfrastructureReadPaths(): string[] {
259
+ return this.config.piInfrastructureReadPaths ?? [];
260
+ }
261
+
262
+ // ── Prompting ──────────────────────────────────────────────────────────
263
+
264
+ /** Whether the current context can show an interactive permission prompt. */
265
+ canPrompt(ctx: ExtensionContext): boolean {
266
+ return this.runtimeDeps.canRequestPermissionConfirmation(ctx);
267
+ }
268
+
269
+ /** Prompt the user for a permission decision, log the outcome, and return it. */
270
+ prompt(
271
+ ctx: ExtensionContext,
272
+ details: PromptPermissionDetails,
273
+ ): Promise<PermissionPromptDecision> {
274
+ return this.runtimeDeps.promptPermission(ctx, details);
275
+ }
276
+
277
+ /** Generate a unique ID for a permission request. */
278
+ createPermissionRequestId(prefix: string): string {
279
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
280
+ }
281
+ }
package/src/runtime.ts CHANGED
@@ -12,10 +12,6 @@ import {
12
12
  getAgentDir,
13
13
  } from "@mariozechner/pi-coding-agent";
14
14
 
15
- import {
16
- getActiveAgentName,
17
- getActiveAgentNameFromSystemPrompt,
18
- } from "./active-agent";
19
15
  import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
20
16
  import {
21
17
  DEBUG_LOG_FILENAME,
@@ -45,11 +41,12 @@ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
45
41
  import { syncPermissionSystemStatus } from "./status";
46
42
 
47
43
  /**
48
- * Mutable session state — the subset of ExtensionRuntime that handlers
49
- * read and write. Lifecycle handlers reset fields here on session
50
- * start/shutdown; gate adapters read permissionManager and sessionRules.
44
+ * Mutable session state — the subset of ExtensionRuntime that holds
45
+ * per-session fields. `PermissionSession` now owns these for handler
46
+ * use; this interface remains so `ExtensionRuntime` can still serve
47
+ * as the internal composition root (config-modal, RPC handlers).
51
48
  */
52
- export interface SessionState {
49
+ interface SessionState {
53
50
  runtimeContext: ExtensionContext | null;
54
51
  permissionManager: PermissionManager;
55
52
  readonly sessionRules: SessionRules;
@@ -209,28 +206,6 @@ export function saveExtensionConfig(
209
206
  });
210
207
  }
211
208
 
212
- /**
213
- * Resolve the active agent name from the Pi session, system prompt, or last
214
- * known name. Updates `runtime.lastKnownActiveAgentName` as a side effect.
215
- */
216
- export function resolveAgentName(
217
- runtime: ExtensionRuntime,
218
- ctx: ExtensionContext,
219
- systemPrompt?: string,
220
- ): string | null {
221
- const fromSession = getActiveAgentName(ctx);
222
- if (fromSession) {
223
- runtime.lastKnownActiveAgentName = fromSession;
224
- return fromSession;
225
- }
226
- const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
227
- if (fromSystemPrompt) {
228
- runtime.lastKnownActiveAgentName = fromSystemPrompt;
229
- return fromSystemPrompt;
230
- }
231
- return runtime.lastKnownActiveAgentName;
232
- }
233
-
234
209
  /**
235
210
  * Write the resolved config path set (global, project, legacy) to the review
236
211
  * and debug logs.
@@ -3,7 +3,7 @@ import type { ExtensionRuntime } from "./runtime";
3
3
  /**
4
4
  * Unified logging + notification surface for handler deps.
5
5
  *
6
- * Replaces three separate HandlerDeps fields (`writeDebugLog`,
6
+ * Replaces three separate logging fields (`writeDebugLog`,
7
7
  * `writeReviewLog`, `notifyWarning`) with a single typed collaborator.
8
8
  * This is an intermediate abstraction on the path to PermissionSession (#129).
9
9
  */
@@ -4,8 +4,19 @@ import {
4
4
  isPathWithinDirectory,
5
5
  normalizePathForComparison,
6
6
  } from "./path-utils";
7
- import type { PermissionManager } from "./permission-manager";
8
- import type { PermissionState } from "./types";
7
+ import type { PermissionCheckResult, PermissionState } from "./types";
8
+
9
+ /**
10
+ * Narrow interface for the permission checker used by skill prompt resolution.
11
+ * Both `PermissionManager` and `PermissionSession` satisfy this structurally.
12
+ */
13
+ export interface SkillPermissionChecker {
14
+ checkPermission(
15
+ surface: string,
16
+ input: unknown,
17
+ agentName?: string,
18
+ ): PermissionCheckResult;
19
+ }
9
20
 
10
21
  const AVAILABLE_SKILLS_OPEN_TAG = "<available_skills>";
11
22
  const AVAILABLE_SKILLS_CLOSE_TAG = "</available_skills>";
@@ -148,7 +159,7 @@ export function parseAllSkillPromptSections(
148
159
 
149
160
  function resolvePermissionState(
150
161
  skillName: string,
151
- permissionManager: PermissionManager,
162
+ permissionManager: SkillPermissionChecker,
152
163
  agentName: string | null,
153
164
  cache: Map<string, PermissionState>,
154
165
  ): PermissionState {
@@ -205,7 +216,7 @@ function removePromptRange(prompt: string, start: number, end: number): string {
205
216
 
206
217
  export function resolveSkillPromptEntries(
207
218
  prompt: string,
208
- permissionManager: PermissionManager,
219
+ permissionManager: SkillPermissionChecker,
209
220
  agentName: string | null,
210
221
  cwd: string,
211
222
  ): { prompt: string; entries: SkillPromptEntry[] } {
@@ -1,5 +1,11 @@
1
1
  import { getNonEmptyString, toRecord } from "./common";
2
2
 
3
+ /** Narrow interface for the Pi tool API subset used by handler classes. */
4
+ export interface ToolRegistry {
5
+ getAll(): unknown[];
6
+ setActive(names: string[]): void;
7
+ }
8
+
3
9
  export type ToolRegistrationCheckResult =
4
10
  | {
5
11
  status: "missing-tool-name";