@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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
import type { AgentPrepSession } from "#src/agent-prep-session";
|
|
3
4
|
import {
|
|
4
5
|
AgentPrepHandler,
|
|
5
6
|
shouldExposeTool,
|
|
6
7
|
} from "#src/handlers/before-agent-start";
|
|
7
|
-
import type { PermissionSession } from "#src/permission-session";
|
|
8
8
|
import type { ToolRegistry } from "#src/tool-registry";
|
|
9
9
|
|
|
10
|
-
import { makeCtx } from "#test/helpers/handler-fixtures";
|
|
10
|
+
import { makeCheckResult, makeCtx } from "#test/helpers/handler-fixtures";
|
|
11
11
|
|
|
12
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
13
13
|
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
@@ -26,24 +26,48 @@ function makeEvent(systemPrompt = "You are an assistant.") {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function makeSession(
|
|
29
|
-
overrides: Partial<
|
|
30
|
-
):
|
|
29
|
+
overrides: Partial<AgentPrepSession> = {},
|
|
30
|
+
): AgentPrepSession {
|
|
31
31
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resolveAgentName:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
activate: overrides.activate ?? vi.fn<AgentPrepSession["activate"]>(),
|
|
33
|
+
refreshConfig:
|
|
34
|
+
overrides.refreshConfig ?? vi.fn<AgentPrepSession["refreshConfig"]>(),
|
|
35
|
+
resolveAgentName:
|
|
36
|
+
overrides.resolveAgentName ??
|
|
37
|
+
vi.fn<AgentPrepSession["resolveAgentName"]>().mockReturnValue(null),
|
|
38
|
+
checkPermission:
|
|
39
|
+
overrides.checkPermission ??
|
|
40
|
+
vi
|
|
41
|
+
.fn<AgentPrepSession["checkPermission"]>()
|
|
42
|
+
.mockReturnValue(makeCheckResult()),
|
|
43
|
+
getToolPermission:
|
|
44
|
+
overrides.getToolPermission ??
|
|
45
|
+
vi.fn<AgentPrepSession["getToolPermission"]>().mockReturnValue("allow"),
|
|
46
|
+
shouldUpdateActiveTools:
|
|
47
|
+
overrides.shouldUpdateActiveTools ??
|
|
48
|
+
vi
|
|
49
|
+
.fn<AgentPrepSession["shouldUpdateActiveTools"]>()
|
|
50
|
+
.mockReturnValue(true),
|
|
51
|
+
commitActiveToolsCacheKey:
|
|
52
|
+
overrides.commitActiveToolsCacheKey ??
|
|
53
|
+
vi.fn<AgentPrepSession["commitActiveToolsCacheKey"]>(),
|
|
54
|
+
getPolicyCacheStamp:
|
|
55
|
+
overrides.getPolicyCacheStamp ??
|
|
56
|
+
vi
|
|
57
|
+
.fn<AgentPrepSession["getPolicyCacheStamp"]>()
|
|
58
|
+
.mockReturnValue("stamp-1"),
|
|
59
|
+
shouldUpdatePromptState:
|
|
60
|
+
overrides.shouldUpdatePromptState ??
|
|
61
|
+
vi
|
|
62
|
+
.fn<AgentPrepSession["shouldUpdatePromptState"]>()
|
|
63
|
+
.mockReturnValue(true),
|
|
64
|
+
commitPromptStateCacheKey:
|
|
65
|
+
overrides.commitPromptStateCacheKey ??
|
|
66
|
+
vi.fn<AgentPrepSession["commitPromptStateCacheKey"]>(),
|
|
67
|
+
setActiveSkillEntries:
|
|
68
|
+
overrides.setActiveSkillEntries ??
|
|
69
|
+
vi.fn<AgentPrepSession["setActiveSkillEntries"]>(),
|
|
70
|
+
};
|
|
47
71
|
}
|
|
48
72
|
|
|
49
73
|
function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
@@ -55,11 +79,11 @@ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
|
55
79
|
}
|
|
56
80
|
|
|
57
81
|
function makeHandler(overrides?: {
|
|
58
|
-
session?: Partial<
|
|
82
|
+
session?: Partial<AgentPrepSession>;
|
|
59
83
|
toolRegistry?: Partial<ToolRegistry>;
|
|
60
84
|
}): {
|
|
61
85
|
handler: AgentPrepHandler;
|
|
62
|
-
session:
|
|
86
|
+
session: AgentPrepSession;
|
|
63
87
|
toolRegistry: ToolRegistry;
|
|
64
88
|
} {
|
|
65
89
|
const session = makeSession(overrides?.session);
|
|
@@ -8,20 +8,18 @@
|
|
|
8
8
|
* Regression guard: importing the four external-directory message helpers
|
|
9
9
|
* ensures the test file fails to load if any helper is removed.
|
|
10
10
|
*/
|
|
11
|
+
|
|
11
12
|
import { describe, expect, it, vi } from "vitest";
|
|
12
13
|
|
|
13
14
|
import { EXTENSION_TAG } from "#src/denial-messages";
|
|
14
|
-
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
15
15
|
import { formatExternalDirectoryAskPrompt } from "#src/handlers/gates/external-directory-messages";
|
|
16
|
-
import {
|
|
17
|
-
import type { PermissionSession } from "#src/permission-session";
|
|
18
|
-
import type { ToolRegistry } from "#src/tool-registry";
|
|
19
|
-
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
16
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
20
17
|
|
|
21
18
|
import {
|
|
22
19
|
getDecisionEvents,
|
|
23
20
|
makeCtx,
|
|
24
|
-
|
|
21
|
+
makeHandler,
|
|
22
|
+
makeSurfaceCheck,
|
|
25
23
|
makeToolCallEvent,
|
|
26
24
|
} from "#test/helpers/handler-fixtures";
|
|
27
25
|
|
|
@@ -37,93 +35,35 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
|
37
35
|
const CWD = "/test/project";
|
|
38
36
|
const EXTERNAL_PATH = "/outside/project/file.ts";
|
|
39
37
|
|
|
40
|
-
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
function makeCheckPermission(
|
|
43
|
-
externalDirectoryState: PermissionState,
|
|
44
|
-
toolState: PermissionState = "allow",
|
|
45
|
-
) {
|
|
46
|
-
return vi
|
|
47
|
-
.fn()
|
|
48
|
-
.mockImplementation((surface: string): PermissionCheckResult => {
|
|
49
|
-
if (surface === "external_directory") {
|
|
50
|
-
return {
|
|
51
|
-
state: externalDirectoryState,
|
|
52
|
-
toolName: surface,
|
|
53
|
-
source: "tool",
|
|
54
|
-
origin: "builtin",
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
// The cross-cutting path gate runs before ext-dir; keep it transparent.
|
|
58
|
-
if (surface === "path") {
|
|
59
|
-
return {
|
|
60
|
-
state: "allow",
|
|
61
|
-
toolName: surface,
|
|
62
|
-
source: "special",
|
|
63
|
-
origin: "builtin",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
state: toolState,
|
|
68
|
-
toolName: surface,
|
|
69
|
-
source: "tool",
|
|
70
|
-
origin: "builtin",
|
|
71
|
-
};
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function makeSession(
|
|
76
|
-
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
77
|
-
): PermissionSession {
|
|
78
|
-
return {
|
|
79
|
-
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
80
|
-
activate: vi.fn(),
|
|
81
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
82
|
-
checkPermission: makeCheckPermission("deny"),
|
|
83
|
-
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
84
|
-
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
85
|
-
recordSessionApproval: vi.fn(),
|
|
86
|
-
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
87
|
-
getInfrastructureDirs: vi.fn().mockReturnValue([]),
|
|
88
|
-
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
89
|
-
config: DEFAULT_EXTENSION_CONFIG,
|
|
90
|
-
canPrompt: vi.fn().mockReturnValue(true),
|
|
91
|
-
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
92
|
-
...overrides,
|
|
93
|
-
} as unknown as PermissionSession;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
38
|
/** All PATH_BEARING_TOOLS members. */
|
|
97
39
|
const ALL_PATH_BEARING_TOOLS = ["read", "write", "edit", "find", "grep", "ls"];
|
|
98
40
|
|
|
99
41
|
/** Tools where path is optional. */
|
|
100
42
|
const OPTIONAL_PATH_TOOLS = ["find", "grep", "ls"];
|
|
101
43
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.mockReturnValue(
|
|
107
|
-
[...ALL_PATH_BEARING_TOOLS, "bash"].map((name) => ({ name })),
|
|
108
|
-
),
|
|
109
|
-
setActive: vi.fn(),
|
|
110
|
-
...overrides,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
44
|
+
/** Full tool set used as the default registry in ext-dir tests. */
|
|
45
|
+
const ALL_TOOLS = [...ALL_PATH_BEARING_TOOLS, "bash"];
|
|
46
|
+
|
|
47
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
113
48
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Builds a `checkPermission` mock for external-directory integration tests.
|
|
51
|
+
*
|
|
52
|
+
* Routes `external_directory` to `externalDirectoryState`, `path` to allow
|
|
53
|
+
* with `source: "special"` (so the cross-cutting path gate is transparent),
|
|
54
|
+
* and every other surface to `toolState` (default: allow).
|
|
55
|
+
*/
|
|
56
|
+
function makeExtDirCheck(
|
|
57
|
+
externalDirectoryState: "allow" | "deny" | "ask",
|
|
58
|
+
toolState: "allow" | "deny" | "ask" = "allow",
|
|
59
|
+
) {
|
|
60
|
+
return makeSurfaceCheck(
|
|
61
|
+
{
|
|
62
|
+
external_directory: { state: externalDirectoryState },
|
|
63
|
+
path: { state: "allow", source: "special" },
|
|
64
|
+
},
|
|
65
|
+
{ state: toolState },
|
|
66
|
+
);
|
|
127
67
|
}
|
|
128
68
|
|
|
129
69
|
// ── Regression guard: helper presence ──────────────────────────────────────
|
|
@@ -150,20 +90,22 @@ describe("external_directory helper regression guard", () => {
|
|
|
150
90
|
describe("external_directory path scope", () => {
|
|
151
91
|
it("skips external_directory check when path is inside CWD", async () => {
|
|
152
92
|
const { handler } = makeHandler({
|
|
153
|
-
session: { checkPermission:
|
|
93
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
94
|
+
tools: ALL_TOOLS,
|
|
154
95
|
});
|
|
155
96
|
const event = makeToolCallEvent("read", {
|
|
156
97
|
input: { path: `${CWD}/src/index.ts` },
|
|
157
98
|
});
|
|
158
99
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
159
100
|
// Should not be blocked — the external_directory gate is skipped,
|
|
160
|
-
// and the tool gate sees "allow" (default toolState in
|
|
101
|
+
// and the tool gate sees "allow" (default toolState in makeExtDirCheck)
|
|
161
102
|
expect(result).toEqual({});
|
|
162
103
|
});
|
|
163
104
|
|
|
164
105
|
it("fires external_directory check when path is outside CWD", async () => {
|
|
165
106
|
const { handler } = makeHandler({
|
|
166
|
-
session: { checkPermission:
|
|
107
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
108
|
+
tools: ALL_TOOLS,
|
|
167
109
|
});
|
|
168
110
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
169
111
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -172,7 +114,8 @@ describe("external_directory path scope", () => {
|
|
|
172
114
|
|
|
173
115
|
it("skips external_directory check for non-path-bearing tool (bash)", async () => {
|
|
174
116
|
const { handler } = makeHandler({
|
|
175
|
-
session: { checkPermission:
|
|
117
|
+
session: { checkPermission: makeExtDirCheck("deny", "allow") },
|
|
118
|
+
tools: ALL_TOOLS,
|
|
176
119
|
});
|
|
177
120
|
const event = makeToolCallEvent("bash", {
|
|
178
121
|
input: { command: `cat ${EXTERNAL_PATH}` },
|
|
@@ -191,7 +134,8 @@ describe("external_directory path scope", () => {
|
|
|
191
134
|
ALL_PATH_BEARING_TOOLS,
|
|
192
135
|
)("blocks %s with an out-of-cwd path when external_directory is deny", async (toolName) => {
|
|
193
136
|
const { handler } = makeHandler({
|
|
194
|
-
session: { checkPermission:
|
|
137
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
138
|
+
tools: ALL_TOOLS,
|
|
195
139
|
});
|
|
196
140
|
const event = makeToolCallEvent(toolName, {
|
|
197
141
|
input: { path: EXTERNAL_PATH },
|
|
@@ -204,7 +148,8 @@ describe("external_directory path scope", () => {
|
|
|
204
148
|
OPTIONAL_PATH_TOOLS,
|
|
205
149
|
)("skips external_directory check for %s when path is omitted", async (toolName) => {
|
|
206
150
|
const { handler } = makeHandler({
|
|
207
|
-
session: { checkPermission:
|
|
151
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
152
|
+
tools: ALL_TOOLS,
|
|
208
153
|
});
|
|
209
154
|
// No path in input — external_directory gate should not fire
|
|
210
155
|
const event = makeToolCallEvent(toolName);
|
|
@@ -218,7 +163,8 @@ describe("external_directory path scope", () => {
|
|
|
218
163
|
describe("external_directory policy state — allow", () => {
|
|
219
164
|
it("falls through to tool gate when external_directory is allow", async () => {
|
|
220
165
|
const { handler } = makeHandler({
|
|
221
|
-
session: { checkPermission:
|
|
166
|
+
session: { checkPermission: makeExtDirCheck("allow") },
|
|
167
|
+
tools: ALL_TOOLS,
|
|
222
168
|
});
|
|
223
169
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
224
170
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -227,7 +173,8 @@ describe("external_directory policy state — allow", () => {
|
|
|
227
173
|
|
|
228
174
|
it("emits decision event with policy_allow on external_directory surface", async () => {
|
|
229
175
|
const { handler, events } = makeHandler({
|
|
230
|
-
session: { checkPermission:
|
|
176
|
+
session: { checkPermission: makeExtDirCheck("allow") },
|
|
177
|
+
tools: ALL_TOOLS,
|
|
231
178
|
});
|
|
232
179
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
233
180
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -244,7 +191,8 @@ describe("external_directory policy state — allow", () => {
|
|
|
244
191
|
|
|
245
192
|
it("does not write a block review-log entry when external_directory is allow", async () => {
|
|
246
193
|
const { handler, session } = makeHandler({
|
|
247
|
-
session: { checkPermission:
|
|
194
|
+
session: { checkPermission: makeExtDirCheck("allow") },
|
|
195
|
+
tools: ALL_TOOLS,
|
|
248
196
|
});
|
|
249
197
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
250
198
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -261,7 +209,8 @@ describe("external_directory policy state — allow", () => {
|
|
|
261
209
|
describe("external_directory — allow external reads, gate external writes (#144)", () => {
|
|
262
210
|
it("allows read of external path when external_directory and read are both allow", async () => {
|
|
263
211
|
const { handler } = makeHandler({
|
|
264
|
-
session: { checkPermission:
|
|
212
|
+
session: { checkPermission: makeExtDirCheck("allow", "allow") },
|
|
213
|
+
tools: ALL_TOOLS,
|
|
265
214
|
});
|
|
266
215
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
267
216
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -274,9 +223,10 @@ describe("external_directory — allow external reads, gate external writes (#14
|
|
|
274
223
|
.mockResolvedValue({ approved: true, state: "approved" });
|
|
275
224
|
const { handler } = makeHandler({
|
|
276
225
|
session: {
|
|
277
|
-
checkPermission:
|
|
226
|
+
checkPermission: makeExtDirCheck("allow", "ask"),
|
|
278
227
|
prompt,
|
|
279
228
|
},
|
|
229
|
+
tools: ALL_TOOLS,
|
|
280
230
|
});
|
|
281
231
|
const event = makeToolCallEvent("write", {
|
|
282
232
|
input: { path: EXTERNAL_PATH },
|
|
@@ -289,7 +239,8 @@ describe("external_directory — allow external reads, gate external writes (#14
|
|
|
289
239
|
|
|
290
240
|
it("blocks write to external path when external_directory allows but write is deny", async () => {
|
|
291
241
|
const { handler } = makeHandler({
|
|
292
|
-
session: { checkPermission:
|
|
242
|
+
session: { checkPermission: makeExtDirCheck("allow", "deny") },
|
|
243
|
+
tools: ALL_TOOLS,
|
|
293
244
|
});
|
|
294
245
|
const event = makeToolCallEvent("write", {
|
|
295
246
|
input: { path: EXTERNAL_PATH },
|
|
@@ -300,7 +251,8 @@ describe("external_directory — allow external reads, gate external writes (#14
|
|
|
300
251
|
|
|
301
252
|
it("emits separate decision events for external_directory and write surfaces", async () => {
|
|
302
253
|
const { handler, events } = makeHandler({
|
|
303
|
-
session: { checkPermission:
|
|
254
|
+
session: { checkPermission: makeExtDirCheck("allow", "deny") },
|
|
255
|
+
tools: ALL_TOOLS,
|
|
304
256
|
});
|
|
305
257
|
const event = makeToolCallEvent("write", {
|
|
306
258
|
input: { path: EXTERNAL_PATH },
|
|
@@ -327,7 +279,8 @@ describe("external_directory — allow external reads, gate external writes (#14
|
|
|
327
279
|
describe("external_directory policy state — deny", () => {
|
|
328
280
|
it("blocks with reason containing the external path", async () => {
|
|
329
281
|
const { handler } = makeHandler({
|
|
330
|
-
session: { checkPermission:
|
|
282
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
283
|
+
tools: ALL_TOOLS,
|
|
331
284
|
});
|
|
332
285
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
333
286
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -337,7 +290,8 @@ describe("external_directory policy state — deny", () => {
|
|
|
337
290
|
|
|
338
291
|
it("block reason contains extension attribution", async () => {
|
|
339
292
|
const { handler } = makeHandler({
|
|
340
|
-
session: { checkPermission:
|
|
293
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
294
|
+
tools: ALL_TOOLS,
|
|
341
295
|
});
|
|
342
296
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
343
297
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -347,7 +301,8 @@ describe("external_directory policy state — deny", () => {
|
|
|
347
301
|
|
|
348
302
|
it("writes review-log entry with resolution policy_denied", async () => {
|
|
349
303
|
const { handler, session } = makeHandler({
|
|
350
|
-
session: { checkPermission:
|
|
304
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
305
|
+
tools: ALL_TOOLS,
|
|
351
306
|
});
|
|
352
307
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
353
308
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -364,7 +319,8 @@ describe("external_directory policy state — deny", () => {
|
|
|
364
319
|
|
|
365
320
|
it("emits decision event with policy_deny on external_directory surface", async () => {
|
|
366
321
|
const { handler, events } = makeHandler({
|
|
367
|
-
session: { checkPermission:
|
|
322
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
323
|
+
tools: ALL_TOOLS,
|
|
368
324
|
});
|
|
369
325
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
370
326
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -386,11 +342,12 @@ describe("external_directory policy state — ask", () => {
|
|
|
386
342
|
it("does not block when user approves", async () => {
|
|
387
343
|
const { handler } = makeHandler({
|
|
388
344
|
session: {
|
|
389
|
-
checkPermission:
|
|
345
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
390
346
|
prompt: vi
|
|
391
347
|
.fn()
|
|
392
348
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
393
349
|
},
|
|
350
|
+
tools: ALL_TOOLS,
|
|
394
351
|
});
|
|
395
352
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
396
353
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -400,11 +357,12 @@ describe("external_directory policy state — ask", () => {
|
|
|
400
357
|
it("emits user_approved decision when user approves", async () => {
|
|
401
358
|
const { handler, events } = makeHandler({
|
|
402
359
|
session: {
|
|
403
|
-
checkPermission:
|
|
360
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
404
361
|
prompt: vi
|
|
405
362
|
.fn()
|
|
406
363
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
407
364
|
},
|
|
365
|
+
tools: ALL_TOOLS,
|
|
408
366
|
});
|
|
409
367
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
410
368
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -422,9 +380,10 @@ describe("external_directory policy state — ask", () => {
|
|
|
422
380
|
it("blocks when user denies", async () => {
|
|
423
381
|
const { handler } = makeHandler({
|
|
424
382
|
session: {
|
|
425
|
-
checkPermission:
|
|
383
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
426
384
|
prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
|
|
427
385
|
},
|
|
386
|
+
tools: ALL_TOOLS,
|
|
428
387
|
});
|
|
429
388
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
430
389
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -434,9 +393,10 @@ describe("external_directory policy state — ask", () => {
|
|
|
434
393
|
it("emits user_denied decision when user denies", async () => {
|
|
435
394
|
const { handler, events } = makeHandler({
|
|
436
395
|
session: {
|
|
437
|
-
checkPermission:
|
|
396
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
438
397
|
prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
|
|
439
398
|
},
|
|
399
|
+
tools: ALL_TOOLS,
|
|
440
400
|
});
|
|
441
401
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
442
402
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -454,13 +414,14 @@ describe("external_directory policy state — ask", () => {
|
|
|
454
414
|
it("block reason includes denialReason when user provides one", async () => {
|
|
455
415
|
const { handler } = makeHandler({
|
|
456
416
|
session: {
|
|
457
|
-
checkPermission:
|
|
417
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
458
418
|
prompt: vi.fn().mockResolvedValue({
|
|
459
419
|
approved: false,
|
|
460
420
|
state: "denied",
|
|
461
421
|
denialReason: "not needed",
|
|
462
422
|
}),
|
|
463
423
|
},
|
|
424
|
+
tools: ALL_TOOLS,
|
|
464
425
|
});
|
|
465
426
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
466
427
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
@@ -471,9 +432,10 @@ describe("external_directory policy state — ask", () => {
|
|
|
471
432
|
it("blocks with confirmation_unavailable when no UI is available", async () => {
|
|
472
433
|
const { handler } = makeHandler({
|
|
473
434
|
session: {
|
|
474
|
-
checkPermission:
|
|
435
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
475
436
|
canPrompt: vi.fn().mockReturnValue(false),
|
|
476
437
|
},
|
|
438
|
+
tools: ALL_TOOLS,
|
|
477
439
|
});
|
|
478
440
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
479
441
|
const result = await handler.handleToolCall(
|
|
@@ -487,9 +449,10 @@ describe("external_directory policy state — ask", () => {
|
|
|
487
449
|
it("writes review-log entry with confirmation_unavailable when no UI", async () => {
|
|
488
450
|
const { handler, session } = makeHandler({
|
|
489
451
|
session: {
|
|
490
|
-
checkPermission:
|
|
452
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
491
453
|
canPrompt: vi.fn().mockReturnValue(false),
|
|
492
454
|
},
|
|
455
|
+
tools: ALL_TOOLS,
|
|
493
456
|
});
|
|
494
457
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
495
458
|
await handler.handleToolCall(event, makeCtx({ hasUI: false }));
|
|
@@ -507,9 +470,10 @@ describe("external_directory policy state — ask", () => {
|
|
|
507
470
|
it("emits confirmation_unavailable decision when no UI", async () => {
|
|
508
471
|
const { handler, events } = makeHandler({
|
|
509
472
|
session: {
|
|
510
|
-
checkPermission:
|
|
473
|
+
checkPermission: makeExtDirCheck("ask"),
|
|
511
474
|
canPrompt: vi.fn().mockReturnValue(false),
|
|
512
475
|
},
|
|
476
|
+
tools: ALL_TOOLS,
|
|
513
477
|
});
|
|
514
478
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
515
479
|
await handler.handleToolCall(event, makeCtx({ hasUI: false }));
|
|
@@ -563,6 +527,7 @@ describe("external_directory per-agent override", () => {
|
|
|
563
527
|
checkPermission: agentAwareCheck,
|
|
564
528
|
resolveAgentName: vi.fn().mockReturnValue("special-agent"),
|
|
565
529
|
},
|
|
530
|
+
tools: ALL_TOOLS,
|
|
566
531
|
});
|
|
567
532
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
568
533
|
const result1 = await handler1.handleToolCall(event, makeCtx());
|
|
@@ -582,6 +547,7 @@ describe("external_directory per-agent override", () => {
|
|
|
582
547
|
checkPermission: agentAwareCheck,
|
|
583
548
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
584
549
|
},
|
|
550
|
+
tools: ALL_TOOLS,
|
|
585
551
|
});
|
|
586
552
|
const result2 = await handler2.handleToolCall(event, makeCtx());
|
|
587
553
|
expect(result2).toMatchObject({ block: true });
|
|
@@ -593,7 +559,8 @@ describe("external_directory per-agent override", () => {
|
|
|
593
559
|
describe("external_directory decision event fields", () => {
|
|
594
560
|
it("decision event value is the external path", async () => {
|
|
595
561
|
const { handler, events } = makeHandler({
|
|
596
|
-
session: { checkPermission:
|
|
562
|
+
session: { checkPermission: makeExtDirCheck("deny") },
|
|
563
|
+
tools: ALL_TOOLS,
|
|
597
564
|
});
|
|
598
565
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
599
566
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -608,9 +575,10 @@ describe("external_directory decision event fields", () => {
|
|
|
608
575
|
it("decision event includes agentName when present", async () => {
|
|
609
576
|
const { handler, events } = makeHandler({
|
|
610
577
|
session: {
|
|
611
|
-
checkPermission:
|
|
578
|
+
checkPermission: makeExtDirCheck("allow"),
|
|
612
579
|
resolveAgentName: vi.fn().mockReturnValue("my-agent"),
|
|
613
580
|
},
|
|
581
|
+
tools: ALL_TOOLS,
|
|
614
582
|
});
|
|
615
583
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
616
584
|
await handler.handleToolCall(event, makeCtx());
|
|
@@ -625,7 +593,8 @@ describe("external_directory decision event fields", () => {
|
|
|
625
593
|
|
|
626
594
|
it("decision event agentName is null when no agent", async () => {
|
|
627
595
|
const { handler, events } = makeHandler({
|
|
628
|
-
session: { checkPermission:
|
|
596
|
+
session: { checkPermission: makeExtDirCheck("allow") },
|
|
597
|
+
tools: ALL_TOOLS,
|
|
629
598
|
});
|
|
630
599
|
const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
|
|
631
600
|
await handler.handleToolCall(event, makeCtx());
|