@gotgenes/pi-permission-system 8.2.0 → 8.2.1
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 +7 -0
- package/package.json +1 -1
- package/src/config-loader.ts +53 -46
- package/src/handlers/gates/bash-path-extractor.ts +135 -169
- package/src/handlers/gates/bash-token-classification.ts +105 -0
- package/test/config-loader.test.ts +82 -0
- package/test/handlers/before-agent-start.test.ts +2 -20
- package/test/handlers/external-directory-integration.test.ts +43 -81
- package/test/handlers/external-directory-session-dedup.test.ts +2 -29
- package/test/handlers/gates/bash-path.test.ts +5 -26
- package/test/handlers/gates/bash-token-classification.test.ts +241 -0
- package/test/handlers/gates/path.test.ts +3 -12
- package/test/handlers/gates/runner.test.ts +78 -91
- package/test/handlers/input-events.test.ts +42 -95
- package/test/handlers/input.test.ts +3 -71
- package/test/handlers/lifecycle.test.ts +3 -20
- package/test/handlers/tool-call-events.test.ts +30 -127
- package/test/handlers/tool-call.test.ts +21 -110
- package/test/helpers/gate-fixtures.ts +105 -0
- package/test/helpers/handler-fixtures.ts +141 -0
- package/test/helpers/manager-harness.ts +51 -0
- package/test/permission-session.test.ts +1 -19
- package/test/permission-system.test.ts +4 -40
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
import { getEventInput } from "#src/handlers/permission-gate-handler";
|
|
4
|
+
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
6
|
+
makeCheckResult,
|
|
7
|
+
makeCtx,
|
|
8
|
+
makeHandler,
|
|
9
|
+
makeToolCallEvent,
|
|
10
|
+
} from "#test/helpers/handler-fixtures";
|
|
11
11
|
|
|
12
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
13
13
|
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
@@ -16,101 +16,6 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
|
16
16
|
return { ...original };
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
// ── helpers ────────────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
function makeCtx(
|
|
22
|
-
overrides: Partial<ExtensionContext> & { cwd?: string } = {},
|
|
23
|
-
): ExtensionContext {
|
|
24
|
-
return {
|
|
25
|
-
cwd: "/test/project",
|
|
26
|
-
hasUI: true,
|
|
27
|
-
ui: {
|
|
28
|
-
setStatus: vi.fn(),
|
|
29
|
-
notify: vi.fn(),
|
|
30
|
-
select: vi.fn(),
|
|
31
|
-
input: vi.fn(),
|
|
32
|
-
},
|
|
33
|
-
sessionManager: {
|
|
34
|
-
getEntries: vi.fn().mockReturnValue([]),
|
|
35
|
-
getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
|
|
36
|
-
addEntry: vi.fn(),
|
|
37
|
-
},
|
|
38
|
-
...overrides,
|
|
39
|
-
} as unknown as ExtensionContext;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function makeToolCallEvent(
|
|
43
|
-
toolName: string,
|
|
44
|
-
extraFields: Record<string, unknown> = {},
|
|
45
|
-
) {
|
|
46
|
-
return {
|
|
47
|
-
type: "tool_call",
|
|
48
|
-
toolCallId: "tc-1",
|
|
49
|
-
name: toolName,
|
|
50
|
-
input: {},
|
|
51
|
-
...extraFields,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function makePermissionResult(
|
|
56
|
-
state: "allow" | "deny" | "ask",
|
|
57
|
-
): PermissionCheckResult {
|
|
58
|
-
return { state, toolName: "read", source: "tool", origin: "builtin" };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function makeSession(
|
|
62
|
-
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
63
|
-
): PermissionSession {
|
|
64
|
-
return {
|
|
65
|
-
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
66
|
-
activate: vi.fn(),
|
|
67
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
68
|
-
checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
|
|
69
|
-
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
70
|
-
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
71
|
-
recordSessionApproval: vi.fn(),
|
|
72
|
-
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
73
|
-
getInfrastructureDirs: vi
|
|
74
|
-
.fn()
|
|
75
|
-
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
76
|
-
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
77
|
-
config: DEFAULT_EXTENSION_CONFIG,
|
|
78
|
-
canPrompt: vi.fn().mockReturnValue(true),
|
|
79
|
-
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
80
|
-
...overrides,
|
|
81
|
-
} as unknown as PermissionSession;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function makeEvents() {
|
|
85
|
-
return {
|
|
86
|
-
emit: vi.fn(),
|
|
87
|
-
on: vi.fn().mockReturnValue(() => undefined),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
92
|
-
return {
|
|
93
|
-
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
94
|
-
setActive: vi.fn(),
|
|
95
|
-
...overrides,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function makeHandler(overrides?: {
|
|
100
|
-
session?: Partial<Record<keyof PermissionSession, unknown>>;
|
|
101
|
-
toolRegistry?: Partial<ToolRegistry>;
|
|
102
|
-
}): {
|
|
103
|
-
handler: PermissionGateHandler;
|
|
104
|
-
session: PermissionSession;
|
|
105
|
-
toolRegistry: ToolRegistry;
|
|
106
|
-
} {
|
|
107
|
-
const session = makeSession(overrides?.session);
|
|
108
|
-
const events = makeEvents();
|
|
109
|
-
const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
|
|
110
|
-
const handler = new PermissionGateHandler(session, events, toolRegistry);
|
|
111
|
-
return { handler, session, toolRegistry };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
19
|
// ── getEventInput ──────────────────────────────────────────────────────────
|
|
115
20
|
|
|
116
21
|
describe("getEventInput", () => {
|
|
@@ -184,7 +89,9 @@ describe("handleToolCall", () => {
|
|
|
184
89
|
it("blocks when tool is denied by policy", async () => {
|
|
185
90
|
const { handler } = makeHandler({
|
|
186
91
|
session: {
|
|
187
|
-
checkPermission: vi
|
|
92
|
+
checkPermission: vi
|
|
93
|
+
.fn()
|
|
94
|
+
.mockReturnValue(makeCheckResult({ state: "deny" })),
|
|
188
95
|
},
|
|
189
96
|
});
|
|
190
97
|
const result = await handler.handleToolCall(
|
|
@@ -259,7 +166,9 @@ describe("handleToolCall — external-directory gate", () => {
|
|
|
259
166
|
it("blocks a read of a path outside cwd when policy is deny", async () => {
|
|
260
167
|
const { handler } = makeHandler({
|
|
261
168
|
session: {
|
|
262
|
-
checkPermission: vi
|
|
169
|
+
checkPermission: vi
|
|
170
|
+
.fn()
|
|
171
|
+
.mockReturnValue(makeCheckResult({ state: "deny" })),
|
|
263
172
|
},
|
|
264
173
|
toolRegistry: {
|
|
265
174
|
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
@@ -282,7 +191,9 @@ describe("handleToolCall — bash external-directory gate", () => {
|
|
|
282
191
|
it("blocks a bash command referencing an external path when policy is deny", async () => {
|
|
283
192
|
const { handler } = makeHandler({
|
|
284
193
|
session: {
|
|
285
|
-
checkPermission: vi
|
|
194
|
+
checkPermission: vi
|
|
195
|
+
.fn()
|
|
196
|
+
.mockReturnValue(makeCheckResult({ state: "deny" })),
|
|
286
197
|
},
|
|
287
198
|
toolRegistry: {
|
|
288
199
|
getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
@@ -308,9 +219,9 @@ describe("handleToolCall — path gate (tools)", () => {
|
|
|
308
219
|
.mockImplementation(
|
|
309
220
|
(surface: string, _input: unknown, _agentName?: string) => {
|
|
310
221
|
if (surface === "path") {
|
|
311
|
-
return {
|
|
222
|
+
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
312
223
|
}
|
|
313
|
-
return
|
|
224
|
+
return makeCheckResult();
|
|
314
225
|
},
|
|
315
226
|
);
|
|
316
227
|
const { handler } = makeHandler({
|
|
@@ -355,9 +266,9 @@ describe("handleToolCall — bash path gate", () => {
|
|
|
355
266
|
.mockImplementation(
|
|
356
267
|
(surface: string, _input: unknown, _agentName?: string) => {
|
|
357
268
|
if (surface === "path") {
|
|
358
|
-
return {
|
|
269
|
+
return makeCheckResult({ state: "deny", matchedPattern: "*.env" });
|
|
359
270
|
}
|
|
360
|
-
return
|
|
271
|
+
return makeCheckResult();
|
|
361
272
|
},
|
|
362
273
|
);
|
|
363
274
|
const { handler } = makeHandler({
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared gate-level test fixtures for gate descriptor and runner tests.
|
|
3
|
+
*/
|
|
4
|
+
import { vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GateDescriptor,
|
|
8
|
+
GateRunnerDeps,
|
|
9
|
+
} from "#src/handlers/gates/descriptor";
|
|
10
|
+
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
11
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
12
|
+
|
|
13
|
+
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gate descriptor factory with runner-test defaults.
|
|
17
|
+
*
|
|
18
|
+
* Uses deny as the default `denialContext` check result so tests that
|
|
19
|
+
* verify block paths don't need to override the surface check.
|
|
20
|
+
*/
|
|
21
|
+
export function makeDescriptor(
|
|
22
|
+
overrides: Partial<GateDescriptor> = {},
|
|
23
|
+
): GateDescriptor {
|
|
24
|
+
return {
|
|
25
|
+
surface: "read",
|
|
26
|
+
input: {},
|
|
27
|
+
denialContext: {
|
|
28
|
+
kind: "tool",
|
|
29
|
+
check: makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
30
|
+
},
|
|
31
|
+
promptDetails: {
|
|
32
|
+
source: "tool_call",
|
|
33
|
+
agentName: null,
|
|
34
|
+
message: "Allow tool 'read'?",
|
|
35
|
+
toolCallId: "tc-1",
|
|
36
|
+
toolName: "read",
|
|
37
|
+
},
|
|
38
|
+
logContext: {
|
|
39
|
+
source: "tool_call",
|
|
40
|
+
toolCallId: "tc-1",
|
|
41
|
+
toolName: "read",
|
|
42
|
+
},
|
|
43
|
+
decision: {
|
|
44
|
+
surface: "read",
|
|
45
|
+
value: "read",
|
|
46
|
+
},
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function makeRunnerDeps(
|
|
52
|
+
overrides: Partial<GateRunnerDeps> = {},
|
|
53
|
+
): GateRunnerDeps {
|
|
54
|
+
return {
|
|
55
|
+
checkPermission: vi
|
|
56
|
+
.fn()
|
|
57
|
+
.mockReturnValue(makeCheckResult({ matchedPattern: "*" })),
|
|
58
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
59
|
+
recordSessionApproval: vi.fn(),
|
|
60
|
+
writeReviewLog: vi.fn(),
|
|
61
|
+
emitDecision: vi.fn(),
|
|
62
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
63
|
+
promptPermission: vi
|
|
64
|
+
.fn()
|
|
65
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
66
|
+
...overrides,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tool-call context factory with bash defaults.
|
|
72
|
+
*
|
|
73
|
+
* path.test.ts uses different defaults (toolName "read", path input) and
|
|
74
|
+
* keeps a local wrapper; bash-path.test.ts uses this factory directly.
|
|
75
|
+
*/
|
|
76
|
+
export function makeTcc(
|
|
77
|
+
overrides: Partial<ToolCallContext> = {},
|
|
78
|
+
): ToolCallContext {
|
|
79
|
+
return {
|
|
80
|
+
toolName: "bash",
|
|
81
|
+
agentName: null,
|
|
82
|
+
input: { command: "cat .env" },
|
|
83
|
+
toolCallId: "tc-1",
|
|
84
|
+
cwd: "/test/project",
|
|
85
|
+
...overrides,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Path-surface check result factory.
|
|
91
|
+
*
|
|
92
|
+
* Shared between bash-path.test.ts and path.test.ts; both use
|
|
93
|
+
* toolName "path", source "special", origin "global" as defaults.
|
|
94
|
+
*/
|
|
95
|
+
export function makeGateCheckResult(
|
|
96
|
+
overrides: Partial<PermissionCheckResult> = {},
|
|
97
|
+
): PermissionCheckResult {
|
|
98
|
+
return {
|
|
99
|
+
toolName: "path",
|
|
100
|
+
state: "allow",
|
|
101
|
+
source: "special",
|
|
102
|
+
origin: "global",
|
|
103
|
+
...overrides,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handler-level test fixtures for PermissionGateHandler tests.
|
|
3
|
+
*
|
|
4
|
+
* All factories use override bags so callers can specialize any field
|
|
5
|
+
* without constructing the full object from scratch.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { vi } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
11
|
+
import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
|
|
12
|
+
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
13
|
+
import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
|
|
14
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
15
|
+
import type { ToolRegistry } from "#src/tool-registry";
|
|
16
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
17
|
+
|
|
18
|
+
export function makeEvents() {
|
|
19
|
+
return {
|
|
20
|
+
emit: vi.fn(),
|
|
21
|
+
on: vi.fn().mockReturnValue(() => undefined),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function makeCtx(
|
|
26
|
+
overrides: Partial<ExtensionContext> = {},
|
|
27
|
+
): ExtensionContext {
|
|
28
|
+
return {
|
|
29
|
+
cwd: "/test/project",
|
|
30
|
+
hasUI: true,
|
|
31
|
+
ui: {
|
|
32
|
+
setStatus: vi.fn(),
|
|
33
|
+
notify: vi.fn(),
|
|
34
|
+
select: vi.fn(),
|
|
35
|
+
input: vi.fn(),
|
|
36
|
+
},
|
|
37
|
+
sessionManager: {
|
|
38
|
+
getEntries: vi.fn().mockReturnValue([]),
|
|
39
|
+
getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
|
|
40
|
+
addEntry: vi.fn(),
|
|
41
|
+
},
|
|
42
|
+
...overrides,
|
|
43
|
+
} as unknown as ExtensionContext;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function makeToolCallEvent(
|
|
47
|
+
toolName: string,
|
|
48
|
+
extraFields: Record<string, unknown> = {},
|
|
49
|
+
) {
|
|
50
|
+
return {
|
|
51
|
+
type: "tool_call",
|
|
52
|
+
toolCallId: "tc-1",
|
|
53
|
+
name: toolName,
|
|
54
|
+
input: {},
|
|
55
|
+
...extraFields,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Neutral-default check-result builder.
|
|
61
|
+
*
|
|
62
|
+
* Pass exactly the fields the original fixture hard-coded so divergent
|
|
63
|
+
* defaults across test files are preserved at their call sites.
|
|
64
|
+
*/
|
|
65
|
+
export function makeCheckResult(
|
|
66
|
+
overrides: Partial<PermissionCheckResult> = {},
|
|
67
|
+
): PermissionCheckResult {
|
|
68
|
+
return {
|
|
69
|
+
state: "allow",
|
|
70
|
+
toolName: "read",
|
|
71
|
+
source: "tool",
|
|
72
|
+
origin: "builtin",
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Full-union session stub.
|
|
79
|
+
*
|
|
80
|
+
* Includes every method mocked across handler test files so each file
|
|
81
|
+
* only needs to override the fields that differ from the defaults.
|
|
82
|
+
*/
|
|
83
|
+
export function makeSession(
|
|
84
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
85
|
+
): PermissionSession {
|
|
86
|
+
return {
|
|
87
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
88
|
+
activate: vi.fn(),
|
|
89
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
90
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult()),
|
|
91
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
92
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
93
|
+
recordSessionApproval: vi.fn(),
|
|
94
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
95
|
+
getInfrastructureDirs: vi
|
|
96
|
+
.fn()
|
|
97
|
+
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
98
|
+
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
99
|
+
config: DEFAULT_EXTENSION_CONFIG,
|
|
100
|
+
canPrompt: vi.fn().mockReturnValue(true),
|
|
101
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
102
|
+
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
103
|
+
...overrides,
|
|
104
|
+
} as unknown as PermissionSession;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function makeToolRegistry(
|
|
108
|
+
overrides: Partial<ToolRegistry> = {},
|
|
109
|
+
): ToolRegistry {
|
|
110
|
+
return {
|
|
111
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
112
|
+
setActive: vi.fn(),
|
|
113
|
+
...overrides,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Constructs a PermissionGateHandler with mocked collaborators.
|
|
119
|
+
*
|
|
120
|
+
* Returns all collaborators so each test file can destructure only what
|
|
121
|
+
* it needs — handler, events, session, and toolRegistry are all available.
|
|
122
|
+
*/
|
|
123
|
+
export function makeHandler(overrides?: {
|
|
124
|
+
session?: Partial<Record<keyof PermissionSession, unknown>>;
|
|
125
|
+
toolRegistry?: Partial<ToolRegistry>;
|
|
126
|
+
}) {
|
|
127
|
+
const session = makeSession(overrides?.session);
|
|
128
|
+
const events = makeEvents();
|
|
129
|
+
const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
|
|
130
|
+
const handler = new PermissionGateHandler(session, events, toolRegistry);
|
|
131
|
+
return { handler, events, session, toolRegistry };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Extract all permissions:decision payloads from the events.emit mock. */
|
|
135
|
+
export function getDecisionEvents(
|
|
136
|
+
events: ReturnType<typeof makeEvents>,
|
|
137
|
+
): PermissionDecisionEvent[] {
|
|
138
|
+
return events.emit.mock.calls
|
|
139
|
+
.filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
|
|
140
|
+
.map(([, payload]) => payload as PermissionDecisionEvent);
|
|
141
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-backed PermissionManager harness for integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Writes a real config file and agents directory to a temp directory so
|
|
5
|
+
* PermissionManager can load them without mocking the file system.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { PermissionManager } from "#src/permission-manager";
|
|
12
|
+
import type { ScopeConfig } from "#src/types";
|
|
13
|
+
|
|
14
|
+
export type CreateManagerOptions = {
|
|
15
|
+
mcpServerNames?: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createManager(
|
|
19
|
+
config: ScopeConfig,
|
|
20
|
+
agentFiles: Record<string, string> = {},
|
|
21
|
+
options: CreateManagerOptions = {},
|
|
22
|
+
) {
|
|
23
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-test-"));
|
|
24
|
+
const globalConfigPath = join(baseDir, "pi-permissions.jsonc");
|
|
25
|
+
const agentsDir = join(baseDir, "agents");
|
|
26
|
+
|
|
27
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
28
|
+
writeFileSync(
|
|
29
|
+
globalConfigPath,
|
|
30
|
+
`${JSON.stringify(config, null, 2)}\n`,
|
|
31
|
+
"utf8",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
for (const [name, content] of Object.entries(agentFiles)) {
|
|
35
|
+
writeFileSync(join(agentsDir, `${name}.md`), content, "utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manager = new PermissionManager({
|
|
39
|
+
globalConfigPath,
|
|
40
|
+
agentsDir,
|
|
41
|
+
mcpServerNames: options.mcpServerNames,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
manager,
|
|
46
|
+
globalConfigPath,
|
|
47
|
+
cleanup: (): void => {
|
|
48
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { SessionApproval } from "#src/session-approval";
|
|
40
40
|
import type { SessionLogger } from "#src/session-logger";
|
|
41
41
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
42
|
+
import { makeCtx } from "#test/helpers/handler-fixtures";
|
|
42
43
|
|
|
43
44
|
function makeSkillEntry(
|
|
44
45
|
name: string,
|
|
@@ -94,25 +95,6 @@ function makeForwarding(): ForwardingController {
|
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
98
|
-
return {
|
|
99
|
-
cwd: "/test/project",
|
|
100
|
-
hasUI: true,
|
|
101
|
-
ui: {
|
|
102
|
-
setStatus: vi.fn(),
|
|
103
|
-
notify: vi.fn(),
|
|
104
|
-
select: vi.fn(),
|
|
105
|
-
input: vi.fn(),
|
|
106
|
-
},
|
|
107
|
-
sessionManager: {
|
|
108
|
-
getEntries: vi.fn().mockReturnValue([]),
|
|
109
|
-
getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
|
|
110
|
-
addEntry: vi.fn(),
|
|
111
|
-
},
|
|
112
|
-
...overrides,
|
|
113
|
-
} as unknown as ExtensionContext;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
98
|
function makePermissionManager(
|
|
117
99
|
overrides: Partial<PermissionManager> = {},
|
|
118
100
|
): PermissionManager {
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { expect, test } from "vitest";
|
|
12
|
-
|
|
13
12
|
import {
|
|
14
13
|
createActiveToolsCacheKey,
|
|
15
14
|
createBeforeAgentStartPromptStateKey,
|
|
@@ -47,45 +46,10 @@ import {
|
|
|
47
46
|
canResolveAskPermissionRequest,
|
|
48
47
|
shouldAutoApprovePermissionState,
|
|
49
48
|
} from "#src/yolo-mode";
|
|
50
|
-
|
|
51
|
-
type CreateManagerOptions
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function createManager(
|
|
56
|
-
config: ScopeConfig,
|
|
57
|
-
agentFiles: Record<string, string> = {},
|
|
58
|
-
options: CreateManagerOptions = {},
|
|
59
|
-
) {
|
|
60
|
-
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-test-"));
|
|
61
|
-
const globalConfigPath = join(baseDir, "pi-permissions.jsonc");
|
|
62
|
-
const agentsDir = join(baseDir, "agents");
|
|
63
|
-
|
|
64
|
-
mkdirSync(agentsDir, { recursive: true });
|
|
65
|
-
writeFileSync(
|
|
66
|
-
globalConfigPath,
|
|
67
|
-
`${JSON.stringify(config, null, 2)}\n`,
|
|
68
|
-
"utf8",
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
for (const [name, content] of Object.entries(agentFiles)) {
|
|
72
|
-
writeFileSync(join(agentsDir, `${name}.md`), content, "utf8");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const manager = new PermissionManager({
|
|
76
|
-
globalConfigPath,
|
|
77
|
-
agentsDir,
|
|
78
|
-
mcpServerNames: options.mcpServerNames,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
manager,
|
|
83
|
-
globalConfigPath,
|
|
84
|
-
cleanup: (): void => {
|
|
85
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
49
|
+
import {
|
|
50
|
+
type CreateManagerOptions,
|
|
51
|
+
createManager,
|
|
52
|
+
} from "#test/helpers/manager-harness";
|
|
89
53
|
|
|
90
54
|
type MockHandler = (
|
|
91
55
|
event: Record<string, unknown>,
|