@gotgenes/pi-permission-system 5.9.0 → 5.11.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 CHANGED
@@ -5,6 +5,36 @@ 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
+ ## [5.11.0](https://github.com/gotgenes/pi-permission-system/compare/v5.10.0...v5.11.0) (2026-05-08)
9
+
10
+
11
+ ### Features
12
+
13
+ * add ToolRegistry interface ([#130](https://github.com/gotgenes/pi-permission-system/issues/130)) ([5e886fd](https://github.com/gotgenes/pi-permission-system/commit/5e886fd4bc67ddac56a7f2b7b445f6f172e60668))
14
+ * PermissionSession absorbs prompting methods ([#130](https://github.com/gotgenes/pi-permission-system/issues/130)) ([4ae81e6](https://github.com/gotgenes/pi-permission-system/commit/4ae81e6e32164926202633ec7831a4f3db69fc70))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * plan handler classes to replace HandlerDeps ([#130](https://github.com/gotgenes/pi-permission-system/issues/130)) ([e8bc1a4](https://github.com/gotgenes/pi-permission-system/commit/e8bc1a40596aa6a032b367e642d19a03ca622394))
20
+ * **retro:** add retro notes for issue [#129](https://github.com/gotgenes/pi-permission-system/issues/129) ([23c29a2](https://github.com/gotgenes/pi-permission-system/commit/23c29a2148f6647565e6997d9c56341b43e74118))
21
+ * update architecture for handler classes ([#130](https://github.com/gotgenes/pi-permission-system/issues/130)) ([02d02b6](https://github.com/gotgenes/pi-permission-system/commit/02d02b647ec366eee3dc572afbaf436b1264b052))
22
+
23
+ ## [5.10.0](https://github.com/gotgenes/pi-permission-system/compare/v5.9.0...v5.10.0) (2026-05-08)
24
+
25
+
26
+ ### Features
27
+
28
+ * PermissionSession class with delegation methods ([#129](https://github.com/gotgenes/pi-permission-system/issues/129)) ([a8486ce](https://github.com/gotgenes/pi-permission-system/commit/a8486ce1d0678a7617e3cc6d8131e5f3080a1bea))
29
+ * PermissionSession lifecycle, cache, agent name, and infra methods ([#129](https://github.com/gotgenes/pi-permission-system/issues/129)) ([8f6edf7](https://github.com/gotgenes/pi-permission-system/commit/8f6edf727856a974dc8061c1ff6003838d916662))
30
+
31
+
32
+ ### Documentation
33
+
34
+ * plan PermissionSession extraction ([#129](https://github.com/gotgenes/pi-permission-system/issues/129)) ([9dc21b4](https://github.com/gotgenes/pi-permission-system/commit/9dc21b457ff5ca5e95b920eed1e5b48511175cdf))
35
+ * **retro:** add retro notes for issue [#128](https://github.com/gotgenes/pi-permission-system/issues/128) ([9794053](https://github.com/gotgenes/pi-permission-system/commit/979405305ee5ddeac97186ea949373aff947a210))
36
+ * update architecture for PermissionSession ([#129](https://github.com/gotgenes/pi-permission-system/issues/129)) ([d452c50](https://github.com/gotgenes/pi-permission-system/commit/d452c50a7edb7ca1c2c7715836b007386814e1b3))
37
+
8
38
  ## [5.9.0](https://github.com/gotgenes/pi-permission-system/compare/v5.8.0...v5.9.0) (2026-05-08)
9
39
 
10
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.9.0",
3
+ "version": "5.11.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -6,7 +6,7 @@ import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding"
6
6
  import { isSubagentExecutionContext } from "./subagent-context";
7
7
 
8
8
  /**
9
- * Narrow interface for the forwarding lifecycle used by `HandlerDeps`.
9
+ * Narrow interface for the forwarding lifecycle used by `PermissionSession`.
10
10
  * `ForwardingManager` satisfies it; tests can provide a plain object mock.
11
11
  */
12
12
  export interface ForwardingController {
@@ -3,21 +3,20 @@ import type {
3
3
  ExtensionContext,
4
4
  } from "@mariozechner/pi-coding-agent";
5
5
 
6
- /** Minimal subset of BeforeAgentStartEvent used by this handler. */
7
- interface BeforeAgentStartPayload {
8
- systemPrompt: string;
9
- }
10
-
11
6
  import {
12
7
  createActiveToolsCacheKey,
13
8
  createBeforeAgentStartPromptStateKey,
14
- shouldApplyCachedAgentStartState,
15
9
  } from "../before-agent-start-cache";
16
- import type { PermissionManager } from "../permission-manager";
10
+ import type { PermissionSession } from "../permission-session";
17
11
  import { resolveSkillPromptEntries } from "../skill-prompt-sanitizer";
18
12
  import { sanitizeAvailableToolsSection } from "../system-prompt-sanitizer";
19
- import { getToolNameFromValue } from "../tool-registry";
20
- import type { HandlerDeps } from "./types";
13
+ import { getToolNameFromValue, type ToolRegistry } from "../tool-registry";
14
+ import type { PermissionState } from "../types";
15
+
16
+ /** Minimal subset of BeforeAgentStartEvent used by this handler. */
17
+ interface BeforeAgentStartPayload {
18
+ systemPrompt: string;
19
+ }
21
20
 
22
21
  /**
23
22
  * Pure helper: returns true when the tool should be exposed to the agent.
@@ -27,86 +26,87 @@ import type { HandlerDeps } from "./types";
27
26
  export function shouldExposeTool(
28
27
  toolName: string,
29
28
  agentName: string | null,
30
- permissionManager: PermissionManager,
29
+ getToolPermission: (toolName: string, agentName?: string) => PermissionState,
31
30
  ): boolean {
32
- const toolPermission = permissionManager.getToolPermission(
33
- toolName,
34
- agentName ?? undefined,
35
- );
31
+ const toolPermission = getToolPermission(toolName, agentName ?? undefined);
36
32
  return toolPermission !== "deny";
37
33
  }
38
34
 
39
- export async function handleBeforeAgentStart(
40
- deps: HandlerDeps,
41
- event: BeforeAgentStartPayload,
42
- ctx: ExtensionContext,
43
- ): Promise<BeforeAgentStartEventResult> {
44
- deps.session.runtimeContext = ctx;
45
- deps.refreshExtensionConfig(ctx);
46
- deps.forwarding.start(ctx);
35
+ /**
36
+ * Handles the `before_agent_start` event: tool filtering + prompt sanitization.
37
+ *
38
+ * Constructor deps:
39
+ * - `session` — encapsulates all mutable session state
40
+ * - `toolRegistry` — Pi tool API subset (getAll + setActive)
41
+ */
42
+ export class AgentPrepHandler {
43
+ constructor(
44
+ private readonly session: PermissionSession,
45
+ private readonly toolRegistry: ToolRegistry,
46
+ ) {}
47
+
48
+ async handle(
49
+ event: BeforeAgentStartPayload,
50
+ ctx: ExtensionContext,
51
+ ): Promise<BeforeAgentStartEventResult> {
52
+ const { session } = this;
53
+ session.activate(ctx);
54
+ session.refreshConfig(ctx);
47
55
 
48
- const agentName = deps.resolveAgentName(ctx, event.systemPrompt);
49
- const { permissionManager } = deps.session;
50
- const allTools = deps.getAllTools();
51
- const allowedTools: string[] = [];
56
+ const agentName = session.resolveAgentName(ctx, event.systemPrompt);
57
+ const allTools = this.toolRegistry.getAll();
58
+ const allowedTools: string[] = [];
52
59
 
53
- for (const tool of allTools) {
54
- const toolName = getToolNameFromValue(tool);
55
- if (!toolName) {
56
- continue;
60
+ for (const tool of allTools) {
61
+ const toolName = getToolNameFromValue(tool);
62
+ if (!toolName) {
63
+ continue;
64
+ }
65
+ if (
66
+ shouldExposeTool(toolName, agentName, (t, a) =>
67
+ session.getToolPermission(t, a),
68
+ )
69
+ ) {
70
+ allowedTools.push(toolName);
71
+ }
57
72
  }
58
- if (shouldExposeTool(toolName, agentName, permissionManager)) {
59
- allowedTools.push(toolName);
73
+
74
+ const activeToolsCacheKey = createActiveToolsCacheKey(allowedTools);
75
+ if (session.shouldUpdateActiveTools(activeToolsCacheKey)) {
76
+ this.toolRegistry.setActive(allowedTools);
77
+ session.commitActiveToolsCacheKey(activeToolsCacheKey);
60
78
  }
61
- }
62
79
 
63
- const activeToolsCacheKey = createActiveToolsCacheKey(allowedTools);
64
- if (
65
- shouldApplyCachedAgentStartState(
66
- deps.session.lastActiveToolsCacheKey,
67
- activeToolsCacheKey,
68
- )
69
- ) {
70
- deps.setActiveTools(allowedTools);
71
- deps.session.lastActiveToolsCacheKey = activeToolsCacheKey;
72
- }
80
+ const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
81
+ agentName,
82
+ cwd: ctx.cwd,
83
+ permissionStamp: session.getPolicyCacheStamp(agentName ?? undefined),
84
+ systemPrompt: event.systemPrompt,
85
+ allowedToolNames: allowedTools,
86
+ });
73
87
 
74
- const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
75
- agentName,
76
- cwd: ctx.cwd,
77
- permissionStamp: permissionManager.getPolicyCacheStamp(
78
- agentName ?? undefined,
79
- ),
80
- systemPrompt: event.systemPrompt,
81
- allowedToolNames: allowedTools,
82
- });
88
+ if (!session.shouldUpdatePromptState(promptStateCacheKey)) {
89
+ return {};
90
+ }
83
91
 
84
- if (
85
- !shouldApplyCachedAgentStartState(
86
- deps.session.lastPromptStateCacheKey,
87
- promptStateCacheKey,
88
- )
89
- ) {
90
- return {};
91
- }
92
+ session.commitPromptStateCacheKey(promptStateCacheKey);
92
93
 
93
- deps.session.lastPromptStateCacheKey = promptStateCacheKey;
94
+ const toolPromptResult = sanitizeAvailableToolsSection(
95
+ event.systemPrompt,
96
+ allowedTools,
97
+ );
98
+ const skillPromptResult = resolveSkillPromptEntries(
99
+ toolPromptResult.prompt,
100
+ session,
101
+ agentName,
102
+ ctx.cwd,
103
+ );
104
+ session.setActiveSkillEntries(skillPromptResult.entries);
94
105
 
95
- const toolPromptResult = sanitizeAvailableToolsSection(
96
- event.systemPrompt,
97
- allowedTools,
98
- );
99
- const skillPromptResult = resolveSkillPromptEntries(
100
- toolPromptResult.prompt,
101
- permissionManager,
102
- agentName,
103
- ctx.cwd,
104
- );
105
- deps.session.activeSkillEntries = skillPromptResult.entries;
106
+ if (skillPromptResult.prompt !== event.systemPrompt) {
107
+ return { systemPrompt: skillPromptResult.prompt };
108
+ }
106
109
 
107
- if (skillPromptResult.prompt !== event.systemPrompt) {
108
- return { systemPrompt: skillPromptResult.prompt };
110
+ return {};
109
111
  }
110
-
111
- return {};
112
112
  }
@@ -3,9 +3,9 @@ import type {
3
3
  PermissionDecisionEvent,
4
4
  PermissionDecisionResolution,
5
5
  } from "../../permission-events";
6
+ import type { PromptPermissionDetails } from "../../permission-prompter";
6
7
  import type { Rule } from "../../rule";
7
8
  import type { PermissionCheckResult, PermissionState } from "../../types";
8
- import type { PromptPermissionDetails } from "../types";
9
9
 
10
10
  // ── Descriptor types ───────────────────────────────────────────────────────
11
11
 
@@ -1,16 +1,7 @@
1
+ export { AgentPrepHandler, shouldExposeTool } from "./before-agent-start";
2
+ export { SessionLifecycleHandler } from "./lifecycle";
1
3
  export {
2
- handleBeforeAgentStart,
3
- shouldExposeTool,
4
- } from "./before-agent-start";
5
- export { extractSkillNameFromInput, handleInput } from "./input";
6
- export {
7
- handleResourcesDiscover,
8
- handleSessionShutdown,
9
- handleSessionStart,
10
- } from "./lifecycle";
11
- export { getEventInput, handleToolCall } from "./tool-call";
12
- export type {
13
- HandlerDeps,
14
- PermissionReviewSource,
15
- PromptPermissionDetails,
16
- } from "./types";
4
+ extractSkillNameFromInput,
5
+ getEventInput,
6
+ PermissionGateHandler,
7
+ } from "./permission-gate-handler";
@@ -1,8 +1,7 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
 
3
- import { getActiveAgentName } from "../active-agent";
3
+ import type { PermissionSession } from "../permission-session";
4
4
  import { PERMISSION_SYSTEM_STATUS_KEY } from "../status";
5
- import type { HandlerDeps } from "./types";
6
5
 
7
6
  /** Minimal subset of SessionStartEvent used by this handler. */
8
7
  interface SessionStartPayload {
@@ -14,69 +13,66 @@ interface ResourcesDiscoverPayload {
14
13
  reason: string;
15
14
  }
16
15
 
17
- export async function handleSessionStart(
18
- deps: HandlerDeps,
19
- event: SessionStartPayload,
20
- ctx: ExtensionContext,
21
- ): Promise<void> {
22
- deps.session.runtimeContext = ctx;
23
- deps.refreshExtensionConfig(ctx);
24
- deps.session.permissionManager = deps.createPermissionManagerForCwd(ctx.cwd);
25
- deps.session.activeSkillEntries = [];
26
- deps.session.lastActiveToolsCacheKey = null;
27
- deps.session.lastPromptStateCacheKey = null;
28
- deps.session.lastKnownActiveAgentName = getActiveAgentName(ctx);
29
- deps.forwarding.start(ctx);
30
- deps.logResolvedConfigPaths();
16
+ /**
17
+ * Handles session lifecycle events: start, reload, and shutdown.
18
+ *
19
+ * Constructor deps:
20
+ * - `session` — encapsulates all mutable session state
21
+ * - `cleanupRpc` — unsubscribes RPC handlers on shutdown
22
+ */
23
+ export class SessionLifecycleHandler {
24
+ constructor(
25
+ private readonly session: PermissionSession,
26
+ private readonly cleanupRpc: () => void,
27
+ ) {}
31
28
 
32
- const agentName = deps.session.lastKnownActiveAgentName;
33
- const policyIssues =
34
- deps.session.permissionManager.getConfigIssues(agentName);
35
- for (const issue of policyIssues) {
36
- deps.logger.warn(issue);
29
+ async handleSessionStart(
30
+ event: SessionStartPayload,
31
+ ctx: ExtensionContext,
32
+ ): Promise<void> {
33
+ const { session } = this;
34
+ session.refreshConfig(ctx);
35
+ session.resetForNewSession(ctx);
36
+ session.logResolvedConfigPaths();
37
+
38
+ const agentName = session.resolveAgentName(ctx);
39
+ const policyIssues = session.getConfigIssues(agentName);
40
+ for (const issue of policyIssues) {
41
+ session.logger.warn(issue);
42
+ }
43
+
44
+ if (event.reason === "reload") {
45
+ session.logger.debug("lifecycle.reload", {
46
+ triggeredBy: "session_start",
47
+ reason: event.reason,
48
+ cwd: ctx.cwd,
49
+ });
50
+ }
37
51
  }
38
52
 
39
- if (event.reason === "reload") {
40
- deps.logger.debug("lifecycle.reload", {
41
- triggeredBy: "session_start",
53
+ async handleResourcesDiscover(
54
+ event: ResourcesDiscoverPayload,
55
+ ): Promise<void> {
56
+ if (event.reason !== "reload") {
57
+ return;
58
+ }
59
+
60
+ const { session } = this;
61
+ session.reload();
62
+ session.logger.debug("lifecycle.reload", {
63
+ triggeredBy: "resources_discover",
42
64
  reason: event.reason,
43
- cwd: ctx.cwd,
65
+ cwd: session.getRuntimeContext()?.cwd ?? null,
44
66
  });
45
67
  }
46
- }
47
-
48
- export async function handleResourcesDiscover(
49
- deps: HandlerDeps,
50
- event: ResourcesDiscoverPayload,
51
- ): Promise<void> {
52
- if (event.reason !== "reload") {
53
- return;
54
- }
55
-
56
- const { runtimeContext } = deps.session;
57
- deps.session.permissionManager = deps.createPermissionManagerForCwd(
58
- runtimeContext?.cwd,
59
- );
60
- deps.session.activeSkillEntries = [];
61
- deps.session.lastActiveToolsCacheKey = null;
62
- deps.session.lastPromptStateCacheKey = null;
63
- deps.logger.debug("lifecycle.reload", {
64
- triggeredBy: "resources_discover",
65
- reason: event.reason,
66
- cwd: runtimeContext?.cwd ?? null,
67
- });
68
- }
69
68
 
70
- export async function handleSessionShutdown(deps: HandlerDeps): Promise<void> {
71
- const { runtimeContext } = deps.session;
72
- if (runtimeContext) {
73
- runtimeContext.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
69
+ async handleSessionShutdown(): Promise<void> {
70
+ const { session } = this;
71
+ const ctx = session.getRuntimeContext();
72
+ if (ctx) {
73
+ ctx.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
74
+ }
75
+ session.shutdown();
76
+ this.cleanupRpc();
74
77
  }
75
- deps.session.runtimeContext = null;
76
- deps.session.activeSkillEntries = [];
77
- deps.session.lastActiveToolsCacheKey = null;
78
- deps.session.lastPromptStateCacheKey = null;
79
- deps.session.sessionRules.clear();
80
- deps.forwarding.stop();
81
- deps.stopPermissionRpcHandlers();
82
78
  }