@komarspn/pi-permission-system 16.0.2
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 +2234 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/config/config.example.json +39 -0
- package/package.json +82 -0
- package/schemas/permissions.schema.json +158 -0
- package/src/active-agent.ts +72 -0
- package/src/async-cache.ts +21 -0
- package/src/bash-arity.ts +210 -0
- package/src/builtin-tool-input-formatters.ts +82 -0
- package/src/canonicalize-path.ts +30 -0
- package/src/common.ts +121 -0
- package/src/config-loader.ts +432 -0
- package/src/config-modal.ts +259 -0
- package/src/config-paths.ts +47 -0
- package/src/config-reporter.ts +34 -0
- package/src/config-store.ts +222 -0
- package/src/decision-audit.ts +75 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +232 -0
- package/src/expand-home.ts +28 -0
- package/src/extension-config.ts +79 -0
- package/src/extension-paths.ts +66 -0
- package/src/forwarded-permissions/io.ts +404 -0
- package/src/forwarded-permissions/permission-forwarder.ts +580 -0
- package/src/forwarding-manager.ts +74 -0
- package/src/gate-prompter.ts +12 -0
- package/src/handlers/before-agent-start.ts +94 -0
- package/src/handlers/gates/bash-command.ts +75 -0
- package/src/handlers/gates/bash-external-directory.ts +127 -0
- package/src/handlers/gates/bash-path-extractor.ts +15 -0
- package/src/handlers/gates/bash-path.ts +152 -0
- package/src/handlers/gates/bash-program.ts +1143 -0
- package/src/handlers/gates/bash-token-classification.ts +105 -0
- package/src/handlers/gates/candidate-check.ts +32 -0
- package/src/handlers/gates/descriptor.ts +81 -0
- package/src/handlers/gates/external-directory-messages.ts +20 -0
- package/src/handlers/gates/external-directory.ts +133 -0
- package/src/handlers/gates/helpers.ts +76 -0
- package/src/handlers/gates/path.ts +91 -0
- package/src/handlers/gates/runner.ts +186 -0
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +46 -0
- package/src/handlers/gates/skill-read.ts +87 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
- package/src/handlers/gates/tool.ts +102 -0
- package/src/handlers/gates/types.ts +13 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/lifecycle.ts +95 -0
- package/src/handlers/permission-gate-handler.ts +190 -0
- package/src/handlers/tool-call-boundary.ts +91 -0
- package/src/index.ts +225 -0
- package/src/input-normalizer.ts +157 -0
- package/src/logging.ts +113 -0
- package/src/mcp-targets.ts +170 -0
- package/src/node-modules-discovery.ts +76 -0
- package/src/normalize.ts +43 -0
- package/src/path-utils.ts +355 -0
- package/src/pattern-suggest.ts +132 -0
- package/src/permission-dialog.ts +138 -0
- package/src/permission-event-rpc.ts +223 -0
- package/src/permission-events.ts +266 -0
- package/src/permission-forwarding.ts +188 -0
- package/src/permission-gate.ts +94 -0
- package/src/permission-manager.ts +392 -0
- package/src/permission-merge.ts +32 -0
- package/src/permission-prompter.ts +142 -0
- package/src/permission-prompts.ts +93 -0
- package/src/permission-resolver.ts +109 -0
- package/src/permission-session.ts +189 -0
- package/src/permission-ui-prompt.ts +127 -0
- package/src/permissions-service.ts +63 -0
- package/src/persistent-approval-recorder.ts +139 -0
- package/src/policy-loader.ts +350 -0
- package/src/prompting-gateway.ts +104 -0
- package/src/rule.ts +188 -0
- package/src/scope-merge.ts +72 -0
- package/src/service-lifecycle.ts +49 -0
- package/src/service.ts +163 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-approval.ts +43 -0
- package/src/session-logger.ts +91 -0
- package/src/session-rules.ts +79 -0
- package/src/skill-prompt-sanitizer.ts +292 -0
- package/src/status.ts +35 -0
- package/src/subagent-context.ts +104 -0
- package/src/subagent-lifecycle-events.ts +72 -0
- package/src/subagent-registry.ts +105 -0
- package/src/synthesize.ts +92 -0
- package/src/system-prompt-sanitizer.ts +274 -0
- package/src/tool-access-extractor-registry.ts +68 -0
- package/src/tool-input-formatter-registry.ts +67 -0
- package/src/tool-input-preview.ts +34 -0
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +207 -0
- package/src/tool-registry.ts +148 -0
- package/src/types.ts +64 -0
- package/src/wildcard-matcher.ts +120 -0
- package/src/yolo-mode.ts +30 -0
- package/test/active-agent.test.ts +155 -0
- package/test/async-cache.test.ts +48 -0
- package/test/bash-arity.test.ts +144 -0
- package/test/bash-external-directory.test.ts +956 -0
- package/test/builtin-tool-input-formatters.test.ts +109 -0
- package/test/canonicalize-path.test.ts +93 -0
- package/test/common.test.ts +287 -0
- package/test/composition-root.test.ts +603 -0
- package/test/config-loader.test.ts +740 -0
- package/test/config-modal.test.ts +320 -0
- package/test/config-paths.test.ts +83 -0
- package/test/config-pipeline.test.ts +90 -0
- package/test/config-reporter.test.ts +147 -0
- package/test/config-store.test.ts +466 -0
- package/test/decision-audit.test.ts +72 -0
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +656 -0
- package/test/detect-permissive-bash-fallback.test.ts +56 -0
- package/test/expand-home.test.ts +93 -0
- package/test/extension-config.test.ts +129 -0
- package/test/extension-paths.test.ts +108 -0
- package/test/forwarded-permissions/io.test.ts +251 -0
- package/test/forwarding-manager.test.ts +194 -0
- package/test/handlers/before-agent-start.test.ts +317 -0
- package/test/handlers/external-directory-integration.test.ts +623 -0
- package/test/handlers/external-directory-session-dedup.test.ts +430 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
- package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
- package/test/handlers/gates/bash-command.test.ts +191 -0
- package/test/handlers/gates/bash-external-directory.test.ts +269 -0
- package/test/handlers/gates/bash-path.test.ts +337 -0
- package/test/handlers/gates/bash-program.test.ts +410 -0
- package/test/handlers/gates/bash-token-classification.test.ts +241 -0
- package/test/handlers/gates/candidate-check.test.ts +52 -0
- package/test/handlers/gates/external-directory-messages.test.ts +61 -0
- package/test/handlers/gates/external-directory.test.ts +259 -0
- package/test/handlers/gates/helpers.test.ts +177 -0
- package/test/handlers/gates/path.test.ts +294 -0
- package/test/handlers/gates/runner.test.ts +447 -0
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +131 -0
- package/test/handlers/gates/skill-read.test.ts +158 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
- package/test/handlers/gates/tool.test.ts +223 -0
- package/test/handlers/input-events.test.ts +168 -0
- package/test/handlers/input.test.ts +199 -0
- package/test/handlers/lifecycle.test.ts +221 -0
- package/test/handlers/tool-call-boundary.test.ts +145 -0
- package/test/handlers/tool-call-events.test.ts +277 -0
- package/test/handlers/tool-call.test.ts +395 -0
- package/test/handlers/validate-requested-tool.test.ts +92 -0
- package/test/helpers/gate-fixtures.ts +323 -0
- package/test/helpers/handler-fixtures.ts +335 -0
- package/test/helpers/make-fake-pi.ts +100 -0
- package/test/helpers/manager-harness.ts +112 -0
- package/test/helpers/session-fixtures.ts +204 -0
- package/test/input-normalizer.test.ts +367 -0
- package/test/logging.test.ts +51 -0
- package/test/mcp-targets.test.ts +233 -0
- package/test/node-modules-discovery.test.ts +97 -0
- package/test/normalize.test.ts +247 -0
- package/test/path-utils.test.ts +650 -0
- package/test/pattern-suggest.test.ts +248 -0
- package/test/permission-dialog.test.ts +241 -0
- package/test/permission-event-rpc.test.ts +541 -0
- package/test/permission-events.test.ts +402 -0
- package/test/permission-forwarder.test.ts +369 -0
- package/test/permission-forwarding.test.ts +315 -0
- package/test/permission-gate.test.ts +305 -0
- package/test/permission-manager-unified.test.ts +3368 -0
- package/test/permission-merge.test.ts +61 -0
- package/test/permission-prompter.test.ts +518 -0
- package/test/permission-prompts.test.ts +363 -0
- package/test/permission-resolver.test.ts +265 -0
- package/test/permission-session.test.ts +363 -0
- package/test/permission-ui-prompt.test.ts +146 -0
- package/test/permissions-service.test.ts +177 -0
- package/test/persistent-approval-recorder.test.ts +133 -0
- package/test/pi-infrastructure-read.test.ts +369 -0
- package/test/policy-loader.test.ts +561 -0
- package/test/prompting-gateway.test.ts +230 -0
- package/test/rule.test.ts +604 -0
- package/test/scope-merge.test.ts +116 -0
- package/test/service-lifecycle.test.ts +163 -0
- package/test/service.test.ts +308 -0
- package/test/session-approval.test.ts +75 -0
- package/test/session-logger.test.ts +200 -0
- package/test/session-rules.test.ts +304 -0
- package/test/session-start.test.ts +112 -0
- package/test/skill-prompt-sanitizer.test.ts +374 -0
- package/test/status.test.ts +10 -0
- package/test/subagent-context.test.ts +326 -0
- package/test/subagent-lifecycle-events.test.ts +132 -0
- package/test/subagent-registry.test.ts +145 -0
- package/test/synthesize.test.ts +300 -0
- package/test/system-prompt-sanitizer.test.ts +382 -0
- package/test/tool-access-extractor-registry.test.ts +77 -0
- package/test/tool-input-formatter-registry.test.ts +75 -0
- package/test/tool-input-preview.test.ts +129 -0
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/test/tool-preview-formatter.test.ts +458 -0
- package/test/tool-registry.test.ts +197 -0
- package/test/wildcard-matcher.test.ts +424 -0
- package/test/yolo-mode.test.ts +188 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared gate-level test fixtures for gate descriptor and runner tests.
|
|
3
|
+
*/
|
|
4
|
+
import { vi } from "vitest";
|
|
5
|
+
import type { DecisionReporter } from "#src/decision-reporter";
|
|
6
|
+
import type { DenialContext } from "#src/denial-messages";
|
|
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";
|
|
12
|
+
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
13
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
14
|
+
import type { PersistentApprovalRecorder } from "#src/persistent-approval-recorder";
|
|
15
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
16
|
+
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
17
|
+
import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
|
|
18
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
19
|
+
|
|
20
|
+
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Permission resolver mock with an optional default check result.
|
|
24
|
+
*
|
|
25
|
+
* Returns a plain object whose `resolve` is a `vi.fn` so callers retain full
|
|
26
|
+
* mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
|
|
27
|
+
*/
|
|
28
|
+
export function makeResolver(defaultCheck?: PermissionCheckResult) {
|
|
29
|
+
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
30
|
+
const resolvePathPolicy =
|
|
31
|
+
vi.fn<ScopedPermissionResolver["resolvePathPolicy"]>();
|
|
32
|
+
if (defaultCheck) {
|
|
33
|
+
resolve.mockReturnValue(defaultCheck);
|
|
34
|
+
resolvePathPolicy.mockReturnValue(defaultCheck);
|
|
35
|
+
}
|
|
36
|
+
return { resolve, resolvePathPolicy };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gate descriptor factory with runner-test defaults.
|
|
41
|
+
*
|
|
42
|
+
* Uses deny as the default `denialContext` check result so tests that
|
|
43
|
+
* verify block paths don't need to override the surface check.
|
|
44
|
+
*/
|
|
45
|
+
export function makeDescriptor(
|
|
46
|
+
overrides: Partial<GateDescriptor> = {},
|
|
47
|
+
): GateDescriptor {
|
|
48
|
+
return {
|
|
49
|
+
surface: "read",
|
|
50
|
+
input: {},
|
|
51
|
+
denialContext: {
|
|
52
|
+
kind: "tool",
|
|
53
|
+
check: makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
54
|
+
},
|
|
55
|
+
promptDetails: {
|
|
56
|
+
source: "tool_call",
|
|
57
|
+
agentName: null,
|
|
58
|
+
message: "Allow tool 'read'?",
|
|
59
|
+
toolCallId: "tc-1",
|
|
60
|
+
toolName: "read",
|
|
61
|
+
},
|
|
62
|
+
logContext: {
|
|
63
|
+
source: "tool_call",
|
|
64
|
+
toolCallId: "tc-1",
|
|
65
|
+
toolName: "read",
|
|
66
|
+
},
|
|
67
|
+
decision: {
|
|
68
|
+
surface: "read",
|
|
69
|
+
value: "read",
|
|
70
|
+
},
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reporter mock with independently inspectable vi.fn() stubs.
|
|
77
|
+
*/
|
|
78
|
+
export function makeReporter(
|
|
79
|
+
overrides: Partial<DecisionReporter> = {},
|
|
80
|
+
): DecisionReporter {
|
|
81
|
+
return {
|
|
82
|
+
writeReviewLog: vi.fn(),
|
|
83
|
+
emitDecision: vi.fn(),
|
|
84
|
+
...overrides,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gate runner factory for `GateRunner` unit tests.
|
|
90
|
+
*
|
|
91
|
+
* Builds one `GateRunner` from four role mocks and returns `{ runner, deps }`
|
|
92
|
+
* so tests can both invoke `runner.run(...)` and assert on the individual
|
|
93
|
+
* mock call records (`deps.reporter.*`, `deps.resolve`, etc.).
|
|
94
|
+
*/
|
|
95
|
+
export function makeGateRunner(
|
|
96
|
+
overrides: {
|
|
97
|
+
resolveResult?: PermissionCheckResult;
|
|
98
|
+
resolve?: ScopedPermissionResolver["resolve"];
|
|
99
|
+
resolvePathPolicy?: ScopedPermissionResolver["resolvePathPolicy"];
|
|
100
|
+
recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
|
|
101
|
+
recordPersistentApproval?: PersistentApprovalRecorder["recordApproval"];
|
|
102
|
+
canConfirm?: GatePrompter["canConfirm"];
|
|
103
|
+
prompt?: GatePrompter["prompt"];
|
|
104
|
+
reporter?: Partial<DecisionReporter>;
|
|
105
|
+
} = {},
|
|
106
|
+
) {
|
|
107
|
+
const reporter = makeReporter(overrides.reporter);
|
|
108
|
+
const resolve =
|
|
109
|
+
overrides.resolve ??
|
|
110
|
+
vi
|
|
111
|
+
.fn<ScopedPermissionResolver["resolve"]>()
|
|
112
|
+
.mockReturnValue(
|
|
113
|
+
overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
|
|
114
|
+
);
|
|
115
|
+
const resolvePathPolicy =
|
|
116
|
+
overrides.resolvePathPolicy ??
|
|
117
|
+
vi
|
|
118
|
+
.fn<ScopedPermissionResolver["resolvePathPolicy"]>()
|
|
119
|
+
.mockReturnValue(
|
|
120
|
+
overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
|
|
121
|
+
);
|
|
122
|
+
const recordSessionApproval =
|
|
123
|
+
overrides.recordSessionApproval ??
|
|
124
|
+
(vi.fn() as SessionApprovalRecorder["recordSessionApproval"]);
|
|
125
|
+
const recordPersistentApproval =
|
|
126
|
+
overrides.recordPersistentApproval ??
|
|
127
|
+
(vi.fn() as PersistentApprovalRecorder["recordApproval"]);
|
|
128
|
+
const canConfirm =
|
|
129
|
+
overrides.canConfirm ??
|
|
130
|
+
(vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
|
|
131
|
+
const prompt =
|
|
132
|
+
overrides.prompt ??
|
|
133
|
+
vi
|
|
134
|
+
.fn<GatePrompter["prompt"]>()
|
|
135
|
+
.mockResolvedValue({ approved: true, state: "approved" });
|
|
136
|
+
const runner = new GateRunner(
|
|
137
|
+
{ resolve, resolvePathPolicy },
|
|
138
|
+
{ recordSessionApproval },
|
|
139
|
+
{ canConfirm, prompt },
|
|
140
|
+
reporter,
|
|
141
|
+
{ recordApproval: recordPersistentApproval } as PersistentApprovalRecorder,
|
|
142
|
+
);
|
|
143
|
+
return {
|
|
144
|
+
runner,
|
|
145
|
+
deps: {
|
|
146
|
+
resolve,
|
|
147
|
+
resolvePathPolicy,
|
|
148
|
+
recordSessionApproval,
|
|
149
|
+
recordPersistentApproval,
|
|
150
|
+
canConfirm,
|
|
151
|
+
prompt,
|
|
152
|
+
reporter,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gate descriptor variant with write-surface defaults and a caller-supplied
|
|
159
|
+
* denialContext.
|
|
160
|
+
*
|
|
161
|
+
* Use instead of `makeDescriptor` when the test exercises denial-message
|
|
162
|
+
* formatting — the write surface and its matching promptDetails/logContext
|
|
163
|
+
* keep the message helpers' field access consistent.
|
|
164
|
+
*/
|
|
165
|
+
export function makeDenialDescriptor(
|
|
166
|
+
denialContext: DenialContext,
|
|
167
|
+
overrides: Partial<GateDescriptor> = {},
|
|
168
|
+
): GateDescriptor {
|
|
169
|
+
return {
|
|
170
|
+
surface: "write",
|
|
171
|
+
input: {},
|
|
172
|
+
denialContext,
|
|
173
|
+
promptDetails: {
|
|
174
|
+
source: "tool_call",
|
|
175
|
+
agentName: null,
|
|
176
|
+
message: "Allow tool 'write'?",
|
|
177
|
+
toolCallId: "tc-1",
|
|
178
|
+
toolName: "write",
|
|
179
|
+
},
|
|
180
|
+
logContext: {
|
|
181
|
+
source: "tool_call",
|
|
182
|
+
toolCallId: "tc-1",
|
|
183
|
+
toolName: "write",
|
|
184
|
+
},
|
|
185
|
+
decision: {
|
|
186
|
+
surface: "write",
|
|
187
|
+
value: "write",
|
|
188
|
+
},
|
|
189
|
+
...overrides,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Tool-call context factory with bash defaults.
|
|
195
|
+
*
|
|
196
|
+
* path.test.ts uses different defaults (toolName "read", path input) and
|
|
197
|
+
* keeps a local wrapper; bash-path.test.ts uses this factory directly.
|
|
198
|
+
*/
|
|
199
|
+
export function makeTcc(
|
|
200
|
+
overrides: Partial<ToolCallContext> = {},
|
|
201
|
+
): ToolCallContext {
|
|
202
|
+
return {
|
|
203
|
+
toolName: "bash",
|
|
204
|
+
agentName: null,
|
|
205
|
+
input: { command: "cat .env" },
|
|
206
|
+
toolCallId: "tc-1",
|
|
207
|
+
cwd: "/test/project",
|
|
208
|
+
...overrides,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolver whose `resolve` dispatches on `input.path`, falling back to a
|
|
214
|
+
* default result for any path not in the map.
|
|
215
|
+
*
|
|
216
|
+
* Use when a test needs different results for different path tokens without
|
|
217
|
+
* writing a full `mockImplementation` block.
|
|
218
|
+
*
|
|
219
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
220
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
|
|
221
|
+
*/
|
|
222
|
+
export function makePathDispatchResolver(
|
|
223
|
+
byPath: Record<string, PermissionCheckResult>,
|
|
224
|
+
defaultResult: PermissionCheckResult,
|
|
225
|
+
) {
|
|
226
|
+
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
227
|
+
resolve.mockImplementation((_surface, input) => {
|
|
228
|
+
const path = (input as Record<string, unknown>).path;
|
|
229
|
+
if (typeof path === "string" && path in byPath) {
|
|
230
|
+
return byPath[path];
|
|
231
|
+
}
|
|
232
|
+
return defaultResult;
|
|
233
|
+
});
|
|
234
|
+
const resolvePathPolicy =
|
|
235
|
+
vi.fn<ScopedPermissionResolver["resolvePathPolicy"]>();
|
|
236
|
+
resolvePathPolicy.mockImplementation((values) => {
|
|
237
|
+
for (const value of values) {
|
|
238
|
+
if (value in byPath) return byPath[value];
|
|
239
|
+
}
|
|
240
|
+
return defaultResult;
|
|
241
|
+
});
|
|
242
|
+
return { resolve, resolvePathPolicy };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Path-surface check result factory.
|
|
247
|
+
*
|
|
248
|
+
* Shared between bash-path.test.ts and path.test.ts; both use
|
|
249
|
+
* toolName "path", source "special", origin "global" as defaults.
|
|
250
|
+
*/
|
|
251
|
+
export function makeGateCheckResult(
|
|
252
|
+
overrides: Partial<PermissionCheckResult> = {},
|
|
253
|
+
): PermissionCheckResult {
|
|
254
|
+
return {
|
|
255
|
+
toolName: "path",
|
|
256
|
+
state: "allow",
|
|
257
|
+
source: "special",
|
|
258
|
+
origin: "global",
|
|
259
|
+
...overrides,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Mock of `ToolCallGateInputs` for `ToolCallGatePipeline` unit tests.
|
|
265
|
+
*
|
|
266
|
+
* Each method is a `vi.fn()` stub so callers retain full mock access
|
|
267
|
+
* (`mock.calls`, `mockReturnValue`, etc.) on the returned object.
|
|
268
|
+
* Pass `overrides` to replace individual stubs without rebuilding the whole
|
|
269
|
+
* mock from scratch.
|
|
270
|
+
*/
|
|
271
|
+
export function makeGateInputs(
|
|
272
|
+
overrides: {
|
|
273
|
+
getActiveSkillEntries?: () => SkillPromptEntry[];
|
|
274
|
+
getInfrastructureReadDirs?: () => string[];
|
|
275
|
+
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
276
|
+
} = {},
|
|
277
|
+
): ToolCallGateInputs {
|
|
278
|
+
return {
|
|
279
|
+
getActiveSkillEntries:
|
|
280
|
+
overrides.getActiveSkillEntries ??
|
|
281
|
+
vi.fn<() => SkillPromptEntry[]>(() => []),
|
|
282
|
+
getInfrastructureReadDirs:
|
|
283
|
+
overrides.getInfrastructureReadDirs ?? vi.fn<() => string[]>(() => []),
|
|
284
|
+
getToolPreviewLimits:
|
|
285
|
+
overrides.getToolPreviewLimits ??
|
|
286
|
+
vi.fn<() => ToolPreviewFormatterOptions>(() => ({
|
|
287
|
+
toolInputPreviewMaxLength: 500,
|
|
288
|
+
toolTextSummaryMaxLength: 100,
|
|
289
|
+
toolInputLogPreviewMaxLength: 200,
|
|
290
|
+
})),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Mock of `SkillInputGateInputs` for `SkillInputGatePipeline` unit tests.
|
|
296
|
+
*
|
|
297
|
+
* Returns a plain object with a `checkPermission` `vi.fn()` stub so callers
|
|
298
|
+
* retain full mock access (`mockReturnValue`, `mock.calls`, etc.).
|
|
299
|
+
*/
|
|
300
|
+
export function makeSkillInputInputs(
|
|
301
|
+
overrides: { checkPermission?: SkillInputGateInputs["checkPermission"] } = {},
|
|
302
|
+
): SkillInputGateInputs {
|
|
303
|
+
return {
|
|
304
|
+
checkPermission:
|
|
305
|
+
overrides.checkPermission ??
|
|
306
|
+
vi
|
|
307
|
+
.fn<SkillInputGateInputs["checkPermission"]>()
|
|
308
|
+
.mockReturnValue(makeCheckResult()),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Mock `GateNotifier` for `SkillInputGatePipeline` unit tests.
|
|
314
|
+
*
|
|
315
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
316
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.) — annotating with
|
|
317
|
+
* `GateNotifier` would erase `Mock<...>` methods from the inferred type.
|
|
318
|
+
*/
|
|
319
|
+
export function makeNotifier() {
|
|
320
|
+
return {
|
|
321
|
+
warn: vi.fn<(message: string) => void>(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handler-level test fixtures for PermissionGateHandler tests.
|
|
3
|
+
*
|
|
4
|
+
* `makeHandler` builds a real PermissionSession + PermissionResolver and wires
|
|
5
|
+
* them into the handler and pipelines exactly as `index.ts` does.
|
|
6
|
+
* Call-site overrides for permission results flow through
|
|
7
|
+
* `permissionManager.checkPermission`; session state overrides are applied
|
|
8
|
+
* via vi.spyOn on the real session instance.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { vi } from "vitest";
|
|
12
|
+
|
|
13
|
+
import { GateDecisionReporter } from "#src/decision-reporter";
|
|
14
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
15
|
+
import { GateRunner } from "#src/handlers/gates/runner";
|
|
16
|
+
import {
|
|
17
|
+
type SkillInputGateInputs,
|
|
18
|
+
SkillInputGatePipeline,
|
|
19
|
+
} from "#src/handlers/gates/skill-input-gate-pipeline";
|
|
20
|
+
import {
|
|
21
|
+
type ToolCallGateInputs,
|
|
22
|
+
ToolCallGatePipeline,
|
|
23
|
+
} from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
24
|
+
import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
|
|
25
|
+
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
26
|
+
import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
|
|
27
|
+
import type { Rule } from "#src/rule";
|
|
28
|
+
import { SessionRules } from "#src/session-rules";
|
|
29
|
+
import type { ToolRegistry } from "#src/tool-registry";
|
|
30
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
31
|
+
import {
|
|
32
|
+
makeRealResolver,
|
|
33
|
+
makeRealSession,
|
|
34
|
+
} from "#test/helpers/session-fixtures";
|
|
35
|
+
|
|
36
|
+
// ── MockGateHandlerSession ────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mock type for gate-pipeline inputs (ToolCallGateInputs + SkillInputGateInputs).
|
|
40
|
+
*
|
|
41
|
+
* Used by `makeSurfaceCheck`, `makeBashCommandCheck`, and the `session`
|
|
42
|
+
* override bag in `makeHandler`. The `GateHandlerSession` role (activate +
|
|
43
|
+
* resolveAgentName) is now satisfied by the real `PermissionSession`; this
|
|
44
|
+
* type covers only the pipeline input surface.
|
|
45
|
+
*
|
|
46
|
+
* The 4-arg `checkPermission` is a superset of `SkillInputGateInputs` —
|
|
47
|
+
* it routes through `permissionManager.checkPermission` in production.
|
|
48
|
+
*/
|
|
49
|
+
export type MockGateHandlerSession = ToolCallGateInputs &
|
|
50
|
+
SkillInputGateInputs & {
|
|
51
|
+
/** 4-arg form so surface-check mocks can receive optional rules. */
|
|
52
|
+
checkPermission(
|
|
53
|
+
surface: string,
|
|
54
|
+
input: unknown,
|
|
55
|
+
agentName?: string,
|
|
56
|
+
rules?: Rule[],
|
|
57
|
+
): PermissionCheckResult;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── Small utility factories ───────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export function makeEvents() {
|
|
63
|
+
return {
|
|
64
|
+
emit: vi.fn(),
|
|
65
|
+
on: vi.fn().mockReturnValue(() => undefined),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function makeCtx(
|
|
70
|
+
overrides: Partial<ExtensionContext> = {},
|
|
71
|
+
): ExtensionContext {
|
|
72
|
+
return {
|
|
73
|
+
cwd: "/test/project",
|
|
74
|
+
hasUI: true,
|
|
75
|
+
ui: {
|
|
76
|
+
setStatus: vi.fn(),
|
|
77
|
+
notify: vi.fn(),
|
|
78
|
+
select: vi.fn(),
|
|
79
|
+
input: vi.fn(),
|
|
80
|
+
},
|
|
81
|
+
sessionManager: {
|
|
82
|
+
getEntries: vi.fn().mockReturnValue([]),
|
|
83
|
+
getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
|
|
84
|
+
addEntry: vi.fn(),
|
|
85
|
+
},
|
|
86
|
+
...overrides,
|
|
87
|
+
} as unknown as ExtensionContext;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function makeToolCallEvent(
|
|
91
|
+
toolName: string,
|
|
92
|
+
extraFields: Record<string, unknown> = {},
|
|
93
|
+
) {
|
|
94
|
+
return {
|
|
95
|
+
type: "tool_call",
|
|
96
|
+
toolCallId: "tc-1",
|
|
97
|
+
name: toolName,
|
|
98
|
+
input: {},
|
|
99
|
+
...extraFields,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Neutral-default check-result builder.
|
|
105
|
+
*
|
|
106
|
+
* Pass exactly the fields the original fixture hard-coded so divergent
|
|
107
|
+
* defaults across test files are preserved at their call sites.
|
|
108
|
+
*/
|
|
109
|
+
export function makeCheckResult(
|
|
110
|
+
overrides: Partial<PermissionCheckResult> = {},
|
|
111
|
+
): PermissionCheckResult {
|
|
112
|
+
return {
|
|
113
|
+
state: "allow",
|
|
114
|
+
toolName: "read",
|
|
115
|
+
source: "tool",
|
|
116
|
+
origin: "builtin",
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function makeToolRegistry(
|
|
122
|
+
overrides: Partial<ToolRegistry> = {},
|
|
123
|
+
): ToolRegistry {
|
|
124
|
+
return {
|
|
125
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
126
|
+
getActive: vi.fn().mockReturnValue(["read", "bash"]),
|
|
127
|
+
setActive: vi.fn(),
|
|
128
|
+
...overrides,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Surface-check factories ────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Surface-dispatching `checkPermission` mock.
|
|
136
|
+
*
|
|
137
|
+
* Returns the matching per-surface result or `defaultResult`.
|
|
138
|
+
* Pass the returned function as `session.checkPermission` in a `makeHandler`
|
|
139
|
+
* override bag — it is applied to `permissionManager.checkPermission`.
|
|
140
|
+
*
|
|
141
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
142
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
|
|
143
|
+
*/
|
|
144
|
+
export function makeSurfaceCheck(
|
|
145
|
+
bySurface: Record<
|
|
146
|
+
string,
|
|
147
|
+
Partial<PermissionCheckResult> & { state: PermissionState }
|
|
148
|
+
>,
|
|
149
|
+
defaultResult: Partial<PermissionCheckResult> & { state: PermissionState } = {
|
|
150
|
+
state: "allow",
|
|
151
|
+
},
|
|
152
|
+
) {
|
|
153
|
+
return vi
|
|
154
|
+
.fn<MockGateHandlerSession["checkPermission"]>()
|
|
155
|
+
.mockImplementation((surface): PermissionCheckResult => {
|
|
156
|
+
const base = bySurface[surface] ?? defaultResult;
|
|
157
|
+
return {
|
|
158
|
+
toolName: surface,
|
|
159
|
+
source: "tool",
|
|
160
|
+
origin: "builtin",
|
|
161
|
+
...base,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Bash-surface `checkPermission` mock that dispatches on a command regex.
|
|
168
|
+
*
|
|
169
|
+
* Pass the returned function as `session.checkPermission` in a `makeHandler`
|
|
170
|
+
* override bag — it is applied to `permissionManager.checkPermission`.
|
|
171
|
+
*
|
|
172
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
173
|
+
* mock access.
|
|
174
|
+
*/
|
|
175
|
+
export function makeBashCommandCheck(opts: {
|
|
176
|
+
deny: RegExp;
|
|
177
|
+
denyMatched: string;
|
|
178
|
+
allowMatched?: string;
|
|
179
|
+
}) {
|
|
180
|
+
return vi
|
|
181
|
+
.fn<MockGateHandlerSession["checkPermission"]>()
|
|
182
|
+
.mockImplementation((surface, input): PermissionCheckResult => {
|
|
183
|
+
if (surface === "bash") {
|
|
184
|
+
const command = (input as { command?: string }).command ?? "";
|
|
185
|
+
return opts.deny.test(command)
|
|
186
|
+
? makeCheckResult({
|
|
187
|
+
state: "deny",
|
|
188
|
+
source: "bash",
|
|
189
|
+
command,
|
|
190
|
+
matchedPattern: opts.denyMatched,
|
|
191
|
+
})
|
|
192
|
+
: makeCheckResult({
|
|
193
|
+
state: "allow",
|
|
194
|
+
source: "bash",
|
|
195
|
+
command,
|
|
196
|
+
matchedPattern: opts.allowMatched,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return makeCheckResult({ state: "allow" });
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── makeHandler ────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Constructs a PermissionGateHandler wired with real collaborators.
|
|
207
|
+
*
|
|
208
|
+
* The `session` override bag maps to the real collaborators:
|
|
209
|
+
* - `checkPermission` → applied to `permissionManager.checkPermission`
|
|
210
|
+
* - `getActiveSkillEntries`, `getInfrastructureReadDirs`, `getToolPreviewLimits`
|
|
211
|
+
* → applied as vi.spyOn overrides on the real session
|
|
212
|
+
* - `resolveAgentName` → applied as a vi.spyOn override on the real session
|
|
213
|
+
*
|
|
214
|
+
* Returns `{ handler, events, session, toolRegistry, prompter, recorder,
|
|
215
|
+
* permissionManager, forwarding }` so each test file can destructure only
|
|
216
|
+
* what it needs.
|
|
217
|
+
* `session.activate` is not a mock — use `forwarding.start` to assert it
|
|
218
|
+
* was called.
|
|
219
|
+
*/
|
|
220
|
+
export function makeHandler(overrides?: {
|
|
221
|
+
session?: Partial<MockGateHandlerSession> & {
|
|
222
|
+
resolveAgentName?: (
|
|
223
|
+
ctx: ExtensionContext,
|
|
224
|
+
systemPrompt?: string,
|
|
225
|
+
) => string | null;
|
|
226
|
+
};
|
|
227
|
+
/** Override the GatePrompter passed to GateRunner. Defaults to an allow-all stub. */
|
|
228
|
+
prompter?: GatePrompter;
|
|
229
|
+
toolRegistry?: Partial<ToolRegistry>;
|
|
230
|
+
/** Sugar: builds the `getAll` mock from a list of tool names. */
|
|
231
|
+
tools?: string[];
|
|
232
|
+
}) {
|
|
233
|
+
const { session, permissionManager, sessionRules, forwarding, logger } =
|
|
234
|
+
makeRealSession();
|
|
235
|
+
const { resolver } = makeRealResolver(permissionManager, sessionRules);
|
|
236
|
+
|
|
237
|
+
// Apply session override bag to the real collaborators.
|
|
238
|
+
const so = overrides?.session;
|
|
239
|
+
const surfaceCheck = so?.checkPermission;
|
|
240
|
+
if (surfaceCheck) {
|
|
241
|
+
vi.mocked(permissionManager.checkPermission).mockImplementation(
|
|
242
|
+
surfaceCheck,
|
|
243
|
+
);
|
|
244
|
+
// The bash path and external-directory gates resolve through
|
|
245
|
+
// checkPathPolicy; route it through the same surface dispatcher (threading
|
|
246
|
+
// the real surface) so `path` / `external_directory` overrides apply to
|
|
247
|
+
// bash tokens and tool paths alike (#418).
|
|
248
|
+
vi.mocked(permissionManager.checkPathPolicy).mockImplementation(
|
|
249
|
+
(values, agentName, sessionRules, surface = "path") =>
|
|
250
|
+
surfaceCheck(
|
|
251
|
+
surface,
|
|
252
|
+
{ path: values[0] ?? "*" },
|
|
253
|
+
agentName,
|
|
254
|
+
sessionRules,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (so?.getActiveSkillEntries) {
|
|
259
|
+
vi.spyOn(session, "getActiveSkillEntries").mockImplementation(
|
|
260
|
+
so.getActiveSkillEntries,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (so?.getInfrastructureReadDirs) {
|
|
264
|
+
vi.spyOn(session, "getInfrastructureReadDirs").mockImplementation(
|
|
265
|
+
so.getInfrastructureReadDirs,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (so?.getToolPreviewLimits) {
|
|
269
|
+
vi.spyOn(session, "getToolPreviewLimits").mockImplementation(
|
|
270
|
+
so.getToolPreviewLimits,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (so?.resolveAgentName) {
|
|
274
|
+
vi.spyOn(session, "resolveAgentName").mockImplementation(
|
|
275
|
+
so.resolveAgentName,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const events = makeEvents();
|
|
280
|
+
const toolRegistry =
|
|
281
|
+
overrides?.tools !== undefined
|
|
282
|
+
? makeToolRegistry({
|
|
283
|
+
getAll: vi
|
|
284
|
+
.fn()
|
|
285
|
+
.mockReturnValue(overrides.tools.map((name) => ({ name }))),
|
|
286
|
+
})
|
|
287
|
+
: makeToolRegistry(overrides?.toolRegistry);
|
|
288
|
+
|
|
289
|
+
const recorder = new SessionRules();
|
|
290
|
+
const pipeline = new ToolCallGatePipeline(resolver, session);
|
|
291
|
+
const skillInputPipeline = new SkillInputGatePipeline(resolver);
|
|
292
|
+
const reporter = new GateDecisionReporter(logger, events);
|
|
293
|
+
const prompter: GatePrompter = overrides?.prompter ?? {
|
|
294
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
295
|
+
prompt: vi
|
|
296
|
+
.fn<GatePrompter["prompt"]>()
|
|
297
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
298
|
+
};
|
|
299
|
+
const runner = new GateRunner(
|
|
300
|
+
resolver,
|
|
301
|
+
recorder,
|
|
302
|
+
prompter,
|
|
303
|
+
reporter,
|
|
304
|
+
{ recordApproval: vi.fn() } as never,
|
|
305
|
+
);
|
|
306
|
+
const handler = new PermissionGateHandler(
|
|
307
|
+
session,
|
|
308
|
+
toolRegistry,
|
|
309
|
+
pipeline,
|
|
310
|
+
skillInputPipeline,
|
|
311
|
+
runner,
|
|
312
|
+
);
|
|
313
|
+
return {
|
|
314
|
+
handler,
|
|
315
|
+
events,
|
|
316
|
+
session,
|
|
317
|
+
logger,
|
|
318
|
+
toolRegistry,
|
|
319
|
+
prompter,
|
|
320
|
+
recorder,
|
|
321
|
+
permissionManager,
|
|
322
|
+
forwarding,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Decision-event helper ─────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
/** Extract all permissions:decision payloads from the events.emit mock. */
|
|
329
|
+
export function getDecisionEvents(
|
|
330
|
+
events: ReturnType<typeof makeEvents>,
|
|
331
|
+
): PermissionDecisionEvent[] {
|
|
332
|
+
return events.emit.mock.calls
|
|
333
|
+
.filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
|
|
334
|
+
.map(([, payload]) => payload as PermissionDecisionEvent);
|
|
335
|
+
}
|