@gotgenes/pi-permission-system 9.0.0 → 9.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 +27 -0
- package/package.json +1 -1
- package/src/denial-messages.ts +44 -3
- package/src/handlers/gates/bash-command.ts +57 -0
- package/src/handlers/gates/bash-external-directory.ts +11 -10
- package/src/handlers/gates/bash-path-extractor.ts +9 -618
- package/src/handlers/gates/bash-path.ts +22 -13
- package/src/handlers/gates/bash-program.ts +807 -0
- package/src/handlers/gates/candidate-check.ts +32 -0
- package/src/handlers/permission-gate-handler.ts +38 -8
- package/src/permission-prompts.ts +7 -4
- package/src/types.ts +15 -0
- package/test/denial-messages.test.ts +16 -0
- package/test/handlers/gates/bash-command.test.ts +205 -0
- package/test/handlers/gates/bash-external-directory.test.ts +40 -14
- package/test/handlers/gates/bash-path.test.ts +41 -14
- package/test/handlers/gates/bash-program.test.ts +179 -0
- package/test/handlers/gates/candidate-check.test.ts +52 -0
- package/test/handlers/tool-call.test.ts +111 -0
- package/test/permission-prompts.test.ts +16 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
2
|
+
|
|
3
|
+
/** Restrictiveness ordering: deny is the most restrictive, allow the least. */
|
|
4
|
+
const RESTRICTIVENESS: Record<PermissionState, number> = {
|
|
5
|
+
allow: 0,
|
|
6
|
+
ask: 1,
|
|
7
|
+
deny: 2,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Select the most restrictive permission result from a list (deny > ask > allow).
|
|
12
|
+
*
|
|
13
|
+
* The first occurrence wins on ties, so a caller passing results in candidate
|
|
14
|
+
* order receives the earliest worst case. Returns `undefined` for an empty list.
|
|
15
|
+
*
|
|
16
|
+
* Shared by the bash gates (path, external-directory) to combine the per-candidate
|
|
17
|
+
* `checkPermission` results their tree-sitter token extraction produces.
|
|
18
|
+
*/
|
|
19
|
+
export function pickMostRestrictive(
|
|
20
|
+
results: readonly PermissionCheckResult[],
|
|
21
|
+
): PermissionCheckResult | undefined {
|
|
22
|
+
let worst: PermissionCheckResult | undefined;
|
|
23
|
+
for (const result of results) {
|
|
24
|
+
if (
|
|
25
|
+
worst === undefined ||
|
|
26
|
+
RESTRICTIVENESS[result.state] > RESTRICTIVENESS[worst.state]
|
|
27
|
+
) {
|
|
28
|
+
worst = result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return worst;
|
|
32
|
+
}
|
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
InputEventResult,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
|
-
import { toRecord } from "#src/common";
|
|
6
|
+
import { getNonEmptyString, toRecord } from "#src/common";
|
|
7
7
|
import {
|
|
8
8
|
emitDecisionEvent,
|
|
9
9
|
type PermissionEventBus,
|
|
@@ -26,8 +26,10 @@ import {
|
|
|
26
26
|
getToolNameFromValue,
|
|
27
27
|
type ToolRegistry,
|
|
28
28
|
} from "#src/tool-registry";
|
|
29
|
+
import { resolveBashCommandCheck } from "./gates/bash-command";
|
|
29
30
|
import { describeBashExternalDirectoryGate } from "./gates/bash-external-directory";
|
|
30
31
|
import { describeBashPathGate } from "./gates/bash-path";
|
|
32
|
+
import { BashProgram } from "./gates/bash-program";
|
|
31
33
|
import type { GateResult, GateRunnerDeps } from "./gates/descriptor";
|
|
32
34
|
import { isGateBypass } from "./gates/descriptor";
|
|
33
35
|
import { describeExternalDirectoryGate } from "./gates/external-directory";
|
|
@@ -86,6 +88,14 @@ export class PermissionGateHandler {
|
|
|
86
88
|
cwd: ctx.cwd,
|
|
87
89
|
};
|
|
88
90
|
|
|
91
|
+
// Parse the bash command exactly once per tool_call; the three bash gates
|
|
92
|
+
// share this single BashProgram instead of each re-parsing (#308).
|
|
93
|
+
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
94
|
+
const bashProgram =
|
|
95
|
+
tcc.toolName === "bash" && command
|
|
96
|
+
? await BashProgram.parse(command)
|
|
97
|
+
: null;
|
|
98
|
+
|
|
89
99
|
// ── Shared gate adapter closures ─────────────────────────────────────
|
|
90
100
|
const canConfirm = () => this.session.canPrompt(ctx);
|
|
91
101
|
const promptPermission = (details: PromptPermissionDetails) =>
|
|
@@ -165,17 +175,37 @@ export class PermissionGateHandler {
|
|
|
165
175
|
() =>
|
|
166
176
|
describeBashExternalDirectoryGate(
|
|
167
177
|
tcc,
|
|
178
|
+
bashProgram,
|
|
179
|
+
checkPermission,
|
|
180
|
+
getSessionRuleset,
|
|
181
|
+
),
|
|
182
|
+
() =>
|
|
183
|
+
describeBashPathGate(
|
|
184
|
+
tcc,
|
|
185
|
+
bashProgram,
|
|
168
186
|
checkPermission,
|
|
169
187
|
getSessionRuleset,
|
|
170
188
|
),
|
|
171
|
-
() => describeBashPathGate(tcc, checkPermission, getSessionRuleset),
|
|
172
189
|
() => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
// Bash commands may chain several sub-commands (`a && b`, `a | b`, …);
|
|
191
|
+
// evaluate each unit from the shared parse on the bash surface and
|
|
192
|
+
// select the most restrictive, rather than matching the whole program
|
|
193
|
+
// string (#301). Other tools evaluate their single input directly.
|
|
194
|
+
const toolCheck =
|
|
195
|
+
tcc.toolName === "bash" && bashProgram
|
|
196
|
+
? resolveBashCommandCheck(
|
|
197
|
+
command ?? "",
|
|
198
|
+
bashProgram.commands(),
|
|
199
|
+
tcc.agentName ?? undefined,
|
|
200
|
+
getSessionRuleset(),
|
|
201
|
+
checkPermission,
|
|
202
|
+
)
|
|
203
|
+
: checkPermission(
|
|
204
|
+
tcc.toolName,
|
|
205
|
+
tcc.input,
|
|
206
|
+
tcc.agentName ?? undefined,
|
|
207
|
+
getSessionRuleset(),
|
|
208
|
+
);
|
|
179
209
|
const toolDescriptor = describeToolGate(tcc, toolCheck, formatter);
|
|
180
210
|
toolDescriptor.preCheck = toolCheck;
|
|
181
211
|
return toolDescriptor;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { matchQualifier } from "./denial-messages";
|
|
1
2
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
2
3
|
import type { ToolPreviewFormatter } from "./tool-preview-formatter";
|
|
3
4
|
import type { PermissionCheckResult } from "./types";
|
|
@@ -36,10 +37,12 @@ export function formatAskPrompt(
|
|
|
36
37
|
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
37
38
|
|
|
38
39
|
if (result.toolName === "bash") {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const qualifier = matchQualifier(
|
|
41
|
+
result.matchedPattern,
|
|
42
|
+
result.commandContext,
|
|
43
|
+
);
|
|
44
|
+
const qualifierInfo = qualifier ? ` ${qualifier}` : "";
|
|
45
|
+
return `${subject} requested bash command '${result.command ?? ""}'${qualifierInfo}. Allow this command?`;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,15 @@ export interface ScopeConfig {
|
|
|
22
22
|
permission?: FlatPermissionConfig;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Execution context of a bash command nested inside a substitution or subshell.
|
|
27
|
+
* Absent for current-shell (top-level) commands.
|
|
28
|
+
*/
|
|
29
|
+
export type BashCommandContext =
|
|
30
|
+
| "command_substitution"
|
|
31
|
+
| "process_substitution"
|
|
32
|
+
| "subshell";
|
|
33
|
+
|
|
25
34
|
export interface PermissionCheckResult {
|
|
26
35
|
toolName: string;
|
|
27
36
|
state: PermissionState;
|
|
@@ -31,4 +40,10 @@ export interface PermissionCheckResult {
|
|
|
31
40
|
source: "tool" | "bash" | "mcp" | "skill" | "special" | "default" | "session";
|
|
32
41
|
/** Which source contributed the winning rule. */
|
|
33
42
|
origin: RuleOrigin;
|
|
43
|
+
/**
|
|
44
|
+
* Execution context of the offending nested command, when the winning bash
|
|
45
|
+
* unit came from a substitution or subshell. Absent for current-shell
|
|
46
|
+
* (top-level) commands.
|
|
47
|
+
*/
|
|
48
|
+
commandContext?: BashCommandContext;
|
|
34
49
|
}
|
|
@@ -98,6 +98,22 @@ describe("formatDenyReason", () => {
|
|
|
98
98
|
);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
test("bash with nested execution context", () => {
|
|
102
|
+
expect(
|
|
103
|
+
formatDenyReason(
|
|
104
|
+
toolCtx(
|
|
105
|
+
toolCheck("bash", {
|
|
106
|
+
command: "rm -rf foo",
|
|
107
|
+
matchedPattern: "rm *",
|
|
108
|
+
commandContext: "command_substitution",
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
).toBe(
|
|
113
|
+
"[pi-permission-system] is not permitted to run 'bash' command 'rm -rf foo' (matched 'rm *', inside command substitution).",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
101
117
|
test("MCP source with target on non-mcp toolName", () => {
|
|
102
118
|
expect(
|
|
103
119
|
formatDenyReason(
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { resolveBashCommandCheck } from "#src/handlers/gates/bash-command";
|
|
4
|
+
import type { Rule } from "#src/rule";
|
|
5
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
6
|
+
|
|
7
|
+
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
8
|
+
|
|
9
|
+
type CheckPermissionFn = (
|
|
10
|
+
surface: string,
|
|
11
|
+
input: unknown,
|
|
12
|
+
agentName?: string,
|
|
13
|
+
sessionRules?: Rule[],
|
|
14
|
+
) => PermissionCheckResult;
|
|
15
|
+
|
|
16
|
+
/** Build a bash-surface check result for a single command unit. */
|
|
17
|
+
function bashResult(
|
|
18
|
+
state: PermissionCheckResult["state"],
|
|
19
|
+
command: string,
|
|
20
|
+
matchedPattern?: string,
|
|
21
|
+
): PermissionCheckResult {
|
|
22
|
+
return makeCheckResult({ state, source: "bash", command, matchedPattern });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("resolveBashCommandCheck", () => {
|
|
26
|
+
it("passes a single command straight through", () => {
|
|
27
|
+
const checkPermission = vi
|
|
28
|
+
.fn<CheckPermissionFn>()
|
|
29
|
+
.mockReturnValue(bashResult("allow", "npm install pkg", "npm *"));
|
|
30
|
+
|
|
31
|
+
const result = resolveBashCommandCheck(
|
|
32
|
+
"npm install pkg",
|
|
33
|
+
[{ text: "npm install pkg" }],
|
|
34
|
+
undefined,
|
|
35
|
+
[],
|
|
36
|
+
checkPermission,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(result.state).toBe("allow");
|
|
40
|
+
expect(checkPermission).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(checkPermission).toHaveBeenCalledWith(
|
|
42
|
+
"bash",
|
|
43
|
+
{ command: "npm install pkg" },
|
|
44
|
+
undefined,
|
|
45
|
+
[],
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("denies the chain when any sub-command is denied, reporting that command's pattern", () => {
|
|
50
|
+
const checkPermission = vi
|
|
51
|
+
.fn<CheckPermissionFn>()
|
|
52
|
+
.mockImplementation((_surface, input) => {
|
|
53
|
+
const command = (input as { command: string }).command;
|
|
54
|
+
return command.startsWith("npm")
|
|
55
|
+
? bashResult("deny", command, "npm *")
|
|
56
|
+
: bashResult("allow", command, "cd *");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = resolveBashCommandCheck(
|
|
60
|
+
"cd /p && npm install pkg",
|
|
61
|
+
[{ text: "cd /p" }, { text: "npm install pkg" }],
|
|
62
|
+
undefined,
|
|
63
|
+
[],
|
|
64
|
+
checkPermission,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(result.state).toBe("deny");
|
|
68
|
+
expect(result.matchedPattern).toBe("npm *");
|
|
69
|
+
expect(result.command).toBe("npm install pkg");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("asks when a sub-command asks and none denies", () => {
|
|
73
|
+
const checkPermission = vi
|
|
74
|
+
.fn<CheckPermissionFn>()
|
|
75
|
+
.mockImplementation((_surface, input) => {
|
|
76
|
+
const command = (input as { command: string }).command;
|
|
77
|
+
return command.startsWith("git")
|
|
78
|
+
? bashResult("ask", command, "git *")
|
|
79
|
+
: bashResult("allow", command, "cd *");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const result = resolveBashCommandCheck(
|
|
83
|
+
"cd /p && git push",
|
|
84
|
+
[{ text: "cd /p" }, { text: "git push" }],
|
|
85
|
+
undefined,
|
|
86
|
+
[],
|
|
87
|
+
checkPermission,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(result.state).toBe("ask");
|
|
91
|
+
expect(result.matchedPattern).toBe("git *");
|
|
92
|
+
expect(result.command).toBe("git push");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns the first allow result when every sub-command is allowed", () => {
|
|
96
|
+
const checkPermission = vi
|
|
97
|
+
.fn<CheckPermissionFn>()
|
|
98
|
+
.mockImplementation((_surface, input) => {
|
|
99
|
+
const command = (input as { command: string }).command;
|
|
100
|
+
return bashResult("allow", command, `${command} *`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = resolveBashCommandCheck(
|
|
104
|
+
"a && b",
|
|
105
|
+
[{ text: "a" }, { text: "b" }],
|
|
106
|
+
undefined,
|
|
107
|
+
[],
|
|
108
|
+
checkPermission,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(result.state).toBe("allow");
|
|
112
|
+
expect(result.matchedPattern).toBe("a *");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("falls back to the whole command when no top-level commands are found", () => {
|
|
116
|
+
const checkPermission = vi
|
|
117
|
+
.fn<CheckPermissionFn>()
|
|
118
|
+
.mockReturnValue(bashResult("ask", "( rm x )", "*"));
|
|
119
|
+
|
|
120
|
+
const result = resolveBashCommandCheck(
|
|
121
|
+
"( rm x )",
|
|
122
|
+
[],
|
|
123
|
+
undefined,
|
|
124
|
+
[],
|
|
125
|
+
checkPermission,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(result.state).toBe("ask");
|
|
129
|
+
expect(result.commandContext).toBeUndefined();
|
|
130
|
+
expect(checkPermission).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(checkPermission).toHaveBeenCalledWith(
|
|
132
|
+
"bash",
|
|
133
|
+
{ command: "( rm x )" },
|
|
134
|
+
undefined,
|
|
135
|
+
[],
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("forwards the agent name and session rules to each sub-command check", () => {
|
|
140
|
+
const sessionRules: Rule[] = [
|
|
141
|
+
{ surface: "bash", pattern: "npm *", action: "allow", origin: "session" },
|
|
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
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(checkPermission).toHaveBeenCalledWith(
|
|
156
|
+
"bash",
|
|
157
|
+
{ command: "npm i" },
|
|
158
|
+
"agent-x",
|
|
159
|
+
sessionRules,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("tags the winning result with the offending command's execution context", () => {
|
|
164
|
+
const checkPermission = vi
|
|
165
|
+
.fn<CheckPermissionFn>()
|
|
166
|
+
.mockImplementation((_surface, input) => {
|
|
167
|
+
const command = (input as { command: string }).command;
|
|
168
|
+
return command.startsWith("rm")
|
|
169
|
+
? bashResult("deny", command, "rm *")
|
|
170
|
+
: bashResult("allow", command, "echo *");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = resolveBashCommandCheck(
|
|
174
|
+
"echo $(rm -rf foo)",
|
|
175
|
+
[
|
|
176
|
+
{ text: "echo $(rm -rf foo)" },
|
|
177
|
+
{ text: "rm -rf foo", context: "command_substitution" },
|
|
178
|
+
],
|
|
179
|
+
undefined,
|
|
180
|
+
[],
|
|
181
|
+
checkPermission,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(result.state).toBe("deny");
|
|
185
|
+
expect(result.command).toBe("rm -rf foo");
|
|
186
|
+
expect(result.commandContext).toBe("command_substitution");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("leaves commandContext unset when the winning command is top-level", () => {
|
|
190
|
+
const checkPermission = vi
|
|
191
|
+
.fn<CheckPermissionFn>()
|
|
192
|
+
.mockReturnValue(bashResult("deny", "rm -rf foo", "rm *"));
|
|
193
|
+
|
|
194
|
+
const result = resolveBashCommandCheck(
|
|
195
|
+
"rm -rf foo",
|
|
196
|
+
[{ text: "rm -rf foo" }],
|
|
197
|
+
undefined,
|
|
198
|
+
[],
|
|
199
|
+
checkPermission,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(result.state).toBe("deny");
|
|
203
|
+
expect(result.commandContext).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
3
|
import { describeBashExternalDirectoryGate } from "#src/handlers/gates/bash-external-directory";
|
|
4
|
+
import { BashProgram } from "#src/handlers/gates/bash-program";
|
|
3
5
|
import type {
|
|
4
6
|
GateBypass,
|
|
5
7
|
GateDescriptor,
|
|
8
|
+
GateResult,
|
|
6
9
|
} from "#src/handlers/gates/descriptor";
|
|
7
10
|
import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
|
|
8
11
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
@@ -34,11 +37,34 @@ function makeCheckResult(
|
|
|
34
37
|
};
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Mirror the handler's parse-once derivation: parse the bash command into a
|
|
42
|
+
* shared `BashProgram` and inject it, exactly as `permission-gate-handler.ts`
|
|
43
|
+
* does, so the gate is exercised through the production wiring.
|
|
44
|
+
*/
|
|
45
|
+
async function describeGate(
|
|
46
|
+
tcc: ToolCallContext,
|
|
47
|
+
checkPermission: Parameters<typeof describeBashExternalDirectoryGate>[2],
|
|
48
|
+
getSessionRuleset: Parameters<typeof describeBashExternalDirectoryGate>[3],
|
|
49
|
+
): Promise<GateResult> {
|
|
50
|
+
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
51
|
+
const bashProgram =
|
|
52
|
+
tcc.toolName === "bash" && command
|
|
53
|
+
? await BashProgram.parse(command)
|
|
54
|
+
: null;
|
|
55
|
+
return describeBashExternalDirectoryGate(
|
|
56
|
+
tcc,
|
|
57
|
+
bashProgram,
|
|
58
|
+
checkPermission,
|
|
59
|
+
getSessionRuleset,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
38
64
|
|
|
39
65
|
describe("describeBashExternalDirectoryGate", () => {
|
|
40
66
|
it("returns null when tool is not bash", async () => {
|
|
41
|
-
const result = await
|
|
67
|
+
const result = await describeGate(
|
|
42
68
|
makeTcc({ toolName: "read" }),
|
|
43
69
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
44
70
|
vi.fn().mockReturnValue([]),
|
|
@@ -47,7 +73,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
47
73
|
});
|
|
48
74
|
|
|
49
75
|
it("returns null when no CWD", async () => {
|
|
50
|
-
const result = await
|
|
76
|
+
const result = await describeGate(
|
|
51
77
|
makeTcc({ cwd: undefined }),
|
|
52
78
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
53
79
|
vi.fn().mockReturnValue([]),
|
|
@@ -56,7 +82,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
56
82
|
});
|
|
57
83
|
|
|
58
84
|
it("returns null when command has no external paths", async () => {
|
|
59
|
-
const result = await
|
|
85
|
+
const result = await describeGate(
|
|
60
86
|
makeTcc({ input: { command: "ls -la" } }),
|
|
61
87
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
62
88
|
vi.fn().mockReturnValue([]),
|
|
@@ -68,7 +94,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
68
94
|
const checkPermission = vi
|
|
69
95
|
.fn()
|
|
70
96
|
.mockReturnValue(makeCheckResult("allow", { source: "session" }));
|
|
71
|
-
const result = await
|
|
97
|
+
const result = await describeGate(
|
|
72
98
|
makeTcc(),
|
|
73
99
|
checkPermission,
|
|
74
100
|
vi.fn().mockReturnValue([]),
|
|
@@ -85,7 +111,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
85
111
|
|
|
86
112
|
it("returns GateDescriptor with multi-pattern sessionApproval for uncovered paths", async () => {
|
|
87
113
|
const checkPermission = vi.fn().mockReturnValue(makeCheckResult("ask"));
|
|
88
|
-
const result = await
|
|
114
|
+
const result = await describeGate(
|
|
89
115
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
90
116
|
checkPermission,
|
|
91
117
|
vi.fn().mockReturnValue([]),
|
|
@@ -110,7 +136,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
110
136
|
return makeCheckResult("ask");
|
|
111
137
|
},
|
|
112
138
|
);
|
|
113
|
-
const result = await
|
|
139
|
+
const result = await describeGate(
|
|
114
140
|
makeTcc(),
|
|
115
141
|
checkPermission,
|
|
116
142
|
vi.fn().mockReturnValue([]),
|
|
@@ -132,7 +158,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
132
158
|
return makeCheckResult("ask");
|
|
133
159
|
},
|
|
134
160
|
);
|
|
135
|
-
const result = await
|
|
161
|
+
const result = await describeGate(
|
|
136
162
|
makeTcc(),
|
|
137
163
|
checkPermission,
|
|
138
164
|
vi.fn().mockReturnValue([]),
|
|
@@ -143,7 +169,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
143
169
|
});
|
|
144
170
|
|
|
145
171
|
it("descriptor surface is 'external_directory'", async () => {
|
|
146
|
-
const result = await
|
|
172
|
+
const result = await describeGate(
|
|
147
173
|
makeTcc(),
|
|
148
174
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
149
175
|
vi.fn().mockReturnValue([]),
|
|
@@ -153,7 +179,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
153
179
|
});
|
|
154
180
|
|
|
155
181
|
it("descriptor decision surface is 'external_directory'", async () => {
|
|
156
|
-
const result = await
|
|
182
|
+
const result = await describeGate(
|
|
157
183
|
makeTcc(),
|
|
158
184
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
159
185
|
vi.fn().mockReturnValue([]),
|
|
@@ -163,7 +189,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
163
189
|
});
|
|
164
190
|
|
|
165
191
|
it("denialContext contains the command and external paths", async () => {
|
|
166
|
-
const result = await
|
|
192
|
+
const result = await describeGate(
|
|
167
193
|
makeTcc({ input: { command: "cat /outside/file.ts" } }),
|
|
168
194
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
169
195
|
vi.fn().mockReturnValue([]),
|
|
@@ -177,7 +203,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
177
203
|
});
|
|
178
204
|
|
|
179
205
|
it("promptDetails includes command and tool_call source", async () => {
|
|
180
|
-
const result = await
|
|
206
|
+
const result = await describeGate(
|
|
181
207
|
makeTcc({ agentName: "agent-1", toolCallId: "tc-5" }),
|
|
182
208
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
183
209
|
vi.fn().mockReturnValue([]),
|
|
@@ -203,7 +229,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
203
229
|
return makeCheckResult("ask");
|
|
204
230
|
},
|
|
205
231
|
);
|
|
206
|
-
const result = await
|
|
232
|
+
const result = await describeGate(
|
|
207
233
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
208
234
|
checkPermission,
|
|
209
235
|
vi.fn().mockReturnValue([]),
|
|
@@ -227,7 +253,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
227
253
|
return makeCheckResult("ask");
|
|
228
254
|
},
|
|
229
255
|
);
|
|
230
|
-
const result = await
|
|
256
|
+
const result = await describeGate(
|
|
231
257
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
232
258
|
checkPermission,
|
|
233
259
|
vi.fn().mockReturnValue([]),
|
|
@@ -252,7 +278,7 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
252
278
|
return makeCheckResult("ask");
|
|
253
279
|
},
|
|
254
280
|
);
|
|
255
|
-
const result = await
|
|
281
|
+
const result = await describeGate(
|
|
256
282
|
makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
|
|
257
283
|
checkPermission,
|
|
258
284
|
vi.fn().mockReturnValue([]),
|