@gotgenes/pi-permission-system 19.0.1 → 20.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 +29 -0
- package/docs/configuration.md +3 -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/access-intent/bash/bash-path-resolver.ts +6 -1
- package/src/access-intent/bash/token-classification.ts +30 -1
- package/src/authority/approval-escalator.ts +37 -89
- package/src/authority/authorizer-selection.ts +87 -0
- package/src/authority/authorizer.ts +72 -0
- package/src/authority/denying-authorizer.ts +12 -0
- package/src/authority/forwarding-io.ts +0 -1
- package/src/authority/local-user-authorizer.ts +46 -0
- package/src/{permission-prompter.ts → authority/permission-prompter.ts} +23 -48
- package/src/authority/subagent-detection.ts +1 -1
- package/src/config-loader.ts +1 -1
- package/src/gate-prompter.ts +1 -1
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/index.ts +19 -34
- package/src/normalize.ts +1 -1
- package/src/path-normalizer.ts +12 -0
- package/src/permission-events.ts +7 -103
- package/src/permission-manager.ts +1 -1
- package/src/permission-session.ts +6 -7
- package/src/permission-ui-prompt.ts +1 -25
- package/src/service.ts +3 -8
- package/src/session-logger.ts +1 -1
- package/src/types.ts +20 -0
- package/src/value-guards.ts +0 -22
- package/src/permission-event-rpc.ts +0 -227
- package/src/prompting-gateway.ts +0 -89
|
@@ -34,7 +34,7 @@ import type {
|
|
|
34
34
|
PermissionCheckResult,
|
|
35
35
|
PermissionState,
|
|
36
36
|
} from "./types";
|
|
37
|
-
import { isPermissionState } from "./
|
|
37
|
+
import { isPermissionState } from "./types";
|
|
38
38
|
import { wildcardMatch } from "./wildcard-matcher";
|
|
39
39
|
|
|
40
40
|
const BUILT_IN_TOOL_PERMISSION_NAMES = new Set([
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getActiveAgentName,
|
|
4
4
|
getActiveAgentNameFromSystemPrompt,
|
|
5
5
|
} from "./active-agent";
|
|
6
|
-
|
|
6
|
+
import type { AuthorizerSelectionLifecycle } from "./authority/authorizer-selection";
|
|
7
7
|
import type { SessionConfigStore } from "./config-store";
|
|
8
8
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
9
9
|
import type { ExtensionPaths } from "./extension-paths";
|
|
@@ -11,7 +11,6 @@ import type { ForwardingController } from "./forwarding-manager";
|
|
|
11
11
|
import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipeline";
|
|
12
12
|
import { PathNormalizer } from "./path-normalizer";
|
|
13
13
|
import type { ScopedPermissionManager } from "./permission-manager";
|
|
14
|
-
import type { PromptingGatewayLifecycle } from "./prompting-gateway";
|
|
15
14
|
|
|
16
15
|
import type { SessionRules } from "./session-rules";
|
|
17
16
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
@@ -33,7 +32,7 @@ import type { PathRuleTokenMatcher } from "./types";
|
|
|
33
32
|
* - `ExtensionPaths` — immutable path constants
|
|
34
33
|
* - `ForwardingController` — polling lifecycle
|
|
35
34
|
* - `SessionConfigStore` — owns extension config; provides refresh, log, read
|
|
36
|
-
* - `
|
|
35
|
+
* - `AuthorizerSelectionLifecycle` — authorizer-selection lifecycle forwarded via activate/deactivate
|
|
37
36
|
*/
|
|
38
37
|
export class PermissionSession implements ToolCallGateInputs {
|
|
39
38
|
private context: ExtensionContext | null = null;
|
|
@@ -47,7 +46,7 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
47
46
|
private readonly permissionManager: ScopedPermissionManager,
|
|
48
47
|
private readonly sessionRules: SessionRules,
|
|
49
48
|
private readonly configStore: SessionConfigStore,
|
|
50
|
-
private readonly
|
|
49
|
+
private readonly authorizerSelection: AuthorizerSelectionLifecycle,
|
|
51
50
|
private readonly platform: NodeJS.Platform,
|
|
52
51
|
) {
|
|
53
52
|
// Placeholder until the first activate(ctx) binds the real cwd; every gate
|
|
@@ -71,14 +70,14 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
71
70
|
this.context = ctx;
|
|
72
71
|
this.pathNormalizer = new PathNormalizer(this.platform, ctx.cwd);
|
|
73
72
|
this.forwarding.start(ctx);
|
|
74
|
-
this.
|
|
73
|
+
this.authorizerSelection.activate(ctx);
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
/** Clear the context, stop forwarding, and deactivate the
|
|
76
|
+
/** Clear the context, stop forwarding, and deactivate the authorizer selection. */
|
|
78
77
|
deactivate(): void {
|
|
79
78
|
this.context = null;
|
|
80
79
|
this.forwarding.stop();
|
|
81
|
-
this.
|
|
80
|
+
this.authorizerSelection.deactivate();
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
/** Return the current runtime context, or null if not activated. */
|
|
@@ -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/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`, `
|
|
22
|
+
* Injected into `ConfigStore`, `ParentAuthorizer`, and `ForwardedRequestServer`.
|
|
23
23
|
*/
|
|
24
24
|
export interface DebugReviewLogger extends ReviewLogger {
|
|
25
25
|
debug(event: string, details?: Record<string, unknown>): void;
|
package/src/types.ts
CHANGED
|
@@ -63,3 +63,23 @@ export interface PermissionCheckResult {
|
|
|
63
63
|
*/
|
|
64
64
|
commandContext?: BashCommandContext;
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
export function isPermissionState(value: unknown): value is PermissionState {
|
|
68
|
+
return value === "allow" || value === "deny" || value === "ask";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Narrow type guard: a raw value representing a DenyWithReason object.
|
|
73
|
+
* Accepts `{ action: "deny" }` and `{ action: "deny", reason: "…" }`.
|
|
74
|
+
* Rejects a non-string `reason` to keep malformed config out of the rule set.
|
|
75
|
+
*/
|
|
76
|
+
export function isDenyWithReason(value: unknown): value is DenyWithReason {
|
|
77
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const record = value as Record<string, unknown>;
|
|
81
|
+
return (
|
|
82
|
+
record.action === "deny" &&
|
|
83
|
+
(record.reason === undefined || typeof record.reason === "string")
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/value-guards.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { DenyWithReason, PermissionState } from "./types";
|
|
2
|
-
|
|
3
1
|
export function toRecord(value: unknown): Record<string, unknown> {
|
|
4
2
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
3
|
return {};
|
|
@@ -16,23 +14,3 @@ export function getNonEmptyString(value: unknown): string | null {
|
|
|
16
14
|
const trimmed = value.trim();
|
|
17
15
|
return trimmed.length > 0 ? trimmed : null;
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
export function isPermissionState(value: unknown): value is PermissionState {
|
|
21
|
-
return value === "allow" || value === "deny" || value === "ask";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Narrow type guard: a raw value representing a DenyWithReason object.
|
|
26
|
-
* Accepts `{ action: "deny" }` and `{ action: "deny", reason: "…" }`.
|
|
27
|
-
* Rejects a non-string `reason` to keep malformed config out of the rule set.
|
|
28
|
-
*/
|
|
29
|
-
export function isDenyWithReason(value: unknown): value is DenyWithReason {
|
|
30
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
const record = value as Record<string, unknown>;
|
|
34
|
-
return (
|
|
35
|
-
record.action === "deny" &&
|
|
36
|
-
(record.reason === undefined || typeof record.reason === "string")
|
|
37
|
-
);
|
|
38
|
-
}
|
|
@@ -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
|
-
}
|
package/src/prompting-gateway.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { SubagentDetector } from "./authority/subagent-detection";
|
|
3
|
-
import type { GatePrompter } from "./gate-prompter";
|
|
4
|
-
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
5
|
-
import type {
|
|
6
|
-
PermissionPrompterApi,
|
|
7
|
-
PromptPermissionDetails,
|
|
8
|
-
} from "./permission-prompter";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Dependencies required by PromptingGateway.
|
|
12
|
-
*
|
|
13
|
-
* All fields are actively consumed:
|
|
14
|
-
* - `detection` drives `canConfirm()`.
|
|
15
|
-
* - `prompter` is called by `prompt()`.
|
|
16
|
-
*/
|
|
17
|
-
export interface PromptingGatewayDeps {
|
|
18
|
-
/** Single owner of subagent detection; drives `canConfirm()`. */
|
|
19
|
-
detection: SubagentDetector;
|
|
20
|
-
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
21
|
-
prompter: PermissionPrompterApi;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The lifecycle slice of the gateway that PermissionSession drives.
|
|
26
|
-
*
|
|
27
|
-
* PermissionSession calls activate/deactivate to keep the gateway's stored
|
|
28
|
-
* context in sync with its own — the same pattern used for ForwardingController.
|
|
29
|
-
*/
|
|
30
|
-
export interface PromptingGatewayLifecycle {
|
|
31
|
-
activate(ctx: ExtensionContext): void;
|
|
32
|
-
deactivate(): void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Context-owning implementation of the GatePrompter role.
|
|
37
|
-
*
|
|
38
|
-
* Owns the stored ExtensionContext and the "can we prompt?" policy
|
|
39
|
-
* (UI / subagent), replacing the four twin methods that previously
|
|
40
|
-
* lived on PermissionSession.
|
|
41
|
-
*
|
|
42
|
-
* Lifecycle: PermissionSession drives activate/deactivate so the stored
|
|
43
|
-
* context mirrors the session context without independent call-site changes.
|
|
44
|
-
*/
|
|
45
|
-
export class PromptingGateway
|
|
46
|
-
implements GatePrompter, PromptingGatewayLifecycle
|
|
47
|
-
{
|
|
48
|
-
private context: ExtensionContext | null = null;
|
|
49
|
-
|
|
50
|
-
constructor(private readonly deps: PromptingGatewayDeps) {}
|
|
51
|
-
|
|
52
|
-
/** Store the current extension context. */
|
|
53
|
-
activate(ctx: ExtensionContext): void {
|
|
54
|
-
this.context = ctx;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Clear the stored context. */
|
|
58
|
-
deactivate(): void {
|
|
59
|
-
this.context = null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Whether an interactive permission prompt can be shown.
|
|
64
|
-
*
|
|
65
|
-
* Returns false when no context is active. Otherwise true when the
|
|
66
|
-
* context has UI or is a forwarding subagent — the two Authorizer-
|
|
67
|
-
* selection predicates the Phase 9 spine will consume. Yolo-mode never
|
|
68
|
-
* reaches this policy: it is resolved upstream at the composition stage.
|
|
69
|
-
*/
|
|
70
|
-
canConfirm(): boolean {
|
|
71
|
-
if (this.context === null) return false;
|
|
72
|
-
return this.context.hasUI || this.deps.detection.isSubagent(this.context);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Prompt the user for a permission decision using the stored context.
|
|
77
|
-
*
|
|
78
|
-
* Rejects if no context is active — canConfirm() guards this in normal use.
|
|
79
|
-
* Implements {@link GatePrompter}.
|
|
80
|
-
*/
|
|
81
|
-
prompt(details: PromptPermissionDetails): Promise<PermissionPromptDecision> {
|
|
82
|
-
if (this.context === null) {
|
|
83
|
-
return Promise.reject(
|
|
84
|
-
new Error("prompt called before the session was activated"),
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return this.deps.prompter.prompt(this.context, details);
|
|
88
|
-
}
|
|
89
|
-
}
|