@gotgenes/pi-permission-system 10.3.1 → 10.5.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 +24 -0
- package/package.json +1 -1
- package/src/config-modal.ts +10 -8
- package/src/config-store.ts +6 -11
- package/src/forwarded-permissions/io.ts +16 -22
- package/src/forwarded-permissions/permission-forwarder.ts +16 -19
- package/src/gate-prompter.ts +1 -3
- package/src/handlers/gates/bash-command.ts +2 -2
- package/src/handlers/gates/bash-external-directory.ts +2 -2
- package/src/handlers/gates/bash-path.ts +2 -2
- package/src/handlers/gates/path.ts +2 -2
- package/src/handlers/gates/runner.ts +3 -3
- package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
- package/src/index.ts +27 -41
- package/src/permission-event-rpc.ts +19 -15
- package/src/permission-prompter.ts +4 -3
- package/src/permission-resolver.ts +69 -2
- package/src/permission-session.ts +7 -83
- package/src/prompting-gateway.ts +104 -0
- package/src/session-logger.ts +17 -3
- package/test/config-modal.test.ts +13 -7
- package/test/config-store.test.ts +7 -9
- package/test/forwarded-permissions/io.test.ts +23 -26
- package/test/handlers/external-directory-integration.test.ts +45 -32
- package/test/handlers/external-directory-session-dedup.test.ts +47 -57
- package/test/handlers/gates/bash-external-directory.test.ts +2 -2
- package/test/handlers/gates/bash-path.test.ts +2 -2
- package/test/handlers/gates/runner.test.ts +10 -16
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
- package/test/handlers/input-events.test.ts +19 -4
- package/test/handlers/input.test.ts +29 -13
- package/test/handlers/tool-call-events.test.ts +23 -5
- package/test/helpers/gate-fixtures.ts +11 -15
- package/test/helpers/handler-fixtures.ts +31 -50
- package/test/permission-event-rpc.test.ts +30 -28
- package/test/permission-forwarder.test.ts +6 -5
- package/test/permission-prompter.test.ts +28 -28
- package/test/permission-resolver.test.ts +194 -0
- package/test/permission-session.test.ts +27 -180
- package/test/prompting-gateway.test.ts +230 -0
package/src/index.ts
CHANGED
|
@@ -23,19 +23,16 @@ import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
|
23
23
|
import { registerPermissionRpcHandlers } from "./permission-event-rpc";
|
|
24
24
|
import { PermissionManager } from "./permission-manager";
|
|
25
25
|
import { PermissionPrompter } from "./permission-prompter";
|
|
26
|
+
import { PermissionResolver } from "./permission-resolver";
|
|
26
27
|
import { PermissionSession } from "./permission-session";
|
|
27
28
|
import { LocalPermissionsService } from "./permissions-service";
|
|
29
|
+
import { PromptingGateway } from "./prompting-gateway";
|
|
28
30
|
import { PermissionServiceLifecycle } from "./service-lifecycle";
|
|
29
31
|
import { createSessionLogger } from "./session-logger";
|
|
30
32
|
import { SessionRules } from "./session-rules";
|
|
31
|
-
import { isSubagentExecutionContext } from "./subagent-context";
|
|
32
33
|
import { subscribeSubagentLifecycle } from "./subagent-lifecycle-events";
|
|
33
34
|
import { getSubagentSessionRegistry } from "./subagent-registry";
|
|
34
35
|
import { ToolInputFormatterRegistry } from "./tool-input-formatter-registry";
|
|
35
|
-
import {
|
|
36
|
-
canResolveAskPermissionRequest,
|
|
37
|
-
shouldAutoApprovePermissionState,
|
|
38
|
-
} from "./yolo-mode";
|
|
39
36
|
|
|
40
37
|
export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
41
38
|
const agentDir = getAgentDir();
|
|
@@ -67,10 +64,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
67
64
|
configStore = new ConfigStore({
|
|
68
65
|
agentDir,
|
|
69
66
|
policyPaths: permissionManager,
|
|
70
|
-
logger
|
|
71
|
-
writeDebugLog: (e, d) => logger.debug(e, d),
|
|
72
|
-
writeReviewLog: (e, d) => logger.review(e, d),
|
|
73
|
-
},
|
|
67
|
+
logger,
|
|
74
68
|
});
|
|
75
69
|
|
|
76
70
|
const forwardingDeps: PermissionForwarderDeps = {
|
|
@@ -78,26 +72,28 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
78
72
|
subagentSessionsDir: paths.subagentSessionsDir,
|
|
79
73
|
registry: subagentRegistry,
|
|
80
74
|
events: pi.events,
|
|
81
|
-
logger
|
|
82
|
-
writeReviewLog: (event, details) => logger.review(event, details),
|
|
83
|
-
writeDebugLog: (event, details) => logger.debug(event, details),
|
|
84
|
-
},
|
|
85
|
-
writeReviewLog: (event, details) => logger.review(event, details),
|
|
75
|
+
logger,
|
|
86
76
|
requestPermissionDecisionFromUi,
|
|
87
|
-
|
|
88
|
-
shouldAutoApprovePermissionState("ask", configStore.current()),
|
|
77
|
+
config: configStore,
|
|
89
78
|
};
|
|
90
79
|
const forwarder = new PermissionForwarder(forwardingDeps);
|
|
91
80
|
|
|
92
81
|
const prompter = new PermissionPrompter({
|
|
93
82
|
config: configStore,
|
|
94
|
-
|
|
83
|
+
logger,
|
|
95
84
|
events: pi.events,
|
|
96
85
|
forwarder,
|
|
97
86
|
});
|
|
98
87
|
|
|
99
88
|
configStore.refresh();
|
|
100
89
|
|
|
90
|
+
const gateway = new PromptingGateway({
|
|
91
|
+
config: configStore,
|
|
92
|
+
subagentSessionsDir: paths.subagentSessionsDir,
|
|
93
|
+
registry: subagentRegistry,
|
|
94
|
+
prompter,
|
|
95
|
+
});
|
|
96
|
+
|
|
101
97
|
const session = new PermissionSession(
|
|
102
98
|
paths,
|
|
103
99
|
logger,
|
|
@@ -109,39 +105,26 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
109
105
|
permissionManager,
|
|
110
106
|
sessionRules,
|
|
111
107
|
configStore,
|
|
112
|
-
|
|
113
|
-
canRequestPermissionConfirmation: (ctx) =>
|
|
114
|
-
canResolveAskPermissionRequest({
|
|
115
|
-
config: configStore.current(),
|
|
116
|
-
hasUI: ctx.hasUI,
|
|
117
|
-
isSubagent: isSubagentExecutionContext(
|
|
118
|
-
ctx,
|
|
119
|
-
paths.subagentSessionsDir,
|
|
120
|
-
subagentRegistry,
|
|
121
|
-
),
|
|
122
|
-
}),
|
|
123
|
-
promptPermission: (ctx, details) => prompter.prompt(ctx, details),
|
|
124
|
-
},
|
|
108
|
+
gateway,
|
|
125
109
|
);
|
|
126
110
|
|
|
127
111
|
// Connect the notify sink now that session is available.
|
|
128
112
|
sessionNotify = session;
|
|
129
113
|
|
|
114
|
+
const configPath = getGlobalConfigPath(agentDir);
|
|
130
115
|
registerPermissionSystemCommand(pi, {
|
|
131
116
|
config: configStore,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
session.lastKnownActiveAgentName ?? undefined,
|
|
136
|
-
),
|
|
117
|
+
configPath,
|
|
118
|
+
permissionManager,
|
|
119
|
+
session,
|
|
137
120
|
});
|
|
138
121
|
|
|
139
122
|
const rpcHandles = registerPermissionRpcHandlers(pi.events, {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
123
|
+
permissionManager,
|
|
124
|
+
sessionRules,
|
|
125
|
+
session,
|
|
143
126
|
requestPermissionDecisionFromUi,
|
|
144
|
-
|
|
127
|
+
logger,
|
|
145
128
|
});
|
|
146
129
|
|
|
147
130
|
const permissionsService = new LocalPermissionsService(
|
|
@@ -176,13 +159,16 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
176
159
|
|
|
177
160
|
const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
|
|
178
161
|
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
162
|
+
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
163
|
+
|
|
179
164
|
const reporter = new GateDecisionReporter(session.logger, pi.events);
|
|
180
|
-
const gateRunner = new GateRunner(
|
|
165
|
+
const gateRunner = new GateRunner(resolver, session, gateway, reporter);
|
|
181
166
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
167
|
+
resolver,
|
|
182
168
|
session,
|
|
183
169
|
formatterRegistry,
|
|
184
170
|
);
|
|
185
|
-
const skillInputGatePipeline = new SkillInputGatePipeline(
|
|
171
|
+
const skillInputGatePipeline = new SkillInputGatePipeline(resolver);
|
|
186
172
|
const gates = new PermissionGateHandler(
|
|
187
173
|
session,
|
|
188
174
|
toolRegistry,
|
|
@@ -28,19 +28,20 @@ import {
|
|
|
28
28
|
} from "./permission-events";
|
|
29
29
|
import type { PermissionManager } from "./permission-manager";
|
|
30
30
|
import { buildRpcUiPrompt } from "./permission-ui-prompt";
|
|
31
|
-
import type {
|
|
31
|
+
import type { ReviewLogger } from "./session-logger";
|
|
32
|
+
import type { SessionRules } from "./session-rules";
|
|
32
33
|
|
|
33
34
|
/** Dependencies injected into the RPC handler registry. */
|
|
34
35
|
export interface PermissionRpcDeps {
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
36
|
+
/** The shared PermissionManager instance. */
|
|
37
|
+
permissionManager: Pick<PermissionManager, "checkPermission">;
|
|
38
|
+
/** The shared SessionRules instance. */
|
|
39
|
+
sessionRules: Pick<SessionRules, "getRuleset">;
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
+
* Narrow session view: provides runtime context.
|
|
41
42
|
* Used by the prompt handler to check hasUI and access the UI dialog.
|
|
42
43
|
*/
|
|
43
|
-
getRuntimeContext(): ExtensionContext | null;
|
|
44
|
+
session: { getRuntimeContext(): ExtensionContext | null };
|
|
44
45
|
/** Show the interactive permission dialog in the parent session UI. */
|
|
45
46
|
requestPermissionDecisionFromUi(
|
|
46
47
|
ui: ExtensionContext["ui"],
|
|
@@ -48,8 +49,8 @@ export interface PermissionRpcDeps {
|
|
|
48
49
|
message: string,
|
|
49
50
|
options?: RequestPermissionOptions,
|
|
50
51
|
): Promise<PermissionPromptDecision>;
|
|
51
|
-
/** Write
|
|
52
|
-
|
|
52
|
+
/** Write review-log entries for prompted decisions. */
|
|
53
|
+
logger: ReviewLogger;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Unsubscribe handles returned from registerPermissionRpcHandlers. */
|
|
@@ -107,10 +108,13 @@ function handleCheckRpc(
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
const input = buildInputForSurface(surface, value);
|
|
110
|
-
const sessionRules = deps.
|
|
111
|
-
const result = deps
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const sessionRules = deps.sessionRules.getRuleset();
|
|
112
|
+
const result = deps.permissionManager.checkPermission(
|
|
113
|
+
surface,
|
|
114
|
+
input,
|
|
115
|
+
agentName ?? undefined,
|
|
116
|
+
sessionRules,
|
|
117
|
+
);
|
|
114
118
|
|
|
115
119
|
const data: PermissionsCheckReplyData = {
|
|
116
120
|
result: result.state,
|
|
@@ -141,7 +145,7 @@ async function handlePromptRpc(
|
|
|
141
145
|
|
|
142
146
|
const replyChannel = `${PERMISSIONS_RPC_PROMPT_CHANNEL}:reply:${requestId}`;
|
|
143
147
|
|
|
144
|
-
const ctx = deps.getRuntimeContext();
|
|
148
|
+
const ctx = deps.session.getRuntimeContext();
|
|
145
149
|
if (!ctx?.hasUI) {
|
|
146
150
|
events.emit(replyChannel, errorReply("no_ui"));
|
|
147
151
|
return;
|
|
@@ -169,7 +173,7 @@ async function handlePromptRpc(
|
|
|
169
173
|
sessionLabel ? { sessionLabel } : undefined,
|
|
170
174
|
);
|
|
171
175
|
|
|
172
|
-
deps.
|
|
176
|
+
deps.logger.review("permission_request.rpc_prompt", {
|
|
173
177
|
requestId,
|
|
174
178
|
surface: surface ?? null,
|
|
175
179
|
value: value ?? null,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type PermissionEventBus,
|
|
8
8
|
} from "./permission-events";
|
|
9
9
|
import { buildDirectUiPrompt } from "./permission-ui-prompt";
|
|
10
|
+
import type { ReviewLogger } from "./session-logger";
|
|
10
11
|
import { shouldAutoApprovePermissionState } from "./yolo-mode";
|
|
11
12
|
|
|
12
13
|
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
@@ -40,14 +41,14 @@ export interface PermissionPrompterApi {
|
|
|
40
41
|
* Dependencies required by PermissionPrompter.
|
|
41
42
|
*
|
|
42
43
|
* Keeps the prompter's external surface narrow: callers provide config
|
|
43
|
-
* access, review
|
|
44
|
+
* access, a review logger, the UI-prompt event bus, and the forwarder
|
|
44
45
|
* that owns the UI/subagent-forwarding branching logic.
|
|
45
46
|
*/
|
|
46
47
|
export interface PermissionPrompterDeps {
|
|
47
48
|
/** Read current config for yolo-mode check (called at prompt time). */
|
|
48
49
|
config: ConfigReader;
|
|
49
50
|
/** Write structured entries to the permission review log. */
|
|
50
|
-
|
|
51
|
+
logger: ReviewLogger;
|
|
51
52
|
/** Event bus used for UI prompt broadcasts. */
|
|
52
53
|
events: PermissionEventBus;
|
|
53
54
|
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
@@ -122,7 +123,7 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
122
123
|
denialReason?: string;
|
|
123
124
|
},
|
|
124
125
|
): void {
|
|
125
|
-
this.deps.
|
|
126
|
+
this.deps.logger.review(event, {
|
|
126
127
|
requestId: details.requestId,
|
|
127
128
|
source: details.source,
|
|
128
129
|
agentName: details.agentName,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScopedPermissionManager } from "./permission-manager";
|
|
2
|
+
import type { Rule } from "./rule";
|
|
3
|
+
import type { SessionRules } from "./session-rules";
|
|
4
|
+
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Resolves the effective permission for a surface/input, applying the current
|
|
@@ -8,10 +11,74 @@ import type { PermissionCheckResult } from "./types";
|
|
|
8
11
|
* previously threaded by hand: the ruleset was only ever fetched to be passed
|
|
9
12
|
* straight back into `checkPermission`, so the two are one operation.
|
|
10
13
|
*/
|
|
11
|
-
export interface
|
|
14
|
+
export interface ScopedPermissionResolver {
|
|
12
15
|
resolve(
|
|
13
16
|
surface: string,
|
|
14
17
|
input: unknown,
|
|
15
18
|
agentName?: string,
|
|
16
19
|
): PermissionCheckResult;
|
|
17
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Concrete collaborator that owns the resolution surface.
|
|
24
|
+
*
|
|
25
|
+
* Holds a `ScopedPermissionManager` and a `SessionRules` store, composing
|
|
26
|
+
* them so callers never thread the session ruleset by hand.
|
|
27
|
+
*
|
|
28
|
+
* Constructor deps:
|
|
29
|
+
* - `permissionManager` — the narrow session-scoped permission-checking interface
|
|
30
|
+
* - `sessionRules` — narrowed to `getRuleset` (ISP: the resolver only reads, never records)
|
|
31
|
+
*/
|
|
32
|
+
export class PermissionResolver implements ScopedPermissionResolver {
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly permissionManager: ScopedPermissionManager,
|
|
35
|
+
private readonly sessionRules: Pick<SessionRules, "getRuleset">,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the effective permission for a surface/input, applying the current
|
|
40
|
+
* session rules. Composes `checkPermission` with `getRuleset()` so callers
|
|
41
|
+
* never thread the ruleset by hand.
|
|
42
|
+
*/
|
|
43
|
+
resolve(
|
|
44
|
+
surface: string,
|
|
45
|
+
input: unknown,
|
|
46
|
+
agentName?: string,
|
|
47
|
+
): PermissionCheckResult {
|
|
48
|
+
return this.checkPermission(
|
|
49
|
+
surface,
|
|
50
|
+
input,
|
|
51
|
+
agentName,
|
|
52
|
+
this.sessionRules.getRuleset(),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
checkPermission(
|
|
57
|
+
surface: string,
|
|
58
|
+
input: unknown,
|
|
59
|
+
agentName?: string,
|
|
60
|
+
sessionRules?: Rule[],
|
|
61
|
+
): PermissionCheckResult {
|
|
62
|
+
return this.permissionManager.checkPermission(
|
|
63
|
+
surface,
|
|
64
|
+
input,
|
|
65
|
+
agentName,
|
|
66
|
+
sessionRules,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// fallow-ignore-next-line unused-class-member
|
|
71
|
+
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
72
|
+
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// fallow-ignore-next-line unused-class-member
|
|
76
|
+
getConfigIssues(agentName?: string): string[] {
|
|
77
|
+
return this.permissionManager.getConfigIssues(agentName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// fallow-ignore-next-line unused-class-member
|
|
81
|
+
getPolicyCacheStamp(agentName?: string): string {
|
|
82
|
+
return this.permissionManager.getPolicyCacheStamp(agentName);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -10,11 +10,8 @@ import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
|
10
10
|
import type { ExtensionPaths } from "./extension-paths";
|
|
11
11
|
import type { ForwardingController } from "./forwarding-manager";
|
|
12
12
|
import type { GateHandlerSession } from "./gate-handler-session";
|
|
13
|
-
import type { GatePrompter } from "./gate-prompter";
|
|
14
|
-
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
15
13
|
import type { ScopedPermissionManager } from "./permission-manager";
|
|
16
|
-
import type {
|
|
17
|
-
import type { PermissionResolver } from "./permission-resolver";
|
|
14
|
+
import type { PromptingGatewayLifecycle } from "./prompting-gateway";
|
|
18
15
|
import type { Rule } from "./rule";
|
|
19
16
|
import type { SessionApproval } from "./session-approval";
|
|
20
17
|
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
@@ -28,21 +25,6 @@ import {
|
|
|
28
25
|
} from "./tool-preview-formatter";
|
|
29
26
|
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
30
27
|
|
|
31
|
-
/**
|
|
32
|
-
* Runtime operations that `PermissionSession` delegates to but does not own.
|
|
33
|
-
*
|
|
34
|
-
* Injected at construction time from the composition root (`index.ts`).
|
|
35
|
-
*/
|
|
36
|
-
export interface PermissionSessionRuntimeDeps {
|
|
37
|
-
/** Whether the current context can show an interactive permission prompt. */
|
|
38
|
-
canRequestPermissionConfirmation(ctx: ExtensionContext): boolean;
|
|
39
|
-
/** Prompt the user for a permission decision, log the outcome, and return it. */
|
|
40
|
-
promptPermission(
|
|
41
|
-
ctx: ExtensionContext,
|
|
42
|
-
details: PromptPermissionDetails,
|
|
43
|
-
): Promise<PermissionPromptDecision>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
28
|
/**
|
|
47
29
|
* Encapsulates all mutable session state and exposes operations instead of
|
|
48
30
|
* fields.
|
|
@@ -56,13 +38,11 @@ export interface PermissionSessionRuntimeDeps {
|
|
|
56
38
|
* - `SessionLogger` — debug + review + warn
|
|
57
39
|
* - `ForwardingController` — polling lifecycle
|
|
58
40
|
* - `SessionConfigStore` — owns extension config; provides refresh, log, read
|
|
59
|
-
* - `
|
|
41
|
+
* - `PromptingGatewayLifecycle` — prompting lifecycle forwarded via activate/deactivate
|
|
60
42
|
*/
|
|
61
43
|
export class PermissionSession
|
|
62
44
|
implements
|
|
63
|
-
PermissionResolver,
|
|
64
45
|
SessionApprovalRecorder,
|
|
65
|
-
GatePrompter,
|
|
66
46
|
GateHandlerSession,
|
|
67
47
|
AgentPrepSession,
|
|
68
48
|
SessionLifecycleSession
|
|
@@ -80,21 +60,23 @@ export class PermissionSession
|
|
|
80
60
|
private readonly permissionManager: ScopedPermissionManager,
|
|
81
61
|
private readonly sessionRules: SessionRules,
|
|
82
62
|
private readonly configStore: SessionConfigStore,
|
|
83
|
-
private readonly
|
|
63
|
+
private readonly gateway: PromptingGatewayLifecycle,
|
|
84
64
|
) {}
|
|
85
65
|
|
|
86
66
|
// ── Context lifecycle ──────────────────────────────────────────────────
|
|
87
67
|
|
|
88
|
-
/** Store the current extension context and
|
|
68
|
+
/** Store the current extension context, start forwarding, and activate the gateway. */
|
|
89
69
|
activate(ctx: ExtensionContext): void {
|
|
90
70
|
this.context = ctx;
|
|
91
71
|
this.forwarding.start(ctx);
|
|
72
|
+
this.gateway.activate(ctx);
|
|
92
73
|
}
|
|
93
74
|
|
|
94
|
-
/** Clear the context and
|
|
75
|
+
/** Clear the context, stop forwarding, and deactivate the gateway. */
|
|
95
76
|
deactivate(): void {
|
|
96
77
|
this.context = null;
|
|
97
78
|
this.forwarding.stop();
|
|
79
|
+
this.gateway.deactivate();
|
|
98
80
|
}
|
|
99
81
|
|
|
100
82
|
/** Return the current runtime context, or null if not activated. */
|
|
@@ -118,24 +100,6 @@ export class PermissionSession
|
|
|
118
100
|
);
|
|
119
101
|
}
|
|
120
102
|
|
|
121
|
-
/**
|
|
122
|
-
* Resolve the effective permission for a surface/input, applying the current
|
|
123
|
-
* session rules. Composes `checkPermission` with `getSessionRuleset` so
|
|
124
|
-
* callers never thread the ruleset by hand.
|
|
125
|
-
*/
|
|
126
|
-
resolve(
|
|
127
|
-
surface: string,
|
|
128
|
-
input: unknown,
|
|
129
|
-
agentName?: string,
|
|
130
|
-
): PermissionCheckResult {
|
|
131
|
-
return this.checkPermission(
|
|
132
|
-
surface,
|
|
133
|
-
input,
|
|
134
|
-
agentName,
|
|
135
|
-
this.getSessionRuleset(),
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
103
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
140
104
|
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
141
105
|
}
|
|
@@ -291,44 +255,4 @@ export class PermissionSession
|
|
|
291
255
|
getToolPreviewLimits(): ToolPreviewFormatterOptions {
|
|
292
256
|
return resolveToolPreviewLimits(this.config);
|
|
293
257
|
}
|
|
294
|
-
|
|
295
|
-
// ── Prompting ──────────────────────────────────────────────────────────
|
|
296
|
-
|
|
297
|
-
/** Whether the current context can show an interactive permission prompt. */
|
|
298
|
-
canPrompt(ctx: ExtensionContext): boolean {
|
|
299
|
-
return this.runtimeDeps.canRequestPermissionConfirmation(ctx);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Prompt the user for a permission decision, log the outcome, and return it. */
|
|
303
|
-
prompt(
|
|
304
|
-
ctx: ExtensionContext,
|
|
305
|
-
details: PromptPermissionDetails,
|
|
306
|
-
): Promise<PermissionPromptDecision> {
|
|
307
|
-
return this.runtimeDeps.promptPermission(ctx, details);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Whether an interactive confirmation is possible using the stored context.
|
|
312
|
-
* Returns `false` when no context is active (before `activate` is called).
|
|
313
|
-
* Implements {@link GatePrompter}.
|
|
314
|
-
*/
|
|
315
|
-
canConfirm(): boolean {
|
|
316
|
-
return this.context !== null && this.canPrompt(this.context);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Prompt the user for a permission decision using the stored context.
|
|
321
|
-
* Throws if no context is active — `canConfirm()` guards this in normal use.
|
|
322
|
-
* Implements {@link GatePrompter}.
|
|
323
|
-
*/
|
|
324
|
-
promptPermission(
|
|
325
|
-
details: PromptPermissionDetails,
|
|
326
|
-
): Promise<PermissionPromptDecision> {
|
|
327
|
-
if (this.context === null) {
|
|
328
|
-
return Promise.reject(
|
|
329
|
-
new Error("promptPermission called before the session was activated"),
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
return this.prompt(this.context, details);
|
|
333
|
-
}
|
|
334
258
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { ConfigReader } from "./config-store";
|
|
4
|
+
import type { GatePrompter } from "./gate-prompter";
|
|
5
|
+
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
6
|
+
import type {
|
|
7
|
+
PermissionPrompterApi,
|
|
8
|
+
PromptPermissionDetails,
|
|
9
|
+
} from "./permission-prompter";
|
|
10
|
+
import { isSubagentExecutionContext } from "./subagent-context";
|
|
11
|
+
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
12
|
+
import { canResolveAskPermissionRequest } from "./yolo-mode";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dependencies required by PromptingGateway.
|
|
16
|
+
*
|
|
17
|
+
* All four fields are actively consumed:
|
|
18
|
+
* - `config` + `subagentSessionsDir` + `registry` drive `canConfirm()`.
|
|
19
|
+
* - `prompter` is called by `prompt()`.
|
|
20
|
+
*/
|
|
21
|
+
export interface PromptingGatewayDeps {
|
|
22
|
+
/** Read current config for the yolo-mode branch of the can-prompt policy. */
|
|
23
|
+
config: ConfigReader;
|
|
24
|
+
/** Static path used to detect a forwarding subagent context. */
|
|
25
|
+
subagentSessionsDir: string;
|
|
26
|
+
/** Process-global registry used to detect a registered child session. */
|
|
27
|
+
registry?: SubagentSessionRegistry;
|
|
28
|
+
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
29
|
+
prompter: PermissionPrompterApi;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The lifecycle slice of the gateway that PermissionSession drives.
|
|
34
|
+
*
|
|
35
|
+
* PermissionSession calls activate/deactivate to keep the gateway's stored
|
|
36
|
+
* context in sync with its own — the same pattern used for ForwardingController.
|
|
37
|
+
*/
|
|
38
|
+
export interface PromptingGatewayLifecycle {
|
|
39
|
+
activate(ctx: ExtensionContext): void;
|
|
40
|
+
deactivate(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Context-owning implementation of the GatePrompter role.
|
|
45
|
+
*
|
|
46
|
+
* Owns the stored ExtensionContext and the "can we prompt?" policy
|
|
47
|
+
* (UI / subagent / yolo-mode), replacing the four twin methods
|
|
48
|
+
* that previously lived on PermissionSession.
|
|
49
|
+
*
|
|
50
|
+
* Lifecycle: PermissionSession drives activate/deactivate so the stored
|
|
51
|
+
* context mirrors the session context without independent call-site changes.
|
|
52
|
+
*/
|
|
53
|
+
export class PromptingGateway
|
|
54
|
+
implements GatePrompter, PromptingGatewayLifecycle
|
|
55
|
+
{
|
|
56
|
+
private context: ExtensionContext | null = null;
|
|
57
|
+
|
|
58
|
+
constructor(private readonly deps: PromptingGatewayDeps) {}
|
|
59
|
+
|
|
60
|
+
/** Store the current extension context. */
|
|
61
|
+
activate(ctx: ExtensionContext): void {
|
|
62
|
+
this.context = ctx;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Clear the stored context. */
|
|
66
|
+
deactivate(): void {
|
|
67
|
+
this.context = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether an interactive permission prompt can be shown.
|
|
72
|
+
*
|
|
73
|
+
* Returns false when no context is active. Otherwise delegates to
|
|
74
|
+
* canResolveAskPermissionRequest, which checks hasUI, subagent status,
|
|
75
|
+
* and yolo-mode — relocating the policy from the index.ts closure.
|
|
76
|
+
*/
|
|
77
|
+
canConfirm(): boolean {
|
|
78
|
+
if (this.context === null) return false;
|
|
79
|
+
return canResolveAskPermissionRequest({
|
|
80
|
+
config: this.deps.config.current(),
|
|
81
|
+
hasUI: this.context.hasUI,
|
|
82
|
+
isSubagent: isSubagentExecutionContext(
|
|
83
|
+
this.context,
|
|
84
|
+
this.deps.subagentSessionsDir,
|
|
85
|
+
this.deps.registry,
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Prompt the user for a permission decision using the stored context.
|
|
92
|
+
*
|
|
93
|
+
* Rejects if no context is active — canConfirm() guards this in normal use.
|
|
94
|
+
* Implements {@link GatePrompter}.
|
|
95
|
+
*/
|
|
96
|
+
prompt(details: PromptPermissionDetails): Promise<PermissionPromptDecision> {
|
|
97
|
+
if (this.context === null) {
|
|
98
|
+
return Promise.reject(
|
|
99
|
+
new Error("prompt called before the session was activated"),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return this.deps.prompter.prompt(this.context, details);
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/session-logger.ts
CHANGED
|
@@ -6,6 +6,22 @@ import {
|
|
|
6
6
|
} from "./extension-config";
|
|
7
7
|
import { createPermissionSystemLogger } from "./logging";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Narrowest logging seam — consumers that only write review-log entries.
|
|
11
|
+
* Injected into `PermissionPrompter` and the RPC handlers.
|
|
12
|
+
*/
|
|
13
|
+
export interface ReviewLogger {
|
|
14
|
+
review(event: string, details?: Record<string, unknown>): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Logging seam for consumers that write both debug and review entries.
|
|
19
|
+
* Injected into `ConfigStore` and `PermissionForwarder`.
|
|
20
|
+
*/
|
|
21
|
+
export interface DebugReviewLogger extends ReviewLogger {
|
|
22
|
+
debug(event: string, details?: Record<string, unknown>): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
/**
|
|
10
26
|
* Unified logging + notification surface for handler deps.
|
|
11
27
|
*
|
|
@@ -13,9 +29,7 @@ import { createPermissionSystemLogger } from "./logging";
|
|
|
13
29
|
* `writeReviewLog`, `notifyWarning`) with a single typed collaborator.
|
|
14
30
|
* This is an intermediate abstraction on the path to PermissionSession (#129).
|
|
15
31
|
*/
|
|
16
|
-
export interface SessionLogger {
|
|
17
|
-
debug(event: string, details?: Record<string, unknown>): void;
|
|
18
|
-
review(event: string, details?: Record<string, unknown>): void;
|
|
32
|
+
export interface SessionLogger extends DebugReviewLogger {
|
|
19
33
|
warn(message: string): void;
|
|
20
34
|
}
|
|
21
35
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
normalizePermissionSystemConfig,
|
|
10
10
|
type PermissionSystemExtensionConfig,
|
|
11
11
|
} from "#src/extension-config";
|
|
12
|
-
import type { Rule } from "#src/rule";
|
|
12
|
+
import type { Rule, Ruleset } from "#src/rule";
|
|
13
13
|
|
|
14
14
|
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
15
15
|
getSettingsListTheme: () => ({}),
|
|
@@ -88,7 +88,9 @@ test("permission-system command completions expose top-level config actions", ()
|
|
|
88
88
|
};
|
|
89
89
|
const controller = {
|
|
90
90
|
config: configStore,
|
|
91
|
-
|
|
91
|
+
configPath,
|
|
92
|
+
permissionManager: { getComposedConfigRules: () => [] as Ruleset },
|
|
93
|
+
session: { lastKnownActiveAgentName: null },
|
|
92
94
|
};
|
|
93
95
|
|
|
94
96
|
let definition: {
|
|
@@ -160,7 +162,9 @@ test("permission-system command handlers manage config summary, persistence, and
|
|
|
160
162
|
};
|
|
161
163
|
const controller = {
|
|
162
164
|
config: configStore,
|
|
163
|
-
|
|
165
|
+
configPath,
|
|
166
|
+
permissionManager: { getComposedConfigRules: () => [] as Ruleset },
|
|
167
|
+
session: { lastKnownActiveAgentName: null },
|
|
164
168
|
};
|
|
165
169
|
|
|
166
170
|
let registeredName = "";
|
|
@@ -257,8 +261,9 @@ test("show output includes rule origins when getComposedRules is provided", asyn
|
|
|
257
261
|
|
|
258
262
|
const controller = {
|
|
259
263
|
config: { current: () => config, save: () => {} } as CommandConfigStore,
|
|
260
|
-
|
|
261
|
-
|
|
264
|
+
configPath: "/fake/config.json",
|
|
265
|
+
permissionManager: { getComposedConfigRules: () => composedRules },
|
|
266
|
+
session: { lastKnownActiveAgentName: null },
|
|
262
267
|
};
|
|
263
268
|
|
|
264
269
|
let definition: {
|
|
@@ -289,8 +294,9 @@ test("show output omits rule summary when getComposedRules is not provided", asy
|
|
|
289
294
|
|
|
290
295
|
const controller = {
|
|
291
296
|
config: { current: () => config, save: () => {} } as CommandConfigStore,
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
configPath: "/fake/config.json",
|
|
298
|
+
permissionManager: { getComposedConfigRules: () => [] as Ruleset },
|
|
299
|
+
session: { lastKnownActiveAgentName: null },
|
|
294
300
|
};
|
|
295
301
|
|
|
296
302
|
let definition: {
|