@gotgenes/pi-permission-system 10.4.0 → 10.5.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +1 -1
  3. package/src/handlers/before-agent-start.ts +11 -6
  4. package/src/handlers/gates/bash-command.ts +2 -2
  5. package/src/handlers/gates/bash-external-directory.ts +2 -2
  6. package/src/handlers/gates/bash-path.ts +2 -2
  7. package/src/handlers/gates/path.ts +2 -2
  8. package/src/handlers/gates/runner.ts +2 -2
  9. package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
  10. package/src/handlers/lifecycle.ts +7 -4
  11. package/src/handlers/permission-gate-handler.ts +3 -3
  12. package/src/index.ts +13 -4
  13. package/src/permission-resolver.ts +66 -2
  14. package/src/permission-session.ts +8 -72
  15. package/src/session-rules.ts +3 -2
  16. package/src/skill-prompt-sanitizer.ts +1 -1
  17. package/test/handlers/before-agent-start.test.ts +56 -86
  18. package/test/handlers/external-directory-session-dedup.test.ts +80 -160
  19. package/test/handlers/gates/bash-external-directory.test.ts +2 -2
  20. package/test/handlers/gates/bash-path.test.ts +2 -2
  21. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
  22. package/test/handlers/input.test.ts +5 -4
  23. package/test/handlers/lifecycle.test.ts +79 -85
  24. package/test/handlers/tool-call.test.ts +3 -2
  25. package/test/helpers/gate-fixtures.ts +5 -9
  26. package/test/helpers/handler-fixtures.ts +100 -107
  27. package/test/helpers/session-fixtures.ts +192 -0
  28. package/test/permission-resolver.test.ts +196 -0
  29. package/test/permission-session.test.ts +14 -266
  30. package/test/session-rules.test.ts +13 -5
  31. package/src/agent-prep-session.ts +0 -28
  32. package/src/gate-handler-session.ts +0 -13
  33. package/src/session-lifecycle-session.ts +0 -24
@@ -40,9 +40,10 @@ describe("ToolCallGatePipeline", () => {
40
40
 
41
41
  describe("evaluate — non-bash tool", () => {
42
42
  it("returns allow when all gates pass", async () => {
43
+ const resolver = makeResolver(makeCheckResult());
43
44
  const inputs = makeGateInputs();
44
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
45
- const pipeline = new ToolCallGatePipeline(inputs);
45
+ const { runner } = makeGateRunner();
46
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
46
47
 
47
48
  const result = await pipeline.evaluate(
48
49
  makeTcc({ toolName: "read", input: {} }),
@@ -53,12 +54,12 @@ describe("ToolCallGatePipeline", () => {
53
54
  });
54
55
 
55
56
  it("returns block when the tool gate denies", async () => {
56
- const { resolve } = makeResolver(
57
+ const resolver = makeResolver(
57
58
  makeCheckResult({ state: "deny", matchedPattern: "*" }),
58
59
  );
59
- const inputs = makeGateInputs({ resolve });
60
- const { runner } = makeGateRunner({ resolve });
61
- const pipeline = new ToolCallGatePipeline(inputs);
60
+ const inputs = makeGateInputs();
61
+ const { runner } = makeGateRunner();
62
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
62
63
 
63
64
  const result = await pipeline.evaluate(
64
65
  makeTcc({ toolName: "read", input: {} }),
@@ -69,13 +70,14 @@ describe("ToolCallGatePipeline", () => {
69
70
  });
70
71
 
71
72
  it("short-circuits after the first blocking gate without evaluating later ones", async () => {
73
+ const resolver = makeResolver(makeCheckResult());
72
74
  const inputs = makeGateInputs();
73
75
  const { runner } = makeGateRunner();
74
76
  const runSpy = vi
75
77
  .spyOn(runner, "run")
76
78
  .mockResolvedValue({ action: "block", reason: "first gate blocked" });
77
79
 
78
- const pipeline = new ToolCallGatePipeline(inputs);
80
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
79
81
  const result = await pipeline.evaluate(
80
82
  makeTcc({ toolName: "read", input: {} }),
81
83
  runner,
@@ -92,9 +94,10 @@ describe("ToolCallGatePipeline", () => {
92
94
  toolTextSummaryMaxLength: 100,
93
95
  toolInputLogPreviewMaxLength: 200,
94
96
  }));
97
+ const resolver = makeResolver(makeCheckResult());
95
98
  const inputs = makeGateInputs({ getToolPreviewLimits });
96
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
97
- const pipeline = new ToolCallGatePipeline(inputs);
99
+ const { runner } = makeGateRunner();
100
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
98
101
 
99
102
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
100
103
 
@@ -103,9 +106,10 @@ describe("ToolCallGatePipeline", () => {
103
106
 
104
107
  it("calls getInfrastructureReadDirs() during evaluate", async () => {
105
108
  const getInfrastructureReadDirs = vi.fn<() => string[]>(() => []);
109
+ const resolver = makeResolver(makeCheckResult());
106
110
  const inputs = makeGateInputs({ getInfrastructureReadDirs });
107
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
108
- const pipeline = new ToolCallGatePipeline(inputs);
111
+ const { runner } = makeGateRunner();
112
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
109
113
 
110
114
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
111
115
 
@@ -114,9 +118,10 @@ describe("ToolCallGatePipeline", () => {
114
118
 
115
119
  it("calls getActiveSkillEntries() during evaluate", async () => {
116
120
  const getActiveSkillEntries = vi.fn<() => []>(() => []);
121
+ const resolver = makeResolver(makeCheckResult());
117
122
  const inputs = makeGateInputs({ getActiveSkillEntries });
118
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
119
- const pipeline = new ToolCallGatePipeline(inputs);
123
+ const { runner } = makeGateRunner();
124
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
120
125
 
121
126
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
122
127
 
@@ -124,9 +129,10 @@ describe("ToolCallGatePipeline", () => {
124
129
  });
125
130
 
126
131
  it("does not call BashProgram.parse for non-bash tools", async () => {
132
+ const resolver = makeResolver(makeCheckResult());
127
133
  const inputs = makeGateInputs();
128
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
129
- const pipeline = new ToolCallGatePipeline(inputs);
134
+ const { runner } = makeGateRunner();
135
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
130
136
 
131
137
  await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
132
138
 
@@ -138,9 +144,10 @@ describe("ToolCallGatePipeline", () => {
138
144
 
139
145
  describe("evaluate — bash tool", () => {
140
146
  it("returns allow when the bash command is permitted", async () => {
147
+ const resolver = makeResolver(makeCheckResult());
141
148
  const inputs = makeGateInputs();
142
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
143
- const pipeline = new ToolCallGatePipeline(inputs);
149
+ const { runner } = makeGateRunner();
150
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
144
151
 
145
152
  const result = await pipeline.evaluate(
146
153
  makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
@@ -151,9 +158,10 @@ describe("ToolCallGatePipeline", () => {
151
158
  });
152
159
 
153
160
  it("parses BashProgram exactly once per evaluate for bash tools with a command", async () => {
161
+ const resolver = makeResolver(makeCheckResult());
154
162
  const inputs = makeGateInputs();
155
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
156
- const pipeline = new ToolCallGatePipeline(inputs);
163
+ const { runner } = makeGateRunner();
164
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
157
165
 
158
166
  await pipeline.evaluate(
159
167
  makeTcc({ toolName: "bash", input: { command: "echo hello" } }),
@@ -165,9 +173,10 @@ describe("ToolCallGatePipeline", () => {
165
173
  });
166
174
 
167
175
  it("does not parse BashProgram when the bash command is empty", async () => {
176
+ const resolver = makeResolver(makeCheckResult());
168
177
  const inputs = makeGateInputs();
169
- const { runner } = makeGateRunner({ resolve: inputs.resolve });
170
- const pipeline = new ToolCallGatePipeline(inputs);
178
+ const { runner } = makeGateRunner();
179
+ const pipeline = new ToolCallGatePipeline(resolver, inputs);
171
180
 
172
181
  await pipeline.evaluate(
173
182
  makeTcc({ toolName: "bash", input: { command: "" } }),
@@ -49,9 +49,10 @@ describe("extractSkillNameFromInput", () => {
49
49
  describe("handleInput", () => {
50
50
  it("activates session with ctx", async () => {
51
51
  const ctx = makeCtx();
52
- const { handler, session } = makeHandler();
52
+ const { handler, forwarding } = makeHandler();
53
53
  await handler.handleInput(makeInputEvent("hello"), ctx);
54
- expect(session.activate).toHaveBeenCalledWith(ctx);
54
+ // session.activate(ctx) calls forwarding.start(ctx) on the real session
55
+ expect(forwarding.start).toHaveBeenCalledWith(ctx);
55
56
  });
56
57
 
57
58
  it("returns continue for non-skill input", async () => {
@@ -64,9 +65,9 @@ describe("handleInput", () => {
64
65
  });
65
66
 
66
67
  it("does not check permissions for non-skill input", async () => {
67
- const { handler, session } = makeHandler();
68
+ const { handler, permissionManager } = makeHandler();
68
69
  await handler.handleInput(makeInputEvent("just a message"), makeCtx());
69
- expect(session.checkPermission).not.toHaveBeenCalled();
70
+ expect(permissionManager.checkPermission).not.toHaveBeenCalled();
70
71
  });
71
72
 
72
73
  it("returns continue when skill is allowed", async () => {
@@ -2,9 +2,12 @@ import { describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import { SessionLifecycleHandler } from "#src/handlers/lifecycle";
4
4
  import type { ServiceLifecycle } from "#src/service-lifecycle";
5
- import type { SessionLifecycleSession } from "#src/session-lifecycle-session";
6
5
 
7
6
  import { makeCtx } from "#test/helpers/handler-fixtures";
7
+ import {
8
+ makeRealResolver,
9
+ makeRealSession,
10
+ } from "#test/helpers/session-fixtures";
8
11
 
9
12
  // ── status stub ────────────────────────────────────────────────────────────
10
13
  vi.mock("../../src/status", () => ({
@@ -15,55 +18,40 @@ vi.mock("../../src/status", () => ({
15
18
 
16
19
  // ── helpers ────────────────────────────────────────────────────────────────
17
20
 
18
- function makeSession(
19
- overrides: Partial<SessionLifecycleSession> = {},
20
- ): SessionLifecycleSession {
21
- return {
22
- logger: overrides.logger ?? {
23
- debug: vi.fn<SessionLifecycleSession["logger"]["debug"]>(),
24
- review: vi.fn<SessionLifecycleSession["logger"]["review"]>(),
25
- warn: vi.fn<SessionLifecycleSession["logger"]["warn"]>(),
26
- },
27
- refreshConfig:
28
- overrides.refreshConfig ??
29
- vi.fn<SessionLifecycleSession["refreshConfig"]>(),
30
- resetForNewSession:
31
- overrides.resetForNewSession ??
32
- vi.fn<SessionLifecycleSession["resetForNewSession"]>(),
33
- logResolvedConfigPaths:
34
- overrides.logResolvedConfigPaths ??
35
- vi.fn<SessionLifecycleSession["logResolvedConfigPaths"]>(),
36
- resolveAgentName:
37
- overrides.resolveAgentName ??
38
- vi
39
- .fn<SessionLifecycleSession["resolveAgentName"]>()
40
- .mockReturnValue(null),
41
- getConfigIssues:
42
- overrides.getConfigIssues ??
43
- vi.fn<SessionLifecycleSession["getConfigIssues"]>().mockReturnValue([]),
44
- reload: overrides.reload ?? vi.fn<SessionLifecycleSession["reload"]>(),
45
- getRuntimeContext:
46
- overrides.getRuntimeContext ??
47
- vi
48
- .fn<SessionLifecycleSession["getRuntimeContext"]>()
49
- .mockReturnValue(null),
50
- shutdown:
51
- overrides.shutdown ?? vi.fn<SessionLifecycleSession["shutdown"]>(),
52
- };
53
- }
54
-
55
- function makeHandler(overrides?: Partial<SessionLifecycleSession>): {
56
- handler: SessionLifecycleHandler;
57
- session: SessionLifecycleSession;
58
- serviceLifecycle: ServiceLifecycle;
59
- } {
60
- const session = makeSession(overrides);
21
+ function makeSetup(opts?: { configIssues?: string[] }) {
22
+ const {
23
+ session,
24
+ permissionManager,
25
+ sessionRules,
26
+ logger,
27
+ forwarding,
28
+ configStore,
29
+ } = makeRealSession();
30
+ const { resolver } = makeRealResolver(permissionManager, sessionRules);
31
+ if (opts?.configIssues) {
32
+ vi.mocked(permissionManager.getConfigIssues).mockReturnValue(
33
+ opts.configIssues,
34
+ );
35
+ }
61
36
  const serviceLifecycle: ServiceLifecycle = {
62
37
  activate: vi.fn<ServiceLifecycle["activate"]>(),
63
38
  teardown: vi.fn<ServiceLifecycle["teardown"]>(),
64
39
  };
65
- const handler = new SessionLifecycleHandler(session, serviceLifecycle);
66
- return { handler, session, serviceLifecycle };
40
+ const handler = new SessionLifecycleHandler(
41
+ session,
42
+ resolver,
43
+ serviceLifecycle,
44
+ );
45
+ return {
46
+ handler,
47
+ session,
48
+ resolver,
49
+ permissionManager,
50
+ logger,
51
+ forwarding,
52
+ configStore,
53
+ serviceLifecycle,
54
+ };
67
55
  }
68
56
 
69
57
  // ── handleSessionStart ─────────────────────────────────────────────────────
@@ -71,51 +59,53 @@ function makeHandler(overrides?: Partial<SessionLifecycleSession>): {
71
59
  describe("handleSessionStart", () => {
72
60
  it("refreshes config with ctx", async () => {
73
61
  const ctx = makeCtx();
74
- const { handler, session } = makeHandler();
62
+ const { handler, configStore } = makeSetup();
75
63
  await handler.handleSessionStart({ reason: "startup" }, ctx);
76
- expect(session.refreshConfig).toHaveBeenCalledWith(ctx);
64
+ expect(configStore.refresh).toHaveBeenCalledWith(ctx);
77
65
  });
78
66
 
79
67
  it("calls resetForNewSession with ctx", async () => {
80
68
  const ctx = makeCtx();
81
- const { handler, session } = makeHandler();
69
+ const { handler, session } = makeSetup();
70
+ const spy = vi.spyOn(session, "resetForNewSession");
82
71
  await handler.handleSessionStart({ reason: "startup" }, ctx);
83
- expect(session.resetForNewSession).toHaveBeenCalledWith(ctx);
72
+ expect(spy).toHaveBeenCalledWith(ctx);
84
73
  });
85
74
 
86
75
  it("logs resolved config paths", async () => {
87
- const { handler, session } = makeHandler();
76
+ const { handler, configStore } = makeSetup();
88
77
  await handler.handleSessionStart({ reason: "startup" }, makeCtx());
89
- expect(session.logResolvedConfigPaths).toHaveBeenCalledOnce();
78
+ expect(configStore.logResolvedPaths).toHaveBeenCalledOnce();
90
79
  });
91
80
 
92
81
  it("resolves agent name from ctx", async () => {
93
82
  const ctx = makeCtx();
94
- const { handler, session } = makeHandler();
83
+ const { handler, session } = makeSetup();
84
+ const spy = vi.spyOn(session, "resolveAgentName");
95
85
  await handler.handleSessionStart({ reason: "startup" }, ctx);
96
- expect(session.resolveAgentName).toHaveBeenCalledWith(ctx);
86
+ expect(spy).toHaveBeenCalledWith(ctx);
97
87
  });
98
88
 
99
89
  it("notifies each policy issue", async () => {
100
- const { handler, session } = makeHandler({
101
- getConfigIssues: vi.fn().mockReturnValue(["issue A", "issue B"]),
90
+ const { handler, logger } = makeSetup({
91
+ configIssues: ["issue A", "issue B"],
102
92
  });
103
93
  await handler.handleSessionStart({ reason: "startup" }, makeCtx());
104
- expect(session.logger.warn).toHaveBeenCalledWith("issue A");
105
- expect(session.logger.warn).toHaveBeenCalledWith("issue B");
94
+ expect(logger.warn).toHaveBeenCalledWith("issue A");
95
+ expect(logger.warn).toHaveBeenCalledWith("issue B");
106
96
  });
107
97
 
108
98
  it("does not warn when there are no policy issues", async () => {
109
- const { handler, session } = makeHandler();
99
+ const { handler, logger } = makeSetup();
110
100
  await handler.handleSessionStart({ reason: "startup" }, makeCtx());
111
- expect(session.logger.warn).not.toHaveBeenCalled();
101
+ expect(logger.warn).not.toHaveBeenCalled();
112
102
  });
113
103
 
114
104
  it("writes lifecycle.reload debug log when reason is reload", async () => {
115
105
  const ctx = makeCtx({ cwd: "/proj" });
116
- const { handler, session } = makeHandler();
106
+ const { handler, logger } = makeSetup();
117
107
  await handler.handleSessionStart({ reason: "reload" }, ctx);
118
- expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
108
+ expect(logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
119
109
  triggeredBy: "session_start",
120
110
  reason: "reload",
121
111
  cwd: "/proj",
@@ -123,23 +113,26 @@ describe("handleSessionStart", () => {
123
113
  });
124
114
 
125
115
  it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
126
- const { handler, session } = makeHandler();
116
+ const { handler, logger } = makeSetup();
127
117
  await handler.handleSessionStart({ reason: "startup" }, makeCtx());
128
- expect(session.logger.debug).not.toHaveBeenCalled();
118
+ expect(logger.debug).not.toHaveBeenCalled();
129
119
  });
130
120
 
131
121
  it("activates the service for the session with ctx", async () => {
132
122
  const ctx = makeCtx();
133
- const { handler, serviceLifecycle } = makeHandler();
123
+ const { handler, serviceLifecycle } = makeSetup();
134
124
  await handler.handleSessionStart({ reason: "startup" }, ctx);
135
125
  expect(serviceLifecycle.activate).toHaveBeenCalledWith(ctx);
136
126
  });
137
127
 
138
128
  it("calls refreshConfig before resetForNewSession", async () => {
139
129
  const callOrder: string[] = [];
140
- const { handler } = makeHandler({
141
- refreshConfig: vi.fn(() => callOrder.push("refreshConfig")),
142
- resetForNewSession: vi.fn(() => callOrder.push("resetForNewSession")),
130
+ const { handler, session, configStore } = makeSetup();
131
+ vi.spyOn(configStore, "refresh").mockImplementation(() => {
132
+ callOrder.push("refreshConfig");
133
+ });
134
+ vi.spyOn(session, "resetForNewSession").mockImplementation(() => {
135
+ callOrder.push("resetForNewSession");
143
136
  });
144
137
  await handler.handleSessionStart({ reason: "startup" }, makeCtx());
145
138
  expect(callOrder).toEqual(["refreshConfig", "resetForNewSession"]);
@@ -150,24 +143,25 @@ describe("handleSessionStart", () => {
150
143
 
151
144
  describe("handleResourcesDiscover", () => {
152
145
  it("does nothing when reason is not reload", async () => {
153
- const { handler, session } = makeHandler();
146
+ const { handler, session } = makeSetup();
147
+ const spy = vi.spyOn(session, "reload");
154
148
  await handler.handleResourcesDiscover({ reason: "startup" });
155
- expect(session.reload).not.toHaveBeenCalled();
149
+ expect(spy).not.toHaveBeenCalled();
156
150
  });
157
151
 
158
152
  it("calls reload on the session on reload", async () => {
159
- const { handler, session } = makeHandler();
153
+ const { handler, session } = makeSetup();
154
+ const spy = vi.spyOn(session, "reload");
160
155
  await handler.handleResourcesDiscover({ reason: "reload" });
161
- expect(session.reload).toHaveBeenCalledOnce();
156
+ expect(spy).toHaveBeenCalledOnce();
162
157
  });
163
158
 
164
159
  it("writes lifecycle.reload debug log on reload", async () => {
165
160
  const ctx = makeCtx({ cwd: "/proj" });
166
- const { handler, session } = makeHandler({
167
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
168
- });
161
+ const { handler, session, logger } = makeSetup();
162
+ session.activate(ctx);
169
163
  await handler.handleResourcesDiscover({ reason: "reload" });
170
- expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
164
+ expect(logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
171
165
  triggeredBy: "resources_discover",
172
166
  reason: "reload",
173
167
  cwd: "/proj",
@@ -175,9 +169,9 @@ describe("handleResourcesDiscover", () => {
175
169
  });
176
170
 
177
171
  it("logs cwd as null when runtimeContext is null on reload", async () => {
178
- const { handler, session } = makeHandler();
172
+ const { handler, logger } = makeSetup();
179
173
  await handler.handleResourcesDiscover({ reason: "reload" });
180
- expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
174
+ expect(logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
181
175
  triggeredBy: "resources_discover",
182
176
  reason: "reload",
183
177
  cwd: null,
@@ -190,9 +184,8 @@ describe("handleResourcesDiscover", () => {
190
184
  describe("handleSessionShutdown", () => {
191
185
  it("clears UI status when runtime context is present", async () => {
192
186
  const ctx = makeCtx();
193
- const { handler } = makeHandler({
194
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
195
- });
187
+ const { handler, session } = makeSetup();
188
+ session.activate(ctx);
196
189
  await handler.handleSessionShutdown();
197
190
  expect(ctx.ui.setStatus).toHaveBeenCalledWith(
198
191
  "permission-system",
@@ -201,18 +194,19 @@ describe("handleSessionShutdown", () => {
201
194
  });
202
195
 
203
196
  it("does not throw when runtime context is null", async () => {
204
- const { handler } = makeHandler();
197
+ const { handler } = makeSetup();
205
198
  await expect(handler.handleSessionShutdown()).resolves.not.toThrow();
206
199
  });
207
200
 
208
201
  it("calls shutdown on the session", async () => {
209
- const { handler, session } = makeHandler();
202
+ const { handler, session } = makeSetup();
203
+ const spy = vi.spyOn(session, "shutdown");
210
204
  await handler.handleSessionShutdown();
211
- expect(session.shutdown).toHaveBeenCalledOnce();
205
+ expect(spy).toHaveBeenCalledOnce();
212
206
  });
213
207
 
214
208
  it("calls serviceLifecycle.teardown", async () => {
215
- const { handler, serviceLifecycle } = makeHandler();
209
+ const { handler, serviceLifecycle } = makeSetup();
216
210
  await handler.handleSessionShutdown();
217
211
  expect(serviceLifecycle.teardown).toHaveBeenCalledOnce();
218
212
  });
@@ -49,9 +49,10 @@ describe("getEventInput", () => {
49
49
  describe("handleToolCall", () => {
50
50
  it("activates session with ctx", async () => {
51
51
  const ctx = makeCtx();
52
- const { handler, session } = makeHandler();
52
+ const { handler, forwarding } = makeHandler();
53
53
  await handler.handleToolCall(makeToolCallEvent("read"), ctx);
54
- expect(session.activate).toHaveBeenCalledWith(ctx);
54
+ // session.activate(ctx) calls forwarding.start(ctx) on the real session
55
+ expect(forwarding.start).toHaveBeenCalledWith(ctx);
55
56
  });
56
57
 
57
58
  it("blocks when tool name cannot be resolved", async () => {
@@ -10,7 +10,7 @@ import { GateRunner } from "#src/handlers/gates/runner";
10
10
  import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
11
11
  import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
12
12
  import type { ToolCallContext } from "#src/handlers/gates/types";
13
- import type { PermissionResolver } from "#src/permission-resolver";
13
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
14
14
  import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
15
15
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
16
16
  import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter";
@@ -25,7 +25,7 @@ import { makeCheckResult } from "#test/helpers/handler-fixtures";
25
25
  * mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`).
26
26
  */
27
27
  export function makeResolver(defaultCheck?: PermissionCheckResult) {
28
- const resolve = vi.fn<PermissionResolver["resolve"]>();
28
+ const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
29
29
  if (defaultCheck) {
30
30
  resolve.mockReturnValue(defaultCheck);
31
31
  }
@@ -91,7 +91,7 @@ export function makeReporter(
91
91
  export function makeGateRunner(
92
92
  overrides: {
93
93
  resolveResult?: PermissionCheckResult;
94
- resolve?: PermissionResolver["resolve"];
94
+ resolve?: ScopedPermissionResolver["resolve"];
95
95
  recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
96
96
  canConfirm?: GatePrompter["canConfirm"];
97
97
  prompt?: GatePrompter["prompt"];
@@ -102,7 +102,7 @@ export function makeGateRunner(
102
102
  const resolve =
103
103
  overrides.resolve ??
104
104
  vi
105
- .fn<PermissionResolver["resolve"]>()
105
+ .fn<ScopedPermissionResolver["resolve"]>()
106
106
  .mockReturnValue(
107
107
  overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
108
108
  );
@@ -204,7 +204,7 @@ export function makePathDispatchResolver(
204
204
  byPath: Record<string, PermissionCheckResult>,
205
205
  defaultResult: PermissionCheckResult,
206
206
  ) {
207
- const resolve = vi.fn<PermissionResolver["resolve"]>();
207
+ const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
208
208
  resolve.mockImplementation((_surface, input) => {
209
209
  const path = (input as Record<string, unknown>).path;
210
210
  if (typeof path === "string" && path in byPath) {
@@ -243,16 +243,12 @@ export function makeGateCheckResult(
243
243
  */
244
244
  export function makeGateInputs(
245
245
  overrides: {
246
- resolve?: PermissionResolver["resolve"];
247
246
  getActiveSkillEntries?: () => SkillPromptEntry[];
248
247
  getInfrastructureReadDirs?: () => string[];
249
248
  getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
250
249
  } = {},
251
250
  ): ToolCallGateInputs {
252
251
  return {
253
- resolve:
254
- overrides.resolve ??
255
- vi.fn<PermissionResolver["resolve"]>().mockReturnValue(makeCheckResult()),
256
252
  getActiveSkillEntries:
257
253
  overrides.getActiveSkillEntries ??
258
254
  vi.fn<() => SkillPromptEntry[]>(() => []),