@gotgenes/pi-permission-system 9.2.0 → 10.1.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 +52 -0
- package/README.md +12 -11
- package/package.json +1 -1
- package/src/agent-prep-session.ts +28 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +11 -0
- package/src/forwarded-permissions/io.ts +29 -0
- package/src/forwarded-permissions/permission-forwarder.ts +549 -0
- package/src/forwarding-manager.ts +3 -7
- package/src/gate-handler-session.ts +13 -0
- package/src/gate-prompter.ts +14 -0
- package/src/handlers/before-agent-start.ts +2 -3
- package/src/handlers/gates/bash-command.ts +4 -18
- package/src/handlers/gates/bash-external-directory.ts +3 -15
- package/src/handlers/gates/bash-path.ts +3 -16
- package/src/handlers/gates/descriptor.ts +0 -28
- package/src/handlers/gates/path.ts +3 -15
- package/src/handlers/gates/runner.ts +142 -105
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +44 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
- package/src/handlers/lifecycle.ts +9 -9
- package/src/handlers/permission-gate-handler.ts +34 -238
- package/src/index.ts +50 -68
- package/src/mcp-targets.ts +56 -46
- package/src/permission-event-rpc.ts +7 -0
- package/src/permission-events.ts +89 -8
- package/src/permission-forwarding.ts +23 -0
- package/src/permission-prompter.ts +27 -56
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +77 -9
- package/src/permission-ui-prompt.ts +127 -0
- package/src/permissions-service.ts +53 -0
- package/src/service-lifecycle.ts +49 -0
- package/src/service.ts +17 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-lifecycle-session.ts +24 -0
- package/src/tool-input-preview.ts +0 -62
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +6 -4
- package/test/composition-root.test.ts +5 -0
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +62 -0
- package/test/forwarding-manager.test.ts +26 -44
- package/test/handlers/before-agent-start.test.ts +45 -21
- package/test/handlers/external-directory-integration.test.ts +86 -22
- package/test/handlers/external-directory-session-dedup.test.ts +102 -55
- package/test/handlers/gates/bash-command.test.ts +49 -90
- package/test/handlers/gates/bash-external-directory.test.ts +54 -95
- package/test/handlers/gates/bash-path.test.ts +63 -148
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +150 -93
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +128 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
- package/test/handlers/input.test.ts +1 -2
- package/test/handlers/lifecycle.test.ts +49 -33
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/helpers/gate-fixtures.ts +147 -16
- package/test/helpers/handler-fixtures.ts +143 -27
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-event-rpc.test.ts +39 -0
- package/test/permission-events.test.ts +78 -10
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-prompter.test.ts +147 -38
- package/test/permission-session.test.ts +160 -27
- package/test/permission-ui-prompt.test.ts +146 -0
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +0 -4
- package/test/service-lifecycle.test.ts +162 -0
- package/test/tool-input-preview.test.ts +0 -111
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/src/forwarded-permissions/polling.ts +0 -379
package/src/mcp-targets.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "./common";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* An ordered accumulator that owns the uniqueness invariant.
|
|
5
|
+
*
|
|
6
|
+
* `add` ignores null/empty values and silently skips duplicates (first-insertion
|
|
7
|
+
* wins). `toArray` returns the ordered result as an independent copy.
|
|
8
|
+
*/
|
|
9
|
+
export class McpTargetList {
|
|
10
|
+
private readonly targets: string[] = [];
|
|
11
|
+
|
|
12
|
+
add(value: string | null): void {
|
|
13
|
+
if (!value) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!this.targets.includes(value)) {
|
|
17
|
+
this.targets.push(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toArray(): string[] {
|
|
22
|
+
return [...this.targets];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
3
26
|
/**
|
|
4
27
|
* Parse a qualified MCP tool name of the form `server:tool`.
|
|
5
28
|
*
|
|
@@ -31,7 +54,7 @@ export function parseQualifiedMcpToolName(
|
|
|
31
54
|
function addDerivedMcpServerTargets(
|
|
32
55
|
toolName: string,
|
|
33
56
|
configuredServerNames: readonly string[],
|
|
34
|
-
|
|
57
|
+
targets: McpTargetList,
|
|
35
58
|
): void {
|
|
36
59
|
const trimmedToolName = toolName.trim();
|
|
37
60
|
if (!trimmedToolName) {
|
|
@@ -52,9 +75,9 @@ function addDerivedMcpServerTargets(
|
|
|
52
75
|
continue;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
targets.add(`${trimmedServerName}_${trimmedToolName}`);
|
|
79
|
+
targets.add(`${trimmedServerName}:${trimmedToolName}`);
|
|
80
|
+
targets.add(trimmedServerName);
|
|
58
81
|
}
|
|
59
82
|
}
|
|
60
83
|
|
|
@@ -62,22 +85,22 @@ function pushMcpToolPermissionTargets(
|
|
|
62
85
|
rawReference: string,
|
|
63
86
|
serverHint: string | null,
|
|
64
87
|
configuredServerNames: readonly string[],
|
|
65
|
-
|
|
88
|
+
targets: McpTargetList,
|
|
66
89
|
): void {
|
|
67
90
|
const qualified = parseQualifiedMcpToolName(rawReference);
|
|
68
91
|
const resolvedServer = serverHint ?? qualified?.server ?? null;
|
|
69
92
|
const resolvedTool = qualified?.tool ?? rawReference;
|
|
70
93
|
|
|
71
94
|
if (resolvedServer) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
targets.add(`${resolvedServer}_${resolvedTool}`);
|
|
96
|
+
targets.add(`${resolvedServer}:${resolvedTool}`);
|
|
97
|
+
targets.add(resolvedServer);
|
|
75
98
|
} else {
|
|
76
|
-
addDerivedMcpServerTargets(resolvedTool, configuredServerNames,
|
|
99
|
+
addDerivedMcpServerTargets(resolvedTool, configuredServerNames, targets);
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
targets.add(resolvedTool);
|
|
103
|
+
targets.add(rawReference);
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
/**
|
|
@@ -98,32 +121,19 @@ export function createMcpPermissionTargets(
|
|
|
98
121
|
const describe = getNonEmptyString(record.describe);
|
|
99
122
|
const search = getNonEmptyString(record.search);
|
|
100
123
|
|
|
101
|
-
const targets
|
|
102
|
-
const pushTarget = (value: string | null) => {
|
|
103
|
-
if (!value) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (!targets.includes(value)) {
|
|
107
|
-
targets.push(value);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
124
|
+
const targets = new McpTargetList();
|
|
110
125
|
|
|
111
126
|
if (tool) {
|
|
112
|
-
pushMcpToolPermissionTargets(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
configuredServerNames,
|
|
116
|
-
pushTarget,
|
|
117
|
-
);
|
|
118
|
-
pushTarget("mcp_call");
|
|
119
|
-
return targets;
|
|
127
|
+
pushMcpToolPermissionTargets(tool, server, configuredServerNames, targets);
|
|
128
|
+
targets.add("mcp_call");
|
|
129
|
+
return targets.toArray();
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
if (connect) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return targets;
|
|
133
|
+
targets.add(`mcp_connect_${connect}`);
|
|
134
|
+
targets.add(connect);
|
|
135
|
+
targets.add("mcp_connect");
|
|
136
|
+
return targets.toArray();
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
if (describe) {
|
|
@@ -131,30 +141,30 @@ export function createMcpPermissionTargets(
|
|
|
131
141
|
describe,
|
|
132
142
|
server,
|
|
133
143
|
configuredServerNames,
|
|
134
|
-
|
|
144
|
+
targets,
|
|
135
145
|
);
|
|
136
|
-
|
|
137
|
-
return targets;
|
|
146
|
+
targets.add("mcp_describe");
|
|
147
|
+
return targets.toArray();
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
if (search) {
|
|
141
151
|
if (server) {
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
targets.add(`mcp_server_${server}`);
|
|
153
|
+
targets.add(server);
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return targets;
|
|
156
|
+
targets.add(search);
|
|
157
|
+
targets.add("mcp_search");
|
|
158
|
+
return targets.toArray();
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
if (server) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return targets;
|
|
162
|
+
targets.add(`mcp_server_${server}`);
|
|
163
|
+
targets.add(server);
|
|
164
|
+
targets.add("mcp_list");
|
|
165
|
+
return targets.toArray();
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
|
|
159
|
-
return targets;
|
|
168
|
+
targets.add("mcp_status");
|
|
169
|
+
return targets.toArray();
|
|
160
170
|
}
|
|
@@ -21,11 +21,13 @@ import type {
|
|
|
21
21
|
PermissionsRpcReply,
|
|
22
22
|
} from "./permission-events";
|
|
23
23
|
import {
|
|
24
|
+
emitUiPromptEvent,
|
|
24
25
|
PERMISSIONS_PROTOCOL_VERSION,
|
|
25
26
|
PERMISSIONS_RPC_CHECK_CHANNEL,
|
|
26
27
|
PERMISSIONS_RPC_PROMPT_CHANNEL,
|
|
27
28
|
} from "./permission-events";
|
|
28
29
|
import type { PermissionManager } from "./permission-manager";
|
|
30
|
+
import { buildRpcUiPrompt } from "./permission-ui-prompt";
|
|
29
31
|
import type { Rule } from "./rule";
|
|
30
32
|
|
|
31
33
|
/** Dependencies injected into the RPC handler registry. */
|
|
@@ -155,6 +157,11 @@ async function handlePromptRpc(
|
|
|
155
157
|
? `Permission request${agentName ? ` from ${agentName}` : ""}`
|
|
156
158
|
: "Permission request";
|
|
157
159
|
|
|
160
|
+
emitUiPromptEvent(
|
|
161
|
+
events,
|
|
162
|
+
buildRpcUiPrompt({ requestId, surface, value, agentName, message }),
|
|
163
|
+
);
|
|
164
|
+
|
|
158
165
|
const decision = await deps.requestPermissionDecisionFromUi(
|
|
159
166
|
ctx.ui,
|
|
160
167
|
title,
|
package/src/permission-events.ts
CHANGED
|
@@ -27,6 +27,9 @@ export const PERMISSIONS_PROTOCOL_VERSION = 1;
|
|
|
27
27
|
/** Emitted at `session_start`, after the service is published. */
|
|
28
28
|
export const PERMISSIONS_READY_CHANNEL = "permissions:ready";
|
|
29
29
|
|
|
30
|
+
/** Emitted when a permission request is committed to the active UI prompt path. */
|
|
31
|
+
export const PERMISSIONS_UI_PROMPT_CHANNEL = "permissions:ui_prompt";
|
|
32
|
+
|
|
30
33
|
/** Emitted after every permission gate resolution. */
|
|
31
34
|
export const PERMISSIONS_DECISION_CHANNEL = "permissions:decision";
|
|
32
35
|
|
|
@@ -61,9 +64,63 @@ export type PermissionsRpcReply<T = void> =
|
|
|
61
64
|
|
|
62
65
|
// ── permissions:ready ──────────────────────────────────────────────────────
|
|
63
66
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Payload emitted on `permissions:ready`.
|
|
69
|
+
*
|
|
70
|
+
* Intentionally empty: the channel is a readiness signal. Version negotiation
|
|
71
|
+
* lives in the RPC envelope (`PermissionsRpcReply`), not in broadcast payloads —
|
|
72
|
+
* the published types plus package semver define the broadcast contract.
|
|
73
|
+
*/
|
|
74
|
+
export type PermissionsReadyEvent = Record<string, never>;
|
|
75
|
+
|
|
76
|
+
// ── permissions:ui_prompt ──────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Origin of a UI prompt.
|
|
80
|
+
*
|
|
81
|
+
* Forwarding is orthogonal to origin: a forwarded subagent prompt keeps its
|
|
82
|
+
* original source and is identified by a non-null `forwarding` field, not by a
|
|
83
|
+
* dedicated source value.
|
|
84
|
+
*/
|
|
85
|
+
export type PermissionUiPromptSource =
|
|
86
|
+
| "tool_call"
|
|
87
|
+
| "skill_input"
|
|
88
|
+
| "skill_read"
|
|
89
|
+
| "rpc_prompt";
|
|
90
|
+
|
|
91
|
+
/** Forwarding context, present only when a prompt was forwarded from a non-UI subagent. */
|
|
92
|
+
export interface ForwardedPromptContext {
|
|
93
|
+
/** Requesting subagent's display name, when known. */
|
|
94
|
+
requesterAgentName: string | null;
|
|
95
|
+
/** Requesting subagent's session id, when known. */
|
|
96
|
+
requesterSessionId: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Payload emitted on `permissions:ui_prompt`, immediately before the active
|
|
101
|
+
* user-facing permission UI is shown.
|
|
102
|
+
*
|
|
103
|
+
* Lean by design: `surface`/`value` are the normalized display projection a
|
|
104
|
+
* notification consumer reads; `source` is the origin; `forwarding` is non-null
|
|
105
|
+
* only for forwarded subagent prompts. There is no `protocolVersion` — the
|
|
106
|
+
* published types plus package semver define the broadcast contract, and
|
|
107
|
+
* consumers should read defensively.
|
|
108
|
+
*/
|
|
109
|
+
export interface PermissionUiPromptEvent {
|
|
110
|
+
/** Unique ID for the permission request being prompted. */
|
|
111
|
+
requestId: string;
|
|
112
|
+
/** Prompt origin. */
|
|
113
|
+
source: PermissionUiPromptSource;
|
|
114
|
+
/** Normalized display surface (e.g. "bash", "skill"), when known. */
|
|
115
|
+
surface: string | null;
|
|
116
|
+
/** Normalized display value (command, path, skill name, etc.), when known. */
|
|
117
|
+
value: string | null;
|
|
118
|
+
/** Agent name (when known). */
|
|
119
|
+
agentName: string | null;
|
|
120
|
+
/** Message displayed to the user. */
|
|
121
|
+
message: string;
|
|
122
|
+
/** Forwarding context, or null for a direct prompt. */
|
|
123
|
+
forwarding: ForwardedPromptContext | null;
|
|
67
124
|
}
|
|
68
125
|
|
|
69
126
|
// ── permissions:decision ───────────────────────────────────────────────────
|
|
@@ -164,10 +221,29 @@ export interface PermissionsPromptReplyData {
|
|
|
164
221
|
* reacting to ready can immediately resolve `getPermissionsService()`.
|
|
165
222
|
*/
|
|
166
223
|
export function emitReadyEvent(events: PermissionEventBus): void {
|
|
167
|
-
const payload: PermissionsReadyEvent = {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
224
|
+
const payload: PermissionsReadyEvent = {};
|
|
225
|
+
try {
|
|
226
|
+
events.emit(PERMISSIONS_READY_CHANNEL, payload);
|
|
227
|
+
} catch {
|
|
228
|
+
// Broadcasts are best-effort. A throwing listener must not block the
|
|
229
|
+
// permission system from completing session startup.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Emit a `permissions:ui_prompt` broadcast.
|
|
235
|
+
* Call immediately before invoking the active user-facing permission UI.
|
|
236
|
+
*/
|
|
237
|
+
export function emitUiPromptEvent(
|
|
238
|
+
events: PermissionEventBus,
|
|
239
|
+
event: PermissionUiPromptEvent,
|
|
240
|
+
): void {
|
|
241
|
+
try {
|
|
242
|
+
events.emit(PERMISSIONS_UI_PROMPT_CHANNEL, event);
|
|
243
|
+
} catch {
|
|
244
|
+
// UI-prompt broadcasts are observational. A consumer failure must not block
|
|
245
|
+
// the permission dialog itself.
|
|
246
|
+
}
|
|
171
247
|
}
|
|
172
248
|
|
|
173
249
|
/**
|
|
@@ -178,5 +254,10 @@ export function emitDecisionEvent(
|
|
|
178
254
|
events: PermissionEventBus,
|
|
179
255
|
event: PermissionDecisionEvent,
|
|
180
256
|
): void {
|
|
181
|
-
|
|
257
|
+
try {
|
|
258
|
+
events.emit(PERMISSIONS_DECISION_CHANNEL, event);
|
|
259
|
+
} catch {
|
|
260
|
+
// Broadcasts are best-effort. A throwing listener must not block the
|
|
261
|
+
// permission gate from resolving.
|
|
262
|
+
}
|
|
182
263
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
3
|
import type { PermissionDecisionState } from "./permission-dialog";
|
|
4
|
+
import type { PermissionUiPromptSource } from "./permission-events";
|
|
4
5
|
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
5
6
|
|
|
6
7
|
export const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
|
|
@@ -38,6 +39,19 @@ const SESSION_FORWARDING_ROOT_DIRECTORY_NAME = "sessions";
|
|
|
38
39
|
const SESSION_FORWARDING_REQUESTS_DIRECTORY_NAME = "requests";
|
|
39
40
|
const SESSION_FORWARDING_RESPONSES_DIRECTORY_NAME = "responses";
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Display fields relayed from a forwarding child to the parent UI so the parent
|
|
44
|
+
* can emit a non-degraded `permissions:ui_prompt` event.
|
|
45
|
+
*
|
|
46
|
+
* Carried separately from the prompt message because the parent reconstructs
|
|
47
|
+
* the original event through `buildForwardedUiPrompt`, not from the message text.
|
|
48
|
+
*/
|
|
49
|
+
export interface ForwardedPromptDisplay {
|
|
50
|
+
source: PermissionUiPromptSource;
|
|
51
|
+
surface: string | null;
|
|
52
|
+
value: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
export type ForwardedPermissionRequest = {
|
|
42
56
|
id: string;
|
|
43
57
|
createdAt: number;
|
|
@@ -45,6 +59,15 @@ export type ForwardedPermissionRequest = {
|
|
|
45
59
|
targetSessionId: string;
|
|
46
60
|
requesterAgentName: string;
|
|
47
61
|
message: string;
|
|
62
|
+
/**
|
|
63
|
+
* Original prompt display fields, persisted so the parent emits a
|
|
64
|
+
* non-degraded event. Optional for version-skew tolerance: a parent on a
|
|
65
|
+
* newer version may read a request written by an older child during an
|
|
66
|
+
* upgrade, in which case the reader defaults `source` to `"tool_call"`.
|
|
67
|
+
*/
|
|
68
|
+
source?: PermissionUiPromptSource;
|
|
69
|
+
surface?: string | null;
|
|
70
|
+
value?: string | null;
|
|
48
71
|
};
|
|
49
72
|
|
|
50
73
|
export type ForwardedPermissionResponse = {
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ApprovalRequester } from "./forwarded-permissions/permission-forwarder";
|
|
4
|
+
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
} from "./
|
|
8
|
-
import
|
|
9
|
-
PermissionPromptDecision,
|
|
10
|
-
RequestPermissionOptions,
|
|
11
|
-
} from "./permission-dialog";
|
|
12
|
-
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
6
|
+
emitUiPromptEvent,
|
|
7
|
+
type PermissionEventBus,
|
|
8
|
+
} from "./permission-events";
|
|
9
|
+
import { buildDirectUiPrompt } from "./permission-ui-prompt";
|
|
13
10
|
import { shouldAutoApprovePermissionState } from "./yolo-mode";
|
|
14
11
|
|
|
15
12
|
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
@@ -43,27 +40,18 @@ export interface PermissionPrompterApi {
|
|
|
43
40
|
* Dependencies required by PermissionPrompter.
|
|
44
41
|
*
|
|
45
42
|
* Keeps the prompter's external surface narrow: callers provide config
|
|
46
|
-
* access, review-log writing,
|
|
47
|
-
*
|
|
43
|
+
* access, review-log writing, the UI-prompt event bus, and the forwarder
|
|
44
|
+
* that owns the UI/subagent-forwarding branching logic.
|
|
48
45
|
*/
|
|
49
46
|
export interface PermissionPrompterDeps {
|
|
50
47
|
/** Read current config for yolo-mode check (called at prompt time). */
|
|
51
48
|
getConfig(): PermissionSystemExtensionConfig;
|
|
52
49
|
/** Write structured entries to the permission review log. */
|
|
53
50
|
writeReviewLog(event: string, details: Record<string, unknown>): void;
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/** In-process subagent session registry for detection and forwarding target resolution. */
|
|
59
|
-
registry?: SubagentSessionRegistry;
|
|
60
|
-
/** Show the interactive permission dialog in the UI. */
|
|
61
|
-
requestPermissionDecisionFromUi(
|
|
62
|
-
ui: ExtensionContext["ui"],
|
|
63
|
-
title: string,
|
|
64
|
-
message: string,
|
|
65
|
-
options?: RequestPermissionOptions,
|
|
66
|
-
): Promise<PermissionPromptDecision>;
|
|
51
|
+
/** Event bus used for UI prompt broadcasts. */
|
|
52
|
+
events: PermissionEventBus;
|
|
53
|
+
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
54
|
+
forwarder: ApprovalRequester;
|
|
67
55
|
}
|
|
68
56
|
|
|
69
57
|
/**
|
|
@@ -91,11 +79,24 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
91
79
|
|
|
92
80
|
this.writeReviewEntry("permission_request.waiting", details);
|
|
93
81
|
|
|
94
|
-
|
|
82
|
+
// Build the event once. When this session has UI it broadcasts directly;
|
|
83
|
+
// when it does not (a forwarding subagent), the display fields ride along
|
|
84
|
+
// to the parent so the parent emits a non-degraded event from the
|
|
85
|
+
// forwarded path instead of here.
|
|
86
|
+
const uiPrompt = buildDirectUiPrompt(details);
|
|
87
|
+
if (ctx.hasUI) {
|
|
88
|
+
emitUiPromptEvent(this.deps.events, uiPrompt);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const decision = await this.deps.forwarder.requestApproval(
|
|
95
92
|
ctx,
|
|
96
93
|
details.message,
|
|
97
|
-
this.buildForwardingDeps(),
|
|
98
94
|
details.sessionLabel ? { sessionLabel: details.sessionLabel } : undefined,
|
|
95
|
+
{
|
|
96
|
+
source: uiPrompt.source,
|
|
97
|
+
surface: uiPrompt.surface,
|
|
98
|
+
value: uiPrompt.value,
|
|
99
|
+
},
|
|
99
100
|
);
|
|
100
101
|
|
|
101
102
|
this.writeReviewEntry(
|
|
@@ -137,34 +138,4 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
137
138
|
denialReason: details.denialReason ?? null,
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Build a PermissionForwardingDeps to pass to confirmPermission.
|
|
143
|
-
*
|
|
144
|
-
* Yolo-mode is already handled at the prompter level, so shouldAutoApprove
|
|
145
|
-
* returns false here (confirmPermission does not call it; only
|
|
146
|
-
* processForwardedPermissionRequests does, and that has its own deps).
|
|
147
|
-
*
|
|
148
|
-
* The logger delegates writeReviewLog to deps and uses a no-op writeDebugLog
|
|
149
|
-
* (trace-level forwarding debug is deferred — see open question in the plan).
|
|
150
|
-
*/
|
|
151
|
-
private buildForwardingDeps(): PermissionForwardingDeps {
|
|
152
|
-
const { deps } = this;
|
|
153
|
-
const logger: ForwardedPermissionLogger = {
|
|
154
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain function closures; no this-binding issue
|
|
155
|
-
writeReviewLog: deps.writeReviewLog,
|
|
156
|
-
writeDebugLog: () => undefined,
|
|
157
|
-
};
|
|
158
|
-
return {
|
|
159
|
-
forwardingDir: deps.forwardingDir,
|
|
160
|
-
subagentSessionsDir: deps.subagentSessionsDir,
|
|
161
|
-
registry: deps.registry,
|
|
162
|
-
logger,
|
|
163
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain function closures; no this-binding issue
|
|
164
|
-
writeReviewLog: deps.writeReviewLog,
|
|
165
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- same as above
|
|
166
|
-
requestPermissionDecisionFromUi: deps.requestPermissionDecisionFromUi,
|
|
167
|
-
shouldAutoApprove: () => false,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
141
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PermissionCheckResult } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the effective permission for a surface/input, applying the current
|
|
5
|
+
* session rules internally.
|
|
6
|
+
*
|
|
7
|
+
* Collapses the `checkPermission` + `getSessionRuleset` relay that every gate
|
|
8
|
+
* previously threaded by hand: the ruleset was only ever fetched to be passed
|
|
9
|
+
* straight back into `checkPermission`, so the two are one operation.
|
|
10
|
+
*/
|
|
11
|
+
export interface PermissionResolver {
|
|
12
|
+
resolve(
|
|
13
|
+
surface: string,
|
|
14
|
+
input: unknown,
|
|
15
|
+
agentName?: string,
|
|
16
|
+
): PermissionCheckResult;
|
|
17
|
+
}
|
|
@@ -4,18 +4,28 @@ import {
|
|
|
4
4
|
getActiveAgentName,
|
|
5
5
|
getActiveAgentNameFromSystemPrompt,
|
|
6
6
|
} from "./active-agent";
|
|
7
|
+
import type { AgentPrepSession } from "./agent-prep-session";
|
|
7
8
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
8
9
|
import type { ExtensionPaths } from "./extension-paths";
|
|
9
10
|
import type { ForwardingController } from "./forwarding-manager";
|
|
11
|
+
import type { GateHandlerSession } from "./gate-handler-session";
|
|
12
|
+
import type { GatePrompter } from "./gate-prompter";
|
|
10
13
|
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
11
14
|
import type { PermissionManager } from "./permission-manager";
|
|
12
15
|
import type { PromptPermissionDetails } from "./permission-prompter";
|
|
16
|
+
import type { PermissionResolver } from "./permission-resolver";
|
|
13
17
|
import type { Rule } from "./rule";
|
|
14
18
|
import { createPermissionManagerForCwd } from "./runtime";
|
|
15
19
|
import type { SessionApproval } from "./session-approval";
|
|
20
|
+
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
21
|
+
import type { SessionLifecycleSession } from "./session-lifecycle-session";
|
|
16
22
|
import type { SessionLogger } from "./session-logger";
|
|
17
23
|
import { SessionRules } from "./session-rules";
|
|
18
24
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
25
|
+
import {
|
|
26
|
+
resolveToolPreviewLimits,
|
|
27
|
+
type ToolPreviewFormatterOptions,
|
|
28
|
+
} from "./tool-preview-formatter";
|
|
19
29
|
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
20
30
|
|
|
21
31
|
/**
|
|
@@ -54,7 +64,15 @@ export interface PermissionSessionRuntimeDeps {
|
|
|
54
64
|
* - `ForwardingController` — polling lifecycle
|
|
55
65
|
* - `PermissionSessionRuntimeDeps` — config refresh + log delegates
|
|
56
66
|
*/
|
|
57
|
-
export class PermissionSession
|
|
67
|
+
export class PermissionSession
|
|
68
|
+
implements
|
|
69
|
+
PermissionResolver,
|
|
70
|
+
SessionApprovalRecorder,
|
|
71
|
+
GatePrompter,
|
|
72
|
+
GateHandlerSession,
|
|
73
|
+
AgentPrepSession,
|
|
74
|
+
SessionLifecycleSession
|
|
75
|
+
{
|
|
58
76
|
private context: ExtensionContext | null = null;
|
|
59
77
|
private permissionManager: PermissionManager;
|
|
60
78
|
private readonly sessionRules = new SessionRules();
|
|
@@ -110,6 +128,24 @@ export class PermissionSession {
|
|
|
110
128
|
);
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the effective permission for a surface/input, applying the current
|
|
133
|
+
* session rules. Composes `checkPermission` with `getSessionRuleset` so
|
|
134
|
+
* callers never thread the ruleset by hand.
|
|
135
|
+
*/
|
|
136
|
+
resolve(
|
|
137
|
+
surface: string,
|
|
138
|
+
input: unknown,
|
|
139
|
+
agentName?: string,
|
|
140
|
+
): PermissionCheckResult {
|
|
141
|
+
return this.checkPermission(
|
|
142
|
+
surface,
|
|
143
|
+
input,
|
|
144
|
+
agentName,
|
|
145
|
+
this.getSessionRuleset(),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
113
149
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
114
150
|
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
115
151
|
}
|
|
@@ -251,13 +287,25 @@ export class PermissionSession {
|
|
|
251
287
|
|
|
252
288
|
// ── Infrastructure paths ───────────────────────────────────────────────
|
|
253
289
|
|
|
254
|
-
|
|
255
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Combined infrastructure read directories: static paths from
|
|
292
|
+
* `ExtensionPaths` plus config-derived paths.
|
|
293
|
+
*/
|
|
294
|
+
getInfrastructureReadDirs(): string[] {
|
|
295
|
+
return [
|
|
296
|
+
...this.paths.piInfrastructureDirs,
|
|
297
|
+
...(this.config.piInfrastructureReadPaths ?? []),
|
|
298
|
+
];
|
|
256
299
|
}
|
|
257
300
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Resolved tool-preview formatter options from the current config.
|
|
303
|
+
*
|
|
304
|
+
* Replaces the handler's `resolveToolPreviewLimits(session.config)` reach
|
|
305
|
+
* so the pipeline reads a clean value rather than pulling raw config.
|
|
306
|
+
*/
|
|
307
|
+
getToolPreviewLimits(): ToolPreviewFormatterOptions {
|
|
308
|
+
return resolveToolPreviewLimits(this.config);
|
|
261
309
|
}
|
|
262
310
|
|
|
263
311
|
// ── Prompting ──────────────────────────────────────────────────────────
|
|
@@ -275,8 +323,28 @@ export class PermissionSession {
|
|
|
275
323
|
return this.runtimeDeps.promptPermission(ctx, details);
|
|
276
324
|
}
|
|
277
325
|
|
|
278
|
-
/**
|
|
279
|
-
|
|
280
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Whether an interactive confirmation is possible using the stored context.
|
|
328
|
+
* Returns `false` when no context is active (before `activate` is called).
|
|
329
|
+
* Implements {@link GatePrompter}.
|
|
330
|
+
*/
|
|
331
|
+
canConfirm(): boolean {
|
|
332
|
+
return this.context !== null && this.canPrompt(this.context);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Prompt the user for a permission decision using the stored context.
|
|
337
|
+
* Throws if no context is active — `canConfirm()` guards this in normal use.
|
|
338
|
+
* Implements {@link GatePrompter}.
|
|
339
|
+
*/
|
|
340
|
+
promptPermission(
|
|
341
|
+
details: PromptPermissionDetails,
|
|
342
|
+
): Promise<PermissionPromptDecision> {
|
|
343
|
+
if (this.context === null) {
|
|
344
|
+
return Promise.reject(
|
|
345
|
+
new Error("promptPermission called before the session was activated"),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return this.prompt(this.context, details);
|
|
281
349
|
}
|
|
282
350
|
}
|