@gotgenes/pi-permission-system 19.0.0 → 20.0.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 +23 -0
- package/docs/cross-extension-api.md +17 -126
- package/docs/guides/permission-frontmatter-for-subagent-extensions.md +18 -50
- package/docs/guides/upstream-issue-template.md +2 -2
- package/docs/subagent-integration.md +0 -1
- package/package.json +1 -1
- package/src/{forwarded-permissions/permission-forwarder.ts → authority/approval-escalator.ts} +35 -301
- package/src/authority/forwarded-request-server.ts +285 -0
- package/src/authority/forwarder-context.ts +32 -0
- package/src/{forwarded-permissions/io.ts → authority/forwarding-io.ts} +0 -1
- package/src/{subagent-context.ts → authority/subagent-context.ts} +2 -2
- package/src/authority/subagent-detection.ts +65 -0
- package/src/forwarding-manager.ts +4 -16
- package/src/index.ts +34 -31
- package/src/permission-events.ts +7 -103
- package/src/permission-prompter.ts +1 -1
- package/src/permission-ui-prompt.ts +1 -25
- package/src/prompting-gateway.ts +5 -19
- package/src/service-lifecycle.ts +3 -5
- package/src/service.ts +3 -8
- package/src/session-logger.ts +1 -1
- package/src/permission-event-rpc.ts +0 -227
package/src/permission-events.ts
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission event channel — public contract.
|
|
3
3
|
*
|
|
4
|
-
* Exports channel name constants,
|
|
5
|
-
*
|
|
4
|
+
* Exports channel name constants, TypeScript types for all emitted events,
|
|
5
|
+
* and thin emit helpers.
|
|
6
6
|
*
|
|
7
7
|
* Stability guarantee: fields may be added, but existing fields will not be
|
|
8
8
|
* removed or renamed without a semver-major version bump.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
/** Minimal event bus interface required by the emit helpers
|
|
11
|
+
/** Minimal event bus interface required by the emit helpers. */
|
|
12
12
|
export interface PermissionEventBus {
|
|
13
13
|
emit(channel: string, data: unknown): void;
|
|
14
14
|
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// ── Protocol version ───────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* RPC protocol version.
|
|
21
|
-
* Bumped when the envelope shape or method contracts change in a breaking way.
|
|
22
|
-
*/
|
|
23
|
-
export const PERMISSIONS_PROTOCOL_VERSION = 1;
|
|
24
|
-
|
|
25
17
|
// ── Channel name constants ─────────────────────────────────────────────────
|
|
26
18
|
|
|
27
19
|
/** Emitted at `session_start`, after the service is published. */
|
|
@@ -33,43 +25,14 @@ export const PERMISSIONS_UI_PROMPT_CHANNEL = "permissions:ui_prompt";
|
|
|
33
25
|
/** Emitted after every permission gate resolution. */
|
|
34
26
|
export const PERMISSIONS_DECISION_CHANNEL = "permissions:decision";
|
|
35
27
|
|
|
36
|
-
/**
|
|
37
|
-
* RPC request channel — query the permission policy (no prompting).
|
|
38
|
-
*
|
|
39
|
-
* @deprecated Use the `Symbol.for()`-backed service accessor instead:
|
|
40
|
-
* ```typescript
|
|
41
|
-
* const { getPermissionsService } = await import("@gotgenes/pi-permission-system");
|
|
42
|
-
* const service = getPermissionsService();
|
|
43
|
-
* if (service) {
|
|
44
|
-
* const result = service.checkPermission("bash", "git push");
|
|
45
|
-
* }
|
|
46
|
-
* ```
|
|
47
|
-
* The event-bus RPC remains available as a zero-dependency fallback.
|
|
48
|
-
*/
|
|
49
|
-
export const PERMISSIONS_RPC_CHECK_CHANNEL = "permissions:rpc:check";
|
|
50
|
-
|
|
51
|
-
/** RPC request channel — forward a permission prompt to the parent UI. */
|
|
52
|
-
export const PERMISSIONS_RPC_PROMPT_CHANNEL = "permissions:rpc:prompt";
|
|
53
|
-
|
|
54
|
-
// ── Shared RPC envelope ────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Standard RPC reply envelope.
|
|
58
|
-
* Success: `{ success: true, protocolVersion, data? }`.
|
|
59
|
-
* Error: `{ success: false, protocolVersion, error }`.
|
|
60
|
-
*/
|
|
61
|
-
export type PermissionsRpcReply<T = void> =
|
|
62
|
-
| { success: true; protocolVersion: number; data?: T }
|
|
63
|
-
| { success: false; protocolVersion: number; error: string };
|
|
64
|
-
|
|
65
28
|
// ── permissions:ready ──────────────────────────────────────────────────────
|
|
66
29
|
|
|
67
30
|
/**
|
|
68
31
|
* Payload emitted on `permissions:ready`.
|
|
69
32
|
*
|
|
70
|
-
* Intentionally empty: the channel is a readiness signal.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
33
|
+
* Intentionally empty: the channel is a readiness signal. There is no
|
|
34
|
+
* `protocolVersion` — the published types plus package semver define the
|
|
35
|
+
* broadcast contract.
|
|
73
36
|
*/
|
|
74
37
|
export type PermissionsReadyEvent = Record<string, never>;
|
|
75
38
|
|
|
@@ -85,8 +48,7 @@ export type PermissionsReadyEvent = Record<string, never>;
|
|
|
85
48
|
export type PermissionUiPromptSource =
|
|
86
49
|
| "tool_call"
|
|
87
50
|
| "skill_input"
|
|
88
|
-
| "skill_read"
|
|
89
|
-
| "rpc_prompt";
|
|
51
|
+
| "skill_read";
|
|
90
52
|
|
|
91
53
|
/** Forwarding context, present only when a prompt was forwarded from a non-UI subagent. */
|
|
92
54
|
export interface ForwardedPromptContext {
|
|
@@ -155,64 +117,6 @@ export interface PermissionDecisionEvent {
|
|
|
155
117
|
matchedPattern: string | null;
|
|
156
118
|
}
|
|
157
119
|
|
|
158
|
-
// ── permissions:rpc:check ──────────────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Request payload for `permissions:rpc:check`.
|
|
162
|
-
*
|
|
163
|
-
* @deprecated Prefer `getPermissionsService().checkPermission()` from the
|
|
164
|
-
* service accessor module. See `PERMISSIONS_RPC_CHECK_CHANNEL` for details.
|
|
165
|
-
*/
|
|
166
|
-
export interface PermissionsCheckRequest {
|
|
167
|
-
requestId: string;
|
|
168
|
-
/** Permission surface to evaluate. */
|
|
169
|
-
surface: string;
|
|
170
|
-
/** The value to evaluate: command string, tool name, skill name, or path. */
|
|
171
|
-
value?: string;
|
|
172
|
-
/** Optional agent name for per-agent policy resolution. */
|
|
173
|
-
agentName?: string;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Data field in a successful `permissions:rpc:check` reply.
|
|
178
|
-
*
|
|
179
|
-
* @deprecated Prefer `getPermissionsService().checkPermission()` from the
|
|
180
|
-
* service accessor module. See `PERMISSIONS_RPC_CHECK_CHANNEL` for details.
|
|
181
|
-
*/
|
|
182
|
-
export interface PermissionsCheckReplyData {
|
|
183
|
-
result: "allow" | "deny" | "ask";
|
|
184
|
-
matchedPattern: string | null;
|
|
185
|
-
origin: string | null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── permissions:rpc:prompt ─────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
/** Request payload for `permissions:rpc:prompt`. */
|
|
191
|
-
export interface PermissionsPromptRequest {
|
|
192
|
-
requestId: string;
|
|
193
|
-
/** Permission surface being evaluated. */
|
|
194
|
-
surface: string;
|
|
195
|
-
/** Value being evaluated (shown in the dialog). */
|
|
196
|
-
value: string;
|
|
197
|
-
/** Optional agent name for display. */
|
|
198
|
-
agentName?: string;
|
|
199
|
-
/** Message to display in the permission dialog. */
|
|
200
|
-
message: string;
|
|
201
|
-
/** Optional label for the "for this session" option. */
|
|
202
|
-
sessionLabel?: string;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Data field in a successful `permissions:rpc:prompt` reply. */
|
|
206
|
-
export interface PermissionsPromptReplyData {
|
|
207
|
-
approved: boolean;
|
|
208
|
-
/**
|
|
209
|
-
* Detailed state: "approved", "approved_for_session",
|
|
210
|
-
* "denied", or "denied_with_reason".
|
|
211
|
-
*/
|
|
212
|
-
state: string;
|
|
213
|
-
denialReason?: string;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
120
|
// ── Emit helpers ───────────────────────────────────────────────────────────
|
|
217
121
|
|
|
218
122
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { ApprovalRequester } from "./
|
|
2
|
+
import type { ApprovalRequester } from "./authority/approval-escalator";
|
|
3
3
|
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
4
4
|
import {
|
|
5
5
|
emitUiPromptEvent,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This module is a leaf: it owns narrow input types that each call site's
|
|
9
9
|
* domain object satisfies structurally, so it imports nothing from the
|
|
10
|
-
* prompter
|
|
10
|
+
* prompter or forwarding modules (no import cycles, correct layering).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type {
|
|
@@ -28,15 +28,6 @@ export interface DirectPromptInput {
|
|
|
28
28
|
target?: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** Input for a `permissions:rpc:prompt` forwarded UI prompt. */
|
|
32
|
-
export interface RpcPromptInput {
|
|
33
|
-
requestId: string;
|
|
34
|
-
surface?: string | null;
|
|
35
|
-
value?: string | null;
|
|
36
|
-
agentName?: string | null;
|
|
37
|
-
message: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
/** Input for a file-forwarded subagent prompt shown by the parent UI. */
|
|
41
32
|
export interface ForwardedPromptInput {
|
|
42
33
|
requestId: string;
|
|
@@ -86,21 +77,6 @@ export function buildDirectUiPrompt(
|
|
|
86
77
|
};
|
|
87
78
|
}
|
|
88
79
|
|
|
89
|
-
/** Build the UI prompt event for an RPC-forwarded prompt. */
|
|
90
|
-
export function buildRpcUiPrompt(
|
|
91
|
-
input: RpcPromptInput,
|
|
92
|
-
): PermissionUiPromptEvent {
|
|
93
|
-
return {
|
|
94
|
-
requestId: input.requestId,
|
|
95
|
-
source: "rpc_prompt",
|
|
96
|
-
surface: input.surface ?? null,
|
|
97
|
-
value: input.value ?? null,
|
|
98
|
-
agentName: input.agentName ?? null,
|
|
99
|
-
message: input.message,
|
|
100
|
-
forwarding: null,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
80
|
/**
|
|
105
81
|
* Build the UI prompt event for a file-forwarded subagent prompt.
|
|
106
82
|
*
|
package/src/prompting-gateway.ts
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
2
|
+
import type { SubagentDetector } from "./authority/subagent-detection";
|
|
3
3
|
import type { GatePrompter } from "./gate-prompter";
|
|
4
4
|
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
5
5
|
import type {
|
|
6
6
|
PermissionPrompterApi,
|
|
7
7
|
PromptPermissionDetails,
|
|
8
8
|
} from "./permission-prompter";
|
|
9
|
-
import { isSubagentExecutionContext } from "./subagent-context";
|
|
10
|
-
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* Dependencies required by PromptingGateway.
|
|
14
12
|
*
|
|
15
13
|
* All fields are actively consumed:
|
|
16
|
-
* - `
|
|
14
|
+
* - `detection` drives `canConfirm()`.
|
|
17
15
|
* - `prompter` is called by `prompt()`.
|
|
18
16
|
*/
|
|
19
17
|
export interface PromptingGatewayDeps {
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
/** Host platform, injected from the composition root, for subagent-context path detection. */
|
|
23
|
-
platform: NodeJS.Platform;
|
|
24
|
-
/** Process-global registry used to detect a registered child session. */
|
|
25
|
-
registry?: SubagentSessionRegistry;
|
|
18
|
+
/** Single owner of subagent detection; drives `canConfirm()`. */
|
|
19
|
+
detection: SubagentDetector;
|
|
26
20
|
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
27
21
|
prompter: PermissionPrompterApi;
|
|
28
22
|
}
|
|
@@ -75,15 +69,7 @@ export class PromptingGateway
|
|
|
75
69
|
*/
|
|
76
70
|
canConfirm(): boolean {
|
|
77
71
|
if (this.context === null) return false;
|
|
78
|
-
return (
|
|
79
|
-
this.context.hasUI ||
|
|
80
|
-
isSubagentExecutionContext(
|
|
81
|
-
this.context,
|
|
82
|
-
this.deps.subagentSessionsDir,
|
|
83
|
-
this.deps.platform,
|
|
84
|
-
this.deps.registry,
|
|
85
|
-
)
|
|
86
|
-
);
|
|
72
|
+
return this.context.hasUI || this.deps.detection.isSubagent(this.context);
|
|
87
73
|
}
|
|
88
74
|
|
|
89
75
|
/**
|
package/src/service-lifecycle.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
2
|
+
import type { RegisteredChildDetector } from "./authority/subagent-detection";
|
|
3
3
|
import { emitReadyEvent, type PermissionEventBus } from "./permission-events";
|
|
4
4
|
import {
|
|
5
5
|
type PermissionsService,
|
|
6
6
|
publishPermissionsService,
|
|
7
7
|
unpublishPermissionsService,
|
|
8
8
|
} from "./service";
|
|
9
|
-
import { isRegisteredSubagentChild } from "./subagent-context";
|
|
10
|
-
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
11
9
|
|
|
12
10
|
/** The session-scoped service lifecycle that the lifecycle handler drives. */
|
|
13
11
|
export interface ServiceLifecycle {
|
|
@@ -28,13 +26,13 @@ export interface ServiceLifecycle {
|
|
|
28
26
|
export class PermissionServiceLifecycle implements ServiceLifecycle {
|
|
29
27
|
constructor(
|
|
30
28
|
private readonly service: PermissionsService,
|
|
31
|
-
private readonly
|
|
29
|
+
private readonly detection: RegisteredChildDetector,
|
|
32
30
|
private readonly events: PermissionEventBus,
|
|
33
31
|
private readonly subscriptions: readonly (() => void)[],
|
|
34
32
|
) {}
|
|
35
33
|
|
|
36
34
|
activate(ctx: ExtensionContext): void {
|
|
37
|
-
if (!
|
|
35
|
+
if (!this.detection.isRegisteredChild(ctx)) {
|
|
38
36
|
publishPermissionsService(this.service);
|
|
39
37
|
}
|
|
40
38
|
emitReadyEvent(this.events);
|
package/src/service.ts
CHANGED
|
@@ -18,18 +18,13 @@ import type { PermissionCheckResult, PermissionState } from "./types";
|
|
|
18
18
|
export type {
|
|
19
19
|
ForwardedPromptContext,
|
|
20
20
|
PermissionDecisionEvent,
|
|
21
|
-
PermissionsPromptReplyData,
|
|
22
|
-
PermissionsPromptRequest,
|
|
23
21
|
PermissionsReadyEvent,
|
|
24
|
-
PermissionsRpcReply,
|
|
25
22
|
PermissionUiPromptEvent,
|
|
26
23
|
PermissionUiPromptSource,
|
|
27
24
|
} from "./permission-events";
|
|
28
25
|
export {
|
|
29
26
|
PERMISSIONS_DECISION_CHANNEL,
|
|
30
|
-
PERMISSIONS_PROTOCOL_VERSION,
|
|
31
27
|
PERMISSIONS_READY_CHANNEL,
|
|
32
|
-
PERMISSIONS_RPC_PROMPT_CHANNEL,
|
|
33
28
|
PERMISSIONS_UI_PROMPT_CHANNEL,
|
|
34
29
|
} from "./permission-events";
|
|
35
30
|
export type { PermissionCheckResult, PermissionState, ToolInputFormatter };
|
|
@@ -40,9 +35,9 @@ const SERVICE_KEY = Symbol.for("@gotgenes/pi-permission-system:service");
|
|
|
40
35
|
/**
|
|
41
36
|
* Public interface exposed to other extensions via `getPermissionsService()`.
|
|
42
37
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
38
|
+
* `checkPermission` takes a surface + optional value + optional agent name,
|
|
39
|
+
* and delegates to `PermissionManager.checkPermission()` with current session
|
|
40
|
+
* rules internally.
|
|
46
41
|
*/
|
|
47
42
|
export interface PermissionsService {
|
|
48
43
|
/**
|
package/src/session-logger.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface ReviewLogger {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Logging seam for consumers that write both debug and review entries.
|
|
22
|
-
* Injected into `ConfigStore` and `
|
|
22
|
+
* Injected into `ConfigStore`, `ApprovalEscalator`, and `ForwardedRequestServer`.
|
|
23
23
|
*/
|
|
24
24
|
export interface DebugReviewLogger extends ReviewLogger {
|
|
25
25
|
debug(event: string, details?: Record<string, unknown>): void;
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-deprecated -- this module implements the deprecated event-bus RPC channel; references to its own deprecated symbols are intentional */
|
|
2
|
-
/**
|
|
3
|
-
* Permission event bus RPC handlers.
|
|
4
|
-
*
|
|
5
|
-
* Registers `permissions:rpc:check` and `permissions:rpc:prompt` handlers on
|
|
6
|
-
* the Pi event bus so other extensions can query our policy and forward
|
|
7
|
-
* permission prompts without importing this package.
|
|
8
|
-
*/
|
|
9
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { buildAccessIntentForSurface } from "./input-normalizer";
|
|
11
|
-
import type { PathNormalizer } from "./path-normalizer";
|
|
12
|
-
import type {
|
|
13
|
-
PermissionPromptDecision,
|
|
14
|
-
RequestPermissionOptions,
|
|
15
|
-
} from "./permission-dialog";
|
|
16
|
-
import type {
|
|
17
|
-
PermissionEventBus,
|
|
18
|
-
PermissionsCheckReplyData,
|
|
19
|
-
PermissionsCheckRequest,
|
|
20
|
-
PermissionsPromptReplyData,
|
|
21
|
-
PermissionsPromptRequest,
|
|
22
|
-
PermissionsRpcReply,
|
|
23
|
-
} from "./permission-events";
|
|
24
|
-
import {
|
|
25
|
-
emitUiPromptEvent,
|
|
26
|
-
PERMISSIONS_PROTOCOL_VERSION,
|
|
27
|
-
PERMISSIONS_RPC_CHECK_CHANNEL,
|
|
28
|
-
PERMISSIONS_RPC_PROMPT_CHANNEL,
|
|
29
|
-
} from "./permission-events";
|
|
30
|
-
import type { ScopedPermissionResolver } from "./permission-resolver";
|
|
31
|
-
import { buildRpcUiPrompt } from "./permission-ui-prompt";
|
|
32
|
-
import type { ReviewLogger } from "./session-logger";
|
|
33
|
-
|
|
34
|
-
/** Dependencies injected into the RPC handler registry. */
|
|
35
|
-
export interface PermissionRpcDeps {
|
|
36
|
-
/**
|
|
37
|
-
* The shared resolver: answers an access intent, composing the session
|
|
38
|
-
* ruleset and unwrapping `access-path` → `path-values` internally so the
|
|
39
|
-
* RPC check matches the same lexical ∪ canonical set the gates do (#503).
|
|
40
|
-
*/
|
|
41
|
-
resolver: Pick<ScopedPermissionResolver, "resolve">;
|
|
42
|
-
/**
|
|
43
|
-
* Narrow session view: runtime context for the prompt handler (hasUI / UI
|
|
44
|
-
* dialog) and the cwd-bound path normalizer for path-surface check queries.
|
|
45
|
-
*/
|
|
46
|
-
session: {
|
|
47
|
-
getRuntimeContext(): ExtensionContext | null;
|
|
48
|
-
getPathNormalizer(): PathNormalizer;
|
|
49
|
-
};
|
|
50
|
-
/** Show the interactive permission dialog in the parent session UI. */
|
|
51
|
-
requestPermissionDecisionFromUi(
|
|
52
|
-
ui: ExtensionContext["ui"],
|
|
53
|
-
title: string,
|
|
54
|
-
message: string,
|
|
55
|
-
options?: RequestPermissionOptions,
|
|
56
|
-
): Promise<PermissionPromptDecision>;
|
|
57
|
-
/** Write review-log entries for prompted decisions. */
|
|
58
|
-
logger: ReviewLogger;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Unsubscribe handles returned from registerPermissionRpcHandlers. */
|
|
62
|
-
export interface PermissionRpcHandles {
|
|
63
|
-
/** Stop the permissions:rpc:check handler. */
|
|
64
|
-
unsubCheck: () => void;
|
|
65
|
-
/** Stop the permissions:rpc:prompt handler. */
|
|
66
|
-
unsubPrompt: () => void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Internal helpers ───────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
/** Build a success reply envelope. */
|
|
72
|
-
function successReply<T>(data?: T): PermissionsRpcReply<T> {
|
|
73
|
-
if (data !== undefined) {
|
|
74
|
-
return {
|
|
75
|
-
success: true,
|
|
76
|
-
protocolVersion: PERMISSIONS_PROTOCOL_VERSION,
|
|
77
|
-
data,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
return { success: true, protocolVersion: PERMISSIONS_PROTOCOL_VERSION };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Build an error reply envelope. */
|
|
84
|
-
function errorReply(error: string): PermissionsRpcReply {
|
|
85
|
-
return {
|
|
86
|
-
success: false,
|
|
87
|
-
protocolVersion: PERMISSIONS_PROTOCOL_VERSION,
|
|
88
|
-
error,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── RPC handler: permissions:rpc:check ────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
function handleCheckRpc(
|
|
95
|
-
raw: unknown,
|
|
96
|
-
events: PermissionEventBus,
|
|
97
|
-
deps: PermissionRpcDeps,
|
|
98
|
-
): void {
|
|
99
|
-
const req = raw as Partial<PermissionsCheckRequest>;
|
|
100
|
-
const { requestId, surface, value, agentName } = req;
|
|
101
|
-
|
|
102
|
-
if (typeof requestId !== "string" || !requestId) {
|
|
103
|
-
// Cannot reply without a requestId — silently discard.
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const replyChannel = `${PERMISSIONS_RPC_CHECK_CHANNEL}:reply:${requestId}`;
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
if (typeof surface !== "string" || !surface) {
|
|
111
|
-
events.emit(replyChannel, errorReply("surface is required"));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const intent = buildAccessIntentForSurface(
|
|
116
|
-
surface,
|
|
117
|
-
value,
|
|
118
|
-
deps.session.getPathNormalizer(),
|
|
119
|
-
agentName ?? undefined,
|
|
120
|
-
);
|
|
121
|
-
const result = deps.resolver.resolve(intent);
|
|
122
|
-
|
|
123
|
-
const data: PermissionsCheckReplyData = {
|
|
124
|
-
result: result.state,
|
|
125
|
-
matchedPattern: result.matchedPattern ?? null,
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ?? null normalises undefined to null for the reply record
|
|
127
|
-
origin: result.origin ?? null,
|
|
128
|
-
};
|
|
129
|
-
events.emit(replyChannel, successReply(data));
|
|
130
|
-
} catch (err) {
|
|
131
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
-
events.emit(replyChannel, errorReply(message));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── RPC handler: permissions:rpc:prompt ───────────────────────────────────
|
|
137
|
-
|
|
138
|
-
async function handlePromptRpc(
|
|
139
|
-
raw: unknown,
|
|
140
|
-
events: PermissionEventBus,
|
|
141
|
-
deps: PermissionRpcDeps,
|
|
142
|
-
): Promise<void> {
|
|
143
|
-
const req = raw as Partial<PermissionsPromptRequest>;
|
|
144
|
-
const { requestId, surface, value, agentName, message, sessionLabel } = req;
|
|
145
|
-
|
|
146
|
-
if (typeof requestId !== "string" || !requestId) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const replyChannel = `${PERMISSIONS_RPC_PROMPT_CHANNEL}:reply:${requestId}`;
|
|
151
|
-
|
|
152
|
-
const ctx = deps.session.getRuntimeContext();
|
|
153
|
-
if (!ctx?.hasUI) {
|
|
154
|
-
events.emit(replyChannel, errorReply("no_ui"));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (typeof message !== "string" || !message) {
|
|
159
|
-
events.emit(replyChannel, errorReply("message is required"));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const title = surface
|
|
165
|
-
? `Permission request${agentName ? ` from ${agentName}` : ""}`
|
|
166
|
-
: "Permission request";
|
|
167
|
-
|
|
168
|
-
emitUiPromptEvent(
|
|
169
|
-
events,
|
|
170
|
-
buildRpcUiPrompt({ requestId, surface, value, agentName, message }),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const decision = await deps.requestPermissionDecisionFromUi(
|
|
174
|
-
ctx.ui,
|
|
175
|
-
title,
|
|
176
|
-
message,
|
|
177
|
-
sessionLabel ? { sessionLabel } : undefined,
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
deps.logger.review("permission_request.rpc_prompt", {
|
|
181
|
-
requestId,
|
|
182
|
-
surface: surface ?? null,
|
|
183
|
-
value: value ?? null,
|
|
184
|
-
agentName: agentName ?? null,
|
|
185
|
-
message,
|
|
186
|
-
approved: decision.approved,
|
|
187
|
-
resolution: decision.state,
|
|
188
|
-
denialReason: decision.denialReason ?? null,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const data: PermissionsPromptReplyData = {
|
|
192
|
-
approved: decision.approved,
|
|
193
|
-
state: decision.state,
|
|
194
|
-
...(decision.denialReason !== undefined
|
|
195
|
-
? { denialReason: decision.denialReason }
|
|
196
|
-
: {}),
|
|
197
|
-
};
|
|
198
|
-
events.emit(replyChannel, successReply(data));
|
|
199
|
-
} catch (err) {
|
|
200
|
-
const message_ = err instanceof Error ? err.message : String(err);
|
|
201
|
-
events.emit(replyChannel, errorReply(message_));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ── Public API ─────────────────────────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Register `permissions:rpc:check` and `permissions:rpc:prompt` handlers on
|
|
209
|
-
* the event bus.
|
|
210
|
-
*
|
|
211
|
-
* Returns unsubscribe handles — call them in session_shutdown to stop the
|
|
212
|
-
* handlers and prevent memory leaks.
|
|
213
|
-
*/
|
|
214
|
-
export function registerPermissionRpcHandlers(
|
|
215
|
-
events: PermissionEventBus,
|
|
216
|
-
deps: PermissionRpcDeps,
|
|
217
|
-
): PermissionRpcHandles {
|
|
218
|
-
const unsubCheck = events.on(PERMISSIONS_RPC_CHECK_CHANNEL, (raw) => {
|
|
219
|
-
handleCheckRpc(raw, events, deps);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const unsubPrompt = events.on(PERMISSIONS_RPC_PROMPT_CHANNEL, (raw) => {
|
|
223
|
-
void handlePromptRpc(raw, events, deps);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
return { unsubCheck, unsubPrompt };
|
|
227
|
-
}
|