@gotgenes/pi-permission-system 9.2.0 → 10.1.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 +52 -0
- package/README.md +12 -11
- 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/io.ts +29 -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 +50 -68
- package/src/mcp-targets.ts +56 -46
- package/src/permission-event-rpc.ts +7 -0
- package/src/permission-events.ts +89 -8
- package/src/permission-forwarding.ts +23 -0
- package/src/permission-prompter.ts +27 -56
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +77 -9
- package/src/permission-ui-prompt.ts +127 -0
- package/src/permissions-service.ts +53 -0
- package/src/service-lifecycle.ts +49 -0
- package/src/service.ts +17 -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/composition-root.test.ts +5 -0
- 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 +86 -22
- 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 +63 -148
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +150 -93
- 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/helpers/gate-fixtures.ts +147 -16
- package/test/helpers/handler-fixtures.ts +143 -27
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-event-rpc.test.ts +39 -0
- package/test/permission-events.test.ts +78 -10
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-prompter.test.ts +147 -38
- package/test/permission-session.test.ts +160 -27
- package/test/permission-ui-prompt.test.ts +146 -0
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +0 -4
- 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 -379
|
@@ -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");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { getNonEmptyString, toRecord } from "#src/common";
|
|
3
3
|
import { describeBashExternalDirectoryGate } from "#src/handlers/gates/bash-external-directory";
|
|
4
4
|
import { BashProgram } from "#src/handlers/gates/bash-program";
|
|
@@ -9,8 +9,11 @@ import type {
|
|
|
9
9
|
} from "#src/handlers/gates/descriptor";
|
|
10
10
|
import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
|
|
11
11
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
12
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
12
13
|
import type { PermissionCheckResult } from "#src/types";
|
|
13
14
|
|
|
15
|
+
import { makeResolver } from "#test/helpers/gate-fixtures";
|
|
16
|
+
|
|
14
17
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
15
18
|
|
|
16
19
|
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
@@ -44,20 +47,14 @@ function makeCheckResult(
|
|
|
44
47
|
*/
|
|
45
48
|
async function describeGate(
|
|
46
49
|
tcc: ToolCallContext,
|
|
47
|
-
|
|
48
|
-
getSessionRuleset: Parameters<typeof describeBashExternalDirectoryGate>[3],
|
|
50
|
+
resolver: PermissionResolver,
|
|
49
51
|
): Promise<GateResult> {
|
|
50
52
|
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
51
53
|
const bashProgram =
|
|
52
54
|
tcc.toolName === "bash" && command
|
|
53
55
|
? await BashProgram.parse(command)
|
|
54
56
|
: null;
|
|
55
|
-
return describeBashExternalDirectoryGate(
|
|
56
|
-
tcc,
|
|
57
|
-
bashProgram,
|
|
58
|
-
checkPermission,
|
|
59
|
-
getSessionRuleset,
|
|
60
|
-
);
|
|
57
|
+
return describeBashExternalDirectoryGate(tcc, bashProgram, resolver);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
@@ -66,8 +63,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
66
63
|
it("returns null when tool is not bash", async () => {
|
|
67
64
|
const result = await describeGate(
|
|
68
65
|
makeTcc({ toolName: "read" }),
|
|
69
|
-
|
|
70
|
-
vi.fn().mockReturnValue([]),
|
|
66
|
+
makeResolver(makeCheckResult("ask")),
|
|
71
67
|
);
|
|
72
68
|
expect(result).toBeNull();
|
|
73
69
|
});
|
|
@@ -75,8 +71,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
75
71
|
it("returns null when no CWD", async () => {
|
|
76
72
|
const result = await describeGate(
|
|
77
73
|
makeTcc({ cwd: undefined }),
|
|
78
|
-
|
|
79
|
-
vi.fn().mockReturnValue([]),
|
|
74
|
+
makeResolver(makeCheckResult("ask")),
|
|
80
75
|
);
|
|
81
76
|
expect(result).toBeNull();
|
|
82
77
|
});
|
|
@@ -84,21 +79,16 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
84
79
|
it("returns null when command has no external paths", async () => {
|
|
85
80
|
const result = await describeGate(
|
|
86
81
|
makeTcc({ input: { command: "ls -la" } }),
|
|
87
|
-
|
|
88
|
-
vi.fn().mockReturnValue([]),
|
|
82
|
+
makeResolver(makeCheckResult("ask")),
|
|
89
83
|
);
|
|
90
84
|
expect(result).toBeNull();
|
|
91
85
|
});
|
|
92
86
|
|
|
93
87
|
it("returns GateBypass when all external paths are session-covered", async () => {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
.mockReturnValue(makeCheckResult("allow", { source: "session" }));
|
|
97
|
-
const result = await describeGate(
|
|
98
|
-
makeTcc(),
|
|
99
|
-
checkPermission,
|
|
100
|
-
vi.fn().mockReturnValue([]),
|
|
88
|
+
const resolver = makeResolver(
|
|
89
|
+
makeCheckResult("allow", { source: "session" }),
|
|
101
90
|
);
|
|
91
|
+
const result = await describeGate(makeTcc(), resolver);
|
|
102
92
|
expect(result).not.toBeNull();
|
|
103
93
|
expect(isGateBypass(result)).toBe(true);
|
|
104
94
|
const bypass = result as GateBypass;
|
|
@@ -110,11 +100,9 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
110
100
|
});
|
|
111
101
|
|
|
112
102
|
it("returns GateDescriptor with multi-pattern sessionApproval for uncovered paths", async () => {
|
|
113
|
-
const checkPermission = vi.fn().mockReturnValue(makeCheckResult("ask"));
|
|
114
103
|
const result = await describeGate(
|
|
115
104
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
116
|
-
|
|
117
|
-
vi.fn().mockReturnValue([]),
|
|
105
|
+
makeResolver(makeCheckResult("ask")),
|
|
118
106
|
);
|
|
119
107
|
expect(isGateDescriptor(result)).toBe(true);
|
|
120
108
|
const desc = result as GateDescriptor;
|
|
@@ -127,20 +115,13 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
127
115
|
// Config-level allow (source: "special") should suppress the prompt,
|
|
128
116
|
// not just session-level allow. This was the bug: source !== "session"
|
|
129
117
|
// kept config-allowed paths in the uncovered set.
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
.
|
|
133
|
-
(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
const result = await describeGate(
|
|
140
|
-
makeTcc(),
|
|
141
|
-
checkPermission,
|
|
142
|
-
vi.fn().mockReturnValue([]),
|
|
143
|
-
);
|
|
118
|
+
const resolver = makeResolver();
|
|
119
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
120
|
+
if ((input as Record<string, unknown>).path)
|
|
121
|
+
return makeCheckResult("allow", { source: "special" });
|
|
122
|
+
return makeCheckResult("ask");
|
|
123
|
+
});
|
|
124
|
+
const result = await describeGate(makeTcc(), resolver);
|
|
144
125
|
expect(result).not.toBeNull();
|
|
145
126
|
expect(isGateBypass(result)).toBe(true);
|
|
146
127
|
});
|
|
@@ -149,20 +130,14 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
149
130
|
// The path-less extCheck used to always return the "*" catch-all (ask),
|
|
150
131
|
// silently downgrading a config-level deny to ask. After the fix, the
|
|
151
132
|
// descriptor's preCheck is derived from the actual path check result.
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
const result = await describeGate(
|
|
162
|
-
makeTcc(),
|
|
163
|
-
checkPermission,
|
|
164
|
-
vi.fn().mockReturnValue([]),
|
|
165
|
-
);
|
|
133
|
+
const resolver = makeResolver();
|
|
134
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
135
|
+
if ((input as Record<string, unknown>).path)
|
|
136
|
+
return makeCheckResult("deny", { source: "special" });
|
|
137
|
+
// Path-less catch-all returns ask — should NOT be used as preCheck.
|
|
138
|
+
return makeCheckResult("ask");
|
|
139
|
+
});
|
|
140
|
+
const result = await describeGate(makeTcc(), resolver);
|
|
166
141
|
expect(isGateDescriptor(result)).toBe(true);
|
|
167
142
|
const desc = result as GateDescriptor;
|
|
168
143
|
expect(desc.preCheck?.state).toBe("deny");
|
|
@@ -171,8 +146,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
171
146
|
it("descriptor surface is 'external_directory'", async () => {
|
|
172
147
|
const result = await describeGate(
|
|
173
148
|
makeTcc(),
|
|
174
|
-
|
|
175
|
-
vi.fn().mockReturnValue([]),
|
|
149
|
+
makeResolver(makeCheckResult("ask")),
|
|
176
150
|
);
|
|
177
151
|
const desc = result as GateDescriptor;
|
|
178
152
|
expect(desc.surface).toBe("external_directory");
|
|
@@ -181,8 +155,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
181
155
|
it("descriptor decision surface is 'external_directory'", async () => {
|
|
182
156
|
const result = await describeGate(
|
|
183
157
|
makeTcc(),
|
|
184
|
-
|
|
185
|
-
vi.fn().mockReturnValue([]),
|
|
158
|
+
makeResolver(makeCheckResult("ask")),
|
|
186
159
|
);
|
|
187
160
|
const desc = result as GateDescriptor;
|
|
188
161
|
expect(desc.decision.surface).toBe("external_directory");
|
|
@@ -191,8 +164,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
191
164
|
it("denialContext contains the command and external paths", async () => {
|
|
192
165
|
const result = await describeGate(
|
|
193
166
|
makeTcc({ input: { command: "cat /outside/file.ts" } }),
|
|
194
|
-
|
|
195
|
-
vi.fn().mockReturnValue([]),
|
|
167
|
+
makeResolver(makeCheckResult("ask")),
|
|
196
168
|
);
|
|
197
169
|
const desc = result as GateDescriptor;
|
|
198
170
|
expect(desc.denialContext).toMatchObject({
|
|
@@ -205,8 +177,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
205
177
|
it("promptDetails includes command and tool_call source", async () => {
|
|
206
178
|
const result = await describeGate(
|
|
207
179
|
makeTcc({ agentName: "agent-1", toolCallId: "tc-5" }),
|
|
208
|
-
|
|
209
|
-
vi.fn().mockReturnValue([]),
|
|
180
|
+
makeResolver(makeCheckResult("ask")),
|
|
210
181
|
);
|
|
211
182
|
const desc = result as GateDescriptor;
|
|
212
183
|
expect(desc.promptDetails).toMatchObject({
|
|
@@ -220,19 +191,15 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
220
191
|
|
|
221
192
|
it("config-allowed path is excluded; remaining ask path produces a descriptor", async () => {
|
|
222
193
|
// One path config-allowed, one config-ask → descriptor with only the ask path.
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
.
|
|
226
|
-
(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return makeCheckResult("ask");
|
|
230
|
-
},
|
|
231
|
-
);
|
|
194
|
+
const resolver = makeResolver();
|
|
195
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
196
|
+
if ((input as Record<string, unknown>).path === "/outside/a.ts")
|
|
197
|
+
return makeCheckResult("allow", { source: "special" });
|
|
198
|
+
return makeCheckResult("ask");
|
|
199
|
+
});
|
|
232
200
|
const result = await describeGate(
|
|
233
201
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
234
|
-
|
|
235
|
-
vi.fn().mockReturnValue([]),
|
|
202
|
+
resolver,
|
|
236
203
|
);
|
|
237
204
|
expect(isGateDescriptor(result)).toBe(true);
|
|
238
205
|
const desc = result as GateDescriptor;
|
|
@@ -244,19 +211,15 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
244
211
|
|
|
245
212
|
it("config-denied path makes worstCheck deny even when another path is ask", async () => {
|
|
246
213
|
// One path config-denied, one config-ask → descriptor with preCheck.state === "deny".
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return makeCheckResult("ask");
|
|
254
|
-
},
|
|
255
|
-
);
|
|
214
|
+
const resolver = makeResolver();
|
|
215
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
216
|
+
if ((input as Record<string, unknown>).path === "/outside/a.ts")
|
|
217
|
+
return makeCheckResult("deny", { source: "special" });
|
|
218
|
+
return makeCheckResult("ask");
|
|
219
|
+
});
|
|
256
220
|
const result = await describeGate(
|
|
257
221
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
258
|
-
|
|
259
|
-
vi.fn().mockReturnValue([]),
|
|
222
|
+
resolver,
|
|
260
223
|
);
|
|
261
224
|
expect(isGateDescriptor(result)).toBe(true);
|
|
262
225
|
const desc = result as GateDescriptor;
|
|
@@ -268,20 +231,16 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
268
231
|
});
|
|
269
232
|
|
|
270
233
|
it("only includes uncovered paths when some are session-covered", async () => {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
.
|
|
274
|
-
(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return makeCheckResult("ask");
|
|
279
|
-
},
|
|
280
|
-
);
|
|
234
|
+
const resolver = makeResolver();
|
|
235
|
+
resolver.resolve.mockImplementation((_surface: string, input: unknown) => {
|
|
236
|
+
if ((input as Record<string, unknown>).path === "/outside/a.ts") {
|
|
237
|
+
return makeCheckResult("allow", { source: "session" });
|
|
238
|
+
}
|
|
239
|
+
return makeCheckResult("ask");
|
|
240
|
+
});
|
|
281
241
|
const result = await describeGate(
|
|
282
242
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
283
|
-
|
|
284
|
-
vi.fn().mockReturnValue([]),
|
|
243
|
+
resolver,
|
|
285
244
|
);
|
|
286
245
|
expect(isGateDescriptor(result)).toBe(true);
|
|
287
246
|
const desc = result as GateDescriptor;
|