@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
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
ToolGateDeps,
|
|
7
|
-
} from "../../../src/handlers/gates/types";
|
|
3
|
+
import type { GateDescriptor } from "../../../src/handlers/gates/descriptor";
|
|
4
|
+
import { describeToolGate } from "../../../src/handlers/gates/tool";
|
|
5
|
+
import type { ToolCallContext } from "../../../src/handlers/gates/types";
|
|
8
6
|
import type { PermissionCheckResult } from "../../../src/types";
|
|
9
7
|
|
|
10
8
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -34,140 +32,149 @@ function makeCheckResult(
|
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
function makeToolGateDeps(overrides: Partial<ToolGateDeps> = {}): ToolGateDeps {
|
|
38
|
-
return {
|
|
39
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
40
|
-
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
41
|
-
approveSessionRule: vi.fn(),
|
|
42
|
-
writeReviewLog: vi.fn(),
|
|
43
|
-
emitDecision: 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
35
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
53
36
|
|
|
54
|
-
describe("
|
|
55
|
-
it("
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
describe("describeToolGate", () => {
|
|
38
|
+
it("returns descriptor with tool name as surface for standard tools", () => {
|
|
39
|
+
const desc = describeToolGate(
|
|
40
|
+
makeTcc({ toolName: "read" }),
|
|
41
|
+
makeCheckResult("ask"),
|
|
42
|
+
);
|
|
43
|
+
expect(desc.surface).toBe("read");
|
|
44
|
+
expect(desc.decision.surface).toBe("read");
|
|
59
45
|
});
|
|
60
46
|
|
|
61
|
-
it("
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(
|
|
47
|
+
it("returns descriptor with tool name as decision value for standard tools", () => {
|
|
48
|
+
const desc = describeToolGate(
|
|
49
|
+
makeTcc({ toolName: "write" }),
|
|
50
|
+
makeCheckResult("ask"),
|
|
51
|
+
);
|
|
52
|
+
expect(desc.decision.value).toBe("write");
|
|
67
53
|
});
|
|
68
54
|
|
|
69
|
-
it("
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
source: "session",
|
|
74
|
-
matchedPattern: "git *",
|
|
75
|
-
}),
|
|
76
|
-
),
|
|
55
|
+
it("returns bash surface with command in decision.value for bash tools", () => {
|
|
56
|
+
const check = makeCheckResult("ask", {
|
|
57
|
+
toolName: "bash",
|
|
58
|
+
command: "git status",
|
|
77
59
|
});
|
|
78
|
-
const
|
|
60
|
+
const desc = describeToolGate(
|
|
79
61
|
makeTcc({ toolName: "bash", input: { command: "git status" } }),
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
expect(result).toEqual({ action: "allow" });
|
|
83
|
-
expect(deps.writeReviewLog).toHaveBeenCalledWith(
|
|
84
|
-
"permission_request.session_approved",
|
|
85
|
-
expect.objectContaining({ resolution: "session_approved" }),
|
|
86
|
-
);
|
|
87
|
-
expect(deps.emitDecision).toHaveBeenCalledWith(
|
|
88
|
-
expect.objectContaining({ resolution: "session_approved" }),
|
|
62
|
+
check,
|
|
89
63
|
);
|
|
64
|
+
expect(desc.surface).toBe("bash");
|
|
65
|
+
expect(desc.decision.surface).toBe("bash");
|
|
66
|
+
expect(desc.decision.value).toBe("git status");
|
|
90
67
|
});
|
|
91
68
|
|
|
92
|
-
it("
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
69
|
+
it("returns mcp surface with target in decision.value for MCP tools", () => {
|
|
70
|
+
const check = makeCheckResult("ask", {
|
|
71
|
+
toolName: "mcp",
|
|
72
|
+
target: "server:tool",
|
|
96
73
|
});
|
|
97
|
-
const
|
|
98
|
-
|
|
74
|
+
const desc = describeToolGate(
|
|
75
|
+
makeTcc({ toolName: "mcp", input: { tool: "server:tool" } }),
|
|
76
|
+
check,
|
|
77
|
+
);
|
|
78
|
+
expect(desc.surface).toBe("mcp");
|
|
79
|
+
expect(desc.decision.surface).toBe("mcp");
|
|
80
|
+
expect(desc.decision.value).toBe("server:tool");
|
|
99
81
|
});
|
|
100
82
|
|
|
101
|
-
it("
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
107
|
-
});
|
|
108
|
-
const result = await evaluateToolGate(makeTcc(), deps);
|
|
109
|
-
expect(result).toEqual({ action: "allow" });
|
|
83
|
+
it("populates messages.denyReason via formatDenyReason", () => {
|
|
84
|
+
const check = makeCheckResult("deny", { toolName: "read" });
|
|
85
|
+
const desc = describeToolGate(makeTcc(), check);
|
|
86
|
+
expect(desc.messages.denyReason).toContain("read");
|
|
87
|
+
expect(desc.messages.denyReason).toContain("not permitted");
|
|
110
88
|
});
|
|
111
89
|
|
|
112
|
-
it("
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
.fn()
|
|
117
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
90
|
+
it("populates messages.unavailableReason with bash command when tool is bash", () => {
|
|
91
|
+
const check = makeCheckResult("ask", {
|
|
92
|
+
toolName: "bash",
|
|
93
|
+
command: "rm -rf /",
|
|
118
94
|
});
|
|
119
|
-
const
|
|
120
|
-
|
|
95
|
+
const desc = describeToolGate(
|
|
96
|
+
makeTcc({ toolName: "bash", input: { command: "rm -rf /" } }),
|
|
97
|
+
check,
|
|
98
|
+
);
|
|
99
|
+
expect(desc.messages.unavailableReason).toContain("rm -rf /");
|
|
100
|
+
expect(desc.messages.unavailableReason).toContain("no interactive UI");
|
|
121
101
|
});
|
|
122
102
|
|
|
123
|
-
it("
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
103
|
+
it("populates messages.unavailableReason with tool name for non-bash tools", () => {
|
|
104
|
+
const desc = describeToolGate(
|
|
105
|
+
makeTcc({ toolName: "write" }),
|
|
106
|
+
makeCheckResult("ask"),
|
|
107
|
+
);
|
|
108
|
+
expect(desc.messages.unavailableReason).toContain("write");
|
|
109
|
+
expect(desc.messages.unavailableReason).toContain("no interactive UI");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("populates messages.unavailableReason with mcp for mcp tool", () => {
|
|
113
|
+
const check = makeCheckResult("ask", { toolName: "mcp", target: "s:t" });
|
|
114
|
+
const desc = describeToolGate(makeTcc({ toolName: "mcp" }), check);
|
|
115
|
+
expect(desc.messages.unavailableReason).toContain("mcp");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("populates messages.userDeniedReason as a function", () => {
|
|
119
|
+
const check = makeCheckResult("ask", { toolName: "read" });
|
|
120
|
+
const desc = describeToolGate(makeTcc(), check);
|
|
121
|
+
const reason = desc.messages.userDeniedReason({
|
|
122
|
+
approved: false,
|
|
123
|
+
state: "denied",
|
|
124
|
+
denialReason: "too risky",
|
|
129
125
|
});
|
|
130
|
-
|
|
131
|
-
expect(deps.approveSessionRule).toHaveBeenCalled();
|
|
126
|
+
expect(reason).toContain("too risky");
|
|
132
127
|
});
|
|
133
128
|
|
|
134
|
-
it("
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.mockReturnValue(
|
|
139
|
-
makeCheckResult("allow", { origin: "global", matchedPattern: "*" }),
|
|
140
|
-
),
|
|
129
|
+
it("populates sessionApproval via suggestSessionPattern", () => {
|
|
130
|
+
const check = makeCheckResult("ask", {
|
|
131
|
+
toolName: "bash",
|
|
132
|
+
command: "git status",
|
|
141
133
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
surface: "write",
|
|
146
|
-
result: "allow",
|
|
147
|
-
resolution: "policy_allow",
|
|
148
|
-
origin: "global",
|
|
149
|
-
}),
|
|
134
|
+
const desc = describeToolGate(
|
|
135
|
+
makeTcc({ toolName: "bash", input: { command: "git status" } }),
|
|
136
|
+
check,
|
|
150
137
|
);
|
|
138
|
+
expect(desc.sessionApproval).toBeDefined();
|
|
139
|
+
expect(desc.sessionApproval!).toHaveProperty("surface", "bash");
|
|
140
|
+
expect(desc.sessionApproval!).toHaveProperty("pattern");
|
|
151
141
|
});
|
|
152
142
|
|
|
153
|
-
it("
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
143
|
+
it("populates promptDetails with correct fields", () => {
|
|
144
|
+
const check = makeCheckResult("ask");
|
|
145
|
+
const desc = describeToolGate(
|
|
146
|
+
makeTcc({ toolName: "read", agentName: "my-agent", toolCallId: "tc-42" }),
|
|
147
|
+
check,
|
|
148
|
+
);
|
|
149
|
+
expect(desc.promptDetails).toMatchObject({
|
|
150
|
+
source: "tool_call",
|
|
151
|
+
agentName: "my-agent",
|
|
152
|
+
toolCallId: "tc-42",
|
|
153
|
+
toolName: "read",
|
|
164
154
|
});
|
|
165
|
-
|
|
166
|
-
expect(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
expect(desc.promptDetails.message).toBeDefined();
|
|
156
|
+
expect(desc.promptDetails.sessionLabel).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("populates logContext with tool input preview fields", () => {
|
|
160
|
+
const check = makeCheckResult("ask", { toolName: "bash", command: "ls" });
|
|
161
|
+
const desc = describeToolGate(
|
|
162
|
+
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
163
|
+
check,
|
|
164
|
+
);
|
|
165
|
+
expect(desc.logContext).toMatchObject({
|
|
166
|
+
source: "tool_call",
|
|
167
|
+
toolName: "bash",
|
|
168
|
+
});
|
|
169
|
+
expect(desc.logContext.command).toBe("ls");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("uses toolName as input for checkPermission surface", () => {
|
|
173
|
+
const desc = describeToolGate(
|
|
174
|
+
makeTcc({ toolName: "edit", input: { path: "/a.ts" } }),
|
|
175
|
+
makeCheckResult("ask", { toolName: "edit" }),
|
|
171
176
|
);
|
|
177
|
+
expect(desc.surface).toBe("edit");
|
|
178
|
+
expect(desc.input).toEqual({ path: "/a.ts" });
|
|
172
179
|
});
|
|
173
180
|
});
|