@gotgenes/pi-permission-system 5.4.0 → 5.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 +28 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/gates/bash-external-directory.ts +22 -24
- package/src/handlers/gates/external-directory.ts +32 -41
- package/src/handlers/gates/skill-read.ts +10 -12
- package/src/handlers/gates/tool.ts +20 -27
- package/src/handlers/gates/types.ts +75 -0
- package/src/handlers/input.ts +3 -3
- package/src/handlers/lifecycle.ts +21 -21
- package/src/handlers/tool-call.ts +77 -7
- package/src/handlers/types.ts +20 -7
- package/src/index.ts +6 -1
- package/src/permission-manager.ts +28 -279
- package/src/policy-loader.ts +350 -0
- package/src/runtime.ts +17 -9
- package/tests/handlers/before-agent-start.test.ts +17 -27
- package/tests/handlers/gates/bash-external-directory.test.ts +48 -105
- package/tests/handlers/gates/external-directory.test.ts +65 -140
- package/tests/handlers/gates/skill-read.test.ts +50 -65
- package/tests/handlers/gates/tool.test.ts +90 -334
- package/tests/handlers/input-events.test.ts +10 -21
- package/tests/handlers/input.test.ts +26 -43
- package/tests/handlers/lifecycle.test.ts +47 -66
- package/tests/handlers/tool-call-events.test.ts +29 -40
- package/tests/handlers/tool-call.test.ts +19 -30
- package/tests/permission-manager-unified.test.ts +319 -0
- package/tests/policy-loader.test.ts +561 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
import { extractFrontmatter, parseSimpleYamlMap, toRecord } from "./common";
|
|
6
|
+
import {
|
|
7
|
+
loadUnifiedConfig,
|
|
8
|
+
normalizeUnifiedConfig,
|
|
9
|
+
stripJsonComments,
|
|
10
|
+
} from "./config-loader";
|
|
11
|
+
import { getGlobalConfigPath } from "./config-paths";
|
|
12
|
+
import type { ScopeConfig } from "./types";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// File-stamp helper
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function getFileStamp(path: string): string {
|
|
19
|
+
try {
|
|
20
|
+
return String(statSync(path).mtimeMs);
|
|
21
|
+
} catch {
|
|
22
|
+
return "missing";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// MCP server-name reading helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function readConfiguredMcpServerNamesFromConfigPath(
|
|
31
|
+
configPath: string,
|
|
32
|
+
): string[] {
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
35
|
+
const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
|
|
36
|
+
const root = toRecord(parsed);
|
|
37
|
+
const serverRecord = toRecord(root.mcpServers ?? root["mcp-servers"]);
|
|
38
|
+
|
|
39
|
+
return Object.keys(serverRecord)
|
|
40
|
+
.map((name) => name.trim())
|
|
41
|
+
.filter((name) => name.length > 0);
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getConfiguredMcpServerNamesFromPaths(
|
|
48
|
+
paths: readonly string[],
|
|
49
|
+
): string[] {
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
|
|
52
|
+
for (const path of paths) {
|
|
53
|
+
for (const name of readConfiguredMcpServerNamesFromConfigPath(path)) {
|
|
54
|
+
seen.add(name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return [...seen].sort(
|
|
59
|
+
(left, right) => right.length - left.length || left.localeCompare(right),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Resolved policy paths
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export interface ResolvedPolicyPaths {
|
|
68
|
+
globalConfigPath: string;
|
|
69
|
+
globalConfigExists: boolean;
|
|
70
|
+
projectConfigPath: string | null;
|
|
71
|
+
projectConfigExists: boolean;
|
|
72
|
+
agentsDir: string;
|
|
73
|
+
agentsDirExists: boolean;
|
|
74
|
+
projectAgentsDir: string | null;
|
|
75
|
+
projectAgentsDirExists: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// PolicyLoader interface
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Abstraction over file I/O for loading permission policy from disk.
|
|
84
|
+
* Implementations handle caching, path resolution, and config-issue
|
|
85
|
+
* accumulation. `PermissionManager` depends on this interface so that
|
|
86
|
+
* merge + evaluation logic can be tested with an in-memory stub.
|
|
87
|
+
*/
|
|
88
|
+
export interface PolicyLoader {
|
|
89
|
+
loadGlobalConfig(): ScopeConfig;
|
|
90
|
+
loadProjectConfig(): ScopeConfig;
|
|
91
|
+
loadAgentConfig(agentName?: string): ScopeConfig;
|
|
92
|
+
loadProjectAgentConfig(agentName?: string): ScopeConfig;
|
|
93
|
+
getConfiguredMcpServerNames(): readonly string[];
|
|
94
|
+
/** Combined mtime stamp for cache invalidation. */
|
|
95
|
+
getCacheStamp(agentName?: string): string;
|
|
96
|
+
/** Accumulated config-parse issues across all loads. */
|
|
97
|
+
getConfigIssues(): string[];
|
|
98
|
+
/** Resolved paths for the /permission-system show command. */
|
|
99
|
+
getResolvedPolicyPaths(): ResolvedPolicyPaths;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Default path factories (deferred until call-time, not module scope)
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function defaultGlobalConfigPath(): string {
|
|
107
|
+
return getGlobalConfigPath(getAgentDir());
|
|
108
|
+
}
|
|
109
|
+
function defaultAgentsDir(): string {
|
|
110
|
+
return join(getAgentDir(), "agents");
|
|
111
|
+
}
|
|
112
|
+
function defaultGlobalMcpConfigPath(): string {
|
|
113
|
+
return join(getAgentDir(), "mcp.json");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// File cache helper type
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
type FileCacheEntry<TValue> = {
|
|
121
|
+
stamp: string;
|
|
122
|
+
value: TValue;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Options shared between FilePolicyLoader and the backward-compat
|
|
127
|
+
// PermissionManager constructor.
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
export interface PolicyLoaderOptions {
|
|
131
|
+
globalConfigPath?: string;
|
|
132
|
+
agentsDir?: string;
|
|
133
|
+
projectGlobalConfigPath?: string;
|
|
134
|
+
projectAgentsDir?: string;
|
|
135
|
+
globalMcpConfigPath?: string;
|
|
136
|
+
mcpServerNames?: readonly string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// FilePolicyLoader — the production implementation
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Production `PolicyLoader` that reads config files from disk with
|
|
145
|
+
* mtime-based caching.
|
|
146
|
+
*/
|
|
147
|
+
export class FilePolicyLoader implements PolicyLoader {
|
|
148
|
+
private readonly globalConfigPath: string;
|
|
149
|
+
private readonly agentsDir: string;
|
|
150
|
+
private readonly projectGlobalConfigPath: string | null;
|
|
151
|
+
private readonly projectAgentsDir: string | null;
|
|
152
|
+
private readonly globalMcpConfigPath: string;
|
|
153
|
+
private readonly configuredMcpServerNamesOverride: readonly string[] | null;
|
|
154
|
+
|
|
155
|
+
private globalConfigCache: FileCacheEntry<ScopeConfig> | null = null;
|
|
156
|
+
private projectGlobalConfigCache: FileCacheEntry<ScopeConfig> | null = null;
|
|
157
|
+
private readonly agentConfigCache = new Map<
|
|
158
|
+
string,
|
|
159
|
+
FileCacheEntry<ScopeConfig>
|
|
160
|
+
>();
|
|
161
|
+
private readonly projectAgentConfigCache = new Map<
|
|
162
|
+
string,
|
|
163
|
+
FileCacheEntry<ScopeConfig>
|
|
164
|
+
>();
|
|
165
|
+
private configuredMcpServerNamesCache: FileCacheEntry<
|
|
166
|
+
readonly string[]
|
|
167
|
+
> | null = null;
|
|
168
|
+
private accumulatedConfigIssues: string[] = [];
|
|
169
|
+
|
|
170
|
+
constructor(options: PolicyLoaderOptions = {}) {
|
|
171
|
+
this.globalConfigPath =
|
|
172
|
+
options.globalConfigPath || defaultGlobalConfigPath();
|
|
173
|
+
this.agentsDir = options.agentsDir || defaultAgentsDir();
|
|
174
|
+
this.projectGlobalConfigPath = options.projectGlobalConfigPath || null;
|
|
175
|
+
this.projectAgentsDir = options.projectAgentsDir || null;
|
|
176
|
+
this.globalMcpConfigPath =
|
|
177
|
+
options.globalMcpConfigPath || defaultGlobalMcpConfigPath();
|
|
178
|
+
this.configuredMcpServerNamesOverride = options.mcpServerNames
|
|
179
|
+
? [
|
|
180
|
+
...new Set(
|
|
181
|
+
options.mcpServerNames
|
|
182
|
+
.map((name) => name.trim())
|
|
183
|
+
.filter((name) => name.length > 0),
|
|
184
|
+
),
|
|
185
|
+
]
|
|
186
|
+
: null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Config issue accumulation ────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
private accumulateConfigIssues(issues: string[]): void {
|
|
192
|
+
for (const issue of issues) {
|
|
193
|
+
if (!this.accumulatedConfigIssues.includes(issue)) {
|
|
194
|
+
this.accumulatedConfigIssues.push(issue);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getConfigIssues(): string[] {
|
|
200
|
+
return [...this.accumulatedConfigIssues];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Scope loaders ────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
loadGlobalConfig(): ScopeConfig {
|
|
206
|
+
const stamp = getFileStamp(this.globalConfigPath);
|
|
207
|
+
if (this.globalConfigCache?.stamp === stamp) {
|
|
208
|
+
return this.globalConfigCache.value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const { config, issues } = loadUnifiedConfig(this.globalConfigPath);
|
|
212
|
+
this.accumulateConfigIssues(issues);
|
|
213
|
+
|
|
214
|
+
const value: ScopeConfig = {
|
|
215
|
+
permission: config.permission,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.globalConfigCache = { stamp, value };
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
loadProjectConfig(): ScopeConfig {
|
|
223
|
+
if (!this.projectGlobalConfigPath) {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const stamp = getFileStamp(this.projectGlobalConfigPath);
|
|
228
|
+
if (this.projectGlobalConfigCache?.stamp === stamp) {
|
|
229
|
+
return this.projectGlobalConfigCache.value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { config, issues } = loadUnifiedConfig(this.projectGlobalConfigPath);
|
|
233
|
+
this.accumulateConfigIssues(issues);
|
|
234
|
+
|
|
235
|
+
const value: ScopeConfig = {
|
|
236
|
+
permission: config.permission,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this.projectGlobalConfigCache = { stamp, value };
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private loadScopeConfigFrom(
|
|
244
|
+
dir: string | null,
|
|
245
|
+
cache: Map<string, FileCacheEntry<ScopeConfig>>,
|
|
246
|
+
agentName?: string,
|
|
247
|
+
): ScopeConfig {
|
|
248
|
+
if (!dir || !agentName) {
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const filePath = join(dir, `${agentName}.md`);
|
|
253
|
+
const stamp = getFileStamp(filePath);
|
|
254
|
+
const cached = cache.get(agentName);
|
|
255
|
+
if (cached?.stamp === stamp) {
|
|
256
|
+
return cached.value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let value: ScopeConfig;
|
|
260
|
+
try {
|
|
261
|
+
const markdown = readFileSync(filePath, "utf-8");
|
|
262
|
+
const frontmatter = extractFrontmatter(markdown);
|
|
263
|
+
if (!frontmatter) {
|
|
264
|
+
value = {};
|
|
265
|
+
} else {
|
|
266
|
+
const parsed = parseSimpleYamlMap(frontmatter);
|
|
267
|
+
const { config, issues } = normalizeUnifiedConfig(parsed);
|
|
268
|
+
this.accumulateConfigIssues(issues);
|
|
269
|
+
value = { permission: config.permission };
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
value = {};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
cache.set(agentName, { stamp, value });
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
loadAgentConfig(agentName?: string): ScopeConfig {
|
|
280
|
+
return this.loadScopeConfigFrom(
|
|
281
|
+
this.agentsDir,
|
|
282
|
+
this.agentConfigCache,
|
|
283
|
+
agentName,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
loadProjectAgentConfig(agentName?: string): ScopeConfig {
|
|
288
|
+
return this.loadScopeConfigFrom(
|
|
289
|
+
this.projectAgentsDir,
|
|
290
|
+
this.projectAgentConfigCache,
|
|
291
|
+
agentName,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── MCP server names ─────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
getConfiguredMcpServerNames(): readonly string[] {
|
|
298
|
+
if (this.configuredMcpServerNamesOverride) {
|
|
299
|
+
return this.configuredMcpServerNamesOverride;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const paths = [this.globalMcpConfigPath];
|
|
303
|
+
const stamp = paths
|
|
304
|
+
.map((path) => `${path}:${getFileStamp(path)}`)
|
|
305
|
+
.join("|");
|
|
306
|
+
if (this.configuredMcpServerNamesCache?.stamp === stamp) {
|
|
307
|
+
return this.configuredMcpServerNamesCache.value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const value = getConfiguredMcpServerNamesFromPaths(paths);
|
|
311
|
+
this.configuredMcpServerNamesCache = { stamp, value };
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Cache stamp ───────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
getCacheStamp(agentName?: string): string {
|
|
318
|
+
const agentStamp = agentName
|
|
319
|
+
? getFileStamp(join(this.agentsDir, `${agentName}.md`))
|
|
320
|
+
: "missing";
|
|
321
|
+
const projectStamp = this.projectGlobalConfigPath
|
|
322
|
+
? getFileStamp(this.projectGlobalConfigPath)
|
|
323
|
+
: "none";
|
|
324
|
+
const projectAgentStamp =
|
|
325
|
+
this.projectAgentsDir && agentName
|
|
326
|
+
? getFileStamp(join(this.projectAgentsDir, `${agentName}.md`))
|
|
327
|
+
: "none";
|
|
328
|
+
|
|
329
|
+
return `${getFileStamp(this.globalConfigPath)}|${projectStamp}|${agentStamp}|${projectAgentStamp}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Resolved paths ────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
getResolvedPolicyPaths(): ResolvedPolicyPaths {
|
|
335
|
+
return {
|
|
336
|
+
globalConfigPath: this.globalConfigPath,
|
|
337
|
+
globalConfigExists: existsSync(this.globalConfigPath),
|
|
338
|
+
projectConfigPath: this.projectGlobalConfigPath,
|
|
339
|
+
projectConfigExists: this.projectGlobalConfigPath
|
|
340
|
+
? existsSync(this.projectGlobalConfigPath)
|
|
341
|
+
: false,
|
|
342
|
+
agentsDir: this.agentsDir,
|
|
343
|
+
agentsDirExists: existsSync(this.agentsDir),
|
|
344
|
+
projectAgentsDir: this.projectAgentsDir,
|
|
345
|
+
projectAgentsDirExists: this.projectAgentsDir
|
|
346
|
+
? existsSync(this.projectAgentsDir)
|
|
347
|
+
: false,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -47,6 +47,21 @@ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
|
47
47
|
import { syncPermissionSystemStatus } from "./status";
|
|
48
48
|
import { isSubagentExecutionContext } from "./subagent-context";
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Mutable session state — the subset of ExtensionRuntime that handlers
|
|
52
|
+
* read and write. Lifecycle handlers reset fields here on session
|
|
53
|
+
* start/shutdown; gate adapters read permissionManager and sessionRules.
|
|
54
|
+
*/
|
|
55
|
+
export interface SessionState {
|
|
56
|
+
runtimeContext: ExtensionContext | null;
|
|
57
|
+
permissionManager: PermissionManager;
|
|
58
|
+
readonly sessionRules: SessionRules;
|
|
59
|
+
activeSkillEntries: SkillPromptEntry[];
|
|
60
|
+
lastKnownActiveAgentName: string | null;
|
|
61
|
+
lastActiveToolsCacheKey: string | null;
|
|
62
|
+
lastPromptStateCacheKey: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
66
|
* Runtime context object created once inside `piPermissionSystemExtension()`.
|
|
52
67
|
*
|
|
@@ -58,7 +73,7 @@ import { isSubagentExecutionContext } from "./subagent-context";
|
|
|
58
73
|
* Tests construct this via `createExtensionRuntime({ agentDir: tmpDir })`
|
|
59
74
|
* without timing issues around `PI_CODING_AGENT_DIR`.
|
|
60
75
|
*/
|
|
61
|
-
export interface ExtensionRuntime {
|
|
76
|
+
export interface ExtensionRuntime extends SessionState {
|
|
62
77
|
// ── Immutable paths (derived from agentDir at construction) ───────────
|
|
63
78
|
readonly agentDir: string;
|
|
64
79
|
readonly sessionsDir: string;
|
|
@@ -74,16 +89,9 @@ export interface ExtensionRuntime {
|
|
|
74
89
|
*/
|
|
75
90
|
readonly piInfrastructureDirs: string[];
|
|
76
91
|
|
|
77
|
-
// ── Mutable state
|
|
92
|
+
// ── Mutable state (beyond SessionState) ───────────────────────────────────
|
|
78
93
|
config: PermissionSystemExtensionConfig;
|
|
79
|
-
runtimeContext: ExtensionContext | null;
|
|
80
|
-
permissionManager: PermissionManager;
|
|
81
|
-
activeSkillEntries: SkillPromptEntry[];
|
|
82
|
-
lastKnownActiveAgentName: string | null;
|
|
83
|
-
lastActiveToolsCacheKey: string | null;
|
|
84
|
-
lastPromptStateCacheKey: string | null;
|
|
85
94
|
lastConfigWarning: string | null;
|
|
86
|
-
readonly sessionRules: SessionRules;
|
|
87
95
|
|
|
88
96
|
// ── Forwarding polling state ───────────────────────────────────────────
|
|
89
97
|
permissionForwardingContext: ExtensionContext | null;
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "../../src/handlers/before-agent-start";
|
|
8
8
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
9
|
import type { PermissionManager } from "../../src/permission-manager";
|
|
10
|
-
import type {
|
|
10
|
+
import type { SessionState } from "../../src/runtime";
|
|
11
11
|
import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
|
|
12
12
|
|
|
13
13
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
@@ -57,40 +57,30 @@ function makePm(
|
|
|
57
57
|
} as unknown as PermissionManager;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function
|
|
61
|
-
overrides: Partial<ExtensionRuntime> = {},
|
|
62
|
-
): ExtensionRuntime {
|
|
60
|
+
function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
63
61
|
return {
|
|
64
|
-
agentDir: "/test/agent",
|
|
65
|
-
sessionsDir: "/test/agent/sessions",
|
|
66
|
-
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
67
|
-
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
68
|
-
globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
|
|
69
|
-
config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
|
|
70
62
|
runtimeContext: null,
|
|
71
63
|
permissionManager: makePm() as unknown as PermissionManager,
|
|
72
64
|
activeSkillEntries: [] as SkillPromptEntry[],
|
|
73
65
|
lastKnownActiveAgentName: null,
|
|
74
66
|
lastActiveToolsCacheKey: null,
|
|
75
67
|
lastPromptStateCacheKey: null,
|
|
76
|
-
lastConfigWarning: null,
|
|
77
68
|
sessionRules: {
|
|
78
69
|
approve: vi.fn(),
|
|
79
70
|
getRuleset: vi.fn().mockReturnValue([]),
|
|
80
71
|
clear: vi.fn(),
|
|
81
|
-
} as unknown as
|
|
82
|
-
permissionForwardingContext: null,
|
|
83
|
-
permissionForwardingTimer: null,
|
|
84
|
-
isProcessingForwardedRequests: false,
|
|
85
|
-
writeDebugLog: vi.fn(),
|
|
86
|
-
writeReviewLog: vi.fn(),
|
|
72
|
+
} as unknown as SessionState["sessionRules"],
|
|
87
73
|
...overrides,
|
|
88
|
-
}
|
|
74
|
+
};
|
|
89
75
|
}
|
|
90
76
|
|
|
91
77
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
92
78
|
return {
|
|
93
|
-
|
|
79
|
+
session: makeSession(),
|
|
80
|
+
writeDebugLog: vi.fn(),
|
|
81
|
+
writeReviewLog: vi.fn(),
|
|
82
|
+
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
83
|
+
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
94
84
|
createPermissionManagerForCwd: vi.fn().mockReturnValue(makePm()),
|
|
95
85
|
refreshExtensionConfig: vi.fn(),
|
|
96
86
|
notifyWarning: vi.fn(),
|
|
@@ -176,7 +166,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
176
166
|
it("filters out denied tools from allowed list", async () => {
|
|
177
167
|
const pm = makePm("deny");
|
|
178
168
|
const deps = makeDeps({
|
|
179
|
-
|
|
169
|
+
session: makeSession({
|
|
180
170
|
permissionManager: pm as unknown as PermissionManager,
|
|
181
171
|
}),
|
|
182
172
|
getAllTools: vi
|
|
@@ -191,7 +181,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
191
181
|
it("includes allowed and ask tools in the active list", async () => {
|
|
192
182
|
const pm = makePm("allow");
|
|
193
183
|
const deps = makeDeps({
|
|
194
|
-
|
|
184
|
+
session: makeSession({
|
|
195
185
|
permissionManager: pm as unknown as PermissionManager,
|
|
196
186
|
}),
|
|
197
187
|
getAllTools: vi
|
|
@@ -207,7 +197,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
207
197
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
208
198
|
});
|
|
209
199
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
210
|
-
expect(deps.
|
|
200
|
+
expect(deps.session.lastActiveToolsCacheKey).not.toBeNull();
|
|
211
201
|
});
|
|
212
202
|
|
|
213
203
|
it("skips setActiveTools when cache key is unchanged", async () => {
|
|
@@ -217,7 +207,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
217
207
|
);
|
|
218
208
|
const key = createActiveToolsCacheKey(["read"]);
|
|
219
209
|
const deps = makeDeps({
|
|
220
|
-
|
|
210
|
+
session: makeSession({ lastActiveToolsCacheKey: key }),
|
|
221
211
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
222
212
|
});
|
|
223
213
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
@@ -238,7 +228,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
238
228
|
);
|
|
239
229
|
// The prompt was modified, so systemPrompt should be returned
|
|
240
230
|
expect(result).toHaveProperty("systemPrompt");
|
|
241
|
-
expect(deps.
|
|
231
|
+
expect(deps.session.lastPromptStateCacheKey).not.toBeNull();
|
|
242
232
|
});
|
|
243
233
|
|
|
244
234
|
it("returns empty object when systemPrompt is unchanged", async () => {
|
|
@@ -259,7 +249,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
259
249
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
260
250
|
});
|
|
261
251
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
262
|
-
expect(deps.
|
|
252
|
+
expect(deps.session.activeSkillEntries).toEqual(expect.any(Array));
|
|
263
253
|
});
|
|
264
254
|
|
|
265
255
|
it("returns empty object and skips prompt work when prompt cache key is unchanged", async () => {
|
|
@@ -277,7 +267,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
277
267
|
allowedToolNames: allowedTools,
|
|
278
268
|
});
|
|
279
269
|
const deps = makeDeps({
|
|
280
|
-
|
|
270
|
+
session: makeSession({
|
|
281
271
|
permissionManager: pm as unknown as PermissionManager,
|
|
282
272
|
lastPromptStateCacheKey: key,
|
|
283
273
|
}),
|
|
@@ -286,6 +276,6 @@ describe("handleBeforeAgentStart", () => {
|
|
|
286
276
|
const result = await handleBeforeAgentStart(deps, makeEvent("hello"), ctx);
|
|
287
277
|
expect(result).toEqual({});
|
|
288
278
|
// activeSkillEntries was not assigned by the handler (early return)
|
|
289
|
-
expect(deps.
|
|
279
|
+
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
290
280
|
});
|
|
291
281
|
});
|