@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.
- package/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +11 -6
- package/src/handlers/gates/bash-command.ts +2 -2
- package/src/handlers/gates/bash-external-directory.ts +2 -2
- package/src/handlers/gates/bash-path.ts +2 -2
- package/src/handlers/gates/path.ts +2 -2
- package/src/handlers/gates/runner.ts +2 -2
- package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
- package/src/handlers/lifecycle.ts +7 -4
- package/src/handlers/permission-gate-handler.ts +3 -3
- package/src/index.ts +13 -4
- package/src/permission-resolver.ts +66 -2
- package/src/permission-session.ts +8 -72
- package/src/session-rules.ts +3 -2
- package/src/skill-prompt-sanitizer.ts +1 -1
- package/test/handlers/before-agent-start.test.ts +56 -86
- package/test/handlers/external-directory-session-dedup.test.ts +80 -160
- package/test/handlers/gates/bash-external-directory.test.ts +2 -2
- package/test/handlers/gates/bash-path.test.ts +2 -2
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
- package/test/handlers/input.test.ts +5 -4
- package/test/handlers/lifecycle.test.ts +79 -85
- package/test/handlers/tool-call.test.ts +3 -2
- package/test/helpers/gate-fixtures.ts +5 -9
- package/test/helpers/handler-fixtures.ts +100 -107
- package/test/helpers/session-fixtures.ts +192 -0
- package/test/permission-resolver.test.ts +196 -0
- package/test/permission-session.test.ts +14 -266
- package/test/session-rules.test.ts +13 -5
- package/src/agent-prep-session.ts +0 -28
- package/src/gate-handler-session.ts +0 -13
- 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(
|
|
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
|
|
57
|
+
const resolver = makeResolver(
|
|
57
58
|
makeCheckResult({ state: "deny", matchedPattern: "*" }),
|
|
58
59
|
);
|
|
59
|
-
const inputs = makeGateInputs(
|
|
60
|
-
const { runner } = makeGateRunner(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
52
|
+
const { handler, forwarding } = makeHandler();
|
|
53
53
|
await handler.handleInput(makeInputEvent("hello"), ctx);
|
|
54
|
-
|
|
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,
|
|
68
|
+
const { handler, permissionManager } = makeHandler();
|
|
68
69
|
await handler.handleInput(makeInputEvent("just a message"), makeCtx());
|
|
69
|
-
expect(
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
66
|
-
|
|
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,
|
|
62
|
+
const { handler, configStore } = makeSetup();
|
|
75
63
|
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
|
76
|
-
expect(
|
|
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 } =
|
|
69
|
+
const { handler, session } = makeSetup();
|
|
70
|
+
const spy = vi.spyOn(session, "resetForNewSession");
|
|
82
71
|
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
|
83
|
-
expect(
|
|
72
|
+
expect(spy).toHaveBeenCalledWith(ctx);
|
|
84
73
|
});
|
|
85
74
|
|
|
86
75
|
it("logs resolved config paths", async () => {
|
|
87
|
-
const { handler,
|
|
76
|
+
const { handler, configStore } = makeSetup();
|
|
88
77
|
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
|
89
|
-
expect(
|
|
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 } =
|
|
83
|
+
const { handler, session } = makeSetup();
|
|
84
|
+
const spy = vi.spyOn(session, "resolveAgentName");
|
|
95
85
|
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
|
96
|
-
expect(
|
|
86
|
+
expect(spy).toHaveBeenCalledWith(ctx);
|
|
97
87
|
});
|
|
98
88
|
|
|
99
89
|
it("notifies each policy issue", async () => {
|
|
100
|
-
const { handler,
|
|
101
|
-
|
|
90
|
+
const { handler, logger } = makeSetup({
|
|
91
|
+
configIssues: ["issue A", "issue B"],
|
|
102
92
|
});
|
|
103
93
|
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
|
104
|
-
expect(
|
|
105
|
-
expect(
|
|
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,
|
|
99
|
+
const { handler, logger } = makeSetup();
|
|
110
100
|
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
|
111
|
-
expect(
|
|
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,
|
|
106
|
+
const { handler, logger } = makeSetup();
|
|
117
107
|
await handler.handleSessionStart({ reason: "reload" }, ctx);
|
|
118
|
-
expect(
|
|
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,
|
|
116
|
+
const { handler, logger } = makeSetup();
|
|
127
117
|
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
|
128
|
-
expect(
|
|
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 } =
|
|
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 } =
|
|
141
|
-
|
|
142
|
-
|
|
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 } =
|
|
146
|
+
const { handler, session } = makeSetup();
|
|
147
|
+
const spy = vi.spyOn(session, "reload");
|
|
154
148
|
await handler.handleResourcesDiscover({ reason: "startup" });
|
|
155
|
-
expect(
|
|
149
|
+
expect(spy).not.toHaveBeenCalled();
|
|
156
150
|
});
|
|
157
151
|
|
|
158
152
|
it("calls reload on the session on reload", async () => {
|
|
159
|
-
const { handler, session } =
|
|
153
|
+
const { handler, session } = makeSetup();
|
|
154
|
+
const spy = vi.spyOn(session, "reload");
|
|
160
155
|
await handler.handleResourcesDiscover({ reason: "reload" });
|
|
161
|
-
expect(
|
|
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 } =
|
|
167
|
-
|
|
168
|
-
});
|
|
161
|
+
const { handler, session, logger } = makeSetup();
|
|
162
|
+
session.activate(ctx);
|
|
169
163
|
await handler.handleResourcesDiscover({ reason: "reload" });
|
|
170
|
-
expect(
|
|
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,
|
|
172
|
+
const { handler, logger } = makeSetup();
|
|
179
173
|
await handler.handleResourcesDiscover({ reason: "reload" });
|
|
180
|
-
expect(
|
|
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 } =
|
|
194
|
-
|
|
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 } =
|
|
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 } =
|
|
202
|
+
const { handler, session } = makeSetup();
|
|
203
|
+
const spy = vi.spyOn(session, "shutdown");
|
|
210
204
|
await handler.handleSessionShutdown();
|
|
211
|
-
expect(
|
|
205
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
212
206
|
});
|
|
213
207
|
|
|
214
208
|
it("calls serviceLifecycle.teardown", async () => {
|
|
215
|
-
const { handler, serviceLifecycle } =
|
|
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,
|
|
52
|
+
const { handler, forwarding } = makeHandler();
|
|
53
53
|
await handler.handleToolCall(makeToolCallEvent("read"), ctx);
|
|
54
|
-
|
|
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 {
|
|
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<
|
|
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?:
|
|
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<
|
|
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<
|
|
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[]>(() => []),
|