@gotgenes/pi-permission-system 10.5.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 +7 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +11 -6
- package/src/handlers/lifecycle.ts +7 -4
- package/src/handlers/permission-gate-handler.ts +3 -3
- package/src/index.ts +8 -3
- package/src/permission-resolver.ts +0 -3
- package/src/permission-session.ts +8 -52
- 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 +79 -159
- 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/handler-fixtures.ts +99 -102
- package/test/helpers/session-fixtures.ts +192 -0
- package/test/permission-resolver.test.ts +3 -1
- package/test/permission-session.test.ts +14 -198
- 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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [10.5.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.5.0...pi-permission-system-v10.5.1) (2026-06-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* correct SkillPermissionChecker comment after resolver rewire ([#341](https://github.com/gotgenes/pi-packages/issues/341)) ([1528382](https://github.com/gotgenes/pi-packages/commit/15283820a920fead92b348410828332b69f0a0d9))
|
|
14
|
+
|
|
8
15
|
## [10.5.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.4.0...pi-permission-system-v10.5.0) (2026-06-07)
|
|
9
16
|
|
|
10
17
|
|
package/package.json
CHANGED
|
@@ -2,11 +2,12 @@ import type {
|
|
|
2
2
|
BeforeAgentStartEventResult,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { AgentPrepSession } from "#src/agent-prep-session";
|
|
6
5
|
import {
|
|
7
6
|
createActiveToolsCacheKey,
|
|
8
7
|
createBeforeAgentStartPromptStateKey,
|
|
9
8
|
} from "#src/before-agent-start-cache";
|
|
9
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
10
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
10
11
|
import { resolveSkillPromptEntries } from "#src/skill-prompt-sanitizer";
|
|
11
12
|
import { sanitizeAvailableToolsSection } from "#src/system-prompt-sanitizer";
|
|
12
13
|
import { getToolNameFromValue, type ToolRegistry } from "#src/tool-registry";
|
|
@@ -35,12 +36,14 @@ export function shouldExposeTool(
|
|
|
35
36
|
* Handles the `before_agent_start` event: tool filtering + prompt sanitization.
|
|
36
37
|
*
|
|
37
38
|
* Constructor deps:
|
|
38
|
-
* - `session` — encapsulates all mutable session state
|
|
39
|
+
* - `session` — encapsulates all mutable session state and lifecycle operations
|
|
40
|
+
* - `resolver` — owns permission-query surface: `getToolPermission`, `getPolicyCacheStamp`, skill check
|
|
39
41
|
* - `toolRegistry` — Pi tool API subset (getAll + setActive)
|
|
40
42
|
*/
|
|
41
43
|
export class AgentPrepHandler {
|
|
42
44
|
constructor(
|
|
43
|
-
private readonly session:
|
|
45
|
+
private readonly session: PermissionSession,
|
|
46
|
+
private readonly resolver: PermissionResolver,
|
|
44
47
|
private readonly toolRegistry: ToolRegistry,
|
|
45
48
|
) {}
|
|
46
49
|
|
|
@@ -63,7 +66,7 @@ export class AgentPrepHandler {
|
|
|
63
66
|
}
|
|
64
67
|
if (
|
|
65
68
|
shouldExposeTool(toolName, agentName, (t, a) =>
|
|
66
|
-
this.
|
|
69
|
+
this.resolver.getToolPermission(t, a),
|
|
67
70
|
)
|
|
68
71
|
) {
|
|
69
72
|
allowedTools.push(toolName);
|
|
@@ -79,7 +82,9 @@ export class AgentPrepHandler {
|
|
|
79
82
|
const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
|
|
80
83
|
agentName,
|
|
81
84
|
cwd: ctx.cwd,
|
|
82
|
-
permissionStamp: this.
|
|
85
|
+
permissionStamp: this.resolver.getPolicyCacheStamp(
|
|
86
|
+
agentName ?? undefined,
|
|
87
|
+
),
|
|
83
88
|
systemPrompt: event.systemPrompt,
|
|
84
89
|
allowedToolNames: allowedTools,
|
|
85
90
|
});
|
|
@@ -96,7 +101,7 @@ export class AgentPrepHandler {
|
|
|
96
101
|
);
|
|
97
102
|
const skillPromptResult = resolveSkillPromptEntries(
|
|
98
103
|
toolPromptResult.prompt,
|
|
99
|
-
this.
|
|
104
|
+
this.resolver,
|
|
100
105
|
agentName,
|
|
101
106
|
ctx.cwd,
|
|
102
107
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
3
5
|
import type { ServiceLifecycle } from "#src/service-lifecycle";
|
|
4
|
-
import type { SessionLifecycleSession } from "#src/session-lifecycle-session";
|
|
5
6
|
import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/status";
|
|
6
7
|
|
|
7
8
|
/** Minimal subset of SessionStartEvent used by this handler. */
|
|
@@ -18,14 +19,16 @@ interface ResourcesDiscoverPayload {
|
|
|
18
19
|
* Handles session lifecycle events: start, reload, and shutdown.
|
|
19
20
|
*
|
|
20
21
|
* Constructor deps:
|
|
21
|
-
* - `session` — encapsulates all mutable session state
|
|
22
|
+
* - `session` — encapsulates all mutable session state and lifecycle operations
|
|
23
|
+
* - `resolver` — owns permission-query surface: `getConfigIssues`
|
|
22
24
|
* - `serviceLifecycle` — owns the process-global service publication;
|
|
23
25
|
* `activate` publishes (skipped for registered subagent children) and emits
|
|
24
26
|
* the ready event; `teardown` unsubscribes all session listeners and unpublishes
|
|
25
27
|
*/
|
|
26
28
|
export class SessionLifecycleHandler {
|
|
27
29
|
constructor(
|
|
28
|
-
private readonly session:
|
|
30
|
+
private readonly session: PermissionSession,
|
|
31
|
+
private readonly resolver: PermissionResolver,
|
|
29
32
|
private readonly serviceLifecycle: ServiceLifecycle,
|
|
30
33
|
) {}
|
|
31
34
|
|
|
@@ -38,7 +41,7 @@ export class SessionLifecycleHandler {
|
|
|
38
41
|
this.session.logResolvedConfigPaths();
|
|
39
42
|
|
|
40
43
|
const agentName = this.session.resolveAgentName(ctx);
|
|
41
|
-
const policyIssues = this.
|
|
44
|
+
const policyIssues = this.resolver.getConfigIssues(agentName ?? undefined);
|
|
42
45
|
for (const issue of policyIssues) {
|
|
43
46
|
this.session.logger.warn(issue);
|
|
44
47
|
}
|
|
@@ -4,11 +4,11 @@ import type {
|
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
import { toRecord } from "#src/common";
|
|
7
|
-
import type { GateHandlerSession } from "#src/gate-handler-session";
|
|
8
7
|
import {
|
|
9
8
|
formatMissingToolNameReason,
|
|
10
9
|
formatUnknownToolReason,
|
|
11
10
|
} from "#src/permission-prompts";
|
|
11
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
12
12
|
import {
|
|
13
13
|
checkRequestedToolRegistration,
|
|
14
14
|
getToolNameFromValue,
|
|
@@ -31,7 +31,7 @@ interface InputPayload {
|
|
|
31
31
|
* Handles permission gate events: tool_call and input.
|
|
32
32
|
*
|
|
33
33
|
* Constructor deps:
|
|
34
|
-
* - `session` —
|
|
34
|
+
* - `session` — state/lifecycle owner: bind per-event context, resolve agent name
|
|
35
35
|
* - `toolRegistry` — Pi tool API subset (getAll + setActive)
|
|
36
36
|
* - `pipeline` — owns tool-call gate-producer assembly and the run loop
|
|
37
37
|
* - `skillInputPipeline` — owns skill-input gate assembly (pre-check, notify, run)
|
|
@@ -39,7 +39,7 @@ interface InputPayload {
|
|
|
39
39
|
*/
|
|
40
40
|
export class PermissionGateHandler {
|
|
41
41
|
constructor(
|
|
42
|
-
private readonly session:
|
|
42
|
+
private readonly session: PermissionSession,
|
|
43
43
|
private readonly toolRegistry: ToolRegistry,
|
|
44
44
|
private readonly pipeline: ToolCallGatePipeline,
|
|
45
45
|
private readonly skillInputPipeline: SkillInputGatePipeline,
|
package/src/index.ts
CHANGED
|
@@ -157,12 +157,17 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
157
157
|
setActive: (names: string[]) => pi.setActiveTools(names),
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
|
|
161
|
-
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
162
160
|
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
163
161
|
|
|
162
|
+
const lifecycle = new SessionLifecycleHandler(
|
|
163
|
+
session,
|
|
164
|
+
resolver,
|
|
165
|
+
serviceLifecycle,
|
|
166
|
+
);
|
|
167
|
+
const agentPrep = new AgentPrepHandler(session, resolver, toolRegistry);
|
|
168
|
+
|
|
164
169
|
const reporter = new GateDecisionReporter(session.logger, pi.events);
|
|
165
|
-
const gateRunner = new GateRunner(resolver,
|
|
170
|
+
const gateRunner = new GateRunner(resolver, sessionRules, gateway, reporter);
|
|
166
171
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
167
172
|
resolver,
|
|
168
173
|
session,
|
|
@@ -67,17 +67,14 @@ export class PermissionResolver implements ScopedPermissionResolver {
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// fallow-ignore-next-line unused-class-member
|
|
71
70
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
72
71
|
return this.permissionManager.getToolPermission(toolName, agentName);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
// fallow-ignore-next-line unused-class-member
|
|
76
74
|
getConfigIssues(agentName?: string): string[] {
|
|
77
75
|
return this.permissionManager.getConfigIssues(agentName);
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
// fallow-ignore-next-line unused-class-member
|
|
81
78
|
getPolicyCacheStamp(agentName?: string): string {
|
|
82
79
|
return this.permissionManager.getPolicyCacheStamp(agentName);
|
|
83
80
|
}
|
|
@@ -4,18 +4,15 @@ import {
|
|
|
4
4
|
getActiveAgentName,
|
|
5
5
|
getActiveAgentNameFromSystemPrompt,
|
|
6
6
|
} from "./active-agent";
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import type { SessionConfigStore } from "./config-store";
|
|
9
9
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
10
10
|
import type { ExtensionPaths } from "./extension-paths";
|
|
11
11
|
import type { ForwardingController } from "./forwarding-manager";
|
|
12
|
-
import type {
|
|
12
|
+
import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipeline";
|
|
13
13
|
import type { ScopedPermissionManager } from "./permission-manager";
|
|
14
14
|
import type { PromptingGatewayLifecycle } from "./prompting-gateway";
|
|
15
|
-
|
|
16
|
-
import type { SessionApproval } from "./session-approval";
|
|
17
|
-
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
18
|
-
import type { SessionLifecycleSession } from "./session-lifecycle-session";
|
|
15
|
+
|
|
19
16
|
import type { SessionLogger } from "./session-logger";
|
|
20
17
|
import type { SessionRules } from "./session-rules";
|
|
21
18
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
@@ -23,7 +20,6 @@ import {
|
|
|
23
20
|
resolveToolPreviewLimits,
|
|
24
21
|
type ToolPreviewFormatterOptions,
|
|
25
22
|
} from "./tool-preview-formatter";
|
|
26
|
-
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
27
23
|
|
|
28
24
|
/**
|
|
29
25
|
* Encapsulates all mutable session state and exposes operations instead of
|
|
@@ -40,13 +36,7 @@ import type { PermissionCheckResult, PermissionState } from "./types";
|
|
|
40
36
|
* - `SessionConfigStore` — owns extension config; provides refresh, log, read
|
|
41
37
|
* - `PromptingGatewayLifecycle` — prompting lifecycle forwarded via activate/deactivate
|
|
42
38
|
*/
|
|
43
|
-
export class PermissionSession
|
|
44
|
-
implements
|
|
45
|
-
SessionApprovalRecorder,
|
|
46
|
-
GateHandlerSession,
|
|
47
|
-
AgentPrepSession,
|
|
48
|
-
SessionLifecycleSession
|
|
49
|
-
{
|
|
39
|
+
export class PermissionSession implements ToolCallGateInputs {
|
|
50
40
|
private context: ExtensionContext | null = null;
|
|
51
41
|
private skillEntries: SkillPromptEntry[] = [];
|
|
52
42
|
private knownAgentName: string | null = null;
|
|
@@ -84,44 +74,6 @@ export class PermissionSession
|
|
|
84
74
|
return this.context;
|
|
85
75
|
}
|
|
86
76
|
|
|
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
|
-
recordSessionApproval(approval: SessionApproval): void {
|
|
122
|
-
this.sessionRules.record(approval);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
77
|
// ── Session lifecycle ────────────────────────────────────────────────────
|
|
126
78
|
|
|
127
79
|
/**
|
|
@@ -212,6 +164,10 @@ export class PermissionSession
|
|
|
212
164
|
return this.knownAgentName;
|
|
213
165
|
}
|
|
214
166
|
|
|
167
|
+
// Read by config-modal (`controller.session.lastKnownActiveAgentName`).
|
|
168
|
+
// fallow cannot trace the getter through the command's object-literal
|
|
169
|
+
// wiring, so it reports a false positive here.
|
|
170
|
+
// fallow-ignore-next-line unused-class-member
|
|
215
171
|
get lastKnownActiveAgentName(): string | null {
|
|
216
172
|
return this.knownAgentName;
|
|
217
173
|
}
|
package/src/session-rules.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { dirname, sep } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import type { Ruleset } from "./rule";
|
|
4
4
|
import type { SessionApproval } from "./session-approval";
|
|
5
|
+
import type { SessionApprovalRecorder } from "./session-approval-recorder";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Ephemeral in-memory store of session-scoped permission approvals.
|
|
@@ -11,7 +12,7 @@ import type { SessionApproval } from "./session-approval";
|
|
|
11
12
|
*
|
|
12
13
|
* Cleared on session_shutdown — never persisted to disk.
|
|
13
14
|
*/
|
|
14
|
-
export class SessionRules {
|
|
15
|
+
export class SessionRules implements SessionApprovalRecorder {
|
|
15
16
|
private rules: Ruleset = [];
|
|
16
17
|
|
|
17
18
|
/** Record a wildcard pattern as approved for the given surface. */
|
|
@@ -36,7 +37,7 @@ export class SessionRules {
|
|
|
36
37
|
* The loop lives here so callers never need to know whether an approval
|
|
37
38
|
* carries one pattern or many — they just tell the store to record it.
|
|
38
39
|
*/
|
|
39
|
-
|
|
40
|
+
recordSessionApproval(approval: SessionApproval): void {
|
|
40
41
|
for (const pattern of approval.patterns) {
|
|
41
42
|
this.approve(approval.surface, pattern);
|
|
42
43
|
}
|
|
@@ -8,7 +8,7 @@ import type { PermissionCheckResult, PermissionState } from "./types";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Narrow interface for the permission checker used by skill prompt resolution.
|
|
11
|
-
* Both `PermissionManager` and `
|
|
11
|
+
* Both `PermissionManager` and `PermissionResolver` satisfy this structurally.
|
|
12
12
|
*/
|
|
13
13
|
export interface SkillPermissionChecker {
|
|
14
14
|
checkPermission(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import type { AgentPrepSession } from "#src/agent-prep-session";
|
|
4
3
|
import {
|
|
5
4
|
AgentPrepHandler,
|
|
6
5
|
shouldExposeTool,
|
|
@@ -8,6 +7,10 @@ import {
|
|
|
8
7
|
import type { ToolRegistry } from "#src/tool-registry";
|
|
9
8
|
|
|
10
9
|
import { makeCheckResult, makeCtx } from "#test/helpers/handler-fixtures";
|
|
10
|
+
import {
|
|
11
|
+
makeRealResolver,
|
|
12
|
+
makeRealSession,
|
|
13
|
+
} from "#test/helpers/session-fixtures";
|
|
11
14
|
|
|
12
15
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
13
16
|
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
@@ -25,51 +28,6 @@ function makeEvent(systemPrompt = "You are an assistant.") {
|
|
|
25
28
|
return { systemPrompt };
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
function makeSession(
|
|
29
|
-
overrides: Partial<AgentPrepSession> = {},
|
|
30
|
-
): AgentPrepSession {
|
|
31
|
-
return {
|
|
32
|
-
activate: overrides.activate ?? vi.fn<AgentPrepSession["activate"]>(),
|
|
33
|
-
refreshConfig:
|
|
34
|
-
overrides.refreshConfig ?? vi.fn<AgentPrepSession["refreshConfig"]>(),
|
|
35
|
-
resolveAgentName:
|
|
36
|
-
overrides.resolveAgentName ??
|
|
37
|
-
vi.fn<AgentPrepSession["resolveAgentName"]>().mockReturnValue(null),
|
|
38
|
-
checkPermission:
|
|
39
|
-
overrides.checkPermission ??
|
|
40
|
-
vi
|
|
41
|
-
.fn<AgentPrepSession["checkPermission"]>()
|
|
42
|
-
.mockReturnValue(makeCheckResult()),
|
|
43
|
-
getToolPermission:
|
|
44
|
-
overrides.getToolPermission ??
|
|
45
|
-
vi.fn<AgentPrepSession["getToolPermission"]>().mockReturnValue("allow"),
|
|
46
|
-
shouldUpdateActiveTools:
|
|
47
|
-
overrides.shouldUpdateActiveTools ??
|
|
48
|
-
vi
|
|
49
|
-
.fn<AgentPrepSession["shouldUpdateActiveTools"]>()
|
|
50
|
-
.mockReturnValue(true),
|
|
51
|
-
commitActiveToolsCacheKey:
|
|
52
|
-
overrides.commitActiveToolsCacheKey ??
|
|
53
|
-
vi.fn<AgentPrepSession["commitActiveToolsCacheKey"]>(),
|
|
54
|
-
getPolicyCacheStamp:
|
|
55
|
-
overrides.getPolicyCacheStamp ??
|
|
56
|
-
vi
|
|
57
|
-
.fn<AgentPrepSession["getPolicyCacheStamp"]>()
|
|
58
|
-
.mockReturnValue("stamp-1"),
|
|
59
|
-
shouldUpdatePromptState:
|
|
60
|
-
overrides.shouldUpdatePromptState ??
|
|
61
|
-
vi
|
|
62
|
-
.fn<AgentPrepSession["shouldUpdatePromptState"]>()
|
|
63
|
-
.mockReturnValue(true),
|
|
64
|
-
commitPromptStateCacheKey:
|
|
65
|
-
overrides.commitPromptStateCacheKey ??
|
|
66
|
-
vi.fn<AgentPrepSession["commitPromptStateCacheKey"]>(),
|
|
67
|
-
setActiveSkillEntries:
|
|
68
|
-
overrides.setActiveSkillEntries ??
|
|
69
|
-
vi.fn<AgentPrepSession["setActiveSkillEntries"]>(),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
31
|
function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
74
32
|
return {
|
|
75
33
|
getAll: vi.fn().mockReturnValue([]),
|
|
@@ -78,18 +36,33 @@ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
|
78
36
|
};
|
|
79
37
|
}
|
|
80
38
|
|
|
81
|
-
function
|
|
82
|
-
|
|
39
|
+
function makeSetup(opts?: {
|
|
40
|
+
toolPermission?: "allow" | "deny" | "ask";
|
|
83
41
|
toolRegistry?: Partial<ToolRegistry>;
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
42
|
+
}) {
|
|
43
|
+
const { session, permissionManager, sessionRules, configStore, forwarding } =
|
|
44
|
+
makeRealSession();
|
|
45
|
+
const { resolver } = makeRealResolver(permissionManager, sessionRules);
|
|
46
|
+
if (opts?.toolPermission !== undefined) {
|
|
47
|
+
vi.mocked(permissionManager.getToolPermission).mockReturnValue(
|
|
48
|
+
opts.toolPermission,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
// Default checkPermission returns allow (for skill-prompt sanitizer)
|
|
52
|
+
vi.mocked(permissionManager.checkPermission).mockReturnValue(
|
|
53
|
+
makeCheckResult(),
|
|
54
|
+
);
|
|
55
|
+
const toolRegistry = makeToolRegistry(opts?.toolRegistry);
|
|
56
|
+
const handler = new AgentPrepHandler(session, resolver, toolRegistry);
|
|
57
|
+
return {
|
|
58
|
+
handler,
|
|
59
|
+
session,
|
|
60
|
+
resolver,
|
|
61
|
+
permissionManager,
|
|
62
|
+
configStore,
|
|
63
|
+
forwarding,
|
|
64
|
+
toolRegistry,
|
|
65
|
+
};
|
|
93
66
|
}
|
|
94
67
|
|
|
95
68
|
// ── shouldExposeTool (pure helper) ─────────────────────────────────────────
|
|
@@ -128,31 +101,30 @@ describe("shouldExposeTool", () => {
|
|
|
128
101
|
describe("AgentPrepHandler.handle", () => {
|
|
129
102
|
it("activates the session with ctx", async () => {
|
|
130
103
|
const ctx = makeCtx();
|
|
131
|
-
const { handler,
|
|
104
|
+
const { handler, forwarding } = makeSetup();
|
|
132
105
|
await handler.handle(makeEvent(), ctx);
|
|
133
|
-
|
|
106
|
+
// Real session.activate calls forwarding.start
|
|
107
|
+
expect(forwarding.start).toHaveBeenCalledWith(ctx);
|
|
134
108
|
});
|
|
135
109
|
|
|
136
110
|
it("refreshes config with ctx", async () => {
|
|
137
111
|
const ctx = makeCtx();
|
|
138
|
-
const { handler,
|
|
112
|
+
const { handler, configStore } = makeSetup();
|
|
139
113
|
await handler.handle(makeEvent(), ctx);
|
|
140
|
-
expect(
|
|
114
|
+
expect(configStore.refresh).toHaveBeenCalledWith(ctx);
|
|
141
115
|
});
|
|
142
116
|
|
|
143
117
|
it("resolves agent name using systemPrompt", async () => {
|
|
144
118
|
const ctx = makeCtx();
|
|
145
|
-
const { handler, session } =
|
|
119
|
+
const { handler, session } = makeSetup();
|
|
120
|
+
const spy = vi.spyOn(session, "resolveAgentName");
|
|
146
121
|
await handler.handle(makeEvent("<active_agent name='x'>"), ctx);
|
|
147
|
-
expect(
|
|
148
|
-
ctx,
|
|
149
|
-
"<active_agent name='x'>",
|
|
150
|
-
);
|
|
122
|
+
expect(spy).toHaveBeenCalledWith(ctx, "<active_agent name='x'>");
|
|
151
123
|
});
|
|
152
124
|
|
|
153
125
|
it("filters out denied tools from allowed list", async () => {
|
|
154
|
-
const { handler, toolRegistry } =
|
|
155
|
-
|
|
126
|
+
const { handler, toolRegistry } = makeSetup({
|
|
127
|
+
toolPermission: "deny",
|
|
156
128
|
toolRegistry: {
|
|
157
129
|
getAll: vi.fn().mockReturnValue([{ name: "write" }, { name: "read" }]),
|
|
158
130
|
},
|
|
@@ -162,7 +134,7 @@ describe("AgentPrepHandler.handle", () => {
|
|
|
162
134
|
});
|
|
163
135
|
|
|
164
136
|
it("includes allowed and ask tools in the active list", async () => {
|
|
165
|
-
const { handler, toolRegistry } =
|
|
137
|
+
const { handler, toolRegistry } = makeSetup({
|
|
166
138
|
toolRegistry: {
|
|
167
139
|
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "write" }]),
|
|
168
140
|
},
|
|
@@ -172,60 +144,58 @@ describe("AgentPrepHandler.handle", () => {
|
|
|
172
144
|
});
|
|
173
145
|
|
|
174
146
|
it("commits active-tools cache key after applying", async () => {
|
|
175
|
-
const { handler, session } =
|
|
147
|
+
const { handler, session } = makeSetup({
|
|
176
148
|
toolRegistry: {
|
|
177
149
|
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
178
150
|
},
|
|
179
151
|
});
|
|
152
|
+
const spy = vi.spyOn(session, "commitActiveToolsCacheKey");
|
|
180
153
|
await handler.handle(makeEvent(), makeCtx());
|
|
181
|
-
expect(
|
|
154
|
+
expect(spy).toHaveBeenCalled();
|
|
182
155
|
});
|
|
183
156
|
|
|
184
157
|
it("skips setActive when cache key is unchanged", async () => {
|
|
185
|
-
const { handler, session, toolRegistry } =
|
|
186
|
-
session: { shouldUpdateActiveTools: vi.fn().mockReturnValue(false) },
|
|
158
|
+
const { handler, session, toolRegistry } = makeSetup({
|
|
187
159
|
toolRegistry: {
|
|
188
160
|
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
189
161
|
},
|
|
190
162
|
});
|
|
163
|
+
vi.spyOn(session, "shouldUpdateActiveTools").mockReturnValue(false);
|
|
191
164
|
await handler.handle(makeEvent(), makeCtx());
|
|
192
165
|
expect(toolRegistry.setActive).not.toHaveBeenCalled();
|
|
193
|
-
expect(session.commitActiveToolsCacheKey).not.toHaveBeenCalled();
|
|
194
166
|
});
|
|
195
167
|
|
|
196
168
|
it("returns empty object when prompt cache is unchanged", async () => {
|
|
197
|
-
const { handler, session } =
|
|
198
|
-
|
|
199
|
-
});
|
|
169
|
+
const { handler, session } = makeSetup();
|
|
170
|
+
vi.spyOn(session, "shouldUpdatePromptState").mockReturnValue(false);
|
|
200
171
|
const result = await handler.handle(makeEvent(), makeCtx());
|
|
201
172
|
expect(result).toEqual({});
|
|
202
|
-
expect(session.commitPromptStateCacheKey).not.toHaveBeenCalled();
|
|
203
173
|
});
|
|
204
174
|
|
|
205
175
|
it("commits prompt-state cache key and processes prompt when cache is new", async () => {
|
|
206
|
-
const { handler, session } =
|
|
176
|
+
const { handler, session } = makeSetup();
|
|
177
|
+
const spy = vi.spyOn(session, "commitPromptStateCacheKey");
|
|
207
178
|
await handler.handle(makeEvent(), makeCtx());
|
|
208
|
-
expect(
|
|
179
|
+
expect(spy).toHaveBeenCalled();
|
|
209
180
|
});
|
|
210
181
|
|
|
211
182
|
it("stores resolved skill entries on the session", async () => {
|
|
212
|
-
const { handler, session } =
|
|
183
|
+
const { handler, session } = makeSetup();
|
|
184
|
+
const spy = vi.spyOn(session, "setActiveSkillEntries");
|
|
213
185
|
await handler.handle(makeEvent(), makeCtx());
|
|
214
|
-
expect(
|
|
215
|
-
expect.any(Array),
|
|
216
|
-
);
|
|
186
|
+
expect(spy).toHaveBeenCalledWith(expect.any(Array));
|
|
217
187
|
});
|
|
218
188
|
|
|
219
189
|
it("returns modified systemPrompt when prompt changes", async () => {
|
|
220
190
|
const systemPrompt = `You are an assistant.\n\nAvailable tools:\n- read\n- write\n`;
|
|
221
|
-
const { handler } =
|
|
191
|
+
const { handler } = makeSetup();
|
|
222
192
|
const result = await handler.handle(makeEvent(systemPrompt), makeCtx());
|
|
223
193
|
expect(result).toHaveProperty("systemPrompt");
|
|
224
194
|
});
|
|
225
195
|
|
|
226
196
|
it("returns empty object when systemPrompt is unchanged", async () => {
|
|
227
197
|
const prompt = "No tools section here.";
|
|
228
|
-
const { handler } =
|
|
198
|
+
const { handler } = makeSetup();
|
|
229
199
|
const result = await handler.handle(makeEvent(prompt), makeCtx());
|
|
230
200
|
expect(result).toEqual({});
|
|
231
201
|
});
|