@gotgenes/pi-permission-system 5.9.0 → 5.10.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 +15 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +19 -32
- package/src/handlers/input.ts +5 -5
- package/src/handlers/lifecycle.ts +17 -33
- package/src/handlers/tool-call.ts +11 -18
- package/src/handlers/types.ts +11 -38
- package/src/index.ts +14 -17
- package/src/permission-session.ts +252 -0
- package/src/runtime.ts +5 -30
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/tests/handlers/before-agent-start.test.ts +79 -110
- package/tests/handlers/input-events.test.ts +19 -31
- package/tests/handlers/input.test.ts +41 -73
- package/tests/handlers/lifecycle.test.ts +61 -179
- package/tests/handlers/tool-call-events.test.ts +66 -92
- package/tests/handlers/tool-call.test.ts +40 -61
- package/tests/permission-session.test.ts +546 -0
- package/tests/runtime.test.ts +2 -77
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getActiveAgentName,
|
|
5
|
+
getActiveAgentNameFromSystemPrompt,
|
|
6
|
+
} from "./active-agent";
|
|
7
|
+
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
8
|
+
import type { ExtensionPaths } from "./extension-paths";
|
|
9
|
+
import type { ForwardingController } from "./forwarding-manager";
|
|
10
|
+
import type { PermissionManager } from "./permission-manager";
|
|
11
|
+
import type { Rule } from "./rule";
|
|
12
|
+
import { createPermissionManagerForCwd } from "./runtime";
|
|
13
|
+
import type { SessionLogger } from "./session-logger";
|
|
14
|
+
import { SessionRules } from "./session-rules";
|
|
15
|
+
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
16
|
+
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Runtime operations that `PermissionSession` delegates to but does not own.
|
|
20
|
+
*
|
|
21
|
+
* Injected at construction time from the composition root (`index.ts`),
|
|
22
|
+
* where the `ExtensionRuntime` is available.
|
|
23
|
+
*/
|
|
24
|
+
export interface PermissionSessionRuntimeDeps {
|
|
25
|
+
/** Reload merged config from disk; optionally update the stored runtime context. */
|
|
26
|
+
refreshExtensionConfig(ctx?: ExtensionContext): void;
|
|
27
|
+
/** Write the resolved config path set to the review and debug logs. */
|
|
28
|
+
logResolvedConfigPaths(): void;
|
|
29
|
+
/** Read current extension config (called at query time). */
|
|
30
|
+
getConfig(): PermissionSystemExtensionConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Encapsulates all mutable session state and exposes operations instead of
|
|
35
|
+
* fields.
|
|
36
|
+
*
|
|
37
|
+
* Replaces the `SessionState` interface + scattered handler field mutations
|
|
38
|
+
* with a single class that owns the `PermissionManager`, `SessionRules`,
|
|
39
|
+
* cache keys, skill entries, and runtime context.
|
|
40
|
+
*
|
|
41
|
+
* Constructor deps:
|
|
42
|
+
* - `ExtensionPaths` — immutable path constants
|
|
43
|
+
* - `SessionLogger` — debug + review + warn
|
|
44
|
+
* - `ForwardingController` — polling lifecycle
|
|
45
|
+
* - `PermissionSessionRuntimeDeps` — config refresh + log delegates
|
|
46
|
+
*/
|
|
47
|
+
export class PermissionSession {
|
|
48
|
+
private context: ExtensionContext | null = null;
|
|
49
|
+
private permissionManager: PermissionManager;
|
|
50
|
+
private readonly sessionRules = new SessionRules();
|
|
51
|
+
private skillEntries: SkillPromptEntry[] = [];
|
|
52
|
+
private knownAgentName: string | null = null;
|
|
53
|
+
private toolsCacheKey: string | null = null;
|
|
54
|
+
private promptCacheKey: string | null = null;
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly paths: ExtensionPaths,
|
|
58
|
+
readonly logger: SessionLogger,
|
|
59
|
+
private readonly forwarding: ForwardingController,
|
|
60
|
+
private readonly runtimeDeps: PermissionSessionRuntimeDeps,
|
|
61
|
+
) {
|
|
62
|
+
this.permissionManager = createPermissionManagerForCwd(
|
|
63
|
+
paths.agentDir,
|
|
64
|
+
undefined,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Context lifecycle ──────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/** Store the current extension context and start forwarding. */
|
|
71
|
+
activate(ctx: ExtensionContext): void {
|
|
72
|
+
this.context = ctx;
|
|
73
|
+
this.forwarding.start(ctx);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Clear the context and stop forwarding. */
|
|
77
|
+
deactivate(): void {
|
|
78
|
+
this.context = null;
|
|
79
|
+
this.forwarding.stop();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Return the current runtime context, or null if not activated. */
|
|
83
|
+
getRuntimeContext(): ExtensionContext | null {
|
|
84
|
+
return this.context;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Permission checking (delegates to PermissionManager) ───────────────
|
|
88
|
+
|
|
89
|
+
checkPermission(
|
|
90
|
+
surface: string,
|
|
91
|
+
input: unknown,
|
|
92
|
+
agentName?: string,
|
|
93
|
+
sessionRules?: Rule[],
|
|
94
|
+
): PermissionCheckResult {
|
|
95
|
+
return this.permissionManager.checkPermission(
|
|
96
|
+
surface,
|
|
97
|
+
input,
|
|
98
|
+
agentName,
|
|
99
|
+
sessionRules,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
104
|
+
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getConfigIssues(agentName?: string): string[] {
|
|
108
|
+
return this.permissionManager.getConfigIssues(agentName);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getPolicyCacheStamp(agentName?: string): string {
|
|
112
|
+
return this.permissionManager.getPolicyCacheStamp(agentName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Session rules (delegates to SessionRules) ──────────────────────────
|
|
116
|
+
|
|
117
|
+
getSessionRuleset(): Rule[] {
|
|
118
|
+
return this.sessionRules.getRuleset();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
approveSessionRule(surface: string, pattern: string): void {
|
|
122
|
+
this.sessionRules.approve(surface, pattern);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Session lifecycle ────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reset all mutable state for a new session.
|
|
129
|
+
*
|
|
130
|
+
* Creates a fresh PermissionManager scoped to `ctx.cwd`, clears caches,
|
|
131
|
+
* skill entries, and activates the new context.
|
|
132
|
+
*/
|
|
133
|
+
resetForNewSession(ctx: ExtensionContext): void {
|
|
134
|
+
this.permissionManager = createPermissionManagerForCwd(
|
|
135
|
+
this.paths.agentDir,
|
|
136
|
+
ctx.cwd,
|
|
137
|
+
);
|
|
138
|
+
this.skillEntries = [];
|
|
139
|
+
this.toolsCacheKey = null;
|
|
140
|
+
this.promptCacheKey = null;
|
|
141
|
+
this.activate(ctx);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Shut down the session: clear rules, caches, skill entries, and
|
|
146
|
+
* deactivate context + forwarding.
|
|
147
|
+
*/
|
|
148
|
+
shutdown(): void {
|
|
149
|
+
this.sessionRules.clear();
|
|
150
|
+
this.skillEntries = [];
|
|
151
|
+
this.toolsCacheKey = null;
|
|
152
|
+
this.promptCacheKey = null;
|
|
153
|
+
this.deactivate();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Reload permission manager and clear caches for the current context.
|
|
158
|
+
* Used on config reload (e.g. `resources_discover` with reason "reload").
|
|
159
|
+
*/
|
|
160
|
+
reload(): void {
|
|
161
|
+
this.permissionManager = createPermissionManagerForCwd(
|
|
162
|
+
this.paths.agentDir,
|
|
163
|
+
this.context?.cwd,
|
|
164
|
+
);
|
|
165
|
+
this.skillEntries = [];
|
|
166
|
+
this.toolsCacheKey = null;
|
|
167
|
+
this.promptCacheKey = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Agent-start caching ────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
shouldUpdateActiveTools(cacheKey: string): boolean {
|
|
173
|
+
return this.toolsCacheKey !== cacheKey;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
commitActiveToolsCacheKey(cacheKey: string): void {
|
|
177
|
+
this.toolsCacheKey = cacheKey;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
shouldUpdatePromptState(cacheKey: string): boolean {
|
|
181
|
+
return this.promptCacheKey !== cacheKey;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
commitPromptStateCacheKey(cacheKey: string): void {
|
|
185
|
+
this.promptCacheKey = cacheKey;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Skill entries ──────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
getActiveSkillEntries(): SkillPromptEntry[] {
|
|
191
|
+
return this.skillEntries;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setActiveSkillEntries(entries: SkillPromptEntry[]): void {
|
|
195
|
+
this.skillEntries = entries;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Agent name ─────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the active agent name from the session context, system prompt,
|
|
202
|
+
* or last known name. Updates lastKnownActiveAgentName as a side effect.
|
|
203
|
+
*/
|
|
204
|
+
resolveAgentName(
|
|
205
|
+
ctx: ExtensionContext,
|
|
206
|
+
systemPrompt?: string,
|
|
207
|
+
): string | null {
|
|
208
|
+
const fromSession = getActiveAgentName(ctx);
|
|
209
|
+
if (fromSession) {
|
|
210
|
+
this.knownAgentName = fromSession;
|
|
211
|
+
return fromSession;
|
|
212
|
+
}
|
|
213
|
+
const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
|
|
214
|
+
if (fromSystemPrompt) {
|
|
215
|
+
this.knownAgentName = fromSystemPrompt;
|
|
216
|
+
return fromSystemPrompt;
|
|
217
|
+
}
|
|
218
|
+
return this.knownAgentName;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get lastKnownActiveAgentName(): string | null {
|
|
222
|
+
return this.knownAgentName;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Config ─────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/** Reload merged config from disk; optionally update the stored runtime context. */
|
|
228
|
+
refreshConfig(ctx?: ExtensionContext): void {
|
|
229
|
+
this.runtimeDeps.refreshExtensionConfig(ctx);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Write the resolved config path set to the review and debug logs. */
|
|
233
|
+
logResolvedConfigPaths(): void {
|
|
234
|
+
this.runtimeDeps.logResolvedConfigPaths();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Read current extension config. */
|
|
238
|
+
get config(): PermissionSystemExtensionConfig {
|
|
239
|
+
return this.runtimeDeps.getConfig();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Infrastructure paths ───────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
getInfrastructureDirs(): readonly string[] {
|
|
245
|
+
return this.paths.piInfrastructureDirs;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Config-derived infrastructure read paths (current at call time). */
|
|
249
|
+
getInfrastructureReadPaths(): string[] {
|
|
250
|
+
return this.config.piInfrastructureReadPaths ?? [];
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -12,10 +12,6 @@ import {
|
|
|
12
12
|
getAgentDir,
|
|
13
13
|
} from "@mariozechner/pi-coding-agent";
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
getActiveAgentName,
|
|
17
|
-
getActiveAgentNameFromSystemPrompt,
|
|
18
|
-
} from "./active-agent";
|
|
19
15
|
import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
|
|
20
16
|
import {
|
|
21
17
|
DEBUG_LOG_FILENAME,
|
|
@@ -45,11 +41,12 @@ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
|
45
41
|
import { syncPermissionSystemStatus } from "./status";
|
|
46
42
|
|
|
47
43
|
/**
|
|
48
|
-
* Mutable session state — the subset of ExtensionRuntime that
|
|
49
|
-
*
|
|
50
|
-
*
|
|
44
|
+
* Mutable session state — the subset of ExtensionRuntime that holds
|
|
45
|
+
* per-session fields. `PermissionSession` now owns these for handler
|
|
46
|
+
* use; this interface remains so `ExtensionRuntime` can still serve
|
|
47
|
+
* as the internal composition root (config-modal, RPC handlers).
|
|
51
48
|
*/
|
|
52
|
-
|
|
49
|
+
interface SessionState {
|
|
53
50
|
runtimeContext: ExtensionContext | null;
|
|
54
51
|
permissionManager: PermissionManager;
|
|
55
52
|
readonly sessionRules: SessionRules;
|
|
@@ -209,28 +206,6 @@ export function saveExtensionConfig(
|
|
|
209
206
|
});
|
|
210
207
|
}
|
|
211
208
|
|
|
212
|
-
/**
|
|
213
|
-
* Resolve the active agent name from the Pi session, system prompt, or last
|
|
214
|
-
* known name. Updates `runtime.lastKnownActiveAgentName` as a side effect.
|
|
215
|
-
*/
|
|
216
|
-
export function resolveAgentName(
|
|
217
|
-
runtime: ExtensionRuntime,
|
|
218
|
-
ctx: ExtensionContext,
|
|
219
|
-
systemPrompt?: string,
|
|
220
|
-
): string | null {
|
|
221
|
-
const fromSession = getActiveAgentName(ctx);
|
|
222
|
-
if (fromSession) {
|
|
223
|
-
runtime.lastKnownActiveAgentName = fromSession;
|
|
224
|
-
return fromSession;
|
|
225
|
-
}
|
|
226
|
-
const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
|
|
227
|
-
if (fromSystemPrompt) {
|
|
228
|
-
runtime.lastKnownActiveAgentName = fromSystemPrompt;
|
|
229
|
-
return fromSystemPrompt;
|
|
230
|
-
}
|
|
231
|
-
return runtime.lastKnownActiveAgentName;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
209
|
/**
|
|
235
210
|
* Write the resolved config path set (global, project, legacy) to the review
|
|
236
211
|
* and debug logs.
|
|
@@ -4,8 +4,19 @@ import {
|
|
|
4
4
|
isPathWithinDirectory,
|
|
5
5
|
normalizePathForComparison,
|
|
6
6
|
} from "./path-utils";
|
|
7
|
-
import type {
|
|
8
|
-
|
|
7
|
+
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Narrow interface for the permission checker used by skill prompt resolution.
|
|
11
|
+
* Both `PermissionManager` and `PermissionSession` satisfy this structurally.
|
|
12
|
+
*/
|
|
13
|
+
export interface SkillPermissionChecker {
|
|
14
|
+
checkPermission(
|
|
15
|
+
surface: string,
|
|
16
|
+
input: unknown,
|
|
17
|
+
agentName?: string,
|
|
18
|
+
): PermissionCheckResult;
|
|
19
|
+
}
|
|
9
20
|
|
|
10
21
|
const AVAILABLE_SKILLS_OPEN_TAG = "<available_skills>";
|
|
11
22
|
const AVAILABLE_SKILLS_CLOSE_TAG = "</available_skills>";
|
|
@@ -148,7 +159,7 @@ export function parseAllSkillPromptSections(
|
|
|
148
159
|
|
|
149
160
|
function resolvePermissionState(
|
|
150
161
|
skillName: string,
|
|
151
|
-
permissionManager:
|
|
162
|
+
permissionManager: SkillPermissionChecker,
|
|
152
163
|
agentName: string | null,
|
|
153
164
|
cache: Map<string, PermissionState>,
|
|
154
165
|
): PermissionState {
|
|
@@ -205,7 +216,7 @@ function removePromptRange(prompt: string, start: number, end: number): string {
|
|
|
205
216
|
|
|
206
217
|
export function resolveSkillPromptEntries(
|
|
207
218
|
prompt: string,
|
|
208
|
-
permissionManager:
|
|
219
|
+
permissionManager: SkillPermissionChecker,
|
|
209
220
|
agentName: string | null,
|
|
210
221
|
cwd: string,
|
|
211
222
|
): { prompt: string; entries: SkillPromptEntry[] } {
|
|
@@ -6,9 +6,8 @@ import {
|
|
|
6
6
|
shouldExposeTool,
|
|
7
7
|
} from "../../src/handlers/before-agent-start";
|
|
8
8
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
11
|
-
import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
|
|
9
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
10
|
+
import type { PermissionState } from "../../src/types";
|
|
12
11
|
|
|
13
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
14
13
|
vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
|
|
@@ -45,52 +44,36 @@ function makeEvent(systemPrompt = "You are an assistant.") {
|
|
|
45
44
|
return { systemPrompt };
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
): PermissionManager {
|
|
47
|
+
function makeSession(
|
|
48
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
49
|
+
): PermissionSession {
|
|
52
50
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
52
|
+
activate: vi.fn(),
|
|
53
|
+
refreshConfig: vi.fn(),
|
|
54
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
55
|
+
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
56
56
|
checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
activeSkillEntries: [] as SkillPromptEntry[],
|
|
65
|
-
lastKnownActiveAgentName: null,
|
|
66
|
-
lastActiveToolsCacheKey: null,
|
|
67
|
-
lastPromptStateCacheKey: null,
|
|
68
|
-
sessionRules: {
|
|
69
|
-
approve: vi.fn(),
|
|
70
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
71
|
-
clear: vi.fn(),
|
|
72
|
-
} as unknown as SessionState["sessionRules"],
|
|
57
|
+
shouldUpdateActiveTools: vi.fn().mockReturnValue(true),
|
|
58
|
+
commitActiveToolsCacheKey: vi.fn(),
|
|
59
|
+
getPolicyCacheStamp: vi.fn().mockReturnValue("stamp-1"),
|
|
60
|
+
shouldUpdatePromptState: vi.fn().mockReturnValue(true),
|
|
61
|
+
commitPromptStateCacheKey: vi.fn(),
|
|
62
|
+
setActiveSkillEntries: vi.fn(),
|
|
63
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
73
64
|
...overrides,
|
|
74
|
-
};
|
|
65
|
+
} as unknown as PermissionSession;
|
|
75
66
|
}
|
|
76
67
|
|
|
77
68
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
78
69
|
return {
|
|
79
70
|
session: makeSession(),
|
|
80
|
-
|
|
81
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
82
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
83
|
-
createPermissionManagerForCwd: vi.fn().mockReturnValue(makePm()),
|
|
84
|
-
refreshExtensionConfig: vi.fn(),
|
|
85
|
-
logResolvedConfigPaths: vi.fn(),
|
|
86
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
71
|
+
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
87
72
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
88
73
|
promptPermission: vi
|
|
89
74
|
.fn()
|
|
90
75
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
91
76
|
createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
|
|
92
|
-
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
93
|
-
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
94
77
|
stopPermissionRpcHandlers: vi.fn(),
|
|
95
78
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
96
79
|
setActiveTools: vi.fn(),
|
|
@@ -102,48 +85,48 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
102
85
|
|
|
103
86
|
describe("shouldExposeTool", () => {
|
|
104
87
|
it("returns true when tool permission is allow", () => {
|
|
105
|
-
const
|
|
106
|
-
expect(shouldExposeTool("read", null,
|
|
88
|
+
const getter = vi.fn().mockReturnValue("allow");
|
|
89
|
+
expect(shouldExposeTool("read", null, getter)).toBe(true);
|
|
107
90
|
});
|
|
108
91
|
|
|
109
92
|
it("returns true when tool permission is ask", () => {
|
|
110
|
-
const
|
|
111
|
-
expect(shouldExposeTool("bash", "agent-x",
|
|
93
|
+
const getter = vi.fn().mockReturnValue("ask");
|
|
94
|
+
expect(shouldExposeTool("bash", "agent-x", getter)).toBe(true);
|
|
112
95
|
});
|
|
113
96
|
|
|
114
97
|
it("returns false when tool permission is deny", () => {
|
|
115
|
-
const
|
|
116
|
-
expect(shouldExposeTool("write", null,
|
|
98
|
+
const getter = vi.fn().mockReturnValue("deny");
|
|
99
|
+
expect(shouldExposeTool("write", null, getter)).toBe(false);
|
|
117
100
|
});
|
|
118
101
|
|
|
119
102
|
it("passes agentName through to getToolPermission", () => {
|
|
120
|
-
const
|
|
121
|
-
shouldExposeTool("read", "my-agent",
|
|
122
|
-
expect(
|
|
103
|
+
const getter = vi.fn().mockReturnValue("allow");
|
|
104
|
+
shouldExposeTool("read", "my-agent", getter);
|
|
105
|
+
expect(getter).toHaveBeenCalledWith("read", "my-agent");
|
|
123
106
|
});
|
|
124
107
|
|
|
125
108
|
it("converts null agentName to undefined for getToolPermission", () => {
|
|
126
|
-
const
|
|
127
|
-
shouldExposeTool("read", null,
|
|
128
|
-
expect(
|
|
109
|
+
const getter = vi.fn().mockReturnValue("allow");
|
|
110
|
+
shouldExposeTool("read", null, getter);
|
|
111
|
+
expect(getter).toHaveBeenCalledWith("read", undefined);
|
|
129
112
|
});
|
|
130
113
|
});
|
|
131
114
|
|
|
132
115
|
// ── handleBeforeAgentStart ─────────────────────────────────────────────────
|
|
133
116
|
|
|
134
117
|
describe("handleBeforeAgentStart", () => {
|
|
135
|
-
it("
|
|
118
|
+
it("activates the session with ctx", async () => {
|
|
136
119
|
const ctx = makeCtx();
|
|
137
120
|
const deps = makeDeps();
|
|
138
121
|
await handleBeforeAgentStart(deps, makeEvent(), ctx);
|
|
139
|
-
expect(deps.
|
|
122
|
+
expect(deps.session.activate).toHaveBeenCalledWith(ctx);
|
|
140
123
|
});
|
|
141
124
|
|
|
142
|
-
it("
|
|
125
|
+
it("refreshes config with ctx", async () => {
|
|
143
126
|
const ctx = makeCtx();
|
|
144
127
|
const deps = makeDeps();
|
|
145
128
|
await handleBeforeAgentStart(deps, makeEvent(), ctx);
|
|
146
|
-
expect(deps.
|
|
129
|
+
expect(deps.session.refreshConfig).toHaveBeenCalledWith(ctx);
|
|
147
130
|
});
|
|
148
131
|
|
|
149
132
|
it("resolves agent name using systemPrompt", async () => {
|
|
@@ -154,33 +137,28 @@ describe("handleBeforeAgentStart", () => {
|
|
|
154
137
|
makeEvent("<active_agent name='x'>"),
|
|
155
138
|
ctx,
|
|
156
139
|
);
|
|
157
|
-
expect(deps.resolveAgentName).toHaveBeenCalledWith(
|
|
140
|
+
expect(deps.session.resolveAgentName).toHaveBeenCalledWith(
|
|
158
141
|
ctx,
|
|
159
142
|
"<active_agent name='x'>",
|
|
160
143
|
);
|
|
161
144
|
});
|
|
162
145
|
|
|
163
146
|
it("filters out denied tools from allowed list", async () => {
|
|
164
|
-
const
|
|
147
|
+
const session = makeSession({
|
|
148
|
+
getToolPermission: vi.fn().mockReturnValue("deny"),
|
|
149
|
+
});
|
|
165
150
|
const deps = makeDeps({
|
|
166
|
-
session
|
|
167
|
-
permissionManager: pm as unknown as PermissionManager,
|
|
168
|
-
}),
|
|
151
|
+
session,
|
|
169
152
|
getAllTools: vi
|
|
170
153
|
.fn()
|
|
171
154
|
.mockReturnValue([{ name: "write" }, { name: "read" }]),
|
|
172
155
|
});
|
|
173
|
-
// write is deny, read is deny (same pm stub — both denied)
|
|
174
156
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
175
157
|
expect(deps.setActiveTools).toHaveBeenCalledWith([]);
|
|
176
158
|
});
|
|
177
159
|
|
|
178
160
|
it("includes allowed and ask tools in the active list", async () => {
|
|
179
|
-
const pm = makePm("allow");
|
|
180
161
|
const deps = makeDeps({
|
|
181
|
-
session: makeSession({
|
|
182
|
-
permissionManager: pm as unknown as PermissionManager,
|
|
183
|
-
}),
|
|
184
162
|
getAllTools: vi
|
|
185
163
|
.fn()
|
|
186
164
|
.mockReturnValue([{ name: "read" }, { name: "write" }]),
|
|
@@ -189,31 +167,59 @@ describe("handleBeforeAgentStart", () => {
|
|
|
189
167
|
expect(deps.setActiveTools).toHaveBeenCalledWith(["read", "write"]);
|
|
190
168
|
});
|
|
191
169
|
|
|
192
|
-
it("
|
|
170
|
+
it("commits active-tools cache key after applying", async () => {
|
|
193
171
|
const deps = makeDeps({
|
|
194
172
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
195
173
|
});
|
|
196
174
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
197
|
-
expect(deps.session.
|
|
175
|
+
expect(deps.session.commitActiveToolsCacheKey).toHaveBeenCalled();
|
|
198
176
|
});
|
|
199
177
|
|
|
200
178
|
it("skips setActiveTools when cache key is unchanged", async () => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
205
|
-
const key = createActiveToolsCacheKey(["read"]);
|
|
179
|
+
const session = makeSession({
|
|
180
|
+
shouldUpdateActiveTools: vi.fn().mockReturnValue(false),
|
|
181
|
+
});
|
|
206
182
|
const deps = makeDeps({
|
|
207
|
-
session
|
|
183
|
+
session,
|
|
208
184
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
209
185
|
});
|
|
210
186
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
211
187
|
expect(deps.setActiveTools).not.toHaveBeenCalled();
|
|
188
|
+
expect(session.commitActiveToolsCacheKey).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns empty object when prompt cache is unchanged", async () => {
|
|
192
|
+
const session = makeSession({
|
|
193
|
+
shouldUpdatePromptState: vi.fn().mockReturnValue(false),
|
|
194
|
+
});
|
|
195
|
+
const deps = makeDeps({
|
|
196
|
+
session,
|
|
197
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
198
|
+
});
|
|
199
|
+
const result = await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
200
|
+
expect(result).toEqual({});
|
|
201
|
+
expect(session.commitPromptStateCacheKey).not.toHaveBeenCalled();
|
|
212
202
|
});
|
|
213
203
|
|
|
214
|
-
it("
|
|
215
|
-
|
|
216
|
-
|
|
204
|
+
it("commits prompt-state cache key and processes prompt when cache is new", async () => {
|
|
205
|
+
const deps = makeDeps({
|
|
206
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
207
|
+
});
|
|
208
|
+
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
209
|
+
expect(deps.session.commitPromptStateCacheKey).toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("stores resolved skill entries on the session", async () => {
|
|
213
|
+
const deps = makeDeps({
|
|
214
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
215
|
+
});
|
|
216
|
+
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
217
|
+
expect(deps.session.setActiveSkillEntries).toHaveBeenCalledWith(
|
|
218
|
+
expect.any(Array),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns modified systemPrompt when prompt changes", async () => {
|
|
217
223
|
const systemPrompt = `You are an assistant.\n\nAvailable tools:\n- read\n- write\n`;
|
|
218
224
|
const deps = makeDeps({
|
|
219
225
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
@@ -223,9 +229,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
223
229
|
makeEvent(systemPrompt),
|
|
224
230
|
makeCtx(),
|
|
225
231
|
);
|
|
226
|
-
// The prompt was modified, so systemPrompt should be returned
|
|
227
232
|
expect(result).toHaveProperty("systemPrompt");
|
|
228
|
-
expect(deps.session.lastPromptStateCacheKey).not.toBeNull();
|
|
229
233
|
});
|
|
230
234
|
|
|
231
235
|
it("returns empty object when systemPrompt is unchanged", async () => {
|
|
@@ -240,39 +244,4 @@ describe("handleBeforeAgentStart", () => {
|
|
|
240
244
|
);
|
|
241
245
|
expect(result).toEqual({});
|
|
242
246
|
});
|
|
243
|
-
|
|
244
|
-
it("stores resolved skill entries on deps", async () => {
|
|
245
|
-
const deps = makeDeps({
|
|
246
|
-
getAllTools: vi.fn().mockReturnValue([]),
|
|
247
|
-
});
|
|
248
|
-
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
249
|
-
expect(deps.session.activeSkillEntries).toEqual(expect.any(Array));
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("returns empty object and skips prompt work when prompt cache key is unchanged", async () => {
|
|
253
|
-
const { createBeforeAgentStartPromptStateKey } = await import(
|
|
254
|
-
"../../src/before-agent-start-cache"
|
|
255
|
-
);
|
|
256
|
-
const pm = makePm("allow");
|
|
257
|
-
const ctx = makeCtx({ cwd: "/proj" });
|
|
258
|
-
const allowedTools: string[] = ["read"];
|
|
259
|
-
const key = createBeforeAgentStartPromptStateKey({
|
|
260
|
-
agentName: null,
|
|
261
|
-
cwd: "/proj",
|
|
262
|
-
permissionStamp: "stamp-1",
|
|
263
|
-
systemPrompt: "hello",
|
|
264
|
-
allowedToolNames: allowedTools,
|
|
265
|
-
});
|
|
266
|
-
const deps = makeDeps({
|
|
267
|
-
session: makeSession({
|
|
268
|
-
permissionManager: pm as unknown as PermissionManager,
|
|
269
|
-
lastPromptStateCacheKey: key,
|
|
270
|
-
}),
|
|
271
|
-
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
272
|
-
});
|
|
273
|
-
const result = await handleBeforeAgentStart(deps, makeEvent("hello"), ctx);
|
|
274
|
-
expect(result).toEqual({});
|
|
275
|
-
// activeSkillEntries was not assigned by the handler (early return)
|
|
276
|
-
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
277
|
-
});
|
|
278
247
|
});
|