@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.
- package/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/src/forwarding-manager.ts +1 -1
- package/src/handlers/before-agent-start.ts +76 -76
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/handlers/index.ts +6 -15
- package/src/handlers/lifecycle.ts +55 -59
- package/src/handlers/permission-gate-handler.ts +346 -0
- package/src/index.ts +46 -54
- package/src/permission-prompter.ts +23 -6
- package/src/permission-session.ts +281 -0
- package/src/runtime.ts +5 -30
- package/src/session-logger.ts +1 -1
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/src/tool-registry.ts +6 -0
- package/tests/handlers/before-agent-start.test.ts +116 -167
- package/tests/handlers/input-events.test.ts +87 -92
- package/tests/handlers/input.test.ts +98 -128
- package/tests/handlers/lifecycle.test.ts +97 -227
- package/tests/handlers/tool-call-events.test.ts +146 -166
- package/tests/handlers/tool-call.test.ts +102 -97
- package/tests/permission-prompter.test.ts +1 -1
- package/tests/permission-session.test.ts +607 -0
- package/tests/runtime.test.ts +2 -77
- package/src/handlers/input.ts +0 -126
- package/src/handlers/tool-call.ts +0 -210
- package/src/handlers/types.ts +0 -90
|
@@ -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
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
|
|
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.
|
package/src/session-logger.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
8
|
-
|
|
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:
|
|
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:
|
|
219
|
+
permissionManager: SkillPermissionChecker,
|
|
209
220
|
agentName: string | null,
|
|
210
221
|
cwd: string,
|
|
211
222
|
): { prompt: string; entries: SkillPromptEntry[] } {
|
package/src/tool-registry.ts
CHANGED
|
@@ -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";
|