@gotgenes/pi-permission-system 3.5.0 → 3.7.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,44 @@ 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
+ ## [3.7.0](https://github.com/gotgenes/pi-permission-system/compare/v3.6.0...v3.7.0) (2026-05-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * define HandlerDeps interface for handler extraction ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([a71e553](https://github.com/gotgenes/pi-permission-system/commit/a71e553ec988b4b222177f90a21519757cb62380))
14
+ * extract before_agent_start handler into src/handlers/before-agent-start.ts ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([9443a99](https://github.com/gotgenes/pi-permission-system/commit/9443a99b0e60b17868fc240782c2b31f53f409af))
15
+ * extract input handler into src/handlers/input.ts ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([196862a](https://github.com/gotgenes/pi-permission-system/commit/196862a86b270628b77f23049eb4902f85cde617))
16
+ * extract lifecycle handlers into src/handlers/lifecycle.ts ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([0edb194](https://github.com/gotgenes/pi-permission-system/commit/0edb194be90b5c5b5465acb4be38fbd2f749cdf9))
17
+ * extract tool_call handler into src/handlers/tool-call.ts ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([a4b81ca](https://github.com/gotgenes/pi-permission-system/commit/a4b81caa34da4959988ac311952a881ffdad72fe))
18
+
19
+
20
+ ### Documentation
21
+
22
+ * align handler extraction plan with architecture docs ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([4d91e03](https://github.com/gotgenes/pi-permission-system/commit/4d91e03b9ba11beccc5855d81f1f15be707495b0))
23
+ * **retro:** add retro notes for issue [#55](https://github.com/gotgenes/pi-permission-system/issues/55) ([ee763ff](https://github.com/gotgenes/pi-permission-system/commit/ee763ffccfbf19b9ec3627ea29847251e1505020))
24
+ * update plan with implementation notes for handler extraction ([#42](https://github.com/gotgenes/pi-permission-system/issues/42)) ([73603b2](https://github.com/gotgenes/pi-permission-system/commit/73603b25f5b5a53b0dd4300620b1f8ef8c844353))
25
+
26
+ ## [3.6.0](https://github.com/gotgenes/pi-permission-system/compare/v3.5.0...v3.6.0) (2026-05-03)
27
+
28
+
29
+ ### Features
30
+
31
+ * add Rule, Ruleset, getDefaultAction, and evaluate() in src/rule.ts ([482e00a](https://github.com/gotgenes/pi-permission-system/commit/482e00a04289f46f14c4b94486fbd98232159d66))
32
+ * add wildcardMatch convenience function to wildcard-matcher ([fa65219](https://github.com/gotgenes/pi-permission-system/commit/fa6521954a71ab146f14b87dc0723434a6dfd5ae))
33
+
34
+
35
+ ### Bug Fixes
36
+
37
+ * replace findLast with manual backwards loop in evaluate() ([1911f37](https://github.com/gotgenes/pi-permission-system/commit/1911f37dd6074d926f959aeefa3d795b29d1681c))
38
+
39
+
40
+ ### Documentation
41
+
42
+ * mark [#55](https://github.com/gotgenes/pi-permission-system/issues/55) complete in target architecture refactoring sequence ([0c87289](https://github.com/gotgenes/pi-permission-system/commit/0c87289d053a7f8c33aaa21a515db28d097a1925))
43
+ * plan extract pure evaluate() function ([#55](https://github.com/gotgenes/pi-permission-system/issues/55)) ([fd11860](https://github.com/gotgenes/pi-permission-system/commit/fd118606ba90af83d2d9eb5e752e76353beaf0ba))
44
+ * **retro:** add retro notes for issue [#54](https://github.com/gotgenes/pi-permission-system/issues/54) ([d7c5e8a](https://github.com/gotgenes/pi-permission-system/commit/d7c5e8aaae31fa658bcfb235547662bf226e6855))
45
+
8
46
  ## [3.5.0](https://github.com/gotgenes/pi-permission-system/compare/v3.4.0...v3.5.0) (2026-05-03)
9
47
 
10
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,112 @@
1
+ import type {
2
+ BeforeAgentStartEventResult,
3
+ ExtensionContext,
4
+ } from "@mariozechner/pi-coding-agent";
5
+
6
+ /** Minimal subset of BeforeAgentStartEvent used by this handler. */
7
+ interface BeforeAgentStartPayload {
8
+ systemPrompt: string;
9
+ }
10
+
11
+ import {
12
+ createActiveToolsCacheKey,
13
+ createBeforeAgentStartPromptStateKey,
14
+ shouldApplyCachedAgentStartState,
15
+ } from "../before-agent-start-cache";
16
+ import type { PermissionManager } from "../permission-manager";
17
+ import { resolveSkillPromptEntries } from "../skill-prompt-sanitizer";
18
+ import { sanitizeAvailableToolsSection } from "../system-prompt-sanitizer";
19
+ import { getToolNameFromValue } from "../tool-registry";
20
+ import type { HandlerDeps } from "./types";
21
+
22
+ /**
23
+ * Pure helper: returns true when the tool should be exposed to the agent.
24
+ * Checks the tool-level permission (not command-level) so that a blanket
25
+ * `bash: deny` hides the tool entirely before any invocation is attempted.
26
+ */
27
+ export function shouldExposeTool(
28
+ toolName: string,
29
+ agentName: string | null,
30
+ permissionManager: PermissionManager,
31
+ ): boolean {
32
+ const toolPermission = permissionManager.getToolPermission(
33
+ toolName,
34
+ agentName ?? undefined,
35
+ );
36
+ return toolPermission !== "deny";
37
+ }
38
+
39
+ export async function handleBeforeAgentStart(
40
+ deps: HandlerDeps,
41
+ event: BeforeAgentStartPayload,
42
+ ctx: ExtensionContext,
43
+ ): Promise<BeforeAgentStartEventResult> {
44
+ deps.setRuntimeContext(ctx);
45
+ deps.refreshExtensionConfig(ctx);
46
+ deps.startForwardedPermissionPolling(ctx);
47
+
48
+ const agentName = deps.resolveAgentName(ctx, event.systemPrompt);
49
+ const permissionManager = deps.getPermissionManager();
50
+ const allTools = deps.getAllTools();
51
+ const allowedTools: string[] = [];
52
+
53
+ for (const tool of allTools) {
54
+ const toolName = getToolNameFromValue(tool);
55
+ if (!toolName) {
56
+ continue;
57
+ }
58
+ if (shouldExposeTool(toolName, agentName, permissionManager)) {
59
+ allowedTools.push(toolName);
60
+ }
61
+ }
62
+
63
+ const activeToolsCacheKey = createActiveToolsCacheKey(allowedTools);
64
+ if (
65
+ shouldApplyCachedAgentStartState(
66
+ deps.getLastActiveToolsCacheKey(),
67
+ activeToolsCacheKey,
68
+ )
69
+ ) {
70
+ deps.setActiveTools(allowedTools);
71
+ deps.setLastActiveToolsCacheKey(activeToolsCacheKey);
72
+ }
73
+
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
+ });
83
+
84
+ if (
85
+ !shouldApplyCachedAgentStartState(
86
+ deps.getLastPromptStateCacheKey(),
87
+ promptStateCacheKey,
88
+ )
89
+ ) {
90
+ return {};
91
+ }
92
+
93
+ deps.setLastPromptStateCacheKey(promptStateCacheKey);
94
+
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.setActiveSkillEntries(skillPromptResult.entries);
106
+
107
+ if (skillPromptResult.prompt !== event.systemPrompt) {
108
+ return { systemPrompt: skillPromptResult.prompt };
109
+ }
110
+
111
+ return {};
112
+ }
@@ -0,0 +1,16 @@
1
+ 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";
@@ -0,0 +1,97 @@
1
+ import type {
2
+ ExtensionContext,
3
+ InputEventResult,
4
+ } from "@mariozechner/pi-coding-agent";
5
+
6
+ /** Minimal subset of InputEvent used by this handler. */
7
+ interface InputPayload {
8
+ text: string;
9
+ }
10
+
11
+ import { applyPermissionGate } from "../permission-gate";
12
+ import { formatSkillAskPrompt } from "../permission-prompts";
13
+ import type { HandlerDeps } from "./types";
14
+
15
+ /**
16
+ * Parse a `/skill:<name>` prefix from user input.
17
+ * Returns the skill name, or null if the text is not a skill invocation.
18
+ */
19
+ export function extractSkillNameFromInput(text: string): string | null {
20
+ const trimmed = text.trim();
21
+ if (!trimmed.startsWith("/skill:")) {
22
+ return null;
23
+ }
24
+
25
+ const afterPrefix = trimmed.slice("/skill:".length);
26
+ if (!afterPrefix) {
27
+ return null;
28
+ }
29
+
30
+ const firstWhitespace = afterPrefix.search(/\s/);
31
+ const skillName = (
32
+ firstWhitespace === -1 ? afterPrefix : afterPrefix.slice(0, firstWhitespace)
33
+ ).trim();
34
+ return skillName || null;
35
+ }
36
+
37
+ export async function handleInput(
38
+ deps: HandlerDeps,
39
+ event: InputPayload,
40
+ ctx: ExtensionContext,
41
+ ): Promise<InputEventResult> {
42
+ deps.setRuntimeContext(ctx);
43
+ deps.startForwardedPermissionPolling(ctx);
44
+
45
+ const skillName = extractSkillNameFromInput(event.text);
46
+ if (!skillName) {
47
+ return { action: "continue" };
48
+ }
49
+
50
+ const agentName = deps.resolveAgentName(ctx);
51
+ const check = deps
52
+ .getPermissionManager()
53
+ .checkPermission("skill", { name: skillName }, agentName ?? undefined);
54
+
55
+ if (check.state === "deny" && ctx.hasUI) {
56
+ const notifyMessage = agentName
57
+ ? `Skill '${skillName}' is not permitted for agent '${agentName}'.`
58
+ : `Skill '${skillName}' is not permitted by the current skill policy.`;
59
+ ctx.ui.notify(notifyMessage, "warning");
60
+ }
61
+
62
+ const skillInputMessage = formatSkillAskPrompt(
63
+ skillName,
64
+ agentName ?? undefined,
65
+ );
66
+ const skillInputGate = await applyPermissionGate({
67
+ state: check.state,
68
+ canConfirm: deps.canRequestPermissionConfirmation(ctx),
69
+ promptForApproval: () =>
70
+ deps.promptPermission(ctx, {
71
+ requestId: deps.createPermissionRequestId("skill-input"),
72
+ source: "skill_input",
73
+ agentName,
74
+ message: skillInputMessage,
75
+ skillName,
76
+ }),
77
+ writeLog: deps.writeReviewLog,
78
+ logContext: {
79
+ source: "skill_input",
80
+ skillName,
81
+ agentName,
82
+ message: skillInputMessage,
83
+ },
84
+ messages: {
85
+ denyReason: skillInputMessage,
86
+ unavailableReason:
87
+ "Skill requires approval, but no interactive UI is available.",
88
+ userDeniedReason: () => "User denied skill.",
89
+ },
90
+ });
91
+
92
+ if (skillInputGate.action === "block") {
93
+ return { action: "handled" };
94
+ }
95
+
96
+ return { action: "continue" };
97
+ }
@@ -0,0 +1,80 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+
3
+ import { getActiveAgentName } from "../active-agent";
4
+ import { PERMISSION_SYSTEM_STATUS_KEY } from "../status";
5
+ import type { HandlerDeps } from "./types";
6
+
7
+ /** Minimal subset of SessionStartEvent used by this handler. */
8
+ interface SessionStartPayload {
9
+ reason: string;
10
+ }
11
+
12
+ /** Minimal subset of ResourcesDiscoverEvent used by this handler. */
13
+ interface ResourcesDiscoverPayload {
14
+ reason: string;
15
+ }
16
+
17
+ export async function handleSessionStart(
18
+ deps: HandlerDeps,
19
+ event: SessionStartPayload,
20
+ ctx: ExtensionContext,
21
+ ): Promise<void> {
22
+ deps.setRuntimeContext(ctx);
23
+ deps.refreshExtensionConfig(ctx);
24
+ deps.setPermissionManager(deps.createPermissionManagerForCwd(ctx.cwd));
25
+ deps.setActiveSkillEntries([]);
26
+ deps.setLastActiveToolsCacheKey(null);
27
+ deps.setLastPromptStateCacheKey(null);
28
+ deps.setLastKnownActiveAgentName(getActiveAgentName(ctx));
29
+ deps.startForwardedPermissionPolling(ctx);
30
+ deps.logResolvedConfigPaths();
31
+
32
+ const agentName = deps.getLastKnownActiveAgentName();
33
+ const policyIssues = deps.getPermissionManager().getConfigIssues(agentName);
34
+ for (const issue of policyIssues) {
35
+ deps.notifyWarning(issue);
36
+ }
37
+
38
+ if (event.reason === "reload") {
39
+ deps.writeDebugLog("lifecycle.reload", {
40
+ triggeredBy: "session_start",
41
+ reason: event.reason,
42
+ cwd: ctx.cwd,
43
+ });
44
+ }
45
+ }
46
+
47
+ export async function handleResourcesDiscover(
48
+ deps: HandlerDeps,
49
+ event: ResourcesDiscoverPayload,
50
+ ): Promise<void> {
51
+ if (event.reason !== "reload") {
52
+ return;
53
+ }
54
+
55
+ const runtimeContext = deps.getRuntimeContext();
56
+ deps.setPermissionManager(
57
+ deps.createPermissionManagerForCwd(runtimeContext?.cwd),
58
+ );
59
+ deps.setActiveSkillEntries([]);
60
+ deps.setLastActiveToolsCacheKey(null);
61
+ deps.setLastPromptStateCacheKey(null);
62
+ deps.writeDebugLog("lifecycle.reload", {
63
+ triggeredBy: "resources_discover",
64
+ reason: event.reason,
65
+ cwd: runtimeContext?.cwd ?? null,
66
+ });
67
+ }
68
+
69
+ export async function handleSessionShutdown(deps: HandlerDeps): Promise<void> {
70
+ const ctx = deps.getRuntimeContext();
71
+ if (ctx) {
72
+ ctx.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
73
+ }
74
+ deps.setRuntimeContext(null);
75
+ deps.setActiveSkillEntries([]);
76
+ deps.setLastActiveToolsCacheKey(null);
77
+ deps.setLastPromptStateCacheKey(null);
78
+ deps.sessionApprovalCache.clear();
79
+ deps.stopForwardedPermissionPolling();
80
+ }