@gotgenes/pi-permission-system 10.0.0 → 10.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 +26 -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 +49 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +77 -9
- package/src/permissions-service.ts +53 -0
- 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 +86 -22
- 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 +63 -148
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +150 -93
- 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/helpers/gate-fixtures.ts +147 -16
- package/test/helpers/handler-fixtures.ts +143 -27
- 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-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +160 -27
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +0 -4
- 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
|
@@ -3,15 +3,35 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { vi } from "vitest";
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "#src/handlers/gates/
|
|
6
|
+
import type { DecisionReporter } from "#src/decision-reporter";
|
|
7
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
8
|
+
import type { GateDescriptor } from "#src/handlers/gates/descriptor";
|
|
9
|
+
import { GateRunner } from "#src/handlers/gates/runner";
|
|
10
|
+
import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
|
|
11
|
+
import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
10
12
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
13
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
14
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
15
|
+
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
16
|
+
import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
|
|
11
17
|
import type { PermissionCheckResult } from "#src/types";
|
|
12
18
|
|
|
13
19
|
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
14
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Permission resolver mock with an optional default check result.
|
|
23
|
+
*
|
|
24
|
+
* Returns a plain object whose `resolve` is a `vi.fn` so callers retain full
|
|
25
|
+
* mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
|
|
26
|
+
*/
|
|
27
|
+
export function makeResolver(defaultCheck?: PermissionCheckResult) {
|
|
28
|
+
const resolve = vi.fn<PermissionResolver["resolve"]>();
|
|
29
|
+
if (defaultCheck) {
|
|
30
|
+
resolve.mockReturnValue(defaultCheck);
|
|
31
|
+
}
|
|
32
|
+
return { resolve };
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
/**
|
|
16
36
|
* Gate descriptor factory with runner-test defaults.
|
|
17
37
|
*
|
|
@@ -48,25 +68,70 @@ export function makeDescriptor(
|
|
|
48
68
|
};
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Reporter mock with independently inspectable vi.fn() stubs.
|
|
73
|
+
*/
|
|
74
|
+
export function makeReporter(
|
|
75
|
+
overrides: Partial<DecisionReporter> = {},
|
|
76
|
+
): DecisionReporter {
|
|
54
77
|
return {
|
|
55
|
-
checkPermission: vi
|
|
56
|
-
.fn()
|
|
57
|
-
.mockReturnValue(makeCheckResult({ matchedPattern: "*" })),
|
|
58
|
-
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
59
|
-
recordSessionApproval: vi.fn(),
|
|
60
78
|
writeReviewLog: vi.fn(),
|
|
61
79
|
emitDecision: vi.fn(),
|
|
62
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
63
|
-
promptPermission: vi
|
|
64
|
-
.fn()
|
|
65
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
66
80
|
...overrides,
|
|
67
81
|
};
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Gate runner factory for `GateRunner` unit tests.
|
|
86
|
+
*
|
|
87
|
+
* Builds one `GateRunner` from four role mocks and returns `{ runner, deps }`
|
|
88
|
+
* so tests can both invoke `runner.run(...)` and assert on the individual
|
|
89
|
+
* mock call records (`deps.reporter.*`, `deps.resolve`, etc.).
|
|
90
|
+
*/
|
|
91
|
+
export function makeGateRunner(
|
|
92
|
+
overrides: {
|
|
93
|
+
resolve?: PermissionResolver["resolve"];
|
|
94
|
+
recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
|
|
95
|
+
canConfirm?: GatePrompter["canConfirm"];
|
|
96
|
+
promptPermission?: GatePrompter["promptPermission"];
|
|
97
|
+
reporter?: Partial<DecisionReporter>;
|
|
98
|
+
} = {},
|
|
99
|
+
) {
|
|
100
|
+
const reporter = makeReporter(overrides.reporter);
|
|
101
|
+
const resolve =
|
|
102
|
+
overrides.resolve ??
|
|
103
|
+
vi
|
|
104
|
+
.fn<PermissionResolver["resolve"]>()
|
|
105
|
+
.mockReturnValue(makeCheckResult({ matchedPattern: "*" }));
|
|
106
|
+
const recordSessionApproval =
|
|
107
|
+
overrides.recordSessionApproval ??
|
|
108
|
+
(vi.fn() as SessionApprovalRecorder["recordSessionApproval"]);
|
|
109
|
+
const canConfirm =
|
|
110
|
+
overrides.canConfirm ??
|
|
111
|
+
(vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
|
|
112
|
+
const promptPermission =
|
|
113
|
+
overrides.promptPermission ??
|
|
114
|
+
vi
|
|
115
|
+
.fn<GatePrompter["promptPermission"]>()
|
|
116
|
+
.mockResolvedValue({ approved: true, state: "approved" });
|
|
117
|
+
const runner = new GateRunner(
|
|
118
|
+
{ resolve },
|
|
119
|
+
{ recordSessionApproval },
|
|
120
|
+
{ canConfirm, promptPermission },
|
|
121
|
+
reporter,
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
runner,
|
|
125
|
+
deps: {
|
|
126
|
+
resolve,
|
|
127
|
+
recordSessionApproval,
|
|
128
|
+
canConfirm,
|
|
129
|
+
promptPermission,
|
|
130
|
+
reporter,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
70
135
|
/**
|
|
71
136
|
* Tool-call context factory with bash defaults.
|
|
72
137
|
*
|
|
@@ -103,3 +168,69 @@ export function makeGateCheckResult(
|
|
|
103
168
|
...overrides,
|
|
104
169
|
};
|
|
105
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Mock of `ToolCallGateInputs` for `ToolCallGatePipeline` unit tests.
|
|
174
|
+
*
|
|
175
|
+
* Each method is a `vi.fn()` stub so callers retain full mock access
|
|
176
|
+
* (`mock.calls`, `mockReturnValue`, etc.) on the returned object.
|
|
177
|
+
* Pass `overrides` to replace individual stubs without rebuilding the whole
|
|
178
|
+
* mock from scratch.
|
|
179
|
+
*/
|
|
180
|
+
export function makeGateInputs(
|
|
181
|
+
overrides: {
|
|
182
|
+
resolve?: PermissionResolver["resolve"];
|
|
183
|
+
getActiveSkillEntries?: () => SkillPromptEntry[];
|
|
184
|
+
getInfrastructureReadDirs?: () => string[];
|
|
185
|
+
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
186
|
+
} = {},
|
|
187
|
+
): ToolCallGateInputs {
|
|
188
|
+
return {
|
|
189
|
+
resolve:
|
|
190
|
+
overrides.resolve ??
|
|
191
|
+
vi.fn<PermissionResolver["resolve"]>().mockReturnValue(makeCheckResult()),
|
|
192
|
+
getActiveSkillEntries:
|
|
193
|
+
overrides.getActiveSkillEntries ??
|
|
194
|
+
vi.fn<() => SkillPromptEntry[]>(() => []),
|
|
195
|
+
getInfrastructureReadDirs:
|
|
196
|
+
overrides.getInfrastructureReadDirs ?? vi.fn<() => string[]>(() => []),
|
|
197
|
+
getToolPreviewLimits:
|
|
198
|
+
overrides.getToolPreviewLimits ??
|
|
199
|
+
vi.fn<() => ToolPreviewFormatterOptions>(() => ({
|
|
200
|
+
toolInputPreviewMaxLength: 500,
|
|
201
|
+
toolTextSummaryMaxLength: 100,
|
|
202
|
+
toolInputLogPreviewMaxLength: 200,
|
|
203
|
+
})),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Mock of `SkillInputGateInputs` for `SkillInputGatePipeline` unit tests.
|
|
209
|
+
*
|
|
210
|
+
* Returns a plain object with a `checkPermission` `vi.fn()` stub so callers
|
|
211
|
+
* retain full mock access (`mockReturnValue`, `mock.calls`, etc.).
|
|
212
|
+
*/
|
|
213
|
+
export function makeSkillInputInputs(
|
|
214
|
+
overrides: { checkPermission?: SkillInputGateInputs["checkPermission"] } = {},
|
|
215
|
+
): SkillInputGateInputs {
|
|
216
|
+
return {
|
|
217
|
+
checkPermission:
|
|
218
|
+
overrides.checkPermission ??
|
|
219
|
+
vi
|
|
220
|
+
.fn<SkillInputGateInputs["checkPermission"]>()
|
|
221
|
+
.mockReturnValue(makeCheckResult()),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Mock `GateNotifier` for `SkillInputGatePipeline` unit tests.
|
|
227
|
+
*
|
|
228
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
229
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.) — annotating with
|
|
230
|
+
* `GateNotifier` would erase `Mock<...>` methods from the inferred type.
|
|
231
|
+
*/
|
|
232
|
+
export function makeNotifier() {
|
|
233
|
+
return {
|
|
234
|
+
warn: vi.fn<(message: string) => void>(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -7,14 +7,67 @@
|
|
|
7
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { vi } from "vitest";
|
|
9
9
|
|
|
10
|
+
import { GateDecisionReporter } from "#src/decision-reporter";
|
|
10
11
|
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
12
|
+
import type { GateHandlerSession } from "#src/gate-handler-session";
|
|
13
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
14
|
+
import { GateRunner } from "#src/handlers/gates/runner";
|
|
15
|
+
import {
|
|
16
|
+
type SkillInputGateInputs,
|
|
17
|
+
SkillInputGatePipeline,
|
|
18
|
+
} from "#src/handlers/gates/skill-input-gate-pipeline";
|
|
19
|
+
import {
|
|
20
|
+
type ToolCallGateInputs,
|
|
21
|
+
ToolCallGatePipeline,
|
|
22
|
+
} from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
11
23
|
import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
|
|
24
|
+
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
12
25
|
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
13
26
|
import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
|
|
14
|
-
import type {
|
|
27
|
+
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
28
|
+
import type { Rule } from "#src/rule";
|
|
29
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
30
|
+
import type { SessionLogger } from "#src/session-logger";
|
|
31
|
+
import { resolveToolPreviewLimits } from "#src/tool-preview-formatter";
|
|
15
32
|
import type { ToolRegistry } from "#src/tool-registry";
|
|
16
33
|
import type { PermissionCheckResult } from "#src/types";
|
|
17
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Precise mock boundary for PermissionGateHandler integration tests.
|
|
37
|
+
*
|
|
38
|
+
* Intersection of every role the handler and its collaborators require,
|
|
39
|
+
* plus the context-bound prompting helpers that GatePrompter delegates to.
|
|
40
|
+
* Without a cast, TypeScript enforces this at the call sites where the
|
|
41
|
+
* mock is passed to GateRunner / ToolCallGatePipeline / PermissionGateHandler.
|
|
42
|
+
*
|
|
43
|
+
* The 4-arg `checkPermission` overrides the 3-arg version from
|
|
44
|
+
* GateHandlerSession so the `resolve` delegation can forward session rules.
|
|
45
|
+
*/
|
|
46
|
+
export type MockGateHandlerSession = ToolCallGateInputs &
|
|
47
|
+
SkillInputGateInputs &
|
|
48
|
+
SessionApprovalRecorder &
|
|
49
|
+
GatePrompter &
|
|
50
|
+
GateHandlerSession & {
|
|
51
|
+
/** Logger source for the reporter the fixture builds. */
|
|
52
|
+
logger: SessionLogger;
|
|
53
|
+
/** Session-rule accessor — used by the resolve delegation. */
|
|
54
|
+
getSessionRuleset(): Rule[];
|
|
55
|
+
/** 4-arg form so the resolve delegation can pass rules. */
|
|
56
|
+
checkPermission(
|
|
57
|
+
surface: string,
|
|
58
|
+
input: unknown,
|
|
59
|
+
agentName?: string,
|
|
60
|
+
rules?: Rule[],
|
|
61
|
+
): PermissionCheckResult;
|
|
62
|
+
/** Context-bound canPrompt — overriding this steers canConfirm. */
|
|
63
|
+
canPrompt(ctx: ExtensionContext): boolean;
|
|
64
|
+
/** Context-bound prompt — overriding this steers promptPermission. */
|
|
65
|
+
prompt(
|
|
66
|
+
ctx: ExtensionContext,
|
|
67
|
+
details: PromptPermissionDetails,
|
|
68
|
+
): Promise<PermissionPromptDecision>;
|
|
69
|
+
};
|
|
70
|
+
|
|
18
71
|
export function makeEvents() {
|
|
19
72
|
return {
|
|
20
73
|
emit: vi.fn(),
|
|
@@ -75,33 +128,86 @@ export function makeCheckResult(
|
|
|
75
128
|
}
|
|
76
129
|
|
|
77
130
|
/**
|
|
78
|
-
* Full-
|
|
131
|
+
* Full-intersection session stub.
|
|
79
132
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
133
|
+
* Uses per-field `??` selection (no spread) so TypeScript verifies every
|
|
134
|
+
* field against `MockGateHandlerSession` individually — a missing field fails
|
|
135
|
+
* `pnpm run check` instead of failing silently at runtime.
|
|
136
|
+
*
|
|
137
|
+
* The `resolve`, `canConfirm`, and `promptPermission` delegations are inlined
|
|
138
|
+
* as closures that read `session` at call time, so overriding `checkPermission`,
|
|
139
|
+
* `canPrompt`, or `prompt` automatically steers them without extra guards.
|
|
82
140
|
*/
|
|
83
141
|
export function makeSession(
|
|
84
|
-
overrides: Partial<
|
|
85
|
-
):
|
|
86
|
-
|
|
87
|
-
logger:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
142
|
+
overrides: Partial<MockGateHandlerSession> = {},
|
|
143
|
+
): MockGateHandlerSession {
|
|
144
|
+
const session: MockGateHandlerSession = {
|
|
145
|
+
logger: overrides.logger ?? {
|
|
146
|
+
debug: vi.fn(),
|
|
147
|
+
review: vi.fn(),
|
|
148
|
+
warn: vi.fn(),
|
|
149
|
+
},
|
|
150
|
+
activate: overrides.activate ?? vi.fn<MockGateHandlerSession["activate"]>(),
|
|
151
|
+
resolveAgentName:
|
|
152
|
+
overrides.resolveAgentName ??
|
|
153
|
+
vi.fn<MockGateHandlerSession["resolveAgentName"]>().mockReturnValue(null),
|
|
154
|
+
checkPermission:
|
|
155
|
+
overrides.checkPermission ??
|
|
156
|
+
vi
|
|
157
|
+
.fn<MockGateHandlerSession["checkPermission"]>()
|
|
158
|
+
.mockReturnValue(makeCheckResult()),
|
|
159
|
+
getSessionRuleset:
|
|
160
|
+
overrides.getSessionRuleset ??
|
|
161
|
+
vi.fn<MockGateHandlerSession["getSessionRuleset"]>().mockReturnValue([]),
|
|
162
|
+
recordSessionApproval:
|
|
163
|
+
overrides.recordSessionApproval ??
|
|
164
|
+
vi.fn<MockGateHandlerSession["recordSessionApproval"]>(),
|
|
165
|
+
getActiveSkillEntries:
|
|
166
|
+
overrides.getActiveSkillEntries ??
|
|
167
|
+
vi
|
|
168
|
+
.fn<MockGateHandlerSession["getActiveSkillEntries"]>()
|
|
169
|
+
.mockReturnValue([]),
|
|
170
|
+
getInfrastructureReadDirs:
|
|
171
|
+
overrides.getInfrastructureReadDirs ??
|
|
172
|
+
vi
|
|
173
|
+
.fn<MockGateHandlerSession["getInfrastructureReadDirs"]>()
|
|
174
|
+
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
175
|
+
getToolPreviewLimits:
|
|
176
|
+
overrides.getToolPreviewLimits ??
|
|
177
|
+
vi
|
|
178
|
+
.fn<MockGateHandlerSession["getToolPreviewLimits"]>()
|
|
179
|
+
.mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
|
|
180
|
+
canPrompt:
|
|
181
|
+
overrides.canPrompt ??
|
|
182
|
+
vi.fn<MockGateHandlerSession["canPrompt"]>().mockReturnValue(true),
|
|
183
|
+
prompt:
|
|
184
|
+
overrides.prompt ??
|
|
185
|
+
vi
|
|
186
|
+
.fn<MockGateHandlerSession["prompt"]>()
|
|
187
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
188
|
+
// Delegations — closures read `session` at call time so overrides win.
|
|
189
|
+
resolve:
|
|
190
|
+
overrides.resolve ??
|
|
191
|
+
vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
|
|
192
|
+
session.checkPermission(
|
|
193
|
+
surface,
|
|
194
|
+
input,
|
|
195
|
+
agentName,
|
|
196
|
+
session.getSessionRuleset(),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
canConfirm:
|
|
200
|
+
overrides.canConfirm ??
|
|
201
|
+
vi.fn<MockGateHandlerSession["canConfirm"]>(() =>
|
|
202
|
+
session.canPrompt(undefined as unknown as ExtensionContext),
|
|
203
|
+
),
|
|
204
|
+
promptPermission:
|
|
205
|
+
overrides.promptPermission ??
|
|
206
|
+
vi.fn<MockGateHandlerSession["promptPermission"]>((details) =>
|
|
207
|
+
session.prompt(undefined as unknown as ExtensionContext, details),
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
return session;
|
|
105
211
|
}
|
|
106
212
|
|
|
107
213
|
export function makeToolRegistry(
|
|
@@ -121,13 +227,23 @@ export function makeToolRegistry(
|
|
|
121
227
|
* it needs — handler, events, session, and toolRegistry are all available.
|
|
122
228
|
*/
|
|
123
229
|
export function makeHandler(overrides?: {
|
|
124
|
-
session?: Partial<
|
|
230
|
+
session?: Partial<MockGateHandlerSession>;
|
|
125
231
|
toolRegistry?: Partial<ToolRegistry>;
|
|
126
232
|
}) {
|
|
127
233
|
const session = makeSession(overrides?.session);
|
|
128
234
|
const events = makeEvents();
|
|
129
235
|
const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
|
|
130
|
-
const
|
|
236
|
+
const pipeline = new ToolCallGatePipeline(session);
|
|
237
|
+
const skillInputPipeline = new SkillInputGatePipeline(session);
|
|
238
|
+
const reporter = new GateDecisionReporter(session.logger, events);
|
|
239
|
+
const runner = new GateRunner(session, session, session, reporter);
|
|
240
|
+
const handler = new PermissionGateHandler(
|
|
241
|
+
session,
|
|
242
|
+
toolRegistry,
|
|
243
|
+
pipeline,
|
|
244
|
+
skillInputPipeline,
|
|
245
|
+
runner,
|
|
246
|
+
);
|
|
131
247
|
return { handler, events, session, toolRegistry };
|
|
132
248
|
}
|
|
133
249
|
|
package/test/mcp-targets.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
createMcpPermissionTargets,
|
|
4
|
+
McpTargetList,
|
|
4
5
|
parseQualifiedMcpToolName,
|
|
5
6
|
} from "#src/mcp-targets";
|
|
6
7
|
|
|
@@ -176,3 +177,57 @@ describe("createMcpPermissionTargets", () => {
|
|
|
176
177
|
});
|
|
177
178
|
});
|
|
178
179
|
});
|
|
180
|
+
|
|
181
|
+
describe("McpTargetList", () => {
|
|
182
|
+
describe("add", () => {
|
|
183
|
+
it("ignores null", () => {
|
|
184
|
+
const list = new McpTargetList();
|
|
185
|
+
list.add(null);
|
|
186
|
+
expect(list.toArray()).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("ignores empty string", () => {
|
|
190
|
+
const list = new McpTargetList();
|
|
191
|
+
list.add("");
|
|
192
|
+
expect(list.toArray()).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("appends a new value", () => {
|
|
196
|
+
const list = new McpTargetList();
|
|
197
|
+
list.add("exa");
|
|
198
|
+
expect(list.toArray()).toEqual(["exa"]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("dedups repeated values", () => {
|
|
202
|
+
const list = new McpTargetList();
|
|
203
|
+
list.add("exa");
|
|
204
|
+
list.add("exa");
|
|
205
|
+
expect(list.toArray()).toEqual(["exa"]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("preserves first-insertion order across a mix of values", () => {
|
|
209
|
+
const list = new McpTargetList();
|
|
210
|
+
list.add("exa_search");
|
|
211
|
+
list.add("exa:search");
|
|
212
|
+
list.add("exa");
|
|
213
|
+
list.add("exa_search"); // duplicate — must not change order
|
|
214
|
+
list.add("mcp_call");
|
|
215
|
+
expect(list.toArray()).toEqual([
|
|
216
|
+
"exa_search",
|
|
217
|
+
"exa:search",
|
|
218
|
+
"exa",
|
|
219
|
+
"mcp_call",
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("toArray", () => {
|
|
225
|
+
it("returns an independent copy that does not mutate the list", () => {
|
|
226
|
+
const list = new McpTargetList();
|
|
227
|
+
list.add("exa");
|
|
228
|
+
const first = list.toArray();
|
|
229
|
+
first.push("mutated");
|
|
230
|
+
expect(list.toArray()).toEqual(["exa"]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|