@gotgenes/pi-permission-system 10.0.0 → 10.2.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 +33 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agent-prep-session.ts +28 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +11 -0
- package/src/forwarded-permissions/permission-forwarder.ts +549 -0
- package/src/forwarding-manager.ts +3 -7
- package/src/gate-handler-session.ts +13 -0
- package/src/gate-prompter.ts +14 -0
- package/src/handlers/before-agent-start.ts +2 -3
- package/src/handlers/gates/bash-command.ts +4 -18
- package/src/handlers/gates/bash-external-directory.ts +3 -15
- package/src/handlers/gates/bash-path.ts +3 -16
- package/src/handlers/gates/descriptor.ts +0 -28
- package/src/handlers/gates/path.ts +3 -15
- package/src/handlers/gates/runner.ts +142 -105
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +44 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
- package/src/handlers/lifecycle.ts +9 -9
- package/src/handlers/permission-gate-handler.ts +34 -238
- package/src/index.ts +53 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +83 -27
- package/src/permissions-service.ts +53 -0
- package/src/runtime.ts +1 -37
- package/src/service-lifecycle.ts +49 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-lifecycle-session.ts +24 -0
- package/src/tool-input-preview.ts +0 -62
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +6 -4
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +62 -0
- package/test/forwarding-manager.test.ts +26 -44
- package/test/handlers/before-agent-start.test.ts +45 -21
- package/test/handlers/external-directory-integration.test.ts +83 -114
- package/test/handlers/external-directory-session-dedup.test.ts +102 -55
- package/test/handlers/gates/bash-command.test.ts +49 -90
- package/test/handlers/gates/bash-external-directory.test.ts +54 -95
- package/test/handlers/gates/bash-path.test.ts +54 -157
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +151 -186
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +128 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
- package/test/handlers/input.test.ts +1 -2
- package/test/handlers/lifecycle.test.ts +49 -33
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +212 -17
- package/test/helpers/handler-fixtures.ts +226 -29
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-forwarding.test.ts +0 -282
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +211 -105
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +2 -86
- package/test/service-lifecycle.test.ts +162 -0
- package/test/tool-input-preview.test.ts +0 -111
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/src/forwarded-permissions/polling.ts +0 -411
package/src/mcp-targets.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "./common";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* An ordered accumulator that owns the uniqueness invariant.
|
|
5
|
+
*
|
|
6
|
+
* `add` ignores null/empty values and silently skips duplicates (first-insertion
|
|
7
|
+
* wins). `toArray` returns the ordered result as an independent copy.
|
|
8
|
+
*/
|
|
9
|
+
export class McpTargetList {
|
|
10
|
+
private readonly targets: string[] = [];
|
|
11
|
+
|
|
12
|
+
add(value: string | null): void {
|
|
13
|
+
if (!value) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!this.targets.includes(value)) {
|
|
17
|
+
this.targets.push(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toArray(): string[] {
|
|
22
|
+
return [...this.targets];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
3
26
|
/**
|
|
4
27
|
* Parse a qualified MCP tool name of the form `server:tool`.
|
|
5
28
|
*
|
|
@@ -31,7 +54,7 @@ export function parseQualifiedMcpToolName(
|
|
|
31
54
|
function addDerivedMcpServerTargets(
|
|
32
55
|
toolName: string,
|
|
33
56
|
configuredServerNames: readonly string[],
|
|
34
|
-
|
|
57
|
+
targets: McpTargetList,
|
|
35
58
|
): void {
|
|
36
59
|
const trimmedToolName = toolName.trim();
|
|
37
60
|
if (!trimmedToolName) {
|
|
@@ -52,9 +75,9 @@ function addDerivedMcpServerTargets(
|
|
|
52
75
|
continue;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
targets.add(`${trimmedServerName}_${trimmedToolName}`);
|
|
79
|
+
targets.add(`${trimmedServerName}:${trimmedToolName}`);
|
|
80
|
+
targets.add(trimmedServerName);
|
|
58
81
|
}
|
|
59
82
|
}
|
|
60
83
|
|
|
@@ -62,22 +85,22 @@ function pushMcpToolPermissionTargets(
|
|
|
62
85
|
rawReference: string,
|
|
63
86
|
serverHint: string | null,
|
|
64
87
|
configuredServerNames: readonly string[],
|
|
65
|
-
|
|
88
|
+
targets: McpTargetList,
|
|
66
89
|
): void {
|
|
67
90
|
const qualified = parseQualifiedMcpToolName(rawReference);
|
|
68
91
|
const resolvedServer = serverHint ?? qualified?.server ?? null;
|
|
69
92
|
const resolvedTool = qualified?.tool ?? rawReference;
|
|
70
93
|
|
|
71
94
|
if (resolvedServer) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
targets.add(`${resolvedServer}_${resolvedTool}`);
|
|
96
|
+
targets.add(`${resolvedServer}:${resolvedTool}`);
|
|
97
|
+
targets.add(resolvedServer);
|
|
75
98
|
} else {
|
|
76
|
-
addDerivedMcpServerTargets(resolvedTool, configuredServerNames,
|
|
99
|
+
addDerivedMcpServerTargets(resolvedTool, configuredServerNames, targets);
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
targets.add(resolvedTool);
|
|
103
|
+
targets.add(rawReference);
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
/**
|
|
@@ -98,32 +121,19 @@ export function createMcpPermissionTargets(
|
|
|
98
121
|
const describe = getNonEmptyString(record.describe);
|
|
99
122
|
const search = getNonEmptyString(record.search);
|
|
100
123
|
|
|
101
|
-
const targets
|
|
102
|
-
const pushTarget = (value: string | null) => {
|
|
103
|
-
if (!value) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (!targets.includes(value)) {
|
|
107
|
-
targets.push(value);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
124
|
+
const targets = new McpTargetList();
|
|
110
125
|
|
|
111
126
|
if (tool) {
|
|
112
|
-
pushMcpToolPermissionTargets(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
configuredServerNames,
|
|
116
|
-
pushTarget,
|
|
117
|
-
);
|
|
118
|
-
pushTarget("mcp_call");
|
|
119
|
-
return targets;
|
|
127
|
+
pushMcpToolPermissionTargets(tool, server, configuredServerNames, targets);
|
|
128
|
+
targets.add("mcp_call");
|
|
129
|
+
return targets.toArray();
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
if (connect) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return targets;
|
|
133
|
+
targets.add(`mcp_connect_${connect}`);
|
|
134
|
+
targets.add(connect);
|
|
135
|
+
targets.add("mcp_connect");
|
|
136
|
+
return targets.toArray();
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
if (describe) {
|
|
@@ -131,30 +141,30 @@ export function createMcpPermissionTargets(
|
|
|
131
141
|
describe,
|
|
132
142
|
server,
|
|
133
143
|
configuredServerNames,
|
|
134
|
-
|
|
144
|
+
targets,
|
|
135
145
|
);
|
|
136
|
-
|
|
137
|
-
return targets;
|
|
146
|
+
targets.add("mcp_describe");
|
|
147
|
+
return targets.toArray();
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
if (search) {
|
|
141
151
|
if (server) {
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
targets.add(`mcp_server_${server}`);
|
|
153
|
+
targets.add(server);
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return targets;
|
|
156
|
+
targets.add(search);
|
|
157
|
+
targets.add("mcp_search");
|
|
158
|
+
return targets.toArray();
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
if (server) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return targets;
|
|
162
|
+
targets.add(`mcp_server_${server}`);
|
|
163
|
+
targets.add(server);
|
|
164
|
+
targets.add("mcp_list");
|
|
165
|
+
return targets.toArray();
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
|
|
159
|
-
return targets;
|
|
168
|
+
targets.add("mcp_status");
|
|
169
|
+
return targets.toArray();
|
|
160
170
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { isPermissionState } from "./common";
|
|
3
|
+
import { getGlobalConfigPath, getProjectConfigPath } from "./config-paths";
|
|
2
4
|
import { normalizeInput } from "./input-normalizer";
|
|
3
5
|
import { normalizeFlatConfig } from "./normalize";
|
|
4
6
|
import {
|
|
@@ -48,19 +50,66 @@ type ResolvedPermissions = {
|
|
|
48
50
|
composedRules: Ruleset;
|
|
49
51
|
};
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Narrow interface for session-scoped permission checking.
|
|
55
|
+
* `PermissionSession` depends on this — not the full concrete class — so
|
|
56
|
+
* test mocks can satisfy it without an `as unknown as PermissionManager` cast.
|
|
57
|
+
*/
|
|
58
|
+
export interface ScopedPermissionManager {
|
|
59
|
+
configureForCwd(cwd: string | undefined | null): void;
|
|
60
|
+
checkPermission(
|
|
61
|
+
toolName: string,
|
|
62
|
+
input: unknown,
|
|
63
|
+
agentName?: string,
|
|
64
|
+
sessionRules?: Ruleset,
|
|
65
|
+
): PermissionCheckResult;
|
|
66
|
+
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
|
67
|
+
getConfigIssues(agentName?: string): string[];
|
|
68
|
+
getPolicyCacheStamp(agentName?: string): string;
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
export interface PermissionManagerOptions extends PolicyLoaderOptions {
|
|
52
72
|
policyLoader?: PolicyLoader;
|
|
73
|
+
/**
|
|
74
|
+
* Pi agent directory. When provided, the manager derives all loader paths
|
|
75
|
+
* from this value and supports {@link PermissionManager.configureForCwd}.
|
|
76
|
+
*/
|
|
77
|
+
agentDir?: string;
|
|
53
78
|
}
|
|
54
79
|
|
|
55
|
-
export class PermissionManager {
|
|
56
|
-
private readonly
|
|
80
|
+
export class PermissionManager implements ScopedPermissionManager {
|
|
81
|
+
private readonly agentDir: string | undefined;
|
|
82
|
+
private loader: PolicyLoader;
|
|
57
83
|
private readonly resolvedPermissionsCache = new Map<
|
|
58
84
|
string,
|
|
59
85
|
FileCacheEntry<ResolvedPermissions>
|
|
60
86
|
>();
|
|
61
87
|
|
|
62
88
|
constructor(options: PermissionManagerOptions = {}) {
|
|
63
|
-
this.
|
|
89
|
+
this.agentDir = options.agentDir;
|
|
90
|
+
this.loader =
|
|
91
|
+
options.policyLoader ??
|
|
92
|
+
new FilePolicyLoader(
|
|
93
|
+
options.agentDir !== undefined
|
|
94
|
+
? derivePolicyLoaderOptions(options.agentDir, undefined)
|
|
95
|
+
: options,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Rebuild the policy loader for a new working directory and clear the
|
|
101
|
+
* resolved-permissions cache.
|
|
102
|
+
*
|
|
103
|
+
* When `agentDir` was not provided at construction (e.g. test managers
|
|
104
|
+
* built with explicit paths), only the cache is cleared.
|
|
105
|
+
*/
|
|
106
|
+
configureForCwd(cwd: string | undefined | null): void {
|
|
107
|
+
if (this.agentDir !== undefined) {
|
|
108
|
+
this.loader = new FilePolicyLoader(
|
|
109
|
+
derivePolicyLoaderOptions(this.agentDir, cwd),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
this.resolvedPermissionsCache.clear();
|
|
64
113
|
}
|
|
65
114
|
|
|
66
115
|
getConfigIssues(agentName?: string): string[] {
|
|
@@ -219,6 +268,23 @@ export class PermissionManager {
|
|
|
219
268
|
}
|
|
220
269
|
}
|
|
221
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Derive `PolicyLoaderOptions` from an agentDir + an optional cwd.
|
|
273
|
+
* Setting agentsDir explicitly from agentDir removes the hidden
|
|
274
|
+
* `getAgentDir()` env-read that FilePolicyLoader's default would perform.
|
|
275
|
+
*/
|
|
276
|
+
function derivePolicyLoaderOptions(
|
|
277
|
+
agentDir: string,
|
|
278
|
+
cwd: string | undefined | null,
|
|
279
|
+
): PolicyLoaderOptions {
|
|
280
|
+
return {
|
|
281
|
+
globalConfigPath: getGlobalConfigPath(agentDir),
|
|
282
|
+
agentsDir: join(agentDir, "agents"),
|
|
283
|
+
projectGlobalConfigPath: cwd ? getProjectConfigPath(cwd) : undefined,
|
|
284
|
+
projectAgentsDir: cwd ? join(cwd, ".pi", "agent", "agents") : undefined,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
222
288
|
/**
|
|
223
289
|
* Map a matched rule + tool name to the correct PermissionCheckResult.source.
|
|
224
290
|
*
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
confirmPermission,
|
|
6
|
-
type PermissionForwardingDeps,
|
|
7
|
-
} from "./forwarded-permissions/polling";
|
|
8
|
-
import type {
|
|
9
|
-
PermissionPromptDecision,
|
|
10
|
-
RequestPermissionOptions,
|
|
11
|
-
} from "./permission-dialog";
|
|
3
|
+
import type { ApprovalRequester } from "./forwarded-permissions/permission-forwarder";
|
|
4
|
+
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
12
5
|
import {
|
|
13
6
|
emitUiPromptEvent,
|
|
14
7
|
type PermissionEventBus,
|
|
15
8
|
} from "./permission-events";
|
|
16
9
|
import { buildDirectUiPrompt } from "./permission-ui-prompt";
|
|
17
|
-
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
18
10
|
import { shouldAutoApprovePermissionState } from "./yolo-mode";
|
|
19
11
|
|
|
20
12
|
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
@@ -48,29 +40,18 @@ export interface PermissionPrompterApi {
|
|
|
48
40
|
* Dependencies required by PermissionPrompter.
|
|
49
41
|
*
|
|
50
42
|
* Keeps the prompter's external surface narrow: callers provide config
|
|
51
|
-
* access, review-log writing,
|
|
52
|
-
*
|
|
43
|
+
* access, review-log writing, the UI-prompt event bus, and the forwarder
|
|
44
|
+
* that owns the UI/subagent-forwarding branching logic.
|
|
53
45
|
*/
|
|
54
46
|
export interface PermissionPrompterDeps {
|
|
55
47
|
/** Read current config for yolo-mode check (called at prompt time). */
|
|
56
48
|
getConfig(): PermissionSystemExtensionConfig;
|
|
57
49
|
/** Write structured entries to the permission review log. */
|
|
58
50
|
writeReviewLog(event: string, details: Record<string, unknown>): void;
|
|
59
|
-
/** Directory containing subagent session state. */
|
|
60
|
-
subagentSessionsDir: string;
|
|
61
|
-
/** Directory used for file-based permission forwarding requests/responses. */
|
|
62
|
-
forwardingDir: string;
|
|
63
|
-
/** In-process subagent session registry for detection and forwarding target resolution. */
|
|
64
|
-
registry?: SubagentSessionRegistry;
|
|
65
51
|
/** Event bus used for UI prompt broadcasts. */
|
|
66
52
|
events: PermissionEventBus;
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
ui: ExtensionContext["ui"],
|
|
70
|
-
title: string,
|
|
71
|
-
message: string,
|
|
72
|
-
options?: RequestPermissionOptions,
|
|
73
|
-
): Promise<PermissionPromptDecision>;
|
|
53
|
+
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
54
|
+
forwarder: ApprovalRequester;
|
|
74
55
|
}
|
|
75
56
|
|
|
76
57
|
/**
|
|
@@ -107,10 +88,9 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
107
88
|
emitUiPromptEvent(this.deps.events, uiPrompt);
|
|
108
89
|
}
|
|
109
90
|
|
|
110
|
-
const decision = await
|
|
91
|
+
const decision = await this.deps.forwarder.requestApproval(
|
|
111
92
|
ctx,
|
|
112
93
|
details.message,
|
|
113
|
-
this.buildForwardingDeps(),
|
|
114
94
|
details.sessionLabel ? { sessionLabel: details.sessionLabel } : undefined,
|
|
115
95
|
{
|
|
116
96
|
source: uiPrompt.source,
|
|
@@ -158,35 +138,4 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
158
138
|
denialReason: details.denialReason ?? null,
|
|
159
139
|
});
|
|
160
140
|
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Build a PermissionForwardingDeps to pass to confirmPermission.
|
|
164
|
-
*
|
|
165
|
-
* Yolo-mode is already handled at the prompter level, so shouldAutoApprove
|
|
166
|
-
* returns false here (confirmPermission does not call it; only
|
|
167
|
-
* processForwardedPermissionRequests does, and that has its own deps).
|
|
168
|
-
*
|
|
169
|
-
* The logger delegates writeReviewLog to deps and uses a no-op writeDebugLog
|
|
170
|
-
* (trace-level forwarding debug is deferred — see open question in the plan).
|
|
171
|
-
*/
|
|
172
|
-
private buildForwardingDeps(): PermissionForwardingDeps {
|
|
173
|
-
const { deps } = this;
|
|
174
|
-
const logger: ForwardedPermissionLogger = {
|
|
175
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain function closures; no this-binding issue
|
|
176
|
-
writeReviewLog: deps.writeReviewLog,
|
|
177
|
-
writeDebugLog: () => undefined,
|
|
178
|
-
};
|
|
179
|
-
return {
|
|
180
|
-
forwardingDir: deps.forwardingDir,
|
|
181
|
-
subagentSessionsDir: deps.subagentSessionsDir,
|
|
182
|
-
registry: deps.registry,
|
|
183
|
-
events: deps.events,
|
|
184
|
-
logger,
|
|
185
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- logger methods are plain function closures; no this-binding issue
|
|
186
|
-
writeReviewLog: deps.writeReviewLog,
|
|
187
|
-
// eslint-disable-next-line @typescript-eslint/unbound-method -- same as above
|
|
188
|
-
requestPermissionDecisionFromUi: deps.requestPermissionDecisionFromUi,
|
|
189
|
-
shouldAutoApprove: () => false,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
141
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PermissionCheckResult } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the effective permission for a surface/input, applying the current
|
|
5
|
+
* session rules internally.
|
|
6
|
+
*
|
|
7
|
+
* Collapses the `checkPermission` + `getSessionRuleset` relay that every gate
|
|
8
|
+
* previously threaded by hand: the ruleset was only ever fetched to be passed
|
|
9
|
+
* straight back into `checkPermission`, so the two are one operation.
|
|
10
|
+
*/
|
|
11
|
+
export interface PermissionResolver {
|
|
12
|
+
resolve(
|
|
13
|
+
surface: string,
|
|
14
|
+
input: unknown,
|
|
15
|
+
agentName?: string,
|
|
16
|
+
): PermissionCheckResult;
|
|
17
|
+
}
|
|
@@ -4,18 +4,27 @@ import {
|
|
|
4
4
|
getActiveAgentName,
|
|
5
5
|
getActiveAgentNameFromSystemPrompt,
|
|
6
6
|
} from "./active-agent";
|
|
7
|
+
import type { AgentPrepSession } from "./agent-prep-session";
|
|
7
8
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
8
9
|
import type { ExtensionPaths } from "./extension-paths";
|
|
9
10
|
import type { ForwardingController } from "./forwarding-manager";
|
|
11
|
+
import type { GateHandlerSession } from "./gate-handler-session";
|
|
12
|
+
import type { GatePrompter } from "./gate-prompter";
|
|
10
13
|
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
11
|
-
import type {
|
|
14
|
+
import type { ScopedPermissionManager } from "./permission-manager";
|
|
12
15
|
import type { PromptPermissionDetails } from "./permission-prompter";
|
|
16
|
+
import type { PermissionResolver } from "./permission-resolver";
|
|
13
17
|
import type { Rule } from "./rule";
|
|
14
|
-
import { createPermissionManagerForCwd } from "./runtime";
|
|
15
18
|
import type { SessionApproval } from "./session-approval";
|
|
19
|
+
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
20
|
+
import type { SessionLifecycleSession } from "./session-lifecycle-session";
|
|
16
21
|
import type { SessionLogger } from "./session-logger";
|
|
17
22
|
import { SessionRules } from "./session-rules";
|
|
18
23
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
24
|
+
import {
|
|
25
|
+
resolveToolPreviewLimits,
|
|
26
|
+
type ToolPreviewFormatterOptions,
|
|
27
|
+
} from "./tool-preview-formatter";
|
|
19
28
|
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
20
29
|
|
|
21
30
|
/**
|
|
@@ -54,9 +63,16 @@ export interface PermissionSessionRuntimeDeps {
|
|
|
54
63
|
* - `ForwardingController` — polling lifecycle
|
|
55
64
|
* - `PermissionSessionRuntimeDeps` — config refresh + log delegates
|
|
56
65
|
*/
|
|
57
|
-
export class PermissionSession
|
|
66
|
+
export class PermissionSession
|
|
67
|
+
implements
|
|
68
|
+
PermissionResolver,
|
|
69
|
+
SessionApprovalRecorder,
|
|
70
|
+
GatePrompter,
|
|
71
|
+
GateHandlerSession,
|
|
72
|
+
AgentPrepSession,
|
|
73
|
+
SessionLifecycleSession
|
|
74
|
+
{
|
|
58
75
|
private context: ExtensionContext | null = null;
|
|
59
|
-
private permissionManager: PermissionManager;
|
|
60
76
|
private readonly sessionRules = new SessionRules();
|
|
61
77
|
private skillEntries: SkillPromptEntry[] = [];
|
|
62
78
|
private knownAgentName: string | null = null;
|
|
@@ -67,13 +83,9 @@ export class PermissionSession {
|
|
|
67
83
|
private readonly paths: ExtensionPaths,
|
|
68
84
|
readonly logger: SessionLogger,
|
|
69
85
|
private readonly forwarding: ForwardingController,
|
|
86
|
+
private readonly permissionManager: ScopedPermissionManager,
|
|
70
87
|
private readonly runtimeDeps: PermissionSessionRuntimeDeps,
|
|
71
|
-
) {
|
|
72
|
-
this.permissionManager = createPermissionManagerForCwd(
|
|
73
|
-
paths.agentDir,
|
|
74
|
-
undefined,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
88
|
+
) {}
|
|
77
89
|
|
|
78
90
|
// ── Context lifecycle ──────────────────────────────────────────────────
|
|
79
91
|
|
|
@@ -110,6 +122,24 @@ export class PermissionSession {
|
|
|
110
122
|
);
|
|
111
123
|
}
|
|
112
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the effective permission for a surface/input, applying the current
|
|
127
|
+
* session rules. Composes `checkPermission` with `getSessionRuleset` so
|
|
128
|
+
* callers never thread the ruleset by hand.
|
|
129
|
+
*/
|
|
130
|
+
resolve(
|
|
131
|
+
surface: string,
|
|
132
|
+
input: unknown,
|
|
133
|
+
agentName?: string,
|
|
134
|
+
): PermissionCheckResult {
|
|
135
|
+
return this.checkPermission(
|
|
136
|
+
surface,
|
|
137
|
+
input,
|
|
138
|
+
agentName,
|
|
139
|
+
this.getSessionRuleset(),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
113
143
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
114
144
|
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
115
145
|
}
|
|
@@ -137,14 +167,11 @@ export class PermissionSession {
|
|
|
137
167
|
/**
|
|
138
168
|
* Reset all mutable state for a new session.
|
|
139
169
|
*
|
|
140
|
-
*
|
|
170
|
+
* Configures the injected PermissionManager for `ctx.cwd`, clears caches,
|
|
141
171
|
* skill entries, and activates the new context.
|
|
142
172
|
*/
|
|
143
173
|
resetForNewSession(ctx: ExtensionContext): void {
|
|
144
|
-
this.permissionManager
|
|
145
|
-
this.paths.agentDir,
|
|
146
|
-
ctx.cwd,
|
|
147
|
-
);
|
|
174
|
+
this.permissionManager.configureForCwd(ctx.cwd);
|
|
148
175
|
this.skillEntries = [];
|
|
149
176
|
this.toolsCacheKey = null;
|
|
150
177
|
this.promptCacheKey = null;
|
|
@@ -168,10 +195,7 @@ export class PermissionSession {
|
|
|
168
195
|
* Used on config reload (e.g. `resources_discover` with reason "reload").
|
|
169
196
|
*/
|
|
170
197
|
reload(): void {
|
|
171
|
-
this.permissionManager
|
|
172
|
-
this.paths.agentDir,
|
|
173
|
-
this.context?.cwd,
|
|
174
|
-
);
|
|
198
|
+
this.permissionManager.configureForCwd(this.context?.cwd);
|
|
175
199
|
this.skillEntries = [];
|
|
176
200
|
this.toolsCacheKey = null;
|
|
177
201
|
this.promptCacheKey = null;
|
|
@@ -251,13 +275,25 @@ export class PermissionSession {
|
|
|
251
275
|
|
|
252
276
|
// ── Infrastructure paths ───────────────────────────────────────────────
|
|
253
277
|
|
|
254
|
-
|
|
255
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Combined infrastructure read directories: static paths from
|
|
280
|
+
* `ExtensionPaths` plus config-derived paths.
|
|
281
|
+
*/
|
|
282
|
+
getInfrastructureReadDirs(): string[] {
|
|
283
|
+
return [
|
|
284
|
+
...this.paths.piInfrastructureDirs,
|
|
285
|
+
...(this.config.piInfrastructureReadPaths ?? []),
|
|
286
|
+
];
|
|
256
287
|
}
|
|
257
288
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Resolved tool-preview formatter options from the current config.
|
|
291
|
+
*
|
|
292
|
+
* Replaces the handler's `resolveToolPreviewLimits(session.config)` reach
|
|
293
|
+
* so the pipeline reads a clean value rather than pulling raw config.
|
|
294
|
+
*/
|
|
295
|
+
getToolPreviewLimits(): ToolPreviewFormatterOptions {
|
|
296
|
+
return resolveToolPreviewLimits(this.config);
|
|
261
297
|
}
|
|
262
298
|
|
|
263
299
|
// ── Prompting ──────────────────────────────────────────────────────────
|
|
@@ -275,8 +311,28 @@ export class PermissionSession {
|
|
|
275
311
|
return this.runtimeDeps.promptPermission(ctx, details);
|
|
276
312
|
}
|
|
277
313
|
|
|
278
|
-
/**
|
|
279
|
-
|
|
280
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Whether an interactive confirmation is possible using the stored context.
|
|
316
|
+
* Returns `false` when no context is active (before `activate` is called).
|
|
317
|
+
* Implements {@link GatePrompter}.
|
|
318
|
+
*/
|
|
319
|
+
canConfirm(): boolean {
|
|
320
|
+
return this.context !== null && this.canPrompt(this.context);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Prompt the user for a permission decision using the stored context.
|
|
325
|
+
* Throws if no context is active — `canConfirm()` guards this in normal use.
|
|
326
|
+
* Implements {@link GatePrompter}.
|
|
327
|
+
*/
|
|
328
|
+
promptPermission(
|
|
329
|
+
details: PromptPermissionDetails,
|
|
330
|
+
): Promise<PermissionPromptDecision> {
|
|
331
|
+
if (this.context === null) {
|
|
332
|
+
return Promise.reject(
|
|
333
|
+
new Error("promptPermission called before the session was activated"),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return this.prompt(this.context, details);
|
|
281
337
|
}
|
|
282
338
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { buildInputForSurface } from "./input-normalizer";
|
|
2
|
+
import type { PermissionManager } from "./permission-manager";
|
|
3
|
+
import type { PermissionsService } from "./service";
|
|
4
|
+
import type { SessionRules } from "./session-rules";
|
|
5
|
+
import type {
|
|
6
|
+
ToolInputFormatter,
|
|
7
|
+
ToolInputFormatterRegistry,
|
|
8
|
+
} from "./tool-input-formatter-registry";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* In-process implementation of the cross-extension {@link PermissionsService}.
|
|
12
|
+
*
|
|
13
|
+
* Constructed once in the composition root and backed by the runtime's
|
|
14
|
+
* permission manager and session rules. Both injected instances are stable
|
|
15
|
+
* for the lifetime of the factory — `runtime.permissionManager` is never
|
|
16
|
+
* reassigned on the runtime object (only `PermissionSession` reassigns its
|
|
17
|
+
* own internal copy), and `runtime.sessionRules` is `readonly`.
|
|
18
|
+
*/
|
|
19
|
+
export class LocalPermissionsService implements PermissionsService {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly permissionManager: PermissionManager,
|
|
22
|
+
private readonly sessionRules: SessionRules,
|
|
23
|
+
private readonly formatterRegistry: ToolInputFormatterRegistry,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
checkPermission(
|
|
27
|
+
surface: string,
|
|
28
|
+
value?: string,
|
|
29
|
+
agentName?: string,
|
|
30
|
+
): ReturnType<PermissionsService["checkPermission"]> {
|
|
31
|
+
const input = buildInputForSurface(surface, value);
|
|
32
|
+
return this.permissionManager.checkPermission(
|
|
33
|
+
surface,
|
|
34
|
+
input,
|
|
35
|
+
agentName,
|
|
36
|
+
this.sessionRules.getRuleset(),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getToolPermission(
|
|
41
|
+
toolName: string,
|
|
42
|
+
agentName?: string,
|
|
43
|
+
): ReturnType<PermissionsService["getToolPermission"]> {
|
|
44
|
+
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
registerToolInputFormatter(
|
|
48
|
+
toolName: string,
|
|
49
|
+
formatter: ToolInputFormatter,
|
|
50
|
+
): ReturnType<PermissionsService["registerToolInputFormatter"]> {
|
|
51
|
+
return this.formatterRegistry.register(toolName, formatter);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
getLegacyExtensionConfigPath,
|
|
20
20
|
getLegacyGlobalPolicyPath,
|
|
21
21
|
getLegacyProjectPolicyPath,
|
|
22
|
-
getProjectConfigPath,
|
|
23
22
|
REVIEW_LOG_FILENAME,
|
|
24
23
|
} from "./config-paths";
|
|
25
24
|
import { buildResolvedConfigLogEntry } from "./config-reporter";
|
|
@@ -77,41 +76,6 @@ export interface ExtensionRuntime extends ExtensionPaths, SessionState {
|
|
|
77
76
|
writeReviewLog(event: string, details?: Record<string, unknown>): void;
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
// ── Pure helpers ───────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Derive Pi project-level config and agents paths from a working directory.
|
|
84
|
-
* Returns null when cwd is absent (headless / global-only config).
|
|
85
|
-
*/
|
|
86
|
-
export function derivePiProjectPaths(cwd: string | undefined | null): {
|
|
87
|
-
projectGlobalConfigPath: string;
|
|
88
|
-
projectAgentsDir: string;
|
|
89
|
-
} | null {
|
|
90
|
-
if (!cwd) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
projectGlobalConfigPath: getProjectConfigPath(cwd),
|
|
95
|
-
projectAgentsDir: join(cwd, ".pi", "agent", "agents"),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Create a new PermissionManager scoped to a working directory's config hierarchy.
|
|
101
|
-
* Pass `cwd` as null/undefined to use global config only.
|
|
102
|
-
*/
|
|
103
|
-
export function createPermissionManagerForCwd(
|
|
104
|
-
agentDir: string,
|
|
105
|
-
cwd: string | undefined | null,
|
|
106
|
-
): PermissionManager {
|
|
107
|
-
const projectPaths = derivePiProjectPaths(cwd);
|
|
108
|
-
return new PermissionManager({
|
|
109
|
-
globalConfigPath: getGlobalConfigPath(agentDir),
|
|
110
|
-
projectGlobalConfigPath: projectPaths?.projectGlobalConfigPath,
|
|
111
|
-
projectAgentsDir: projectPaths?.projectAgentsDir,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
79
|
/**
|
|
116
80
|
* Reload merged config from disk into the runtime.
|
|
117
81
|
* If `ctx` is provided, updates `runtime.runtimeContext` first.
|
|
@@ -261,7 +225,7 @@ export function createExtensionRuntime(options?: {
|
|
|
261
225
|
...paths,
|
|
262
226
|
config: { ...DEFAULT_EXTENSION_CONFIG },
|
|
263
227
|
runtimeContext: null,
|
|
264
|
-
permissionManager:
|
|
228
|
+
permissionManager: new PermissionManager({ agentDir }),
|
|
265
229
|
activeSkillEntries: [],
|
|
266
230
|
lastKnownActiveAgentName: null,
|
|
267
231
|
lastActiveToolsCacheKey: null,
|