@gotgenes/pi-permission-system 5.5.0 → 5.5.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 +9 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/gates/bash-external-directory.ts +22 -24
- package/src/handlers/gates/external-directory.ts +32 -41
- package/src/handlers/gates/skill-read.ts +10 -12
- package/src/handlers/gates/tool.ts +20 -27
- package/src/handlers/gates/types.ts +75 -0
- package/src/handlers/input.ts +3 -3
- package/src/handlers/lifecycle.ts +21 -21
- package/src/handlers/tool-call.ts +77 -7
- package/src/handlers/types.ts +20 -7
- package/src/index.ts +6 -1
- package/src/runtime.ts +17 -9
- package/tests/handlers/before-agent-start.test.ts +17 -27
- package/tests/handlers/gates/bash-external-directory.test.ts +48 -105
- package/tests/handlers/gates/external-directory.test.ts +65 -140
- package/tests/handlers/gates/skill-read.test.ts +50 -65
- package/tests/handlers/gates/tool.test.ts +90 -334
- package/tests/handlers/input-events.test.ts +10 -21
- package/tests/handlers/input.test.ts +26 -43
- package/tests/handlers/lifecycle.test.ts +47 -66
- package/tests/handlers/tool-call-events.test.ts +29 -40
- package/tests/handlers/tool-call.test.ts +19 -30
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { evaluateBashExternalDirectoryGate } from "../../../src/handlers/gates/bash-external-directory";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import type {
|
|
5
|
+
BashExternalDirectoryGateDeps,
|
|
6
|
+
ToolCallContext,
|
|
7
|
+
} from "../../../src/handlers/gates/types";
|
|
7
8
|
import type { PermissionCheckResult } from "../../../src/types";
|
|
8
9
|
|
|
9
|
-
// ── helpers
|
|
10
|
+
// ── helpers ─────────────��───────────────────────────────────────────���──────
|
|
10
11
|
|
|
11
12
|
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
12
13
|
return {
|
|
@@ -32,91 +33,70 @@ function makeCheckResult(
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
function makeRuntime(
|
|
40
|
-
overrides: Record<string, unknown> = {},
|
|
41
|
-
): HandlerDeps["runtime"] {
|
|
36
|
+
function makeBashExtGateDeps(
|
|
37
|
+
overrides: Partial<BashExternalDirectoryGateDeps> = {},
|
|
38
|
+
): BashExternalDirectoryGateDeps {
|
|
42
39
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
47
|
-
},
|
|
48
|
-
sessionRules: {
|
|
49
|
-
approve: vi.fn(),
|
|
50
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
51
|
-
clear: vi.fn(),
|
|
52
|
-
},
|
|
40
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
41
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
42
|
+
approveSessionRule: vi.fn(),
|
|
53
43
|
writeReviewLog: vi.fn(),
|
|
54
|
-
|
|
55
|
-
} as unknown as HandlerDeps["runtime"];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
|
|
59
|
-
const { runtime: runtimeOverrides, events, ...rest } = overrides;
|
|
60
|
-
return {
|
|
61
|
-
runtime: makeRuntime(runtimeOverrides as Record<string, unknown>),
|
|
62
|
-
events: events ?? makeEvents(),
|
|
63
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
44
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
64
45
|
promptPermission: vi
|
|
65
46
|
.fn()
|
|
66
47
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
67
|
-
...
|
|
68
|
-
}
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
69
50
|
}
|
|
70
51
|
|
|
71
|
-
// ── tests
|
|
52
|
+
// ── tests ─────────────────────────────��───────────────────────────────���────
|
|
72
53
|
|
|
73
54
|
describe("evaluateBashExternalDirectoryGate", () => {
|
|
74
55
|
it("returns null when tool is not bash", async () => {
|
|
75
56
|
const tcc = makeTcc({ toolName: "read" });
|
|
76
|
-
const result = await evaluateBashExternalDirectoryGate(
|
|
57
|
+
const result = await evaluateBashExternalDirectoryGate(
|
|
58
|
+
tcc,
|
|
59
|
+
makeBashExtGateDeps(),
|
|
60
|
+
);
|
|
77
61
|
expect(result).toBeNull();
|
|
78
62
|
});
|
|
79
63
|
|
|
80
64
|
it("returns null when no CWD", async () => {
|
|
81
65
|
const tcc = makeTcc({ cwd: undefined });
|
|
82
|
-
const result = await evaluateBashExternalDirectoryGate(
|
|
66
|
+
const result = await evaluateBashExternalDirectoryGate(
|
|
67
|
+
tcc,
|
|
68
|
+
makeBashExtGateDeps(),
|
|
69
|
+
);
|
|
83
70
|
expect(result).toBeNull();
|
|
84
71
|
});
|
|
85
72
|
|
|
86
73
|
it("returns null when command has no external paths", async () => {
|
|
87
74
|
const tcc = makeTcc({ input: { command: "ls -la" } });
|
|
88
|
-
const result = await evaluateBashExternalDirectoryGate(
|
|
75
|
+
const result = await evaluateBashExternalDirectoryGate(
|
|
76
|
+
tcc,
|
|
77
|
+
makeBashExtGateDeps(),
|
|
78
|
+
);
|
|
89
79
|
expect(result).toBeNull();
|
|
90
80
|
});
|
|
91
81
|
|
|
92
82
|
it("returns null and logs when all external paths are session-covered", async () => {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
checkPermission: vi
|
|
98
|
-
.fn()
|
|
99
|
-
.mockReturnValue(makeCheckResult("allow", { source: "session" })),
|
|
100
|
-
},
|
|
101
|
-
writeReviewLog,
|
|
102
|
-
},
|
|
83
|
+
const deps = makeBashExtGateDeps({
|
|
84
|
+
checkPermission: vi
|
|
85
|
+
.fn()
|
|
86
|
+
.mockReturnValue(makeCheckResult("allow", { source: "session" })),
|
|
103
87
|
});
|
|
104
88
|
const tcc = makeTcc();
|
|
105
89
|
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
106
90
|
expect(result).toBeNull();
|
|
107
|
-
expect(writeReviewLog).toHaveBeenCalledWith(
|
|
91
|
+
expect(deps.writeReviewLog).toHaveBeenCalledWith(
|
|
108
92
|
"permission_request.session_approved",
|
|
109
93
|
expect.objectContaining({ resolution: "session_approved" }),
|
|
110
94
|
);
|
|
111
95
|
});
|
|
112
96
|
|
|
113
97
|
it("blocks when policy is deny", async () => {
|
|
114
|
-
const deps =
|
|
115
|
-
|
|
116
|
-
permissionManager: {
|
|
117
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
118
|
-
},
|
|
119
|
-
},
|
|
98
|
+
const deps = makeBashExtGateDeps({
|
|
99
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
120
100
|
});
|
|
121
101
|
const tcc = makeTcc();
|
|
122
102
|
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
@@ -124,18 +104,8 @@ describe("evaluateBashExternalDirectoryGate", () => {
|
|
|
124
104
|
});
|
|
125
105
|
|
|
126
106
|
it("allows without recording session rules when user approves once", async () => {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
130
|
-
clear: vi.fn(),
|
|
131
|
-
};
|
|
132
|
-
const deps = makeDeps({
|
|
133
|
-
runtime: {
|
|
134
|
-
permissionManager: {
|
|
135
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
136
|
-
},
|
|
137
|
-
sessionRules,
|
|
138
|
-
},
|
|
107
|
+
const deps = makeBashExtGateDeps({
|
|
108
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
139
109
|
promptPermission: vi
|
|
140
110
|
.fn()
|
|
141
111
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
@@ -143,22 +113,12 @@ describe("evaluateBashExternalDirectoryGate", () => {
|
|
|
143
113
|
const tcc = makeTcc();
|
|
144
114
|
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
145
115
|
expect(result).toEqual({ action: "allow" });
|
|
146
|
-
expect(
|
|
116
|
+
expect(deps.approveSessionRule).not.toHaveBeenCalled();
|
|
147
117
|
});
|
|
148
118
|
|
|
149
119
|
it("records one session rule per uncovered path on approved_for_session", async () => {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
153
|
-
clear: vi.fn(),
|
|
154
|
-
};
|
|
155
|
-
const deps = makeDeps({
|
|
156
|
-
runtime: {
|
|
157
|
-
permissionManager: {
|
|
158
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
159
|
-
},
|
|
160
|
-
sessionRules,
|
|
161
|
-
},
|
|
120
|
+
const deps = makeBashExtGateDeps({
|
|
121
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
162
122
|
promptPermission: vi
|
|
163
123
|
.fn()
|
|
164
124
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
@@ -172,20 +132,16 @@ describe("evaluateBashExternalDirectoryGate", () => {
|
|
|
172
132
|
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
173
133
|
expect(result).toEqual({ action: "allow" });
|
|
174
134
|
// Each uncovered path gets its own session rule
|
|
175
|
-
expect(
|
|
176
|
-
for (const call of (
|
|
177
|
-
.calls) {
|
|
135
|
+
expect(deps.approveSessionRule).toHaveBeenCalledTimes(2);
|
|
136
|
+
for (const call of (deps.approveSessionRule as ReturnType<typeof vi.fn>)
|
|
137
|
+
.mock.calls) {
|
|
178
138
|
expect(call[0]).toBe("external_directory");
|
|
179
139
|
}
|
|
180
140
|
});
|
|
181
141
|
|
|
182
142
|
it("blocks when user denies", async () => {
|
|
183
|
-
const deps =
|
|
184
|
-
|
|
185
|
-
permissionManager: {
|
|
186
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
187
|
-
},
|
|
188
|
-
},
|
|
143
|
+
const deps = makeBashExtGateDeps({
|
|
144
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
189
145
|
promptPermission: vi
|
|
190
146
|
.fn()
|
|
191
147
|
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
@@ -196,13 +152,9 @@ describe("evaluateBashExternalDirectoryGate", () => {
|
|
|
196
152
|
});
|
|
197
153
|
|
|
198
154
|
it("blocks when no UI available", async () => {
|
|
199
|
-
const deps =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
155
|
+
const deps = makeBashExtGateDeps({
|
|
156
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
157
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
206
158
|
});
|
|
207
159
|
const tcc = makeTcc();
|
|
208
160
|
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
@@ -210,8 +162,6 @@ describe("evaluateBashExternalDirectoryGate", () => {
|
|
|
210
162
|
});
|
|
211
163
|
|
|
212
164
|
it("only prompts about uncovered paths when some are session-covered", async () => {
|
|
213
|
-
// First call (for getRuleset path filter): session covers /outside/a.ts
|
|
214
|
-
// Second call (for config-level policy): returns ask
|
|
215
165
|
const checkPermission = vi
|
|
216
166
|
.fn()
|
|
217
167
|
.mockImplementation(
|
|
@@ -228,14 +178,7 @@ describe("evaluateBashExternalDirectoryGate", () => {
|
|
|
228
178
|
return makeCheckResult("ask");
|
|
229
179
|
},
|
|
230
180
|
);
|
|
231
|
-
const deps =
|
|
232
|
-
runtime: {
|
|
233
|
-
permissionManager: { checkPermission },
|
|
234
|
-
},
|
|
235
|
-
promptPermission: vi
|
|
236
|
-
.fn()
|
|
237
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
238
|
-
});
|
|
181
|
+
const deps = makeBashExtGateDeps({ checkPermission });
|
|
239
182
|
const tcc = makeTcc({
|
|
240
183
|
input: { command: "diff /outside/a.ts /outside/b.ts" },
|
|
241
184
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { evaluateExternalDirectoryGate } from "../../../src/handlers/gates/external-directory";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import type {
|
|
5
|
+
ExternalDirectoryGateDeps,
|
|
6
|
+
ToolCallContext,
|
|
7
|
+
} from "../../../src/handlers/gates/types";
|
|
7
8
|
import type { PermissionCheckResult } from "../../../src/types";
|
|
8
9
|
|
|
9
10
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -32,41 +33,24 @@ function makeCheckResult(
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
function makeRuntime(
|
|
40
|
-
overrides: Record<string, unknown> = {},
|
|
41
|
-
): HandlerDeps["runtime"] {
|
|
36
|
+
function makeExtDirGateDeps(
|
|
37
|
+
overrides: Partial<ExternalDirectoryGateDeps> = {},
|
|
38
|
+
): ExternalDirectoryGateDeps {
|
|
42
39
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
permissionManager: {
|
|
47
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
48
|
-
},
|
|
49
|
-
sessionRules: {
|
|
50
|
-
approve: vi.fn(),
|
|
51
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
52
|
-
clear: vi.fn(),
|
|
53
|
-
},
|
|
40
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
41
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
42
|
+
approveSessionRule: vi.fn(),
|
|
54
43
|
writeReviewLog: vi.fn(),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
|
|
60
|
-
const { runtime: runtimeOverrides, events, ...rest } = overrides;
|
|
61
|
-
return {
|
|
62
|
-
runtime: makeRuntime(runtimeOverrides as Record<string, unknown>),
|
|
63
|
-
events: events ?? makeEvents(),
|
|
64
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
44
|
+
emitDecision: vi.fn(),
|
|
45
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
65
46
|
promptPermission: vi
|
|
66
47
|
.fn()
|
|
67
48
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
68
|
-
|
|
69
|
-
|
|
49
|
+
getInfrastructureDirs: vi
|
|
50
|
+
.fn()
|
|
51
|
+
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
70
54
|
}
|
|
71
55
|
|
|
72
56
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
@@ -74,35 +58,42 @@ function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
|
|
|
74
58
|
describe("evaluateExternalDirectoryGate", () => {
|
|
75
59
|
it("returns null when no CWD", async () => {
|
|
76
60
|
const tcc = makeTcc({ cwd: undefined });
|
|
77
|
-
const result = await evaluateExternalDirectoryGate(
|
|
61
|
+
const result = await evaluateExternalDirectoryGate(
|
|
62
|
+
tcc,
|
|
63
|
+
makeExtDirGateDeps(),
|
|
64
|
+
);
|
|
78
65
|
expect(result).toBeNull();
|
|
79
66
|
});
|
|
80
67
|
|
|
81
68
|
it("returns null when tool is not path-bearing", async () => {
|
|
82
69
|
const tcc = makeTcc({ toolName: "bash", input: { command: "ls" } });
|
|
83
|
-
const result = await evaluateExternalDirectoryGate(
|
|
70
|
+
const result = await evaluateExternalDirectoryGate(
|
|
71
|
+
tcc,
|
|
72
|
+
makeExtDirGateDeps(),
|
|
73
|
+
);
|
|
84
74
|
expect(result).toBeNull();
|
|
85
75
|
});
|
|
86
76
|
|
|
87
77
|
it("returns null when path is inside CWD", async () => {
|
|
88
78
|
const tcc = makeTcc({ input: { path: "/test/project/src/index.ts" } });
|
|
89
|
-
const result = await evaluateExternalDirectoryGate(
|
|
79
|
+
const result = await evaluateExternalDirectoryGate(
|
|
80
|
+
tcc,
|
|
81
|
+
makeExtDirGateDeps(),
|
|
82
|
+
);
|
|
90
83
|
expect(result).toBeNull();
|
|
91
84
|
});
|
|
92
85
|
|
|
93
86
|
// ── Pi infrastructure read bypass ──────────────────────────────────────
|
|
94
87
|
|
|
95
88
|
it("allows and emits infrastructure_auto_allowed for read targeting infra dir", async () => {
|
|
96
|
-
const
|
|
97
|
-
const deps = makeDeps({ events });
|
|
89
|
+
const deps = makeExtDirGateDeps();
|
|
98
90
|
const tcc = makeTcc({
|
|
99
91
|
toolName: "read",
|
|
100
92
|
input: { path: "/test/agent/git/some-package/SKILL.md" },
|
|
101
93
|
});
|
|
102
94
|
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
103
95
|
expect(result).toEqual({ action: "allow" });
|
|
104
|
-
expect(
|
|
105
|
-
"permissions:decision",
|
|
96
|
+
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
106
97
|
expect.objectContaining({
|
|
107
98
|
resolution: "infrastructure_auto_allowed",
|
|
108
99
|
result: "allow",
|
|
@@ -111,18 +102,8 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
111
102
|
});
|
|
112
103
|
|
|
113
104
|
it("respects config.piInfrastructureReadPaths for bypass", async () => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
runtime: {
|
|
117
|
-
piInfrastructureDirs: [],
|
|
118
|
-
config: {
|
|
119
|
-
debugLog: false,
|
|
120
|
-
permissionReviewLog: true,
|
|
121
|
-
yoloMode: false,
|
|
122
|
-
piInfrastructureReadPaths: ["/custom/infra"],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
events,
|
|
105
|
+
const deps = makeExtDirGateDeps({
|
|
106
|
+
getInfrastructureDirs: vi.fn().mockReturnValue(["/custom/infra"]),
|
|
126
107
|
});
|
|
127
108
|
const tcc = makeTcc({
|
|
128
109
|
toolName: "read",
|
|
@@ -133,12 +114,8 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
133
114
|
});
|
|
134
115
|
|
|
135
116
|
it("does NOT bypass for write tools targeting infra dirs", async () => {
|
|
136
|
-
const deps =
|
|
137
|
-
|
|
138
|
-
permissionManager: {
|
|
139
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
140
|
-
},
|
|
141
|
-
},
|
|
117
|
+
const deps = makeExtDirGateDeps({
|
|
118
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
142
119
|
});
|
|
143
120
|
const tcc = makeTcc({
|
|
144
121
|
toolName: "write",
|
|
@@ -151,36 +128,25 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
151
128
|
// ── Session-rule hit ─────────────────────────────────────────────────────
|
|
152
129
|
|
|
153
130
|
it("allows and emits session_approved when session rule covers the path", async () => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
approve: vi.fn(),
|
|
167
|
-
getRuleset: vi.fn().mockReturnValue([
|
|
168
|
-
{
|
|
169
|
-
surface: "external_directory",
|
|
170
|
-
pattern: "/outside/project/*",
|
|
171
|
-
action: "allow",
|
|
172
|
-
},
|
|
173
|
-
]),
|
|
174
|
-
clear: vi.fn(),
|
|
131
|
+
const deps = makeExtDirGateDeps({
|
|
132
|
+
checkPermission: vi.fn().mockReturnValue(
|
|
133
|
+
makeCheckResult("allow", {
|
|
134
|
+
source: "session",
|
|
135
|
+
matchedPattern: "/outside/project/*",
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
getSessionRuleset: vi.fn().mockReturnValue([
|
|
139
|
+
{
|
|
140
|
+
surface: "external_directory",
|
|
141
|
+
pattern: "/outside/project/*",
|
|
142
|
+
action: "allow",
|
|
175
143
|
},
|
|
176
|
-
|
|
177
|
-
events,
|
|
144
|
+
]),
|
|
178
145
|
});
|
|
179
146
|
const tcc = makeTcc();
|
|
180
147
|
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
181
148
|
expect(result).toEqual({ action: "allow" });
|
|
182
|
-
expect(
|
|
183
|
-
"permissions:decision",
|
|
149
|
+
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
184
150
|
expect.objectContaining({
|
|
185
151
|
resolution: "session_approved",
|
|
186
152
|
matchedPattern: "/outside/project/*",
|
|
@@ -191,20 +157,13 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
191
157
|
// ── Policy deny ──────────────────────────────────────────────────────────
|
|
192
158
|
|
|
193
159
|
it("blocks and emits policy_deny when policy is deny", async () => {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
runtime: {
|
|
197
|
-
permissionManager: {
|
|
198
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
events,
|
|
160
|
+
const deps = makeExtDirGateDeps({
|
|
161
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
202
162
|
});
|
|
203
163
|
const tcc = makeTcc();
|
|
204
164
|
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
205
165
|
expect(result).toMatchObject({ action: "block" });
|
|
206
|
-
expect(
|
|
207
|
-
"permissions:decision",
|
|
166
|
+
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
208
167
|
expect.objectContaining({
|
|
209
168
|
surface: "external_directory",
|
|
210
169
|
result: "deny",
|
|
@@ -216,18 +175,8 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
216
175
|
// ── Policy ask — user approves once ──────────────────────────────────────
|
|
217
176
|
|
|
218
177
|
it("allows without recording session rule when user approves once", async () => {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
222
|
-
clear: vi.fn(),
|
|
223
|
-
};
|
|
224
|
-
const deps = makeDeps({
|
|
225
|
-
runtime: {
|
|
226
|
-
permissionManager: {
|
|
227
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
228
|
-
},
|
|
229
|
-
sessionRules,
|
|
230
|
-
},
|
|
178
|
+
const deps = makeExtDirGateDeps({
|
|
179
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
231
180
|
promptPermission: vi
|
|
232
181
|
.fn()
|
|
233
182
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
@@ -235,24 +184,14 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
235
184
|
const tcc = makeTcc();
|
|
236
185
|
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
237
186
|
expect(result).toEqual({ action: "allow" });
|
|
238
|
-
expect(
|
|
187
|
+
expect(deps.approveSessionRule).not.toHaveBeenCalled();
|
|
239
188
|
});
|
|
240
189
|
|
|
241
190
|
// ── Policy ask — user approves for session ───────────────────────────────
|
|
242
191
|
|
|
243
192
|
it("records session rule when user approves for session", async () => {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
247
|
-
clear: vi.fn(),
|
|
248
|
-
};
|
|
249
|
-
const deps = makeDeps({
|
|
250
|
-
runtime: {
|
|
251
|
-
permissionManager: {
|
|
252
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
253
|
-
},
|
|
254
|
-
sessionRules,
|
|
255
|
-
},
|
|
193
|
+
const deps = makeExtDirGateDeps({
|
|
194
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
256
195
|
promptPermission: vi
|
|
257
196
|
.fn()
|
|
258
197
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
@@ -260,7 +199,7 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
260
199
|
const tcc = makeTcc();
|
|
261
200
|
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
262
201
|
expect(result).toEqual({ action: "allow" });
|
|
263
|
-
expect(
|
|
202
|
+
expect(deps.approveSessionRule).toHaveBeenCalledWith(
|
|
264
203
|
"external_directory",
|
|
265
204
|
expect.any(String),
|
|
266
205
|
);
|
|
@@ -269,14 +208,8 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
269
208
|
// ── Policy ask — user denies ─────────────────────────────────────────────
|
|
270
209
|
|
|
271
210
|
it("blocks and emits user_denied when user denies", async () => {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
runtime: {
|
|
275
|
-
permissionManager: {
|
|
276
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
events,
|
|
211
|
+
const deps = makeExtDirGateDeps({
|
|
212
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
280
213
|
promptPermission: vi
|
|
281
214
|
.fn()
|
|
282
215
|
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
@@ -284,8 +217,7 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
284
217
|
const tcc = makeTcc();
|
|
285
218
|
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
286
219
|
expect(result).toMatchObject({ action: "block" });
|
|
287
|
-
expect(
|
|
288
|
-
"permissions:decision",
|
|
220
|
+
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
289
221
|
expect.objectContaining({
|
|
290
222
|
result: "deny",
|
|
291
223
|
resolution: "user_denied",
|
|
@@ -296,21 +228,14 @@ describe("evaluateExternalDirectoryGate", () => {
|
|
|
296
228
|
// ── Policy ask — no UI ───────────────────────────────────────────────────
|
|
297
229
|
|
|
298
230
|
it("blocks and emits confirmation_unavailable when no UI", async () => {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
permissionManager: {
|
|
303
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
events,
|
|
307
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
231
|
+
const deps = makeExtDirGateDeps({
|
|
232
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
233
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
308
234
|
});
|
|
309
235
|
const tcc = makeTcc();
|
|
310
236
|
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
311
237
|
expect(result).toMatchObject({ action: "block" });
|
|
312
|
-
expect(
|
|
313
|
-
"permissions:decision",
|
|
238
|
+
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
314
239
|
expect.objectContaining({
|
|
315
240
|
result: "deny",
|
|
316
241
|
resolution: "confirmation_unavailable",
|