@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.
@@ -34,7 +34,7 @@ import type {
34
34
  PermissionCheckResult,
35
35
  PermissionState,
36
36
  } from "./types";
37
- import { isPermissionState } from "./value-guards";
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
- * - `PromptingGatewayLifecycle` — prompting lifecycle forwarded via activate/deactivate
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 gateway: PromptingGatewayLifecycle,
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.gateway.activate(ctx);
73
+ this.authorizerSelection.activate(ctx);
75
74
  }
76
75
 
77
- /** Clear the context, stop forwarding, and deactivate the gateway. */
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.gateway.deactivate();
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, RPC, or forwarding modules (no import cycles, correct layering).
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
- * Mirrors the simplified RPC signature — surface + optional value + optional
44
- * agent name — and delegates to `PermissionManager.checkPermission()` with
45
- * current session rules internally.
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
  /**
@@ -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`, `ApprovalEscalator`, and `ForwardedRequestServer`.
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
+ }
@@ -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
- }
@@ -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
- }