@gotgenes/pi-permission-system 10.3.1 → 10.5.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 +24 -0
- package/package.json +1 -1
- package/src/config-modal.ts +10 -8
- package/src/config-store.ts +6 -11
- package/src/forwarded-permissions/io.ts +16 -22
- package/src/forwarded-permissions/permission-forwarder.ts +16 -19
- package/src/gate-prompter.ts +1 -3
- package/src/handlers/gates/bash-command.ts +2 -2
- package/src/handlers/gates/bash-external-directory.ts +2 -2
- package/src/handlers/gates/bash-path.ts +2 -2
- package/src/handlers/gates/path.ts +2 -2
- package/src/handlers/gates/runner.ts +3 -3
- package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
- package/src/index.ts +27 -41
- package/src/permission-event-rpc.ts +19 -15
- package/src/permission-prompter.ts +4 -3
- package/src/permission-resolver.ts +69 -2
- package/src/permission-session.ts +7 -83
- package/src/prompting-gateway.ts +104 -0
- package/src/session-logger.ts +17 -3
- package/test/config-modal.test.ts +13 -7
- package/test/config-store.test.ts +7 -9
- package/test/forwarded-permissions/io.test.ts +23 -26
- package/test/handlers/external-directory-integration.test.ts +45 -32
- package/test/handlers/external-directory-session-dedup.test.ts +47 -57
- package/test/handlers/gates/bash-external-directory.test.ts +2 -2
- package/test/handlers/gates/bash-path.test.ts +2 -2
- package/test/handlers/gates/runner.test.ts +10 -16
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
- package/test/handlers/input-events.test.ts +19 -4
- package/test/handlers/input.test.ts +29 -13
- package/test/handlers/tool-call-events.test.ts +23 -5
- package/test/helpers/gate-fixtures.ts +11 -15
- package/test/helpers/handler-fixtures.ts +31 -50
- package/test/permission-event-rpc.test.ts +30 -28
- package/test/permission-forwarder.test.ts +6 -5
- package/test/permission-prompter.test.ts +28 -28
- package/test/permission-resolver.test.ts +194 -0
- package/test/permission-session.test.ts +27 -180
- package/test/prompting-gateway.test.ts +230 -0
|
@@ -81,9 +81,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
81
81
|
it("returns allow and emits user_approved when ask + user approves", async () => {
|
|
82
82
|
const { runner, deps } = makeGateRunner({
|
|
83
83
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
84
|
-
|
|
85
|
-
.fn()
|
|
86
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
84
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
87
85
|
});
|
|
88
86
|
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
89
87
|
expect(result).toEqual({ action: "allow" });
|
|
@@ -98,7 +96,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
98
96
|
it("returns allow, emits user_approved_for_session, and records session rule on approved_for_session", async () => {
|
|
99
97
|
const { runner, deps } = makeGateRunner({
|
|
100
98
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
101
|
-
|
|
99
|
+
prompt: vi
|
|
102
100
|
.fn()
|
|
103
101
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
104
102
|
});
|
|
@@ -120,7 +118,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
120
118
|
it("calls recordSessionApproval once with the full SessionApproval when sessionApproval has multiple patterns", async () => {
|
|
121
119
|
const { runner, deps } = makeGateRunner({
|
|
122
120
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
123
|
-
|
|
121
|
+
prompt: vi
|
|
124
122
|
.fn()
|
|
125
123
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
126
124
|
});
|
|
@@ -138,9 +136,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
138
136
|
it("returns block and emits user_denied when ask + user denies", async () => {
|
|
139
137
|
const { runner, deps } = makeGateRunner({
|
|
140
138
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
141
|
-
|
|
142
|
-
.fn()
|
|
143
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
139
|
+
prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
|
|
144
140
|
});
|
|
145
141
|
const result = await runner.run(makeDescriptor(), null, "tc-1");
|
|
146
142
|
expect(result).toMatchObject({ action: "block" });
|
|
@@ -170,7 +166,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
170
166
|
it("emits auto_approved resolution when decision has autoApproved flag", async () => {
|
|
171
167
|
const { runner, deps } = makeGateRunner({
|
|
172
168
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
173
|
-
|
|
169
|
+
prompt: vi.fn().mockResolvedValue({
|
|
174
170
|
approved: true,
|
|
175
171
|
state: "approved",
|
|
176
172
|
autoApproved: true,
|
|
@@ -227,12 +223,12 @@ describe("GateRunner — descriptor path", () => {
|
|
|
227
223
|
);
|
|
228
224
|
});
|
|
229
225
|
|
|
230
|
-
it("passes requestId from toolCallId to
|
|
226
|
+
it("passes requestId from toolCallId to prompt", async () => {
|
|
231
227
|
const { runner, deps } = makeGateRunner({
|
|
232
228
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
233
229
|
});
|
|
234
230
|
await runner.run(makeDescriptor(), null, "tc-42");
|
|
235
|
-
expect(deps.
|
|
231
|
+
expect(deps.prompt).toHaveBeenCalledWith(
|
|
236
232
|
expect.objectContaining({ requestId: "tc-42" }),
|
|
237
233
|
);
|
|
238
234
|
});
|
|
@@ -240,9 +236,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
240
236
|
it("does not call recordSessionApproval when user approves once (no sessionApproval)", async () => {
|
|
241
237
|
const { runner, deps } = makeGateRunner({
|
|
242
238
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
243
|
-
|
|
244
|
-
.fn()
|
|
245
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
239
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
246
240
|
});
|
|
247
241
|
await runner.run(makeDescriptor(), null, "tc-1");
|
|
248
242
|
expect(deps.recordSessionApproval).not.toHaveBeenCalled();
|
|
@@ -272,7 +266,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
272
266
|
it("does not call recordSessionApproval when user approves for session but no sessionApproval on descriptor", async () => {
|
|
273
267
|
const { runner, deps } = makeGateRunner({
|
|
274
268
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
275
|
-
|
|
269
|
+
prompt: vi
|
|
276
270
|
.fn()
|
|
277
271
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
278
272
|
});
|
|
@@ -323,7 +317,7 @@ describe("GateRunner — descriptor path", () => {
|
|
|
323
317
|
it("uses denialContext to format userDeniedReason with extension tag", async () => {
|
|
324
318
|
const { runner } = makeGateRunner({
|
|
325
319
|
resolveResult: makeCheckResult({ state: "ask", matchedPattern: "*" }),
|
|
326
|
-
|
|
320
|
+
prompt: vi.fn().mockResolvedValue({
|
|
327
321
|
approved: false,
|
|
328
322
|
state: "denied",
|
|
329
323
|
denialReason: "too risky",
|
|
@@ -40,9 +40,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
40
40
|
|
|
41
41
|
describe("evaluate — non-bash tool", () => {
|
|
42
42
|
it("returns allow when all gates pass", async () => {
|
|
43
|
+
const resolver = makeResolver(makeCheckResult());
|
|
43
44
|
const inputs = makeGateInputs();
|
|
44
|
-
const { runner } = makeGateRunner(
|
|
45
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
45
|
+
const { runner } = makeGateRunner();
|
|
46
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
46
47
|
|
|
47
48
|
const result = await pipeline.evaluate(
|
|
48
49
|
makeTcc({ toolName: "read", input: {} }),
|
|
@@ -53,12 +54,12 @@ describe("ToolCallGatePipeline", () => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
it("returns block when the tool gate denies", async () => {
|
|
56
|
-
const
|
|
57
|
+
const resolver = makeResolver(
|
|
57
58
|
makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
58
59
|
);
|
|
59
|
-
const inputs = makeGateInputs(
|
|
60
|
-
const { runner } = makeGateRunner(
|
|
61
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
60
|
+
const inputs = makeGateInputs();
|
|
61
|
+
const { runner } = makeGateRunner();
|
|
62
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
62
63
|
|
|
63
64
|
const result = await pipeline.evaluate(
|
|
64
65
|
makeTcc({ toolName: "read", input: {} }),
|
|
@@ -69,13 +70,14 @@ describe("ToolCallGatePipeline", () => {
|
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
it("short-circuits after the first blocking gate without evaluating later ones", async () => {
|
|
73
|
+
const resolver = makeResolver(makeCheckResult());
|
|
72
74
|
const inputs = makeGateInputs();
|
|
73
75
|
const { runner } = makeGateRunner();
|
|
74
76
|
const runSpy = vi
|
|
75
77
|
.spyOn(runner, "run")
|
|
76
78
|
.mockResolvedValue({ action: "block", reason: "first gate blocked" });
|
|
77
79
|
|
|
78
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
80
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
79
81
|
const result = await pipeline.evaluate(
|
|
80
82
|
makeTcc({ toolName: "read", input: {} }),
|
|
81
83
|
runner,
|
|
@@ -92,9 +94,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
92
94
|
toolTextSummaryMaxLength: 100,
|
|
93
95
|
toolInputLogPreviewMaxLength: 200,
|
|
94
96
|
}));
|
|
97
|
+
const resolver = makeResolver(makeCheckResult());
|
|
95
98
|
const inputs = makeGateInputs({ getToolPreviewLimits });
|
|
96
|
-
const { runner } = makeGateRunner(
|
|
97
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
99
|
+
const { runner } = makeGateRunner();
|
|
100
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
98
101
|
|
|
99
102
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
100
103
|
|
|
@@ -103,9 +106,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
103
106
|
|
|
104
107
|
it("calls getInfrastructureReadDirs() during evaluate", async () => {
|
|
105
108
|
const getInfrastructureReadDirs = vi.fn<() => string[]>(() => []);
|
|
109
|
+
const resolver = makeResolver(makeCheckResult());
|
|
106
110
|
const inputs = makeGateInputs({ getInfrastructureReadDirs });
|
|
107
|
-
const { runner } = makeGateRunner(
|
|
108
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
111
|
+
const { runner } = makeGateRunner();
|
|
112
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
109
113
|
|
|
110
114
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
111
115
|
|
|
@@ -114,9 +118,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
114
118
|
|
|
115
119
|
it("calls getActiveSkillEntries() during evaluate", async () => {
|
|
116
120
|
const getActiveSkillEntries = vi.fn<() => []>(() => []);
|
|
121
|
+
const resolver = makeResolver(makeCheckResult());
|
|
117
122
|
const inputs = makeGateInputs({ getActiveSkillEntries });
|
|
118
|
-
const { runner } = makeGateRunner(
|
|
119
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
123
|
+
const { runner } = makeGateRunner();
|
|
124
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
120
125
|
|
|
121
126
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
122
127
|
|
|
@@ -124,9 +129,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
124
129
|
});
|
|
125
130
|
|
|
126
131
|
it("does not call BashProgram.parse for non-bash tools", async () => {
|
|
132
|
+
const resolver = makeResolver(makeCheckResult());
|
|
127
133
|
const inputs = makeGateInputs();
|
|
128
|
-
const { runner } = makeGateRunner(
|
|
129
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
134
|
+
const { runner } = makeGateRunner();
|
|
135
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
130
136
|
|
|
131
137
|
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
132
138
|
|
|
@@ -138,9 +144,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
138
144
|
|
|
139
145
|
describe("evaluate — bash tool", () => {
|
|
140
146
|
it("returns allow when the bash command is permitted", async () => {
|
|
147
|
+
const resolver = makeResolver(makeCheckResult());
|
|
141
148
|
const inputs = makeGateInputs();
|
|
142
|
-
const { runner } = makeGateRunner(
|
|
143
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
149
|
+
const { runner } = makeGateRunner();
|
|
150
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
144
151
|
|
|
145
152
|
const result = await pipeline.evaluate(
|
|
146
153
|
makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
|
|
@@ -151,9 +158,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
151
158
|
});
|
|
152
159
|
|
|
153
160
|
it("parses BashProgram exactly once per evaluate for bash tools with a command", async () => {
|
|
161
|
+
const resolver = makeResolver(makeCheckResult());
|
|
154
162
|
const inputs = makeGateInputs();
|
|
155
|
-
const { runner } = makeGateRunner(
|
|
156
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
163
|
+
const { runner } = makeGateRunner();
|
|
164
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
157
165
|
|
|
158
166
|
await pipeline.evaluate(
|
|
159
167
|
makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
|
|
@@ -165,9 +173,10 @@ describe("ToolCallGatePipeline", () => {
|
|
|
165
173
|
});
|
|
166
174
|
|
|
167
175
|
it("does not parse BashProgram when the bash command is empty", async () => {
|
|
176
|
+
const resolver = makeResolver(makeCheckResult());
|
|
168
177
|
const inputs = makeGateInputs();
|
|
169
|
-
const { runner } = makeGateRunner(
|
|
170
|
-
const pipeline = new ToolCallGatePipeline(inputs);
|
|
178
|
+
const { runner } = makeGateRunner();
|
|
179
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
171
180
|
|
|
172
181
|
await pipeline.evaluate(
|
|
173
182
|
makeTcc({ toolName: "bash", input: { command: "" } }),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
6
7
|
import {
|
|
7
8
|
getDecisionEvents,
|
|
8
9
|
makeCheckResult,
|
|
@@ -70,8 +71,11 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
70
71
|
const { handler, events } = makeHandler({
|
|
71
72
|
session: {
|
|
72
73
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
74
|
+
},
|
|
75
|
+
prompter: {
|
|
76
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
73
77
|
prompt: vi
|
|
74
|
-
.fn()
|
|
78
|
+
.fn<GatePrompter["prompt"]>()
|
|
75
79
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
76
80
|
},
|
|
77
81
|
});
|
|
@@ -91,7 +95,12 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
91
95
|
const { handler, events } = makeHandler({
|
|
92
96
|
session: {
|
|
93
97
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
94
|
-
|
|
98
|
+
},
|
|
99
|
+
prompter: {
|
|
100
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
101
|
+
prompt: vi
|
|
102
|
+
.fn<GatePrompter["prompt"]>()
|
|
103
|
+
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
95
104
|
},
|
|
96
105
|
});
|
|
97
106
|
await handler.handleInput({ text: "/skill:explorer" }, makeCtx());
|
|
@@ -110,7 +119,10 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
110
119
|
const { handler, events } = makeHandler({
|
|
111
120
|
session: {
|
|
112
121
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
113
|
-
|
|
122
|
+
},
|
|
123
|
+
prompter: {
|
|
124
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
125
|
+
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
114
126
|
},
|
|
115
127
|
});
|
|
116
128
|
await handler.handleInput(
|
|
@@ -132,7 +144,10 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
132
144
|
const { handler, events } = makeHandler({
|
|
133
145
|
session: {
|
|
134
146
|
checkPermission: makeSkillCheckPermission("ask"),
|
|
135
|
-
|
|
147
|
+
},
|
|
148
|
+
prompter: {
|
|
149
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
150
|
+
prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
|
|
136
151
|
approved: true,
|
|
137
152
|
state: "approved",
|
|
138
153
|
autoApproved: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
2
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
3
3
|
import { extractSkillNameFromInput } from "#src/handlers/permission-gate-handler";
|
|
4
4
|
|
|
5
5
|
import { makeCtx, makeHandler } from "#test/helpers/handler-fixtures";
|
|
@@ -120,7 +120,10 @@ describe("handleInput", () => {
|
|
|
120
120
|
const { handler } = makeHandler({
|
|
121
121
|
session: {
|
|
122
122
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
123
|
-
|
|
123
|
+
},
|
|
124
|
+
prompter: {
|
|
125
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
126
|
+
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
124
127
|
},
|
|
125
128
|
});
|
|
126
129
|
const result = await handler.handleInput(
|
|
@@ -131,12 +134,16 @@ describe("handleInput", () => {
|
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
it("prompts and returns continue when skill ask is approved", async () => {
|
|
134
|
-
const
|
|
137
|
+
const approvePrompt = vi
|
|
138
|
+
.fn<GatePrompter["prompt"]>()
|
|
139
|
+
.mockResolvedValue({ approved: true, state: "approved" });
|
|
140
|
+
const { handler, prompter } = makeHandler({
|
|
135
141
|
session: {
|
|
136
142
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
},
|
|
144
|
+
prompter: {
|
|
145
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
146
|
+
prompt: approvePrompt,
|
|
140
147
|
},
|
|
141
148
|
});
|
|
142
149
|
const result = await handler.handleInput(
|
|
@@ -144,14 +151,19 @@ describe("handleInput", () => {
|
|
|
144
151
|
makeCtx(),
|
|
145
152
|
);
|
|
146
153
|
expect(result).toEqual({ action: "continue" });
|
|
147
|
-
expect(
|
|
154
|
+
expect(prompter.prompt).toHaveBeenCalledOnce();
|
|
148
155
|
});
|
|
149
156
|
|
|
150
157
|
it("returns handled when skill ask is denied by user", async () => {
|
|
151
158
|
const { handler } = makeHandler({
|
|
152
159
|
session: {
|
|
153
160
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
154
|
-
|
|
161
|
+
},
|
|
162
|
+
prompter: {
|
|
163
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
164
|
+
prompt: vi
|
|
165
|
+
.fn<GatePrompter["prompt"]>()
|
|
166
|
+
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
155
167
|
},
|
|
156
168
|
});
|
|
157
169
|
const result = await handler.handleInput(
|
|
@@ -162,17 +174,21 @@ describe("handleInput", () => {
|
|
|
162
174
|
});
|
|
163
175
|
|
|
164
176
|
it("passes agentName in the prompt permission request", async () => {
|
|
165
|
-
const
|
|
177
|
+
const approvePrompt = vi
|
|
178
|
+
.fn<GatePrompter["prompt"]>()
|
|
179
|
+
.mockResolvedValue({ approved: true, state: "approved" });
|
|
180
|
+
const { handler, prompter } = makeHandler({
|
|
166
181
|
session: {
|
|
167
182
|
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
168
183
|
resolveAgentName: vi.fn().mockReturnValue("code-agent"),
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
},
|
|
185
|
+
prompter: {
|
|
186
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
187
|
+
prompt: approvePrompt,
|
|
172
188
|
},
|
|
173
189
|
});
|
|
174
190
|
await handler.handleInput(makeInputEvent("/skill:librarian"), makeCtx());
|
|
175
|
-
expect(
|
|
191
|
+
expect(prompter.prompt).toHaveBeenCalledWith(
|
|
176
192
|
expect.objectContaining({
|
|
177
193
|
agentName: "code-agent",
|
|
178
194
|
skillName: "librarian",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
+
import type { GatePrompter } from "#src/gate-prompter";
|
|
7
8
|
import {
|
|
8
9
|
getDecisionEvents,
|
|
9
10
|
makeCheckResult,
|
|
@@ -110,8 +111,11 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
110
111
|
checkPermission: vi
|
|
111
112
|
.fn()
|
|
112
113
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
114
|
+
},
|
|
115
|
+
prompter: {
|
|
116
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
113
117
|
prompt: vi
|
|
114
|
-
.fn()
|
|
118
|
+
.fn<GatePrompter["prompt"]>()
|
|
115
119
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
116
120
|
},
|
|
117
121
|
});
|
|
@@ -132,7 +136,10 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
132
136
|
checkPermission: vi
|
|
133
137
|
.fn()
|
|
134
138
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
135
|
-
|
|
139
|
+
},
|
|
140
|
+
prompter: {
|
|
141
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
142
|
+
prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
|
|
136
143
|
approved: true,
|
|
137
144
|
state: "approved_for_session",
|
|
138
145
|
}),
|
|
@@ -159,7 +166,12 @@ describe("handleToolCall decision events — user_denied", () => {
|
|
|
159
166
|
checkPermission: vi
|
|
160
167
|
.fn()
|
|
161
168
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
162
|
-
|
|
169
|
+
},
|
|
170
|
+
prompter: {
|
|
171
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
172
|
+
prompt: vi
|
|
173
|
+
.fn<GatePrompter["prompt"]>()
|
|
174
|
+
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
163
175
|
},
|
|
164
176
|
});
|
|
165
177
|
|
|
@@ -183,7 +195,10 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
|
|
|
183
195
|
checkPermission: vi
|
|
184
196
|
.fn()
|
|
185
197
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
186
|
-
|
|
198
|
+
},
|
|
199
|
+
prompter: {
|
|
200
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
201
|
+
prompt: vi.fn<GatePrompter["prompt"]>(),
|
|
187
202
|
},
|
|
188
203
|
});
|
|
189
204
|
|
|
@@ -239,7 +254,10 @@ describe("handleToolCall decision events — auto_approved", () => {
|
|
|
239
254
|
checkPermission: vi
|
|
240
255
|
.fn()
|
|
241
256
|
.mockReturnValue(makeCheckResult({ state: "ask" })),
|
|
242
|
-
|
|
257
|
+
},
|
|
258
|
+
prompter: {
|
|
259
|
+
canConfirm: vi.fn().mockReturnValue(true),
|
|
260
|
+
prompt: vi.fn<GatePrompter["prompt"]>().mockResolvedValue({
|
|
243
261
|
approved: true,
|
|
244
262
|
state: "approved",
|
|
245
263
|
autoApproved: true,
|
|
@@ -10,7 +10,7 @@ import { GateRunner } from "#src/handlers/gates/runner";
|
|
|
10
10
|
import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
|
|
11
11
|
import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
12
12
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
13
|
-
import type {
|
|
13
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
14
14
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
15
15
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
16
16
|
import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
|
|
@@ -25,7 +25,7 @@ import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
|
25
25
|
* mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
|
|
26
26
|
*/
|
|
27
27
|
export function makeResolver(defaultCheck?: PermissionCheckResult) {
|
|
28
|
-
const resolve = vi.fn<
|
|
28
|
+
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
29
29
|
if (defaultCheck) {
|
|
30
30
|
resolve.mockReturnValue(defaultCheck);
|
|
31
31
|
}
|
|
@@ -91,10 +91,10 @@ export function makeReporter(
|
|
|
91
91
|
export function makeGateRunner(
|
|
92
92
|
overrides: {
|
|
93
93
|
resolveResult?: PermissionCheckResult;
|
|
94
|
-
resolve?:
|
|
94
|
+
resolve?: ScopedPermissionResolver["resolve"];
|
|
95
95
|
recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
|
|
96
96
|
canConfirm?: GatePrompter["canConfirm"];
|
|
97
|
-
|
|
97
|
+
prompt?: GatePrompter["prompt"];
|
|
98
98
|
reporter?: Partial<DecisionReporter>;
|
|
99
99
|
} = {},
|
|
100
100
|
) {
|
|
@@ -102,7 +102,7 @@ export function makeGateRunner(
|
|
|
102
102
|
const resolve =
|
|
103
103
|
overrides.resolve ??
|
|
104
104
|
vi
|
|
105
|
-
.fn<
|
|
105
|
+
.fn<ScopedPermissionResolver["resolve"]>()
|
|
106
106
|
.mockReturnValue(
|
|
107
107
|
overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
|
|
108
108
|
);
|
|
@@ -112,15 +112,15 @@ export function makeGateRunner(
|
|
|
112
112
|
const canConfirm =
|
|
113
113
|
overrides.canConfirm ??
|
|
114
114
|
(vi.fn().mockReturnValue(true) as GatePrompter["canConfirm"]);
|
|
115
|
-
const
|
|
116
|
-
overrides.
|
|
115
|
+
const prompt =
|
|
116
|
+
overrides.prompt ??
|
|
117
117
|
vi
|
|
118
|
-
.fn<GatePrompter["
|
|
118
|
+
.fn<GatePrompter["prompt"]>()
|
|
119
119
|
.mockResolvedValue({ approved: true, state: "approved" });
|
|
120
120
|
const runner = new GateRunner(
|
|
121
121
|
{ resolve },
|
|
122
122
|
{ recordSessionApproval },
|
|
123
|
-
{ canConfirm,
|
|
123
|
+
{ canConfirm, prompt },
|
|
124
124
|
reporter,
|
|
125
125
|
);
|
|
126
126
|
return {
|
|
@@ -129,7 +129,7 @@ export function makeGateRunner(
|
|
|
129
129
|
resolve,
|
|
130
130
|
recordSessionApproval,
|
|
131
131
|
canConfirm,
|
|
132
|
-
|
|
132
|
+
prompt,
|
|
133
133
|
reporter,
|
|
134
134
|
},
|
|
135
135
|
};
|
|
@@ -204,7 +204,7 @@ export function makePathDispatchResolver(
|
|
|
204
204
|
byPath: Record<string, PermissionCheckResult>,
|
|
205
205
|
defaultResult: PermissionCheckResult,
|
|
206
206
|
) {
|
|
207
|
-
const resolve = vi.fn<
|
|
207
|
+
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
208
208
|
resolve.mockImplementation((_surface, input) => {
|
|
209
209
|
const path = (input as Record<string, unknown>).path;
|
|
210
210
|
if (typeof path === "string" && path in byPath) {
|
|
@@ -243,16 +243,12 @@ export function makeGateCheckResult(
|
|
|
243
243
|
*/
|
|
244
244
|
export function makeGateInputs(
|
|
245
245
|
overrides: {
|
|
246
|
-
resolve?: PermissionResolver["resolve"];
|
|
247
246
|
getActiveSkillEntries?: () => SkillPromptEntry[];
|
|
248
247
|
getInfrastructureReadDirs?: () => string[];
|
|
249
248
|
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
250
249
|
} = {},
|
|
251
250
|
): ToolCallGateInputs {
|
|
252
251
|
return {
|
|
253
|
-
resolve:
|
|
254
|
-
overrides.resolve ??
|
|
255
|
-
vi.fn<PermissionResolver["resolve"]>().mockReturnValue(makeCheckResult()),
|
|
256
252
|
getActiveSkillEntries:
|
|
257
253
|
overrides.getActiveSkillEntries ??
|
|
258
254
|
vi.fn<() => SkillPromptEntry[]>(() => []),
|