@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,346 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
InputEventResult,
|
|
4
|
+
} from "@mariozechner/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
|
@@ -4,24 +4,19 @@ 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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";
|
|
17
13
|
import { emitReadyEvent } from "./permission-events";
|
|
18
14
|
import { PermissionPrompter } from "./permission-prompter";
|
|
15
|
+
import { PermissionSession } from "./permission-session";
|
|
19
16
|
import {
|
|
20
17
|
createExtensionRuntime,
|
|
21
|
-
createPermissionManagerForCwd,
|
|
22
18
|
logResolvedConfigPaths,
|
|
23
19
|
refreshExtensionConfig,
|
|
24
|
-
resolveAgentName,
|
|
25
20
|
saveExtensionConfig,
|
|
26
21
|
} from "./runtime";
|
|
27
22
|
import { createSessionLogger } from "./session-logger";
|
|
@@ -56,6 +51,28 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
56
51
|
};
|
|
57
52
|
|
|
58
53
|
refreshExtensionConfig(runtime);
|
|
54
|
+
|
|
55
|
+
const session = new PermissionSession(
|
|
56
|
+
runtime,
|
|
57
|
+
createSessionLogger(runtime),
|
|
58
|
+
new ForwardingManager(runtime.subagentSessionsDir, forwardingDeps),
|
|
59
|
+
{
|
|
60
|
+
refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
|
|
61
|
+
logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
|
|
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),
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
59
76
|
registerPermissionSystemCommand(pi, {
|
|
60
77
|
getConfig: () => runtime.config,
|
|
61
78
|
setConfig: (next, ctx) => saveExtensionConfig(runtime, next, ctx),
|
|
@@ -66,9 +83,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
66
83
|
),
|
|
67
84
|
});
|
|
68
85
|
|
|
69
|
-
const createPermissionRequestId = (prefix: string): string =>
|
|
70
|
-
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
71
|
-
|
|
72
86
|
const rpcHandles = registerPermissionRpcHandlers(pi.events, {
|
|
73
87
|
getPermissionManager: () => runtime.permissionManager,
|
|
74
88
|
getSessionRules: () => runtime.sessionRules.getRuleset(),
|
|
@@ -77,50 +91,28 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
77
91
|
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
78
92
|
});
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
runtime.config.piInfrastructureReadPaths ?? [],
|
|
86
|
-
events: pi.events,
|
|
87
|
-
createPermissionManagerForCwd: (cwd) =>
|
|
88
|
-
createPermissionManagerForCwd(runtime.agentDir, cwd),
|
|
89
|
-
refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
|
|
90
|
-
logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
|
|
91
|
-
resolveAgentName: (ctx, systemPrompt) =>
|
|
92
|
-
resolveAgentName(runtime, ctx, systemPrompt),
|
|
93
|
-
canRequestPermissionConfirmation: (ctx) =>
|
|
94
|
-
canResolveAskPermissionRequest({
|
|
95
|
-
config: runtime.config,
|
|
96
|
-
hasUI: ctx.hasUI,
|
|
97
|
-
isSubagent: isSubagentExecutionContext(
|
|
98
|
-
ctx,
|
|
99
|
-
runtime.subagentSessionsDir,
|
|
100
|
-
),
|
|
101
|
-
}),
|
|
102
|
-
promptPermission: (ctx, details) => prompter.prompt(ctx, details),
|
|
103
|
-
createPermissionRequestId,
|
|
104
|
-
forwarding: new ForwardingManager(
|
|
105
|
-
runtime.subagentSessionsDir,
|
|
106
|
-
forwardingDeps,
|
|
107
|
-
),
|
|
108
|
-
stopPermissionRpcHandlers: () => {
|
|
109
|
-
rpcHandles.unsubCheck();
|
|
110
|
-
rpcHandles.unsubPrompt();
|
|
111
|
-
},
|
|
112
|
-
getAllTools: () => pi.getAllTools(),
|
|
113
|
-
setActiveTools: (names) => pi.setActiveTools(names),
|
|
94
|
+
emitReadyEvent(pi.events);
|
|
95
|
+
|
|
96
|
+
const toolRegistry = {
|
|
97
|
+
getAll: () => pi.getAllTools(),
|
|
98
|
+
setActive: (names: string[]) => pi.setActiveTools(names),
|
|
114
99
|
};
|
|
115
100
|
|
|
116
|
-
|
|
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);
|
|
117
107
|
|
|
118
|
-
pi.on("session_start", (event, ctx) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
pi.on("
|
|
122
|
-
|
|
108
|
+
pi.on("session_start", (event, ctx) =>
|
|
109
|
+
lifecycle.handleSessionStart(event, ctx),
|
|
110
|
+
);
|
|
111
|
+
pi.on("resources_discover", (event) =>
|
|
112
|
+
lifecycle.handleResourcesDiscover(event),
|
|
123
113
|
);
|
|
124
|
-
pi.on("
|
|
125
|
-
pi.on("
|
|
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));
|
|
126
118
|
}
|
|
@@ -5,14 +5,32 @@ 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
|
-
|
|
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
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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) {}
|