@gotgenes/pi-permission-system 5.10.0 → 5.11.1

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 (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/package.json +5 -5
  3. package/src/active-agent.ts +1 -1
  4. package/src/config-modal.ts +2 -2
  5. package/src/forwarded-permissions/polling.ts +1 -1
  6. package/src/forwarding-manager.ts +2 -2
  7. package/src/handlers/before-agent-start.ts +74 -61
  8. package/src/handlers/gates/descriptor.ts +1 -1
  9. package/src/handlers/index.ts +6 -15
  10. package/src/handlers/lifecycle.ts +55 -43
  11. package/src/handlers/permission-gate-handler.ts +346 -0
  12. package/src/index.ts +34 -39
  13. package/src/permission-event-rpc.ts +1 -1
  14. package/src/permission-prompter.ts +24 -7
  15. package/src/permission-session.ts +30 -1
  16. package/src/policy-loader.ts +1 -1
  17. package/src/runtime.ts +1 -1
  18. package/src/session-logger.ts +1 -1
  19. package/src/status.ts +1 -1
  20. package/src/subagent-context.ts +1 -1
  21. package/src/tool-registry.ts +6 -0
  22. package/tests/active-agent.test.ts +1 -1
  23. package/tests/config-modal.test.ts +2 -2
  24. package/tests/forwarding-manager.test.ts +1 -1
  25. package/tests/handlers/before-agent-start.test.ts +73 -93
  26. package/tests/handlers/gates/skill-read.test.ts +2 -2
  27. package/tests/handlers/input-events.test.ts +71 -64
  28. package/tests/handlers/input.test.ts +86 -84
  29. package/tests/handlers/lifecycle.test.ts +61 -73
  30. package/tests/handlers/tool-call-events.test.ts +129 -123
  31. package/tests/handlers/tool-call.test.ts +87 -61
  32. package/tests/permission-event-rpc.test.ts +1 -1
  33. package/tests/permission-prompter.test.ts +2 -2
  34. package/tests/permission-session.test.ts +62 -1
  35. package/tests/runtime.test.ts +1 -1
  36. package/tests/subagent-context.test.ts +1 -1
  37. package/src/handlers/input.ts +0 -126
  38. package/src/handlers/tool-call.ts +0 -203
  39. package/src/handlers/types.ts +0 -63
@@ -0,0 +1,346 @@
1
+ import type {
2
+ ExtensionContext,
3
+ InputEventResult,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ import { toRecord } from "../common";
7
+ import {
8
+ emitDecisionEvent,
9
+ type PermissionEventBus,
10
+ } from "../permission-events";
11
+ import { applyPermissionGate } from "../permission-gate";
12
+ import type { PromptPermissionDetails } from "../permission-prompter";
13
+ import {
14
+ formatMissingToolNameReason,
15
+ formatSkillAskPrompt,
16
+ formatUnknownToolReason,
17
+ } from "../permission-prompts";
18
+ import type { PermissionSession } from "../permission-session";
19
+ import {
20
+ checkRequestedToolRegistration,
21
+ getToolNameFromValue,
22
+ type ToolRegistry,
23
+ } from "../tool-registry";
24
+ import { describeBashExternalDirectoryGate } from "./gates/bash-external-directory";
25
+ import type { GateRunnerDeps } from "./gates/descriptor";
26
+ import { isGateBypass } from "./gates/descriptor";
27
+ import { describeExternalDirectoryGate } from "./gates/external-directory";
28
+ import { runGateCheck } from "./gates/runner";
29
+ import { describeSkillReadGate } from "./gates/skill-read";
30
+ import { describeToolGate } from "./gates/tool";
31
+ import type { ToolCallContext } from "./gates/types";
32
+
33
+ /** Minimal subset of InputEvent used by handleInput. */
34
+ interface InputPayload {
35
+ text: string;
36
+ }
37
+
38
+ /**
39
+ * Handles permission gate events: tool_call and input.
40
+ *
41
+ * Constructor deps:
42
+ * - `session` — encapsulates all mutable session state and permission operations
43
+ * - `events` — event bus for emitting permissions:decision broadcasts
44
+ * - `toolRegistry` — Pi tool API subset (getAll + setActive)
45
+ */
46
+ export class PermissionGateHandler {
47
+ constructor(
48
+ private readonly session: PermissionSession,
49
+ private readonly events: PermissionEventBus,
50
+ private readonly toolRegistry: ToolRegistry,
51
+ ) {}
52
+
53
+ async handleToolCall(
54
+ event: unknown,
55
+ ctx: ExtensionContext,
56
+ ): Promise<{ block?: true; reason?: string }> {
57
+ const { session } = this;
58
+ session.activate(ctx);
59
+
60
+ const agentName = session.resolveAgentName(ctx);
61
+ const toolName = getToolNameFromValue(event);
62
+
63
+ if (!toolName) {
64
+ return { block: true, reason: formatMissingToolNameReason() };
65
+ }
66
+
67
+ const registrationCheck = checkRequestedToolRegistration(
68
+ toolName,
69
+ this.toolRegistry.getAll(),
70
+ );
71
+ if (registrationCheck.status === "missing-tool-name") {
72
+ return { block: true, reason: formatMissingToolNameReason() };
73
+ }
74
+
75
+ if (registrationCheck.status === "unregistered") {
76
+ return {
77
+ block: true,
78
+ reason: formatUnknownToolReason(
79
+ registrationCheck.requestedToolName,
80
+ registrationCheck.availableToolNames,
81
+ ),
82
+ };
83
+ }
84
+
85
+ const input = getEventInput(event);
86
+ const toolCallId =
87
+ typeof (event as Record<string, unknown>).toolCallId === "string"
88
+ ? ((event as Record<string, unknown>).toolCallId as string)
89
+ : "";
90
+
91
+ const tcc: ToolCallContext = {
92
+ toolName,
93
+ agentName,
94
+ input,
95
+ toolCallId,
96
+ cwd: ctx.cwd,
97
+ };
98
+
99
+ // ── Shared gate adapter closures ─────────────────────────────────────
100
+ const canConfirm = () => session.canPrompt(ctx);
101
+ const promptPermission = (details: PromptPermissionDetails) =>
102
+ session.prompt(ctx, details);
103
+ const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
104
+ emitDecisionEvent(this.events, e);
105
+ const writeReviewLog = session.logger.review;
106
+ const checkPermission: GateRunnerDeps["checkPermission"] = (
107
+ surface,
108
+ input,
109
+ agent,
110
+ sessionRules,
111
+ ) => session.checkPermission(surface, input, agent, sessionRules);
112
+ const getSessionRuleset = () => session.getSessionRuleset();
113
+ const approveSessionRule = (surface: string, pattern: string) =>
114
+ session.approveSessionRule(surface, pattern);
115
+
116
+ // ── Shared runner deps (built once, reused for all gates) ────────────
117
+ const runnerDeps: GateRunnerDeps = {
118
+ checkPermission,
119
+ getSessionRuleset,
120
+ approveSessionRule,
121
+ writeReviewLog,
122
+ emitDecision,
123
+ canConfirm,
124
+ promptPermission,
125
+ };
126
+
127
+ // ── Skill-read gate (descriptor + runner) ───────────────────────────────
128
+ const skillDescriptor = describeSkillReadGate(tcc, () =>
129
+ session.getActiveSkillEntries(),
130
+ );
131
+ if (skillDescriptor) {
132
+ const skillResult = await runGateCheck(
133
+ skillDescriptor,
134
+ tcc.agentName,
135
+ tcc.toolCallId,
136
+ runnerDeps,
137
+ );
138
+ if (skillResult.action === "block") {
139
+ return { block: true, reason: skillResult.reason };
140
+ }
141
+ }
142
+
143
+ // ── External-directory gate (descriptor + runner) ────────────────────────
144
+ const infraDirs = [
145
+ ...session.getInfrastructureDirs(),
146
+ ...session.getInfrastructureReadPaths(),
147
+ ];
148
+ const extDirDesc = describeExternalDirectoryGate(tcc, infraDirs);
149
+ if (extDirDesc) {
150
+ if (isGateBypass(extDirDesc)) {
151
+ if (extDirDesc.log) {
152
+ writeReviewLog(extDirDesc.log.event, extDirDesc.log.details);
153
+ }
154
+ if (extDirDesc.decision) {
155
+ emitDecision(extDirDesc.decision);
156
+ }
157
+ } else {
158
+ const extDirResult = await runGateCheck(
159
+ extDirDesc,
160
+ tcc.agentName,
161
+ tcc.toolCallId,
162
+ runnerDeps,
163
+ );
164
+ if (extDirResult.action === "block") {
165
+ return { block: true, reason: extDirResult.reason };
166
+ }
167
+ }
168
+ }
169
+
170
+ // ── Bash external-directory gate (descriptor + runner) ───────────────────
171
+ const bashExtDesc = await describeBashExternalDirectoryGate(
172
+ tcc,
173
+ checkPermission,
174
+ getSessionRuleset,
175
+ );
176
+ if (bashExtDesc) {
177
+ if (isGateBypass(bashExtDesc)) {
178
+ if (bashExtDesc.log) {
179
+ writeReviewLog(bashExtDesc.log.event, bashExtDesc.log.details);
180
+ }
181
+ } else {
182
+ const bashExtResult = await runGateCheck(
183
+ bashExtDesc,
184
+ tcc.agentName,
185
+ tcc.toolCallId,
186
+ runnerDeps,
187
+ );
188
+ if (bashExtResult.action === "block") {
189
+ return { block: true, reason: bashExtResult.reason };
190
+ }
191
+ }
192
+ }
193
+
194
+ // ── Normal tool permission gate (descriptor + runner) ────────────────────
195
+ const toolCheck = checkPermission(
196
+ tcc.toolName,
197
+ tcc.input,
198
+ tcc.agentName ?? undefined,
199
+ getSessionRuleset(),
200
+ );
201
+ const toolDescriptor = describeToolGate(tcc, toolCheck);
202
+ toolDescriptor.preCheck = toolCheck;
203
+ const toolResult = await runGateCheck(
204
+ toolDescriptor,
205
+ tcc.agentName,
206
+ tcc.toolCallId,
207
+ runnerDeps,
208
+ );
209
+ if (toolResult.action === "block") {
210
+ return { block: true, reason: toolResult.reason };
211
+ }
212
+
213
+ return {};
214
+ }
215
+
216
+ async handleInput(
217
+ event: InputPayload,
218
+ ctx: ExtensionContext,
219
+ ): Promise<InputEventResult> {
220
+ const { session } = this;
221
+ session.activate(ctx);
222
+
223
+ const skillName = extractSkillNameFromInput(event.text);
224
+ if (!skillName) {
225
+ return { action: "continue" };
226
+ }
227
+
228
+ const agentName = session.resolveAgentName(ctx);
229
+ const check = session.checkPermission(
230
+ "skill",
231
+ { name: skillName },
232
+ agentName ?? undefined,
233
+ );
234
+
235
+ if (check.state === "deny" && ctx.hasUI) {
236
+ const notifyMessage = agentName
237
+ ? `Skill '${skillName}' is not permitted for agent '${agentName}'.`
238
+ : `Skill '${skillName}' is not permitted by the current skill policy.`;
239
+ ctx.ui.notify(notifyMessage, "warning");
240
+ }
241
+
242
+ const skillInputMessage = formatSkillAskPrompt(
243
+ skillName,
244
+ agentName ?? undefined,
245
+ );
246
+ const skillInputCanConfirm = session.canPrompt(ctx);
247
+ let skillInputAutoApproved = false;
248
+ const skillInputGate = await applyPermissionGate({
249
+ state: check.state,
250
+ canConfirm: skillInputCanConfirm,
251
+ promptForApproval: async () => {
252
+ const decision = await session.prompt(ctx, {
253
+ requestId: session.createPermissionRequestId("skill-input"),
254
+ source: "skill_input",
255
+ agentName,
256
+ message: skillInputMessage,
257
+ skillName,
258
+ });
259
+ skillInputAutoApproved = decision.autoApproved === true;
260
+ return decision;
261
+ },
262
+ writeLog: session.logger.review,
263
+ logContext: {
264
+ source: "skill_input",
265
+ skillName,
266
+ agentName,
267
+ message: skillInputMessage,
268
+ },
269
+ messages: {
270
+ denyReason: skillInputMessage,
271
+ unavailableReason:
272
+ "Skill requires approval, but no interactive UI is available.",
273
+ userDeniedReason: () => "User denied skill.",
274
+ },
275
+ });
276
+
277
+ emitDecisionEvent(this.events, {
278
+ surface: "skill",
279
+ value: skillName,
280
+ result: skillInputGate.action === "allow" ? "allow" : "deny",
281
+ resolution:
282
+ check.state === "allow"
283
+ ? "policy_allow"
284
+ : check.state === "deny"
285
+ ? "policy_deny"
286
+ : skillInputGate.action === "allow"
287
+ ? skillInputAutoApproved
288
+ ? "auto_approved"
289
+ : "user_approved"
290
+ : skillInputCanConfirm
291
+ ? "user_denied"
292
+ : "confirmation_unavailable",
293
+ origin: check.origin ?? null,
294
+ agentName: agentName ?? null,
295
+ matchedPattern: check.matchedPattern ?? null,
296
+ });
297
+
298
+ if (skillInputGate.action === "block") {
299
+ return { action: "handled" };
300
+ }
301
+
302
+ return { action: "continue" };
303
+ }
304
+ }
305
+
306
+ // ── Pure helpers (re-exported from original modules) ──────────────────────
307
+
308
+ /**
309
+ * Extract the tool input from an event, checking both `input` and `arguments`
310
+ * fields (different Pi SDK versions use different names).
311
+ */
312
+ export function getEventInput(event: unknown): unknown {
313
+ const record = toRecord(event);
314
+
315
+ if (record.input !== undefined) {
316
+ return record.input;
317
+ }
318
+
319
+ if (record.arguments !== undefined) {
320
+ return record.arguments;
321
+ }
322
+
323
+ return {};
324
+ }
325
+
326
+ /**
327
+ * Parse a `/skill:<name>` prefix from user input.
328
+ * Returns the skill name, or null if the text is not a skill invocation.
329
+ */
330
+ export function extractSkillNameFromInput(text: string): string | null {
331
+ const trimmed = text.trim();
332
+ if (!trimmed.startsWith("/skill:")) {
333
+ return null;
334
+ }
335
+
336
+ const afterPrefix = trimmed.slice("/skill:".length);
337
+ if (!afterPrefix) {
338
+ return null;
339
+ }
340
+
341
+ const firstWhitespace = afterPrefix.search(/\s/);
342
+ const skillName = (
343
+ firstWhitespace === -1 ? afterPrefix : afterPrefix.slice(0, firstWhitespace)
344
+ ).trim();
345
+ return skillName || null;
346
+ }
package/src/index.ts CHANGED
@@ -1,16 +1,12 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerPermissionSystemCommand } from "./config-modal";
3
3
  import { getGlobalConfigPath } from "./config-paths";
4
4
  import type { PermissionForwardingDeps } from "./forwarded-permissions/polling";
5
5
  import { ForwardingManager } from "./forwarding-manager";
6
6
  import {
7
- type HandlerDeps,
8
- handleBeforeAgentStart,
9
- handleInput,
10
- handleResourcesDiscover,
11
- handleSessionShutdown,
12
- handleSessionStart,
13
- handleToolCall,
7
+ AgentPrepHandler,
8
+ PermissionGateHandler,
9
+ SessionLifecycleHandler,
14
10
  } from "./handlers";
15
11
  import { requestPermissionDecisionFromUi } from "./permission-dialog";
16
12
  import { registerPermissionRpcHandlers } from "./permission-event-rpc";
@@ -64,6 +60,16 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
64
60
  refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
65
61
  logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
66
62
  getConfig: () => runtime.config,
63
+ canRequestPermissionConfirmation: (ctx) =>
64
+ canResolveAskPermissionRequest({
65
+ config: runtime.config,
66
+ hasUI: ctx.hasUI,
67
+ isSubagent: isSubagentExecutionContext(
68
+ ctx,
69
+ runtime.subagentSessionsDir,
70
+ ),
71
+ }),
72
+ promptPermission: (ctx, details) => prompter.prompt(ctx, details),
67
73
  },
68
74
  );
69
75
 
@@ -77,9 +83,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
77
83
  ),
78
84
  });
79
85
 
80
- const createPermissionRequestId = (prefix: string): string =>
81
- `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
82
-
83
86
  const rpcHandles = registerPermissionRpcHandlers(pi.events, {
84
87
  getPermissionManager: () => runtime.permissionManager,
85
88
  getSessionRules: () => runtime.sessionRules.getRuleset(),
@@ -88,36 +91,28 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
88
91
  writeReviewLog: runtime.writeReviewLog.bind(runtime),
89
92
  });
90
93
 
91
- const deps: HandlerDeps = {
92
- session,
93
- events: pi.events,
94
- canRequestPermissionConfirmation: (ctx) =>
95
- canResolveAskPermissionRequest({
96
- config: runtime.config,
97
- hasUI: ctx.hasUI,
98
- isSubagent: isSubagentExecutionContext(
99
- ctx,
100
- runtime.subagentSessionsDir,
101
- ),
102
- }),
103
- promptPermission: (ctx, details) => prompter.prompt(ctx, details),
104
- createPermissionRequestId,
105
- stopPermissionRpcHandlers: () => {
106
- rpcHandles.unsubCheck();
107
- rpcHandles.unsubPrompt();
108
- },
109
- getAllTools: () => pi.getAllTools(),
110
- setActiveTools: (names) => pi.setActiveTools(names),
94
+ emitReadyEvent(pi.events);
95
+
96
+ const toolRegistry = {
97
+ getAll: () => pi.getAllTools(),
98
+ setActive: (names: string[]) => pi.setActiveTools(names),
111
99
  };
112
100
 
113
- emitReadyEvent(pi.events);
101
+ const lifecycle = new SessionLifecycleHandler(session, () => {
102
+ rpcHandles.unsubCheck();
103
+ rpcHandles.unsubPrompt();
104
+ });
105
+ const agentPrep = new AgentPrepHandler(session, toolRegistry);
106
+ const gates = new PermissionGateHandler(session, pi.events, toolRegistry);
114
107
 
115
- pi.on("session_start", (event, ctx) => handleSessionStart(deps, event, ctx));
116
- pi.on("resources_discover", (event) => handleResourcesDiscover(deps, event));
117
- pi.on("session_shutdown", () => handleSessionShutdown(deps));
118
- pi.on("before_agent_start", (event, ctx) =>
119
- handleBeforeAgentStart(deps, event, ctx),
108
+ pi.on("session_start", (event, ctx) =>
109
+ lifecycle.handleSessionStart(event, ctx),
110
+ );
111
+ pi.on("resources_discover", (event) =>
112
+ lifecycle.handleResourcesDiscover(event),
120
113
  );
121
- pi.on("input", (event, ctx) => handleInput(deps, event, ctx));
122
- pi.on("tool_call", (event, ctx) => handleToolCall(deps, event, ctx));
114
+ pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
115
+ pi.on("before_agent_start", (event, ctx) => agentPrep.handle(event, ctx));
116
+ pi.on("input", (event, ctx) => gates.handleInput(event, ctx));
117
+ pi.on("tool_call", (event, ctx) => gates.handleToolCall(event, ctx));
123
118
  }
@@ -5,7 +5,7 @@
5
5
  * the Pi event bus so other extensions can query our policy and forward
6
6
  * permission prompts without importing this package.
7
7
  */
8
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import type {
10
10
  PermissionPromptDecision,
11
11
  RequestPermissionOptions,
@@ -1,18 +1,36 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { PermissionSystemExtensionConfig } from "./extension-config";
3
3
  import type { ForwardedPermissionLogger } from "./forwarded-permissions/io";
4
4
  import {
5
5
  confirmPermission,
6
6
  type PermissionForwardingDeps,
7
7
  } from "./forwarded-permissions/polling";
8
- import type { PromptPermissionDetails } from "./handlers/types";
9
8
  import type {
10
9
  PermissionPromptDecision,
11
10
  RequestPermissionOptions,
12
11
  } from "./permission-dialog";
13
12
  import { shouldAutoApprovePermissionState } from "./yolo-mode";
14
13
 
15
- /** Mockable contract exposed to handlers via HandlerDeps. */
14
+ export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
15
+
16
+ /** Details passed when prompting the user for a permission decision. */
17
+ export interface PromptPermissionDetails {
18
+ requestId: string;
19
+ source: PermissionReviewSource;
20
+ agentName: string | null;
21
+ message: string;
22
+ toolCallId?: string;
23
+ toolName?: string;
24
+ skillName?: string;
25
+ path?: string;
26
+ command?: string;
27
+ target?: string;
28
+ toolInputPreview?: string;
29
+ /** Override label for the "for this session" dialog option. */
30
+ sessionLabel?: string;
31
+ }
32
+
33
+ /** Mockable contract for permission prompting. */
16
34
  export interface PermissionPrompterApi {
17
35
  prompt(
18
36
  ctx: ExtensionContext,
@@ -52,10 +70,9 @@ export interface PermissionPrompterDeps {
52
70
  * 3. UI-present vs. subagent-forwarding branching (via confirmPermission).
53
71
  * 4. Review-log "approved" / "denied" entry.
54
72
  *
55
- * Injecting a single PermissionPrompter instance into HandlerDeps means
56
- * adding a new prompt parameter (e.g. a future sessionLabel variant) only
57
- * requires changing PromptPermissionDetails and this class — not the full
58
- * 4-file threading chain.
73
+ * Injecting a single PermissionPrompter instance means adding a new prompt
74
+ * parameter (e.g. a future sessionLabel variant) only requires changing
75
+ * PromptPermissionDetails and this class — not the full threading chain.
59
76
  */
60
77
  export class PermissionPrompter implements PermissionPrompterApi {
61
78
  constructor(private readonly deps: PermissionPrompterDeps) {}
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  import {
4
4
  getActiveAgentName,
@@ -7,7 +7,9 @@ import {
7
7
  import type { PermissionSystemExtensionConfig } from "./extension-config";
8
8
  import type { ExtensionPaths } from "./extension-paths";
9
9
  import type { ForwardingController } from "./forwarding-manager";
10
+ import type { PermissionPromptDecision } from "./permission-dialog";
10
11
  import type { PermissionManager } from "./permission-manager";
12
+ import type { PromptPermissionDetails } from "./permission-prompter";
11
13
  import type { Rule } from "./rule";
12
14
  import { createPermissionManagerForCwd } from "./runtime";
13
15
  import type { SessionLogger } from "./session-logger";
@@ -28,6 +30,13 @@ export interface PermissionSessionRuntimeDeps {
28
30
  logResolvedConfigPaths(): void;
29
31
  /** Read current extension config (called at query time). */
30
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>;
31
40
  }
32
41
 
33
42
  /**
@@ -249,4 +258,24 @@ export class PermissionSession {
249
258
  getInfrastructureReadPaths(): string[] {
250
259
  return this.config.piInfrastructureReadPaths ?? [];
251
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
+ }
252
281
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  import { extractFrontmatter, parseSimpleYamlMap, toRecord } from "./common";
6
6
  import {
package/src/runtime.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  type ExtensionCommandContext,
11
11
  type ExtensionContext,
12
12
  getAgentDir,
13
- } from "@mariozechner/pi-coding-agent";
13
+ } from "@earendil-works/pi-coding-agent";
14
14
 
15
15
  import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
16
16
  import {
@@ -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
  */
package/src/status.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  ExtensionCommandContext,
3
3
  ExtensionContext,
4
- } from "@mariozechner/pi-coding-agent";
4
+ } from "@earendil-works/pi-coding-agent";
5
5
 
6
6
  import {
7
7
  EXTENSION_ID,
@@ -1,5 +1,5 @@
1
1
  import { normalize } from "node:path";
2
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  import { SUBAGENT_ENV_HINT_KEYS } from "./permission-forwarding";
5
5
 
@@ -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";
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { afterEach, describe, expect, test, vi } from "vitest";
3
3
  import {
4
4
  ACTIVE_AGENT_TAG_REGEX,
@@ -12,11 +12,11 @@ import {
12
12
  } from "../src/extension-config";
13
13
  import type { Rule } from "../src/rule";
14
14
 
15
- vi.mock("@mariozechner/pi-coding-agent", () => ({
15
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
16
16
  getSettingsListTheme: () => ({}),
17
17
  }));
18
18
 
19
- vi.mock("@mariozechner/pi-tui", () => ({
19
+ vi.mock("@earendil-works/pi-tui", () => ({
20
20
  SettingsList: class {
21
21
  handleInput(): void {}
22
22
  updateValue(): void {}
@@ -24,7 +24,7 @@ function makeCtx(overrides: { hasUI?: boolean; sessionId?: string } = {}) {
24
24
  getSessionId: vi.fn().mockReturnValue(overrides.sessionId ?? "sess-1"),
25
25
  },
26
26
  cwd: "/project",
27
- } as unknown as import("@mariozechner/pi-coding-agent").ExtensionContext;
27
+ } as unknown as import("@earendil-works/pi-coding-agent").ExtensionContext;
28
28
  }
29
29
 
30
30
  function makeForwardingDeps() {