@gotgenes/pi-permission-system 3.6.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 +18 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +112 -0
- package/src/handlers/index.ts +16 -0
- package/src/handlers/input.ts +97 -0
- package/src/handlers/lifecycle.ts +80 -0
- package/src/handlers/tool-call.ts +400 -0
- package/src/handlers/types.ts +95 -0
- package/src/index.ts +101 -701
- package/tests/handlers/before-agent-start.test.ts +274 -0
- package/tests/handlers/input.test.ts +271 -0
- package/tests/handlers/lifecycle.test.ts +331 -0
- package/tests/handlers/tool-call.test.ts +418 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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
|
+
|
|
8
26
|
## [3.6.0](https://github.com/gotgenes/pi-permission-system/compare/v3.5.0...v3.6.0) (2026-05-03)
|
|
9
27
|
|
|
10
28
|
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|