@gotgenes/pi-permission-system 5.5.1 → 5.6.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 +23 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/handlers/gates/bash-external-directory.ts +70 -65
- package/src/handlers/gates/descriptor.ts +115 -0
- package/src/handlers/gates/external-directory.ts +63 -122
- package/src/handlers/gates/index.ts +12 -4
- package/src/handlers/gates/runner.ts +144 -0
- package/src/handlers/gates/skill-read.ts +37 -54
- package/src/handlers/gates/tool.ts +35 -97
- package/src/handlers/gates/types.ts +0 -77
- package/src/handlers/tool-call.ts +89 -58
- package/tests/handlers/gates/bash-external-directory.test.ts +128 -126
- package/tests/handlers/gates/external-directory.test.ts +117 -188
- package/tests/handlers/gates/runner.test.ts +361 -0
- package/tests/handlers/gates/skill-read.test.ts +87 -123
- package/tests/handlers/gates/tool.test.ts +119 -112
|
@@ -10,17 +10,14 @@ import {
|
|
|
10
10
|
checkRequestedToolRegistration,
|
|
11
11
|
getToolNameFromValue,
|
|
12
12
|
} from "../tool-registry";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ToolCallContext,
|
|
22
|
-
ToolGateDeps,
|
|
23
|
-
} from "./gates/types";
|
|
13
|
+
import { describeBashExternalDirectoryGate } from "./gates/bash-external-directory";
|
|
14
|
+
import type { GateRunnerDeps } from "./gates/descriptor";
|
|
15
|
+
import { isGateBypass } from "./gates/descriptor";
|
|
16
|
+
import { describeExternalDirectoryGate } from "./gates/external-directory";
|
|
17
|
+
import { runGateCheck } from "./gates/runner";
|
|
18
|
+
import { describeSkillReadGate } from "./gates/skill-read";
|
|
19
|
+
import { describeToolGate } from "./gates/tool";
|
|
20
|
+
import type { ToolCallContext } from "./gates/types";
|
|
24
21
|
import type { HandlerDeps, PromptPermissionDetails } from "./types";
|
|
25
22
|
|
|
26
23
|
/**
|
|
@@ -92,10 +89,10 @@ export async function handleToolCall(
|
|
|
92
89
|
const canConfirm = () => deps.canRequestPermissionConfirmation(ctx);
|
|
93
90
|
const promptPermission = (details: PromptPermissionDetails) =>
|
|
94
91
|
deps.promptPermission(ctx, details);
|
|
95
|
-
const emitDecision
|
|
92
|
+
const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
|
|
96
93
|
emitDecisionEvent(deps.events, e);
|
|
97
94
|
const { writeReviewLog } = deps;
|
|
98
|
-
const checkPermission:
|
|
95
|
+
const checkPermission: GateRunnerDeps["checkPermission"] = (
|
|
99
96
|
surface,
|
|
100
97
|
input,
|
|
101
98
|
agent,
|
|
@@ -111,21 +108,8 @@ export async function handleToolCall(
|
|
|
111
108
|
const approveSessionRule = (surface: string, pattern: string) =>
|
|
112
109
|
deps.session.sessionRules.approve(surface, pattern);
|
|
113
110
|
|
|
114
|
-
// ──
|
|
115
|
-
const
|
|
116
|
-
getActiveSkillEntries: () => deps.session.activeSkillEntries,
|
|
117
|
-
writeReviewLog,
|
|
118
|
-
emitDecision,
|
|
119
|
-
canConfirm,
|
|
120
|
-
promptPermission,
|
|
121
|
-
};
|
|
122
|
-
const skillResult = await evaluateSkillReadGate(tcc, skillReadGateDeps);
|
|
123
|
-
if (skillResult?.action === "block") {
|
|
124
|
-
return { block: true, reason: skillResult.reason };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── External-directory gate (file tools) ─────────────────────────────────
|
|
128
|
-
const extDirGateDeps: ExternalDirectoryGateDeps = {
|
|
111
|
+
// ── Shared runner deps (built once, reused for all gates) ─────────────
|
|
112
|
+
const runnerDeps: GateRunnerDeps = {
|
|
129
113
|
checkPermission,
|
|
130
114
|
getSessionRuleset,
|
|
131
115
|
approveSessionRule,
|
|
@@ -133,44 +117,91 @@ export async function handleToolCall(
|
|
|
133
117
|
emitDecision,
|
|
134
118
|
canConfirm,
|
|
135
119
|
promptPermission,
|
|
136
|
-
getInfrastructureDirs: () => [
|
|
137
|
-
...deps.piInfrastructureDirs,
|
|
138
|
-
...deps.getPiInfrastructureReadPaths(),
|
|
139
|
-
],
|
|
140
120
|
};
|
|
141
|
-
const extDirResult = await evaluateExternalDirectoryGate(tcc, extDirGateDeps);
|
|
142
|
-
if (extDirResult?.action === "block") {
|
|
143
|
-
return { block: true, reason: extDirResult.reason };
|
|
144
|
-
}
|
|
145
121
|
|
|
146
|
-
// ──
|
|
147
|
-
const
|
|
148
|
-
checkPermission,
|
|
149
|
-
getSessionRuleset,
|
|
150
|
-
approveSessionRule,
|
|
151
|
-
writeReviewLog,
|
|
152
|
-
canConfirm,
|
|
153
|
-
promptPermission,
|
|
154
|
-
};
|
|
155
|
-
const bashExtResult = await evaluateBashExternalDirectoryGate(
|
|
122
|
+
// ── Skill-read gate (descriptor + runner) ────────────────────────────────
|
|
123
|
+
const skillDescriptor = describeSkillReadGate(
|
|
156
124
|
tcc,
|
|
157
|
-
|
|
125
|
+
() => deps.session.activeSkillEntries,
|
|
158
126
|
);
|
|
159
|
-
if (
|
|
160
|
-
|
|
127
|
+
if (skillDescriptor) {
|
|
128
|
+
const skillResult = await runGateCheck(
|
|
129
|
+
skillDescriptor,
|
|
130
|
+
tcc.agentName,
|
|
131
|
+
tcc.toolCallId,
|
|
132
|
+
runnerDeps,
|
|
133
|
+
);
|
|
134
|
+
if (skillResult.action === "block") {
|
|
135
|
+
return { block: true, reason: skillResult.reason };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── External-directory gate (descriptor + runner) ─────────────────────────
|
|
140
|
+
const infraDirs = [
|
|
141
|
+
...deps.piInfrastructureDirs,
|
|
142
|
+
...deps.getPiInfrastructureReadPaths(),
|
|
143
|
+
];
|
|
144
|
+
const extDirDesc = describeExternalDirectoryGate(tcc, infraDirs);
|
|
145
|
+
if (extDirDesc) {
|
|
146
|
+
if (isGateBypass(extDirDesc)) {
|
|
147
|
+
if (extDirDesc.log) {
|
|
148
|
+
writeReviewLog(extDirDesc.log.event, extDirDesc.log.details);
|
|
149
|
+
}
|
|
150
|
+
if (extDirDesc.decision) {
|
|
151
|
+
emitDecision(extDirDesc.decision);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
const extDirResult = await runGateCheck(
|
|
155
|
+
extDirDesc,
|
|
156
|
+
tcc.agentName,
|
|
157
|
+
tcc.toolCallId,
|
|
158
|
+
runnerDeps,
|
|
159
|
+
);
|
|
160
|
+
if (extDirResult.action === "block") {
|
|
161
|
+
return { block: true, reason: extDirResult.reason };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
// ──
|
|
164
|
-
const
|
|
166
|
+
// ── Bash external-directory gate (descriptor + runner) ─────────────────────
|
|
167
|
+
const bashExtDesc = await describeBashExternalDirectoryGate(
|
|
168
|
+
tcc,
|
|
165
169
|
checkPermission,
|
|
166
170
|
getSessionRuleset,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
);
|
|
172
|
+
if (bashExtDesc) {
|
|
173
|
+
if (isGateBypass(bashExtDesc)) {
|
|
174
|
+
if (bashExtDesc.log) {
|
|
175
|
+
writeReviewLog(bashExtDesc.log.event, bashExtDesc.log.details);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const bashExtResult = await runGateCheck(
|
|
179
|
+
bashExtDesc,
|
|
180
|
+
tcc.agentName,
|
|
181
|
+
tcc.toolCallId,
|
|
182
|
+
runnerDeps,
|
|
183
|
+
);
|
|
184
|
+
if (bashExtResult.action === "block") {
|
|
185
|
+
return { block: true, reason: bashExtResult.reason };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Normal tool permission gate (descriptor + runner) ───────────────────────────
|
|
191
|
+
const toolCheck = checkPermission(
|
|
192
|
+
tcc.toolName,
|
|
193
|
+
tcc.input,
|
|
194
|
+
tcc.agentName ?? undefined,
|
|
195
|
+
getSessionRuleset(),
|
|
196
|
+
);
|
|
197
|
+
const toolDescriptor = describeToolGate(tcc, toolCheck);
|
|
198
|
+
toolDescriptor.preCheck = toolCheck;
|
|
199
|
+
const toolResult = await runGateCheck(
|
|
200
|
+
toolDescriptor,
|
|
201
|
+
tcc.agentName,
|
|
202
|
+
tcc.toolCallId,
|
|
203
|
+
runnerDeps,
|
|
204
|
+
);
|
|
174
205
|
if (toolResult.action === "block") {
|
|
175
206
|
return { block: true, reason: toolResult.reason };
|
|
176
207
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { evaluateBashExternalDirectoryGate } from "../../../src/handlers/gates/bash-external-directory";
|
|
2
|
+
import { describeBashExternalDirectoryGate } from "../../../src/handlers/gates/bash-external-directory";
|
|
4
3
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "../../../src/handlers/gates/
|
|
4
|
+
GateBypass,
|
|
5
|
+
GateDescriptor,
|
|
6
|
+
} from "../../../src/handlers/gates/descriptor";
|
|
7
|
+
import {
|
|
8
|
+
isGateBypass,
|
|
9
|
+
isGateDescriptor,
|
|
10
|
+
} from "../../../src/handlers/gates/descriptor";
|
|
11
|
+
import type { ToolCallContext } from "../../../src/handlers/gates/types";
|
|
8
12
|
import type { PermissionCheckResult } from "../../../src/types";
|
|
9
13
|
|
|
10
|
-
// ── helpers
|
|
14
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
11
15
|
|
|
12
16
|
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
13
17
|
return {
|
|
@@ -33,158 +37,156 @@ function makeCheckResult(
|
|
|
33
37
|
};
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
overrides: Partial<BashExternalDirectoryGateDeps> = {},
|
|
38
|
-
): BashExternalDirectoryGateDeps {
|
|
39
|
-
return {
|
|
40
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
41
|
-
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
42
|
-
approveSessionRule: vi.fn(),
|
|
43
|
-
writeReviewLog: vi.fn(),
|
|
44
|
-
canConfirm: vi.fn().mockReturnValue(true),
|
|
45
|
-
promptPermission: vi
|
|
46
|
-
.fn()
|
|
47
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
48
|
-
...overrides,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── tests ─────────────────────────────��───────────────────────────────���────
|
|
40
|
+
// ── tests ──────────────────────────────────────────────────────────────────
|
|
53
41
|
|
|
54
|
-
describe("
|
|
42
|
+
describe("describeBashExternalDirectoryGate", () => {
|
|
55
43
|
it("returns null when tool is not bash", async () => {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
const result = await describeBashExternalDirectoryGate(
|
|
45
|
+
makeTcc({ toolName: "read" }),
|
|
46
|
+
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
47
|
+
vi.fn().mockReturnValue([]),
|
|
60
48
|
);
|
|
61
49
|
expect(result).toBeNull();
|
|
62
50
|
});
|
|
63
51
|
|
|
64
52
|
it("returns null when no CWD", async () => {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
const result = await describeBashExternalDirectoryGate(
|
|
54
|
+
makeTcc({ cwd: undefined }),
|
|
55
|
+
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
56
|
+
vi.fn().mockReturnValue([]),
|
|
69
57
|
);
|
|
70
58
|
expect(result).toBeNull();
|
|
71
59
|
});
|
|
72
60
|
|
|
73
61
|
it("returns null when command has no external paths", async () => {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
const result = await describeBashExternalDirectoryGate(
|
|
63
|
+
makeTcc({ input: { command: "ls -la" } }),
|
|
64
|
+
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
65
|
+
vi.fn().mockReturnValue([]),
|
|
78
66
|
);
|
|
79
67
|
expect(result).toBeNull();
|
|
80
68
|
});
|
|
81
69
|
|
|
82
|
-
it("returns
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
it("returns GateBypass when all external paths are session-covered", async () => {
|
|
71
|
+
const checkPermission = vi
|
|
72
|
+
.fn()
|
|
73
|
+
.mockReturnValue(makeCheckResult("allow", { source: "session" }));
|
|
74
|
+
const result = await describeBashExternalDirectoryGate(
|
|
75
|
+
makeTcc(),
|
|
76
|
+
checkPermission,
|
|
77
|
+
vi.fn().mockReturnValue([]),
|
|
78
|
+
);
|
|
79
|
+
expect(result).not.toBeNull();
|
|
80
|
+
expect(isGateBypass(result)).toBe(true);
|
|
81
|
+
const bypass = result as GateBypass;
|
|
82
|
+
expect(bypass.action).toBe("allow");
|
|
83
|
+
expect(bypass.log).toMatchObject({
|
|
84
|
+
event: "permission_request.session_approved",
|
|
85
|
+
details: expect.objectContaining({ resolution: "session_approved" }),
|
|
87
86
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns GateDescriptor with multi-pattern sessionApproval for uncovered paths", async () => {
|
|
90
|
+
const checkPermission = vi.fn().mockReturnValue(makeCheckResult("ask"));
|
|
91
|
+
const result = await describeBashExternalDirectoryGate(
|
|
92
|
+
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
93
|
+
checkPermission,
|
|
94
|
+
vi.fn().mockReturnValue([]),
|
|
94
95
|
);
|
|
96
|
+
expect(isGateDescriptor(result)).toBe(true);
|
|
97
|
+
const desc = result as GateDescriptor;
|
|
98
|
+
expect(desc.sessionApproval).toBeDefined();
|
|
99
|
+
expect(desc.sessionApproval).toHaveProperty("patterns");
|
|
100
|
+
const patterns = (desc.sessionApproval as { patterns: string[] }).patterns;
|
|
101
|
+
expect(patterns.length).toBeGreaterThan(0);
|
|
95
102
|
});
|
|
96
103
|
|
|
97
|
-
it("
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
it("uses config-level checkPermission for the policy state", async () => {
|
|
105
|
+
const checkPermission = vi
|
|
106
|
+
.fn()
|
|
107
|
+
.mockImplementation((surface: string, input: Record<string, unknown>) => {
|
|
108
|
+
// Path-specific check returns session for coverage filtering
|
|
109
|
+
if (input.path) return makeCheckResult("allow", { source: "special" });
|
|
110
|
+
// Config-level check (no path) returns deny
|
|
111
|
+
return makeCheckResult("deny");
|
|
112
|
+
});
|
|
113
|
+
const result = await describeBashExternalDirectoryGate(
|
|
114
|
+
makeTcc(),
|
|
115
|
+
checkPermission,
|
|
116
|
+
vi.fn().mockReturnValue([]),
|
|
117
|
+
);
|
|
118
|
+
expect(isGateDescriptor(result)).toBe(true);
|
|
119
|
+
// The descriptor should carry the deny state from the config-level check
|
|
120
|
+
// (it will be checked as preCheck by the runner)
|
|
121
|
+
const desc = result as GateDescriptor;
|
|
122
|
+
expect(desc.preCheck?.state).toBe("deny");
|
|
104
123
|
});
|
|
105
124
|
|
|
106
|
-
it("
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
115
|
-
expect(result).toEqual({ action: "allow" });
|
|
116
|
-
expect(deps.approveSessionRule).not.toHaveBeenCalled();
|
|
125
|
+
it("descriptor surface is 'external_directory'", async () => {
|
|
126
|
+
const result = await describeBashExternalDirectoryGate(
|
|
127
|
+
makeTcc(),
|
|
128
|
+
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
129
|
+
vi.fn().mockReturnValue([]),
|
|
130
|
+
);
|
|
131
|
+
const desc = result as GateDescriptor;
|
|
132
|
+
expect(desc.surface).toBe("external_directory");
|
|
117
133
|
});
|
|
118
134
|
|
|
119
|
-
it("
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const tcc = makeTcc({
|
|
128
|
-
input: {
|
|
129
|
-
command: "diff /outside/a.ts /outside/b.ts",
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
133
|
-
expect(result).toEqual({ action: "allow" });
|
|
134
|
-
// Each uncovered path gets its own session rule
|
|
135
|
-
expect(deps.approveSessionRule).toHaveBeenCalledTimes(2);
|
|
136
|
-
for (const call of (deps.approveSessionRule as ReturnType<typeof vi.fn>)
|
|
137
|
-
.mock.calls) {
|
|
138
|
-
expect(call[0]).toBe("external_directory");
|
|
139
|
-
}
|
|
135
|
+
it("descriptor decision surface is 'external_directory'", async () => {
|
|
136
|
+
const result = await describeBashExternalDirectoryGate(
|
|
137
|
+
makeTcc(),
|
|
138
|
+
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
139
|
+
vi.fn().mockReturnValue([]),
|
|
140
|
+
);
|
|
141
|
+
const desc = result as GateDescriptor;
|
|
142
|
+
expect(desc.decision.surface).toBe("external_directory");
|
|
140
143
|
});
|
|
141
144
|
|
|
142
|
-
it("
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
expect(result).toMatchObject({ action: "block" });
|
|
145
|
+
it("messages contain the command", async () => {
|
|
146
|
+
const result = await describeBashExternalDirectoryGate(
|
|
147
|
+
makeTcc({ input: { command: "cat /outside/file.ts" } }),
|
|
148
|
+
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
149
|
+
vi.fn().mockReturnValue([]),
|
|
150
|
+
);
|
|
151
|
+
const desc = result as GateDescriptor;
|
|
152
|
+
expect(desc.messages.denyReason).toContain("cat /outside/file.ts");
|
|
153
|
+
expect(desc.messages.unavailableReason).toContain("cat /outside/file.ts");
|
|
152
154
|
});
|
|
153
155
|
|
|
154
|
-
it("
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
it("promptDetails includes command and tool_call source", async () => {
|
|
157
|
+
const result = await describeBashExternalDirectoryGate(
|
|
158
|
+
makeTcc({ agentName: "agent-1", toolCallId: "tc-5" }),
|
|
159
|
+
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
160
|
+
vi.fn().mockReturnValue([]),
|
|
161
|
+
);
|
|
162
|
+
const desc = result as GateDescriptor;
|
|
163
|
+
expect(desc.promptDetails).toMatchObject({
|
|
164
|
+
source: "tool_call",
|
|
165
|
+
agentName: "agent-1",
|
|
166
|
+
toolCallId: "tc-5",
|
|
167
|
+
toolName: "bash",
|
|
168
|
+
command: "cat /outside/project/file.ts",
|
|
158
169
|
});
|
|
159
|
-
const tcc = makeTcc();
|
|
160
|
-
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
161
|
-
expect(result).toMatchObject({ action: "block" });
|
|
162
170
|
});
|
|
163
171
|
|
|
164
|
-
it("only
|
|
172
|
+
it("only includes uncovered paths when some are session-covered", async () => {
|
|
165
173
|
const checkPermission = vi
|
|
166
174
|
.fn()
|
|
167
|
-
.mockImplementation(
|
|
168
|
-
(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
input: { command: "diff /outside/a.ts /outside/b.ts" },
|
|
184
|
-
});
|
|
185
|
-
const result = await evaluateBashExternalDirectoryGate(tcc, deps);
|
|
186
|
-
expect(result).toEqual({ action: "allow" });
|
|
187
|
-
// The prompt should have been called (for uncovered /outside/b.ts)
|
|
188
|
-
expect(deps.promptPermission).toHaveBeenCalled();
|
|
175
|
+
.mockImplementation((surface: string, input: Record<string, unknown>) => {
|
|
176
|
+
if (input.path === "/outside/a.ts") {
|
|
177
|
+
return makeCheckResult("allow", { source: "session" });
|
|
178
|
+
}
|
|
179
|
+
return makeCheckResult("ask");
|
|
180
|
+
});
|
|
181
|
+
const result = await describeBashExternalDirectoryGate(
|
|
182
|
+
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
183
|
+
checkPermission,
|
|
184
|
+
vi.fn().mockReturnValue([]),
|
|
185
|
+
);
|
|
186
|
+
expect(isGateDescriptor(result)).toBe(true);
|
|
187
|
+
const desc = result as GateDescriptor;
|
|
188
|
+
// Should have patterns only for the uncovered path
|
|
189
|
+
const patterns = (desc.sessionApproval as { patterns: string[] }).patterns;
|
|
190
|
+
expect(patterns.length).toBe(1);
|
|
189
191
|
});
|
|
190
192
|
});
|