@gotgenes/pi-permission-system 10.0.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agent-prep-session.ts +28 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +11 -0
- package/src/forwarded-permissions/permission-forwarder.ts +549 -0
- package/src/forwarding-manager.ts +3 -7
- package/src/gate-handler-session.ts +13 -0
- package/src/gate-prompter.ts +14 -0
- package/src/handlers/before-agent-start.ts +2 -3
- package/src/handlers/gates/bash-command.ts +4 -18
- package/src/handlers/gates/bash-external-directory.ts +3 -15
- package/src/handlers/gates/bash-path.ts +3 -16
- package/src/handlers/gates/descriptor.ts +0 -28
- package/src/handlers/gates/path.ts +3 -15
- package/src/handlers/gates/runner.ts +142 -105
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +44 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
- package/src/handlers/lifecycle.ts +9 -9
- package/src/handlers/permission-gate-handler.ts +34 -238
- package/src/index.ts +53 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +83 -27
- package/src/permissions-service.ts +53 -0
- package/src/runtime.ts +1 -37
- package/src/service-lifecycle.ts +49 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-lifecycle-session.ts +24 -0
- package/src/tool-input-preview.ts +0 -62
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +6 -4
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +62 -0
- package/test/forwarding-manager.test.ts +26 -44
- package/test/handlers/before-agent-start.test.ts +45 -21
- package/test/handlers/external-directory-integration.test.ts +83 -114
- package/test/handlers/external-directory-session-dedup.test.ts +102 -55
- package/test/handlers/gates/bash-command.test.ts +49 -90
- package/test/handlers/gates/bash-external-directory.test.ts +54 -95
- package/test/handlers/gates/bash-path.test.ts +54 -157
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +151 -186
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +128 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
- package/test/handlers/input.test.ts +1 -2
- package/test/handlers/lifecycle.test.ts +49 -33
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +212 -17
- package/test/helpers/handler-fixtures.ts +226 -29
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-forwarding.test.ts +0 -282
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +211 -105
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +2 -86
- package/test/service-lifecycle.test.ts +162 -0
- package/test/tool-input-preview.test.ts +0 -111
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/src/forwarded-permissions/polling.ts +0 -411
|
@@ -8,18 +8,29 @@
|
|
|
8
8
|
* the real interaction between PermissionSession, SessionRules, and
|
|
9
9
|
* PermissionManager.
|
|
10
10
|
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
13
|
import { describe, expect, it, vi } from "vitest";
|
|
12
14
|
|
|
15
|
+
import { GateDecisionReporter } from "#src/decision-reporter";
|
|
13
16
|
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
17
|
+
import { GateRunner } from "#src/handlers/gates/runner";
|
|
18
|
+
import { SkillInputGatePipeline } from "#src/handlers/gates/skill-input-gate-pipeline";
|
|
19
|
+
import { ToolCallGatePipeline } from "#src/handlers/gates/tool-call-gate-pipeline";
|
|
14
20
|
import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
|
|
15
|
-
import type {
|
|
21
|
+
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
16
22
|
import type { Rule } from "#src/rule";
|
|
17
23
|
import type { SessionApproval } from "#src/session-approval";
|
|
24
|
+
import { resolveToolPreviewLimits } from "#src/tool-preview-formatter";
|
|
18
25
|
import type { ToolRegistry } from "#src/tool-registry";
|
|
19
26
|
import type { PermissionCheckResult } from "#src/types";
|
|
20
27
|
import { wildcardMatch } from "#src/wildcard-matcher";
|
|
21
28
|
|
|
22
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
type MockGateHandlerSession,
|
|
31
|
+
makeCtx,
|
|
32
|
+
makeEvents,
|
|
33
|
+
} from "#test/helpers/handler-fixtures";
|
|
23
34
|
|
|
24
35
|
// ── SDK stub ───────────────────────────────────────────────────────────────
|
|
25
36
|
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
@@ -39,12 +50,12 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
|
39
50
|
* "allow" by default.
|
|
40
51
|
*/
|
|
41
52
|
function makeStatefulSession(
|
|
42
|
-
overrides: Partial<
|
|
43
|
-
):
|
|
53
|
+
overrides: Partial<MockGateHandlerSession> = {},
|
|
54
|
+
): MockGateHandlerSession {
|
|
44
55
|
const sessionRules: Rule[] = [];
|
|
45
56
|
|
|
46
57
|
const checkPermission = vi
|
|
47
|
-
.fn()
|
|
58
|
+
.fn<MockGateHandlerSession["checkPermission"]>()
|
|
48
59
|
.mockImplementation(
|
|
49
60
|
(
|
|
50
61
|
surface: string,
|
|
@@ -97,7 +108,7 @@ function makeStatefulSession(
|
|
|
97
108
|
);
|
|
98
109
|
|
|
99
110
|
const recordSessionApproval = vi
|
|
100
|
-
.fn()
|
|
111
|
+
.fn<MockGateHandlerSession["recordSessionApproval"]>()
|
|
101
112
|
.mockImplementation((approval: SessionApproval) => {
|
|
102
113
|
for (const pattern of approval.patterns) {
|
|
103
114
|
sessionRules.push({
|
|
@@ -110,26 +121,86 @@ function makeStatefulSession(
|
|
|
110
121
|
}
|
|
111
122
|
});
|
|
112
123
|
|
|
113
|
-
const getSessionRuleset = vi
|
|
124
|
+
const getSessionRuleset = vi
|
|
125
|
+
.fn<MockGateHandlerSession["getSessionRuleset"]>()
|
|
126
|
+
.mockImplementation(() => [...sessionRules]);
|
|
127
|
+
|
|
128
|
+
const session: MockGateHandlerSession = {
|
|
129
|
+
logger: overrides.logger ?? {
|
|
130
|
+
debug: vi.fn(),
|
|
131
|
+
review: vi.fn(),
|
|
132
|
+
warn: vi.fn(),
|
|
133
|
+
},
|
|
134
|
+
activate: overrides.activate ?? vi.fn<MockGateHandlerSession["activate"]>(),
|
|
135
|
+
resolveAgentName:
|
|
136
|
+
overrides.resolveAgentName ??
|
|
137
|
+
vi.fn<MockGateHandlerSession["resolveAgentName"]>().mockReturnValue(null),
|
|
138
|
+
checkPermission: overrides.checkPermission ?? checkPermission,
|
|
139
|
+
getSessionRuleset: overrides.getSessionRuleset ?? getSessionRuleset,
|
|
140
|
+
recordSessionApproval:
|
|
141
|
+
overrides.recordSessionApproval ?? recordSessionApproval,
|
|
142
|
+
getActiveSkillEntries:
|
|
143
|
+
overrides.getActiveSkillEntries ??
|
|
144
|
+
vi
|
|
145
|
+
.fn<MockGateHandlerSession["getActiveSkillEntries"]>()
|
|
146
|
+
.mockReturnValue([]),
|
|
147
|
+
getInfrastructureReadDirs:
|
|
148
|
+
overrides.getInfrastructureReadDirs ??
|
|
149
|
+
vi
|
|
150
|
+
.fn<MockGateHandlerSession["getInfrastructureReadDirs"]>()
|
|
151
|
+
.mockReturnValue([]),
|
|
152
|
+
getToolPreviewLimits:
|
|
153
|
+
overrides.getToolPreviewLimits ??
|
|
154
|
+
vi
|
|
155
|
+
.fn<MockGateHandlerSession["getToolPreviewLimits"]>()
|
|
156
|
+
.mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
|
|
157
|
+
canPrompt:
|
|
158
|
+
overrides.canPrompt ??
|
|
159
|
+
vi.fn<MockGateHandlerSession["canPrompt"]>().mockReturnValue(true),
|
|
160
|
+
prompt:
|
|
161
|
+
overrides.prompt ??
|
|
162
|
+
vi
|
|
163
|
+
.fn<MockGateHandlerSession["prompt"]>()
|
|
164
|
+
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
165
|
+
// Delegations — closures read `session` at call time so overrides win.
|
|
166
|
+
resolve:
|
|
167
|
+
overrides.resolve ??
|
|
168
|
+
vi.fn<MockGateHandlerSession["resolve"]>((surface, input, agentName) =>
|
|
169
|
+
session.checkPermission(
|
|
170
|
+
surface,
|
|
171
|
+
input,
|
|
172
|
+
agentName,
|
|
173
|
+
session.getSessionRuleset(),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
canConfirm:
|
|
177
|
+
overrides.canConfirm ??
|
|
178
|
+
vi.fn<MockGateHandlerSession["canConfirm"]>(() =>
|
|
179
|
+
session.canPrompt(undefined as unknown as ExtensionContext),
|
|
180
|
+
),
|
|
181
|
+
promptPermission:
|
|
182
|
+
overrides.promptPermission ??
|
|
183
|
+
vi.fn<MockGateHandlerSession["promptPermission"]>(
|
|
184
|
+
(details: PromptPermissionDetails) =>
|
|
185
|
+
session.prompt(undefined as unknown as ExtensionContext, details),
|
|
186
|
+
),
|
|
187
|
+
};
|
|
188
|
+
return session;
|
|
189
|
+
}
|
|
114
190
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
prompt: vi
|
|
129
|
-
.fn()
|
|
130
|
-
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
131
|
-
...overrides,
|
|
132
|
-
} as unknown as PermissionSession;
|
|
191
|
+
function makeHandlerForSession(
|
|
192
|
+
session: MockGateHandlerSession,
|
|
193
|
+
): PermissionGateHandler {
|
|
194
|
+
const events = makeEvents();
|
|
195
|
+
const reporter = new GateDecisionReporter(session.logger, events);
|
|
196
|
+
const runner = new GateRunner(session, session, session, reporter);
|
|
197
|
+
return new PermissionGateHandler(
|
|
198
|
+
session,
|
|
199
|
+
makeToolRegistry(),
|
|
200
|
+
new ToolCallGatePipeline(session),
|
|
201
|
+
new SkillInputGatePipeline(session),
|
|
202
|
+
runner,
|
|
203
|
+
);
|
|
133
204
|
}
|
|
134
205
|
|
|
135
206
|
function makeToolRegistry(): ToolRegistry {
|
|
@@ -152,11 +223,7 @@ describe("external-directory session dedup", () => {
|
|
|
152
223
|
describe("path-bearing tools (read, write, edit)", () => {
|
|
153
224
|
it("does not re-prompt for the same external path after session approval", async () => {
|
|
154
225
|
const session = makeStatefulSession();
|
|
155
|
-
const handler =
|
|
156
|
-
session,
|
|
157
|
-
makeEvents(),
|
|
158
|
-
makeToolRegistry(),
|
|
159
|
-
);
|
|
226
|
+
const handler = makeHandlerForSession(session);
|
|
160
227
|
const ctx = makeCtx();
|
|
161
228
|
const externalPath = "/outside/project/data.txt";
|
|
162
229
|
|
|
@@ -185,11 +252,7 @@ describe("external-directory session dedup", () => {
|
|
|
185
252
|
|
|
186
253
|
it("does not re-prompt for a different file in the same external directory", async () => {
|
|
187
254
|
const session = makeStatefulSession();
|
|
188
|
-
const handler =
|
|
189
|
-
session,
|
|
190
|
-
makeEvents(),
|
|
191
|
-
makeToolRegistry(),
|
|
192
|
-
);
|
|
255
|
+
const handler = makeHandlerForSession(session);
|
|
193
256
|
const ctx = makeCtx();
|
|
194
257
|
|
|
195
258
|
// First call — prompt for /outside/project/a.txt
|
|
@@ -215,11 +278,7 @@ describe("external-directory session dedup", () => {
|
|
|
215
278
|
|
|
216
279
|
it("does prompt for a file in a different external directory", async () => {
|
|
217
280
|
const session = makeStatefulSession();
|
|
218
|
-
const handler =
|
|
219
|
-
session,
|
|
220
|
-
makeEvents(),
|
|
221
|
-
makeToolRegistry(),
|
|
222
|
-
);
|
|
281
|
+
const handler = makeHandlerForSession(session);
|
|
223
282
|
const ctx = makeCtx();
|
|
224
283
|
|
|
225
284
|
// First call — /outside/alpha/file.txt
|
|
@@ -249,11 +308,7 @@ describe("external-directory session dedup", () => {
|
|
|
249
308
|
.fn()
|
|
250
309
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
251
310
|
});
|
|
252
|
-
const handler =
|
|
253
|
-
session,
|
|
254
|
-
makeEvents(),
|
|
255
|
-
makeToolRegistry(),
|
|
256
|
-
);
|
|
311
|
+
const handler = makeHandlerForSession(session);
|
|
257
312
|
const ctx = makeCtx();
|
|
258
313
|
const externalPath = "/outside/project/data.txt";
|
|
259
314
|
|
|
@@ -282,11 +337,7 @@ describe("external-directory session dedup", () => {
|
|
|
282
337
|
describe("bash commands with external paths", () => {
|
|
283
338
|
it("does not re-prompt for a bash command referencing the same external path after session approval", async () => {
|
|
284
339
|
const session = makeStatefulSession();
|
|
285
|
-
const handler =
|
|
286
|
-
session,
|
|
287
|
-
makeEvents(),
|
|
288
|
-
makeToolRegistry(),
|
|
289
|
-
);
|
|
340
|
+
const handler = makeHandlerForSession(session);
|
|
290
341
|
const ctx = makeCtx();
|
|
291
342
|
|
|
292
343
|
// First call — bash referencing /tmp/out.txt
|
|
@@ -314,11 +365,7 @@ describe("external-directory session dedup", () => {
|
|
|
314
365
|
|
|
315
366
|
it("does not re-prompt for read after bash already approved the same directory", async () => {
|
|
316
367
|
const session = makeStatefulSession();
|
|
317
|
-
const handler =
|
|
318
|
-
session,
|
|
319
|
-
makeEvents(),
|
|
320
|
-
makeToolRegistry(),
|
|
321
|
-
);
|
|
368
|
+
const handler = makeHandlerForSession(session);
|
|
322
369
|
const ctx = makeCtx();
|
|
323
370
|
|
|
324
371
|
// First call — bash writes to /tmp/out.txt
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { resolveBashCommandCheck } from "#src/handlers/gates/bash-command";
|
|
4
|
-
import type { Rule } from "#src/rule";
|
|
5
4
|
import type { PermissionCheckResult } from "#src/types";
|
|
6
5
|
|
|
6
|
+
import { makeResolver } from "#test/helpers/gate-fixtures";
|
|
7
7
|
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
8
8
|
|
|
9
|
-
type CheckPermissionFn = (
|
|
10
|
-
surface: string,
|
|
11
|
-
input: unknown,
|
|
12
|
-
agentName?: string,
|
|
13
|
-
sessionRules?: Rule[],
|
|
14
|
-
) => PermissionCheckResult;
|
|
15
|
-
|
|
16
9
|
/** Build a bash-surface check result for a single command unit. */
|
|
17
10
|
function bashResult(
|
|
18
11
|
state: PermissionCheckResult["state"],
|
|
@@ -24,44 +17,40 @@ function bashResult(
|
|
|
24
17
|
|
|
25
18
|
describe("resolveBashCommandCheck", () => {
|
|
26
19
|
it("passes a single command straight through", () => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const resolver = makeResolver(
|
|
21
|
+
bashResult("allow", "npm install pkg", "npm *"),
|
|
22
|
+
);
|
|
30
23
|
|
|
31
24
|
const result = resolveBashCommandCheck(
|
|
32
25
|
"npm install pkg",
|
|
33
26
|
[{ text: "npm install pkg" }],
|
|
34
27
|
undefined,
|
|
35
|
-
|
|
36
|
-
checkPermission,
|
|
28
|
+
resolver,
|
|
37
29
|
);
|
|
38
30
|
|
|
39
31
|
expect(result.state).toBe("allow");
|
|
40
|
-
expect(
|
|
41
|
-
expect(
|
|
32
|
+
expect(resolver.resolve).toHaveBeenCalledTimes(1);
|
|
33
|
+
expect(resolver.resolve).toHaveBeenCalledWith(
|
|
42
34
|
"bash",
|
|
43
35
|
{ command: "npm install pkg" },
|
|
44
36
|
undefined,
|
|
45
|
-
[],
|
|
46
37
|
);
|
|
47
38
|
});
|
|
48
39
|
|
|
49
40
|
it("denies the chain when any sub-command is denied, reporting that command's pattern", () => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
41
|
+
const resolver = makeResolver();
|
|
42
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
43
|
+
const command = (input as { command: string }).command;
|
|
44
|
+
return command.startsWith("npm")
|
|
45
|
+
? bashResult("deny", command, "npm *")
|
|
46
|
+
: bashResult("allow", command, "cd *");
|
|
47
|
+
});
|
|
58
48
|
|
|
59
49
|
const result = resolveBashCommandCheck(
|
|
60
50
|
"cd /p && npm install pkg",
|
|
61
51
|
[{ text: "cd /p" }, { text: "npm install pkg" }],
|
|
62
52
|
undefined,
|
|
63
|
-
|
|
64
|
-
checkPermission,
|
|
53
|
+
resolver,
|
|
65
54
|
);
|
|
66
55
|
|
|
67
56
|
expect(result.state).toBe("deny");
|
|
@@ -70,21 +59,19 @@ describe("resolveBashCommandCheck", () => {
|
|
|
70
59
|
});
|
|
71
60
|
|
|
72
61
|
it("asks when a sub-command asks and none denies", () => {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
62
|
+
const resolver = makeResolver();
|
|
63
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
64
|
+
const command = (input as { command: string }).command;
|
|
65
|
+
return command.startsWith("git")
|
|
66
|
+
? bashResult("ask", command, "git *")
|
|
67
|
+
: bashResult("allow", command, "cd *");
|
|
68
|
+
});
|
|
81
69
|
|
|
82
70
|
const result = resolveBashCommandCheck(
|
|
83
71
|
"cd /p && git push",
|
|
84
72
|
[{ text: "cd /p" }, { text: "git push" }],
|
|
85
73
|
undefined,
|
|
86
|
-
|
|
87
|
-
checkPermission,
|
|
74
|
+
resolver,
|
|
88
75
|
);
|
|
89
76
|
|
|
90
77
|
expect(result.state).toBe("ask");
|
|
@@ -93,19 +80,17 @@ describe("resolveBashCommandCheck", () => {
|
|
|
93
80
|
});
|
|
94
81
|
|
|
95
82
|
it("returns the first allow result when every sub-command is allowed", () => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
83
|
+
const resolver = makeResolver();
|
|
84
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
85
|
+
const command = (input as { command: string }).command;
|
|
86
|
+
return bashResult("allow", command, `${command} *`);
|
|
87
|
+
});
|
|
102
88
|
|
|
103
89
|
const result = resolveBashCommandCheck(
|
|
104
90
|
"a && b",
|
|
105
91
|
[{ text: "a" }, { text: "b" }],
|
|
106
92
|
undefined,
|
|
107
|
-
|
|
108
|
-
checkPermission,
|
|
93
|
+
resolver,
|
|
109
94
|
);
|
|
110
95
|
|
|
111
96
|
expect(result.state).toBe("allow");
|
|
@@ -113,62 +98,40 @@ describe("resolveBashCommandCheck", () => {
|
|
|
113
98
|
});
|
|
114
99
|
|
|
115
100
|
it("falls back to the whole command when no top-level commands are found", () => {
|
|
116
|
-
const
|
|
117
|
-
.fn<CheckPermissionFn>()
|
|
118
|
-
.mockReturnValue(bashResult("ask", "( rm x )", "*"));
|
|
101
|
+
const resolver = makeResolver(bashResult("ask", "( rm x )", "*"));
|
|
119
102
|
|
|
120
|
-
const result = resolveBashCommandCheck(
|
|
121
|
-
"( rm x )",
|
|
122
|
-
[],
|
|
123
|
-
undefined,
|
|
124
|
-
[],
|
|
125
|
-
checkPermission,
|
|
126
|
-
);
|
|
103
|
+
const result = resolveBashCommandCheck("( rm x )", [], undefined, resolver);
|
|
127
104
|
|
|
128
105
|
expect(result.state).toBe("ask");
|
|
129
106
|
expect(result.commandContext).toBeUndefined();
|
|
130
|
-
expect(
|
|
131
|
-
expect(
|
|
107
|
+
expect(resolver.resolve).toHaveBeenCalledTimes(1);
|
|
108
|
+
expect(resolver.resolve).toHaveBeenCalledWith(
|
|
132
109
|
"bash",
|
|
133
110
|
{ command: "( rm x )" },
|
|
134
111
|
undefined,
|
|
135
|
-
[],
|
|
136
112
|
);
|
|
137
113
|
});
|
|
138
114
|
|
|
139
|
-
it("forwards the agent name
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
];
|
|
143
|
-
const checkPermission = vi
|
|
144
|
-
.fn<CheckPermissionFn>()
|
|
145
|
-
.mockReturnValue(bashResult("allow", "npm i"));
|
|
146
|
-
|
|
147
|
-
resolveBashCommandCheck(
|
|
148
|
-
"npm i",
|
|
149
|
-
[{ text: "npm i" }],
|
|
150
|
-
"agent-x",
|
|
151
|
-
sessionRules,
|
|
152
|
-
checkPermission,
|
|
153
|
-
);
|
|
115
|
+
it("forwards the agent name to each sub-command check", () => {
|
|
116
|
+
const resolver = makeResolver(bashResult("allow", "npm i"));
|
|
117
|
+
|
|
118
|
+
resolveBashCommandCheck("npm i", [{ text: "npm i" }], "agent-x", resolver);
|
|
154
119
|
|
|
155
|
-
expect(
|
|
120
|
+
expect(resolver.resolve).toHaveBeenCalledWith(
|
|
156
121
|
"bash",
|
|
157
122
|
{ command: "npm i" },
|
|
158
123
|
"agent-x",
|
|
159
|
-
sessionRules,
|
|
160
124
|
);
|
|
161
125
|
});
|
|
162
126
|
|
|
163
127
|
it("tags the winning result with the offending command's execution context", () => {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
});
|
|
128
|
+
const resolver = makeResolver();
|
|
129
|
+
resolver.resolve.mockImplementation((_surface, input) => {
|
|
130
|
+
const command = (input as { command: string }).command;
|
|
131
|
+
return command.startsWith("rm")
|
|
132
|
+
? bashResult("deny", command, "rm *")
|
|
133
|
+
: bashResult("allow", command, "echo *");
|
|
134
|
+
});
|
|
172
135
|
|
|
173
136
|
const result = resolveBashCommandCheck(
|
|
174
137
|
"echo $(rm -rf foo)",
|
|
@@ -177,8 +140,7 @@ describe("resolveBashCommandCheck", () => {
|
|
|
177
140
|
{ text: "rm -rf foo", context: "command_substitution" },
|
|
178
141
|
],
|
|
179
142
|
undefined,
|
|
180
|
-
|
|
181
|
-
checkPermission,
|
|
143
|
+
resolver,
|
|
182
144
|
);
|
|
183
145
|
|
|
184
146
|
expect(result.state).toBe("deny");
|
|
@@ -187,16 +149,13 @@ describe("resolveBashCommandCheck", () => {
|
|
|
187
149
|
});
|
|
188
150
|
|
|
189
151
|
it("leaves commandContext unset when the winning command is top-level", () => {
|
|
190
|
-
const
|
|
191
|
-
.fn<CheckPermissionFn>()
|
|
192
|
-
.mockReturnValue(bashResult("deny", "rm -rf foo", "rm *"));
|
|
152
|
+
const resolver = makeResolver(bashResult("deny", "rm -rf foo", "rm *"));
|
|
193
153
|
|
|
194
154
|
const result = resolveBashCommandCheck(
|
|
195
155
|
"rm -rf foo",
|
|
196
156
|
[{ text: "rm -rf foo" }],
|
|
197
157
|
undefined,
|
|
198
|
-
|
|
199
|
-
checkPermission,
|
|
158
|
+
resolver,
|
|
200
159
|
);
|
|
201
160
|
|
|
202
161
|
expect(result.state).toBe("deny");
|