@gotgenes/pi-permission-system 5.5.1 → 5.6.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.
@@ -1,10 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { evaluateToolGate } from "../../../src/handlers/gates/tool";
4
- import type {
5
- ToolCallContext,
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("evaluateToolGate", () => {
55
- it("allows when policy is allow", async () => {
56
- const deps = makeToolGateDeps();
57
- const result = await evaluateToolGate(makeTcc(), deps);
58
- expect(result).toEqual({ action: "allow" });
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("blocks when policy is deny", async () => {
62
- const deps = makeToolGateDeps({
63
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
64
- });
65
- const result = await evaluateToolGate(makeTcc(), deps);
66
- expect(result).toMatchObject({ action: "block" });
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("allows on session-approved fast path", async () => {
70
- const deps = makeToolGateDeps({
71
- checkPermission: vi.fn().mockReturnValue(
72
- makeCheckResult("allow", {
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 result = await evaluateToolGate(
60
+ const desc = describeToolGate(
79
61
  makeTcc({ toolName: "bash", input: { command: "git status" } }),
80
- deps,
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("blocks when state is ask but canConfirm is false", async () => {
93
- const deps = makeToolGateDeps({
94
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
95
- canConfirm: vi.fn().mockReturnValue(false),
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 result = await evaluateToolGate(makeTcc(), deps);
98
- expect(result).toMatchObject({ action: "block" });
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("allows when state is ask and user approves", async () => {
102
- const deps = makeToolGateDeps({
103
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
104
- promptPermission: vi
105
- .fn()
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("blocks when state is ask and user denies", async () => {
113
- const deps = makeToolGateDeps({
114
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
115
- promptPermission: vi
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 result = await evaluateToolGate(makeTcc(), deps);
120
- expect(result).toMatchObject({ action: "block" });
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("approves session rule when user approves for session", async () => {
124
- const deps = makeToolGateDeps({
125
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
126
- promptPermission: vi
127
- .fn()
128
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
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
- await evaluateToolGate(makeTcc(), deps);
131
- expect(deps.approveSessionRule).toHaveBeenCalled();
126
+ expect(reason).toContain("too risky");
132
127
  });
133
128
 
134
- it("emits decision event with correct surface and result", async () => {
135
- const deps = makeToolGateDeps({
136
- checkPermission: vi
137
- .fn()
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
- await evaluateToolGate(makeTcc({ toolName: "write" }), deps);
143
- expect(deps.emitDecision).toHaveBeenCalledWith(
144
- expect.objectContaining({
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("passes session ruleset to checkPermission", async () => {
154
- const sessionRules = [
155
- {
156
- surface: "bash",
157
- pattern: "git *",
158
- action: "allow" as const,
159
- origin: "session" as const,
160
- },
161
- ];
162
- const deps = makeToolGateDeps({
163
- getSessionRuleset: vi.fn().mockReturnValue(sessionRules),
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
- await evaluateToolGate(makeTcc({ toolName: "bash" }), deps);
166
- expect(deps.checkPermission).toHaveBeenCalledWith(
167
- "bash",
168
- expect.anything(),
169
- undefined,
170
- sessionRules,
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
  });