@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
|
@@ -17,20 +17,19 @@ vi.mock("../src/active-agent", () => ({
|
|
|
17
17
|
|
|
18
18
|
// ── Test helpers ───────────────────────────────────────────────────────────
|
|
19
19
|
|
|
20
|
-
import type {
|
|
21
|
-
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
22
|
-
import type { ExtensionPaths } from "#src/extension-paths";
|
|
23
|
-
import type { ForwardingController } from "#src/forwarding-manager";
|
|
24
|
-
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
25
|
-
import { PermissionSession } from "#src/permission-session";
|
|
26
|
-
import type { PromptingGatewayLifecycle } from "#src/prompting-gateway";
|
|
27
|
-
import type { Ruleset } from "#src/rule";
|
|
20
|
+
import type { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
28
21
|
import { SessionApproval } from "#src/session-approval";
|
|
29
|
-
import type { SessionLogger } from "#src/session-logger";
|
|
30
|
-
import { SessionRules } from "#src/session-rules";
|
|
31
22
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
32
|
-
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
33
23
|
import { makeCtx } from "#test/helpers/handler-fixtures";
|
|
24
|
+
import {
|
|
25
|
+
makeConfigStore,
|
|
26
|
+
makeFakePermissionManager,
|
|
27
|
+
makeRealSession,
|
|
28
|
+
} from "#test/helpers/session-fixtures";
|
|
29
|
+
|
|
30
|
+
// Alias so the existing tests read naturally.
|
|
31
|
+
const createSession = makeRealSession;
|
|
32
|
+
const makePermissionManager = makeFakePermissionManager;
|
|
34
33
|
|
|
35
34
|
function makeSkillEntry(
|
|
36
35
|
name: string,
|
|
@@ -47,125 +46,6 @@ function makeSkillEntry(
|
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
function makePaths(overrides: Partial<ExtensionPaths> = {}): ExtensionPaths {
|
|
51
|
-
return {
|
|
52
|
-
agentDir: "/test/agent",
|
|
53
|
-
sessionsDir: "/test/agent/sessions",
|
|
54
|
-
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
55
|
-
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
56
|
-
globalLogsDir: "/test/agent/logs",
|
|
57
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
58
|
-
...overrides,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function makeLogger(): SessionLogger {
|
|
63
|
-
return {
|
|
64
|
-
debug: vi.fn(),
|
|
65
|
-
review: vi.fn(),
|
|
66
|
-
warn: vi.fn(),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function makeConfigStore(
|
|
71
|
-
overrides: Partial<SessionConfigStore> = {},
|
|
72
|
-
): SessionConfigStore {
|
|
73
|
-
return {
|
|
74
|
-
current:
|
|
75
|
-
overrides.current ??
|
|
76
|
-
vi
|
|
77
|
-
.fn<() => typeof DEFAULT_EXTENSION_CONFIG>()
|
|
78
|
-
.mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }),
|
|
79
|
-
refresh: overrides.refresh ?? vi.fn<(ctx?: ExtensionContext) => void>(),
|
|
80
|
-
logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function makeGateway(): PromptingGatewayLifecycle {
|
|
85
|
-
return {
|
|
86
|
-
activate: vi.fn<PromptingGatewayLifecycle["activate"]>(),
|
|
87
|
-
deactivate: vi.fn<PromptingGatewayLifecycle["deactivate"]>(),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function makeForwarding(): ForwardingController {
|
|
92
|
-
return {
|
|
93
|
-
start: vi.fn(),
|
|
94
|
-
stop: vi.fn(),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function makePermissionManager() {
|
|
99
|
-
return {
|
|
100
|
-
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
101
|
-
checkPermission: vi
|
|
102
|
-
.fn<
|
|
103
|
-
(
|
|
104
|
-
toolName: string,
|
|
105
|
-
input: unknown,
|
|
106
|
-
agentName?: string,
|
|
107
|
-
sessionRules?: Ruleset,
|
|
108
|
-
) => PermissionCheckResult
|
|
109
|
-
>()
|
|
110
|
-
.mockReturnValue({
|
|
111
|
-
state: "allow",
|
|
112
|
-
toolName: "read",
|
|
113
|
-
source: "tool",
|
|
114
|
-
origin: "builtin",
|
|
115
|
-
}),
|
|
116
|
-
getToolPermission: vi
|
|
117
|
-
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
118
|
-
.mockReturnValue("allow"),
|
|
119
|
-
getConfigIssues: vi.fn((): string[] => []),
|
|
120
|
-
getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function createSession(overrides?: {
|
|
125
|
-
paths?: Partial<ExtensionPaths>;
|
|
126
|
-
logger?: SessionLogger;
|
|
127
|
-
forwarding?: ForwardingController;
|
|
128
|
-
permissionManager?: ScopedPermissionManager;
|
|
129
|
-
sessionRules?: SessionRules;
|
|
130
|
-
configStore?: SessionConfigStore;
|
|
131
|
-
gateway?: PromptingGatewayLifecycle;
|
|
132
|
-
}): {
|
|
133
|
-
session: PermissionSession;
|
|
134
|
-
paths: ExtensionPaths;
|
|
135
|
-
logger: SessionLogger;
|
|
136
|
-
forwarding: ForwardingController;
|
|
137
|
-
sessionRules: SessionRules;
|
|
138
|
-
configStore: SessionConfigStore;
|
|
139
|
-
gateway: PromptingGatewayLifecycle;
|
|
140
|
-
} {
|
|
141
|
-
const paths = makePaths(overrides?.paths);
|
|
142
|
-
const logger = overrides?.logger ?? makeLogger();
|
|
143
|
-
const forwarding = overrides?.forwarding ?? makeForwarding();
|
|
144
|
-
const permissionManager =
|
|
145
|
-
overrides?.permissionManager ?? makePermissionManager();
|
|
146
|
-
const sessionRules = overrides?.sessionRules ?? new SessionRules();
|
|
147
|
-
const configStore = overrides?.configStore ?? makeConfigStore();
|
|
148
|
-
const gateway = overrides?.gateway ?? makeGateway();
|
|
149
|
-
const session = new PermissionSession(
|
|
150
|
-
paths,
|
|
151
|
-
logger,
|
|
152
|
-
forwarding,
|
|
153
|
-
permissionManager,
|
|
154
|
-
sessionRules,
|
|
155
|
-
configStore,
|
|
156
|
-
gateway,
|
|
157
|
-
);
|
|
158
|
-
return {
|
|
159
|
-
session,
|
|
160
|
-
paths,
|
|
161
|
-
logger,
|
|
162
|
-
forwarding,
|
|
163
|
-
sessionRules,
|
|
164
|
-
configStore,
|
|
165
|
-
gateway,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
49
|
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
170
50
|
|
|
171
51
|
beforeEach(() => {
|
|
@@ -176,70 +56,6 @@ beforeEach(() => {
|
|
|
176
56
|
});
|
|
177
57
|
|
|
178
58
|
describe("PermissionSession", () => {
|
|
179
|
-
describe("constructor and delegation", () => {
|
|
180
|
-
it("delegates checkPermission to internal PermissionManager", () => {
|
|
181
|
-
const pm = makePermissionManager();
|
|
182
|
-
const { session } = createSession({ permissionManager: pm });
|
|
183
|
-
|
|
184
|
-
const result = session.checkPermission("bash", { command: "ls" });
|
|
185
|
-
|
|
186
|
-
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
187
|
-
"bash",
|
|
188
|
-
{ command: "ls" },
|
|
189
|
-
undefined,
|
|
190
|
-
undefined,
|
|
191
|
-
);
|
|
192
|
-
expect(result.state).toBe("allow");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("delegates getToolPermission to internal PermissionManager", () => {
|
|
196
|
-
const pm = makePermissionManager();
|
|
197
|
-
const { session } = createSession({ permissionManager: pm });
|
|
198
|
-
|
|
199
|
-
const result = session.getToolPermission("read");
|
|
200
|
-
|
|
201
|
-
expect(pm.getToolPermission).toHaveBeenCalledWith("read", undefined);
|
|
202
|
-
expect(result).toBe("allow");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("delegates getConfigIssues to internal PermissionManager", () => {
|
|
206
|
-
const pm = makePermissionManager();
|
|
207
|
-
vi.mocked(pm.getConfigIssues).mockReturnValue(["issue1"]);
|
|
208
|
-
const { session } = createSession({ permissionManager: pm });
|
|
209
|
-
|
|
210
|
-
expect(session.getConfigIssues("agent1")).toEqual(["issue1"]);
|
|
211
|
-
expect(pm.getConfigIssues).toHaveBeenCalledWith("agent1");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("delegates getPolicyCacheStamp to internal PermissionManager", () => {
|
|
215
|
-
const pm = makePermissionManager();
|
|
216
|
-
const { session } = createSession({ permissionManager: pm });
|
|
217
|
-
|
|
218
|
-
expect(session.getPolicyCacheStamp("agent1")).toBe("stamp-1");
|
|
219
|
-
expect(pm.getPolicyCacheStamp).toHaveBeenCalledWith("agent1");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("delegates getSessionRuleset to internal SessionRules", () => {
|
|
223
|
-
const { session } = createSession();
|
|
224
|
-
const rules = session.getSessionRuleset();
|
|
225
|
-
expect(rules).toEqual([]);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("delegates recordSessionApproval to internal SessionRules", () => {
|
|
229
|
-
const { session } = createSession();
|
|
230
|
-
session.recordSessionApproval(
|
|
231
|
-
SessionApproval.single("bash", "/usr/bin/*"),
|
|
232
|
-
);
|
|
233
|
-
const rules = session.getSessionRuleset();
|
|
234
|
-
expect(rules).toHaveLength(1);
|
|
235
|
-
expect(rules[0]).toMatchObject({
|
|
236
|
-
surface: "bash",
|
|
237
|
-
pattern: "/usr/bin/*",
|
|
238
|
-
action: "allow",
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
59
|
describe("activate and deactivate", () => {
|
|
244
60
|
it("stores the context on activate", () => {
|
|
245
61
|
const { session, forwarding } = createSession();
|
|
@@ -335,13 +151,13 @@ describe("PermissionSession", () => {
|
|
|
335
151
|
|
|
336
152
|
describe("shutdown", () => {
|
|
337
153
|
it("clears session rules", () => {
|
|
338
|
-
const { session } = createSession();
|
|
339
|
-
|
|
340
|
-
expect(
|
|
154
|
+
const { session, sessionRules } = createSession();
|
|
155
|
+
sessionRules.recordSessionApproval(SessionApproval.single("bash", "*"));
|
|
156
|
+
expect(sessionRules.getRuleset()).toHaveLength(1);
|
|
341
157
|
|
|
342
158
|
session.shutdown();
|
|
343
159
|
|
|
344
|
-
expect(
|
|
160
|
+
expect(sessionRules.getRuleset()).toEqual([]);
|
|
345
161
|
});
|
|
346
162
|
|
|
347
163
|
it("clears cache keys", () => {
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { evaluate } from "#src/rule";
|
|
4
4
|
import { SessionApproval } from "#src/session-approval";
|
|
5
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
5
6
|
import { deriveApprovalPattern, SessionRules } from "#src/session-rules";
|
|
6
7
|
|
|
7
8
|
// ── SessionRules ───────────────────────────────────────────────────────────
|
|
@@ -67,10 +68,15 @@ describe("SessionRules", () => {
|
|
|
67
68
|
});
|
|
68
69
|
});
|
|
69
70
|
|
|
70
|
-
describe("
|
|
71
|
+
describe("recordSessionApproval", () => {
|
|
72
|
+
it("satisfies the SessionApprovalRecorder interface", () => {
|
|
73
|
+
const rules: SessionApprovalRecorder = new SessionRules();
|
|
74
|
+
expect(typeof rules.recordSessionApproval).toBe("function");
|
|
75
|
+
});
|
|
76
|
+
|
|
71
77
|
it("records a single-pattern approval as one rule", () => {
|
|
72
78
|
const rules = new SessionRules();
|
|
73
|
-
rules.
|
|
79
|
+
rules.recordSessionApproval(SessionApproval.single("bash", "git *"));
|
|
74
80
|
expect(rules.getRuleset()).toEqual([
|
|
75
81
|
{
|
|
76
82
|
surface: "bash",
|
|
@@ -84,7 +90,7 @@ describe("SessionRules", () => {
|
|
|
84
90
|
|
|
85
91
|
it("records a multi-pattern approval as one rule per pattern", () => {
|
|
86
92
|
const rules = new SessionRules();
|
|
87
|
-
rules.
|
|
93
|
+
rules.recordSessionApproval(
|
|
88
94
|
SessionApproval.multiple("external_directory", [
|
|
89
95
|
"/outside/a/*",
|
|
90
96
|
"/outside/b/*",
|
|
@@ -97,7 +103,7 @@ describe("SessionRules", () => {
|
|
|
97
103
|
|
|
98
104
|
it("records each rule with the correct surface", () => {
|
|
99
105
|
const rules = new SessionRules();
|
|
100
|
-
rules.
|
|
106
|
+
rules.recordSessionApproval(
|
|
101
107
|
SessionApproval.multiple("external_directory", [
|
|
102
108
|
"/outside/a/*",
|
|
103
109
|
"/outside/b/*",
|
|
@@ -110,7 +116,9 @@ describe("SessionRules", () => {
|
|
|
110
116
|
|
|
111
117
|
it("records nothing for an empty patterns list", () => {
|
|
112
118
|
const rules = new SessionRules();
|
|
113
|
-
rules.
|
|
119
|
+
rules.recordSessionApproval(
|
|
120
|
+
SessionApproval.multiple("external_directory", []),
|
|
121
|
+
);
|
|
114
122
|
expect(rules.getRuleset()).toEqual([]);
|
|
115
123
|
});
|
|
116
124
|
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import type { GateHandlerSession } from "./gate-handler-session";
|
|
4
|
-
import type {
|
|
5
|
-
SkillPermissionChecker,
|
|
6
|
-
SkillPromptEntry,
|
|
7
|
-
} from "./skill-prompt-sanitizer";
|
|
8
|
-
import type { PermissionState } from "./types";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The session surface `AgentPrepHandler` invokes during `before_agent_start`:
|
|
12
|
-
* bind context + identify the agent (via {@link GateHandlerSession}), check
|
|
13
|
-
* skill permissions for prompt sanitization (via {@link SkillPermissionChecker}),
|
|
14
|
-
* refresh config, decide tool exposure, manage the active-tools / prompt-state
|
|
15
|
-
* cache keys, and store the resolved skill entries.
|
|
16
|
-
*/
|
|
17
|
-
export interface AgentPrepSession
|
|
18
|
-
extends GateHandlerSession,
|
|
19
|
-
SkillPermissionChecker {
|
|
20
|
-
refreshConfig(ctx?: ExtensionContext): void;
|
|
21
|
-
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
|
22
|
-
shouldUpdateActiveTools(cacheKey: string): boolean;
|
|
23
|
-
commitActiveToolsCacheKey(cacheKey: string): void;
|
|
24
|
-
getPolicyCacheStamp(agentName?: string): string;
|
|
25
|
-
shouldUpdatePromptState(cacheKey: string): boolean;
|
|
26
|
-
commitPromptStateCacheKey(cacheKey: string): void;
|
|
27
|
-
setActiveSkillEntries(entries: SkillPromptEntry[]): void;
|
|
28
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The session surface `PermissionGateHandler` invokes directly: bind the
|
|
5
|
-
* per-event context and identify the agent.
|
|
6
|
-
*
|
|
7
|
-
* This is the two-method context role both entry points share after [#329]
|
|
8
|
-
* extracted `SkillInputGatePipeline` to own the skill-input gate assembly.
|
|
9
|
-
*/
|
|
10
|
-
export interface GateHandlerSession {
|
|
11
|
-
activate(ctx: ExtensionContext): void;
|
|
12
|
-
resolveAgentName(ctx: ExtensionContext, systemPrompt?: string): string | null;
|
|
13
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import type { SessionLogger } from "./session-logger";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The session surface `SessionLifecycleHandler` invokes across
|
|
7
|
-
* `session_start`, `resources_discover`, and `session_shutdown`: refresh and
|
|
8
|
-
* report config, reset / reload / shut down session state, resolve the agent
|
|
9
|
-
* name, surface config issues, read the runtime context, and log.
|
|
10
|
-
*
|
|
11
|
-
* `activate` is intentionally absent — the lifecycle handler never calls it
|
|
12
|
-
* directly (ISP: do not depend on methods you do not use).
|
|
13
|
-
*/
|
|
14
|
-
export interface SessionLifecycleSession {
|
|
15
|
-
refreshConfig(ctx?: ExtensionContext): void;
|
|
16
|
-
resetForNewSession(ctx: ExtensionContext): void;
|
|
17
|
-
logResolvedConfigPaths(): void;
|
|
18
|
-
resolveAgentName(ctx: ExtensionContext, systemPrompt?: string): string | null;
|
|
19
|
-
getConfigIssues(agentName?: string): string[];
|
|
20
|
-
reload(): void;
|
|
21
|
-
getRuntimeContext(): ExtensionContext | null;
|
|
22
|
-
shutdown(): void;
|
|
23
|
-
readonly logger: SessionLogger;
|
|
24
|
-
}
|