@gotgenes/pi-permission-system 5.5.0 → 5.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 +9 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/gates/bash-external-directory.ts +22 -24
- package/src/handlers/gates/external-directory.ts +32 -41
- package/src/handlers/gates/skill-read.ts +10 -12
- package/src/handlers/gates/tool.ts +20 -27
- package/src/handlers/gates/types.ts +75 -0
- package/src/handlers/input.ts +3 -3
- package/src/handlers/lifecycle.ts +21 -21
- package/src/handlers/tool-call.ts +77 -7
- package/src/handlers/types.ts +20 -7
- package/src/index.ts +6 -1
- package/src/runtime.ts +17 -9
- package/tests/handlers/before-agent-start.test.ts +17 -27
- package/tests/handlers/gates/bash-external-directory.test.ts +48 -105
- package/tests/handlers/gates/external-directory.test.ts +65 -140
- package/tests/handlers/gates/skill-read.test.ts +50 -65
- package/tests/handlers/gates/tool.test.ts +90 -334
- package/tests/handlers/input-events.test.ts +10 -21
- package/tests/handlers/input.test.ts +26 -43
- package/tests/handlers/lifecycle.test.ts +47 -66
- package/tests/handlers/tool-call-events.test.ts +29 -40
- package/tests/handlers/tool-call.test.ts +19 -30
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.5.1](https://github.com/gotgenes/pi-permission-system/compare/v5.5.0...v5.5.1) (2026-05-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan narrow handler dependencies by splitting ExtensionRuntime ([#111](https://github.com/gotgenes/pi-permission-system/issues/111)) ([cb44dee](https://github.com/gotgenes/pi-permission-system/commit/cb44deea383fbcca8c8ba25bd4098f7dd8c35bfb))
|
|
14
|
+
* **retro:** add retro notes for issue [#108](https://github.com/gotgenes/pi-permission-system/issues/108) ([e967fd9](https://github.com/gotgenes/pi-permission-system/commit/e967fd96c6ed0a9ac0a4f0037889f1d9c88ebd97))
|
|
15
|
+
* update target architecture for gate interfaces and SessionState ([#111](https://github.com/gotgenes/pi-permission-system/issues/111)) ([85620c4](https://github.com/gotgenes/pi-permission-system/commit/85620c4486fdcf756c7039c15bdd8a295621ad37))
|
|
16
|
+
|
|
8
17
|
## [5.5.0](https://github.com/gotgenes/pi-permission-system/compare/v5.4.0...v5.5.0) (2026-05-07)
|
|
9
18
|
|
|
10
19
|
|
package/package.json
CHANGED
|
@@ -41,12 +41,12 @@ export async function handleBeforeAgentStart(
|
|
|
41
41
|
event: BeforeAgentStartPayload,
|
|
42
42
|
ctx: ExtensionContext,
|
|
43
43
|
): Promise<BeforeAgentStartEventResult> {
|
|
44
|
-
deps.
|
|
44
|
+
deps.session.runtimeContext = ctx;
|
|
45
45
|
deps.refreshExtensionConfig(ctx);
|
|
46
46
|
deps.startForwardedPermissionPolling(ctx);
|
|
47
47
|
|
|
48
48
|
const agentName = deps.resolveAgentName(ctx, event.systemPrompt);
|
|
49
|
-
const { permissionManager } = deps.
|
|
49
|
+
const { permissionManager } = deps.session;
|
|
50
50
|
const allTools = deps.getAllTools();
|
|
51
51
|
const allowedTools: string[] = [];
|
|
52
52
|
|
|
@@ -63,12 +63,12 @@ export async function handleBeforeAgentStart(
|
|
|
63
63
|
const activeToolsCacheKey = createActiveToolsCacheKey(allowedTools);
|
|
64
64
|
if (
|
|
65
65
|
shouldApplyCachedAgentStartState(
|
|
66
|
-
deps.
|
|
66
|
+
deps.session.lastActiveToolsCacheKey,
|
|
67
67
|
activeToolsCacheKey,
|
|
68
68
|
)
|
|
69
69
|
) {
|
|
70
70
|
deps.setActiveTools(allowedTools);
|
|
71
|
-
deps.
|
|
71
|
+
deps.session.lastActiveToolsCacheKey = activeToolsCacheKey;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
|
|
@@ -83,14 +83,14 @@ export async function handleBeforeAgentStart(
|
|
|
83
83
|
|
|
84
84
|
if (
|
|
85
85
|
!shouldApplyCachedAgentStartState(
|
|
86
|
-
deps.
|
|
86
|
+
deps.session.lastPromptStateCacheKey,
|
|
87
87
|
promptStateCacheKey,
|
|
88
88
|
)
|
|
89
89
|
) {
|
|
90
90
|
return {};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
deps.
|
|
93
|
+
deps.session.lastPromptStateCacheKey = promptStateCacheKey;
|
|
94
94
|
|
|
95
95
|
const toolPromptResult = sanitizeAvailableToolsSection(
|
|
96
96
|
event.systemPrompt,
|
|
@@ -102,7 +102,7 @@ export async function handleBeforeAgentStart(
|
|
|
102
102
|
agentName,
|
|
103
103
|
ctx.cwd,
|
|
104
104
|
);
|
|
105
|
-
deps.
|
|
105
|
+
deps.session.activeSkillEntries = skillPromptResult.entries;
|
|
106
106
|
|
|
107
107
|
if (skillPromptResult.prompt !== event.systemPrompt) {
|
|
108
108
|
return { systemPrompt: skillPromptResult.prompt };
|
|
@@ -8,8 +8,11 @@ import {
|
|
|
8
8
|
import type { PermissionPromptDecision } from "../../permission-dialog";
|
|
9
9
|
import { applyPermissionGate } from "../../permission-gate";
|
|
10
10
|
import { deriveApprovalPattern } from "../../session-rules";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
11
|
+
import type {
|
|
12
|
+
BashExternalDirectoryGateDeps,
|
|
13
|
+
GateOutcome,
|
|
14
|
+
ToolCallContext,
|
|
15
|
+
} from "./types";
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Evaluate the bash external-directory permission gate.
|
|
@@ -20,7 +23,7 @@ import type { GateOutcome, ToolCallContext } from "./types";
|
|
|
20
23
|
*/
|
|
21
24
|
export async function evaluateBashExternalDirectoryGate(
|
|
22
25
|
tcc: ToolCallContext,
|
|
23
|
-
deps:
|
|
26
|
+
deps: BashExternalDirectoryGateDeps,
|
|
24
27
|
): Promise<GateOutcome | null> {
|
|
25
28
|
if (tcc.toolName !== "bash" || !tcc.cwd) return null;
|
|
26
29
|
|
|
@@ -33,10 +36,10 @@ export async function evaluateBashExternalDirectoryGate(
|
|
|
33
36
|
);
|
|
34
37
|
if (externalPaths.length === 0) return null;
|
|
35
38
|
|
|
36
|
-
const bashSessionRules = deps.
|
|
39
|
+
const bashSessionRules = deps.getSessionRuleset();
|
|
37
40
|
const uncoveredPaths = externalPaths.filter(
|
|
38
41
|
(p) =>
|
|
39
|
-
deps.
|
|
42
|
+
deps.checkPermission(
|
|
40
43
|
"external_directory",
|
|
41
44
|
{ path: p },
|
|
42
45
|
tcc.agentName ?? undefined,
|
|
@@ -45,7 +48,7 @@ export async function evaluateBashExternalDirectoryGate(
|
|
|
45
48
|
);
|
|
46
49
|
|
|
47
50
|
if (uncoveredPaths.length === 0) {
|
|
48
|
-
deps.
|
|
51
|
+
deps.writeReviewLog("permission_request.session_approved", {
|
|
49
52
|
source: "tool_call",
|
|
50
53
|
toolCallId: tcc.toolCallId,
|
|
51
54
|
toolName: tcc.toolName,
|
|
@@ -58,7 +61,7 @@ export async function evaluateBashExternalDirectoryGate(
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
// Get the config-level policy (no path → no session check).
|
|
61
|
-
const extCheck = deps.
|
|
64
|
+
const extCheck = deps.checkPermission(
|
|
62
65
|
"external_directory",
|
|
63
66
|
{},
|
|
64
67
|
tcc.agentName ?? undefined,
|
|
@@ -73,26 +76,21 @@ export async function evaluateBashExternalDirectoryGate(
|
|
|
73
76
|
);
|
|
74
77
|
const bashExtGate = await applyPermissionGate({
|
|
75
78
|
state: extCheck.state,
|
|
76
|
-
canConfirm: deps.
|
|
77
|
-
deps.runtime.runtimeContext!,
|
|
78
|
-
),
|
|
79
|
+
canConfirm: deps.canConfirm(),
|
|
79
80
|
promptForApproval: async () => {
|
|
80
|
-
const decision = await deps.promptPermission(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
command,
|
|
90
|
-
},
|
|
91
|
-
);
|
|
81
|
+
const decision = await deps.promptPermission({
|
|
82
|
+
requestId: tcc.toolCallId,
|
|
83
|
+
source: "tool_call",
|
|
84
|
+
agentName: tcc.agentName,
|
|
85
|
+
message: bashExtMessage,
|
|
86
|
+
toolCallId: tcc.toolCallId,
|
|
87
|
+
toolName: tcc.toolName,
|
|
88
|
+
command,
|
|
89
|
+
});
|
|
92
90
|
bashExtDecision = decision;
|
|
93
91
|
return decision;
|
|
94
92
|
},
|
|
95
|
-
writeLog: deps.
|
|
93
|
+
writeLog: deps.writeReviewLog,
|
|
96
94
|
logContext: {
|
|
97
95
|
source: "tool_call",
|
|
98
96
|
toolCallId: tcc.toolCallId,
|
|
@@ -126,7 +124,7 @@ export async function evaluateBashExternalDirectoryGate(
|
|
|
126
124
|
if (bashExtDecision?.state === "approved_for_session") {
|
|
127
125
|
for (const extPath of uncoveredPaths) {
|
|
128
126
|
const pattern = deriveApprovalPattern(extPath);
|
|
129
|
-
deps.
|
|
127
|
+
deps.approveSessionRule("external_directory", pattern);
|
|
130
128
|
}
|
|
131
129
|
}
|
|
132
130
|
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
normalizePathForComparison,
|
|
9
9
|
} from "../../external-directory";
|
|
10
10
|
import type { PermissionPromptDecision } from "../../permission-dialog";
|
|
11
|
-
import { emitDecisionEvent } from "../../permission-events";
|
|
12
11
|
import { applyPermissionGate } from "../../permission-gate";
|
|
13
12
|
import { deriveApprovalPattern } from "../../session-rules";
|
|
14
|
-
import type { HandlerDeps } from "../types";
|
|
15
13
|
import { deriveResolution } from "./helpers";
|
|
16
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ExternalDirectoryGateDeps,
|
|
16
|
+
GateOutcome,
|
|
17
|
+
ToolCallContext,
|
|
18
|
+
} from "./types";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Evaluate the external-directory permission gate for file tools.
|
|
@@ -23,7 +25,7 @@ import type { GateOutcome, ToolCallContext } from "./types";
|
|
|
23
25
|
*/
|
|
24
26
|
export async function evaluateExternalDirectoryGate(
|
|
25
27
|
tcc: ToolCallContext,
|
|
26
|
-
deps:
|
|
28
|
+
deps: ExternalDirectoryGateDeps,
|
|
27
29
|
): Promise<GateOutcome | null> {
|
|
28
30
|
if (!tcc.cwd) return null;
|
|
29
31
|
|
|
@@ -40,10 +42,7 @@ export async function evaluateExternalDirectoryGate(
|
|
|
40
42
|
);
|
|
41
43
|
|
|
42
44
|
// ── Pi infrastructure read bypass ──────────────────────────────────────
|
|
43
|
-
const allInfraDirs =
|
|
44
|
-
...deps.runtime.piInfrastructureDirs,
|
|
45
|
-
...(deps.runtime.config.piInfrastructureReadPaths ?? []),
|
|
46
|
-
];
|
|
45
|
+
const allInfraDirs = deps.getInfrastructureDirs();
|
|
47
46
|
if (
|
|
48
47
|
isPiInfrastructureRead(
|
|
49
48
|
tcc.toolName,
|
|
@@ -52,17 +51,14 @@ export async function evaluateExternalDirectoryGate(
|
|
|
52
51
|
tcc.cwd,
|
|
53
52
|
)
|
|
54
53
|
) {
|
|
55
|
-
deps.
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
emitDecisionEvent(deps.events, {
|
|
54
|
+
deps.writeReviewLog("permission_request.infrastructure_auto_allowed", {
|
|
55
|
+
source: "tool_call",
|
|
56
|
+
toolCallId: tcc.toolCallId,
|
|
57
|
+
toolName: tcc.toolName,
|
|
58
|
+
agentName: tcc.agentName,
|
|
59
|
+
path: externalDirectoryPath,
|
|
60
|
+
});
|
|
61
|
+
deps.emitDecision({
|
|
66
62
|
surface: tcc.toolName,
|
|
67
63
|
value: externalDirectoryPath,
|
|
68
64
|
result: "allow",
|
|
@@ -75,16 +71,16 @@ export async function evaluateExternalDirectoryGate(
|
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
// ── Policy check ───────────────────────────────────────────────────────
|
|
78
|
-
const extCheck = deps.
|
|
74
|
+
const extCheck = deps.checkPermission(
|
|
79
75
|
"external_directory",
|
|
80
76
|
{ path: normalizedExtPath },
|
|
81
77
|
tcc.agentName ?? undefined,
|
|
82
|
-
deps.
|
|
78
|
+
deps.getSessionRuleset(),
|
|
83
79
|
);
|
|
84
80
|
|
|
85
81
|
// Session-rule hit
|
|
86
82
|
if (extCheck.source === "session") {
|
|
87
|
-
deps.
|
|
83
|
+
deps.writeReviewLog("permission_request.session_approved", {
|
|
88
84
|
source: "tool_call",
|
|
89
85
|
toolCallId: tcc.toolCallId,
|
|
90
86
|
toolName: tcc.toolName,
|
|
@@ -93,7 +89,7 @@ export async function evaluateExternalDirectoryGate(
|
|
|
93
89
|
resolution: "session_approved",
|
|
94
90
|
sessionApprovalPattern: extCheck.matchedPattern,
|
|
95
91
|
});
|
|
96
|
-
|
|
92
|
+
deps.emitDecision({
|
|
97
93
|
surface: "external_directory",
|
|
98
94
|
value: externalDirectoryPath,
|
|
99
95
|
result: "allow",
|
|
@@ -113,29 +109,24 @@ export async function evaluateExternalDirectoryGate(
|
|
|
113
109
|
tcc.cwd,
|
|
114
110
|
tcc.agentName ?? undefined,
|
|
115
111
|
);
|
|
116
|
-
const extDirCanConfirm = deps.
|
|
117
|
-
deps.runtime.runtimeContext!,
|
|
118
|
-
);
|
|
112
|
+
const extDirCanConfirm = deps.canConfirm();
|
|
119
113
|
const extDirGateResult = await applyPermissionGate({
|
|
120
114
|
state: extCheck.state,
|
|
121
115
|
canConfirm: extDirCanConfirm,
|
|
122
116
|
promptForApproval: async () => {
|
|
123
|
-
const decision = await deps.promptPermission(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
path: externalDirectoryPath,
|
|
133
|
-
},
|
|
134
|
-
);
|
|
117
|
+
const decision = await deps.promptPermission({
|
|
118
|
+
requestId: tcc.toolCallId,
|
|
119
|
+
source: "tool_call",
|
|
120
|
+
agentName: tcc.agentName,
|
|
121
|
+
message: extDirMessage,
|
|
122
|
+
toolCallId: tcc.toolCallId,
|
|
123
|
+
toolName: tcc.toolName,
|
|
124
|
+
path: externalDirectoryPath,
|
|
125
|
+
});
|
|
135
126
|
extDirDecision = decision;
|
|
136
127
|
return decision;
|
|
137
128
|
},
|
|
138
|
-
writeLog: deps.
|
|
129
|
+
writeLog: deps.writeReviewLog,
|
|
139
130
|
logContext: {
|
|
140
131
|
source: "tool_call",
|
|
141
132
|
toolCallId: tcc.toolCallId,
|
|
@@ -161,7 +152,7 @@ export async function evaluateExternalDirectoryGate(
|
|
|
161
152
|
},
|
|
162
153
|
});
|
|
163
154
|
|
|
164
|
-
|
|
155
|
+
deps.emitDecision({
|
|
165
156
|
surface: "external_directory",
|
|
166
157
|
value: externalDirectoryPath,
|
|
167
158
|
result: extDirGateResult.action === "allow" ? "allow" : "deny",
|
|
@@ -182,7 +173,7 @@ export async function evaluateExternalDirectoryGate(
|
|
|
182
173
|
|
|
183
174
|
if (extDirDecision?.state === "approved_for_session") {
|
|
184
175
|
const pattern = deriveApprovalPattern(normalizedExtPath);
|
|
185
|
-
deps.
|
|
176
|
+
deps.approveSessionRule("external_directory", pattern);
|
|
186
177
|
}
|
|
187
178
|
|
|
188
179
|
return { action: "allow" };
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { toRecord } from "../../common";
|
|
2
2
|
import { normalizePathForComparison } from "../../external-directory";
|
|
3
|
-
import { emitDecisionEvent } from "../../permission-events";
|
|
4
3
|
import { applyPermissionGate } from "../../permission-gate";
|
|
5
4
|
import {
|
|
6
5
|
formatSkillPathAskPrompt,
|
|
7
6
|
formatSkillPathDenyReason,
|
|
8
7
|
} from "../../permission-prompts";
|
|
9
8
|
import { findSkillPathMatch } from "../../skill-prompt-sanitizer";
|
|
10
|
-
import type { HandlerDeps } from "../types";
|
|
11
9
|
import { deriveResolution } from "./helpers";
|
|
12
|
-
import type { GateOutcome, ToolCallContext } from "./types";
|
|
10
|
+
import type { GateOutcome, SkillReadGateDeps, ToolCallContext } from "./types";
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* Evaluate the skill-read permission gate.
|
|
@@ -19,10 +17,12 @@ import type { GateOutcome, ToolCallContext } from "./types";
|
|
|
19
17
|
*/
|
|
20
18
|
export async function evaluateSkillReadGate(
|
|
21
19
|
tcc: ToolCallContext,
|
|
22
|
-
deps:
|
|
20
|
+
deps: SkillReadGateDeps,
|
|
23
21
|
): Promise<GateOutcome | null> {
|
|
22
|
+
const activeSkillEntries = deps.getActiveSkillEntries();
|
|
23
|
+
|
|
24
24
|
// Only applies to read tool calls with active skill entries
|
|
25
|
-
if (tcc.toolName !== "read" ||
|
|
25
|
+
if (tcc.toolName !== "read" || activeSkillEntries.length === 0) {
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -35,7 +35,7 @@ export async function evaluateSkillReadGate(
|
|
|
35
35
|
const normalizedReadPath = normalizePathForComparison(path, tcc.cwd);
|
|
36
36
|
const matchedSkill = findSkillPathMatch(
|
|
37
37
|
normalizedReadPath,
|
|
38
|
-
|
|
38
|
+
activeSkillEntries,
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
if (!matchedSkill) {
|
|
@@ -47,14 +47,12 @@ export async function evaluateSkillReadGate(
|
|
|
47
47
|
path,
|
|
48
48
|
tcc.agentName ?? undefined,
|
|
49
49
|
);
|
|
50
|
-
const skillReadCanConfirm = deps.
|
|
51
|
-
deps.runtime.runtimeContext!,
|
|
52
|
-
);
|
|
50
|
+
const skillReadCanConfirm = deps.canConfirm();
|
|
53
51
|
const skillReadGate = await applyPermissionGate({
|
|
54
52
|
state: matchedSkill.state,
|
|
55
53
|
canConfirm: skillReadCanConfirm,
|
|
56
54
|
promptForApproval: () =>
|
|
57
|
-
deps.promptPermission(
|
|
55
|
+
deps.promptPermission({
|
|
58
56
|
requestId: tcc.toolCallId,
|
|
59
57
|
source: "skill_read",
|
|
60
58
|
agentName: tcc.agentName,
|
|
@@ -64,7 +62,7 @@ export async function evaluateSkillReadGate(
|
|
|
64
62
|
skillName: matchedSkill.name,
|
|
65
63
|
path,
|
|
66
64
|
}),
|
|
67
|
-
writeLog: deps.
|
|
65
|
+
writeLog: deps.writeReviewLog,
|
|
68
66
|
logContext: {
|
|
69
67
|
source: "skill_read",
|
|
70
68
|
skillName: matchedSkill.name,
|
|
@@ -88,7 +86,7 @@ export async function evaluateSkillReadGate(
|
|
|
88
86
|
},
|
|
89
87
|
});
|
|
90
88
|
|
|
91
|
-
|
|
89
|
+
deps.emitDecision({
|
|
92
90
|
surface: "skill",
|
|
93
91
|
value: matchedSkill.name,
|
|
94
92
|
result: skillReadGate.action === "allow" ? "allow" : "deny",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PATH_BEARING_TOOLS } from "../../external-directory";
|
|
2
2
|
import { suggestSessionPattern } from "../../pattern-suggest";
|
|
3
|
-
import { emitDecisionEvent } from "../../permission-events";
|
|
4
3
|
import { applyPermissionGate } from "../../permission-gate";
|
|
5
4
|
import {
|
|
6
5
|
formatAskPrompt,
|
|
@@ -8,9 +7,8 @@ import {
|
|
|
8
7
|
formatUserDeniedReason,
|
|
9
8
|
} from "../../permission-prompts";
|
|
10
9
|
import { getPermissionLogContext } from "../../tool-input-preview";
|
|
11
|
-
import type { HandlerDeps } from "../types";
|
|
12
10
|
import { deriveDecisionValue, deriveResolution } from "./helpers";
|
|
13
|
-
import type { GateOutcome, ToolCallContext } from "./types";
|
|
11
|
+
import type { GateOutcome, ToolCallContext, ToolGateDeps } from "./types";
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* Evaluate the normal tool permission gate.
|
|
@@ -19,18 +17,18 @@ import type { GateOutcome, ToolCallContext } from "./types";
|
|
|
19
17
|
*/
|
|
20
18
|
export async function evaluateToolGate(
|
|
21
19
|
tcc: ToolCallContext,
|
|
22
|
-
deps:
|
|
20
|
+
deps: ToolGateDeps,
|
|
23
21
|
): Promise<GateOutcome> {
|
|
24
|
-
const check = deps.
|
|
22
|
+
const check = deps.checkPermission(
|
|
25
23
|
tcc.toolName,
|
|
26
24
|
tcc.input,
|
|
27
25
|
tcc.agentName ?? undefined,
|
|
28
|
-
deps.
|
|
26
|
+
deps.getSessionRuleset(),
|
|
29
27
|
);
|
|
30
28
|
|
|
31
29
|
// Session-hit: already approved by a session rule — skip the gate entirely.
|
|
32
30
|
if (check.source === "session") {
|
|
33
|
-
deps.
|
|
31
|
+
deps.writeReviewLog("permission_request.session_approved", {
|
|
34
32
|
source: "tool_call",
|
|
35
33
|
toolCallId: tcc.toolCallId,
|
|
36
34
|
toolName: tcc.toolName,
|
|
@@ -38,7 +36,7 @@ export async function evaluateToolGate(
|
|
|
38
36
|
resolution: "session_approved",
|
|
39
37
|
sessionApprovalPattern: check.matchedPattern,
|
|
40
38
|
});
|
|
41
|
-
|
|
39
|
+
deps.emitDecision({
|
|
42
40
|
surface: tcc.toolName,
|
|
43
41
|
value: deriveDecisionValue(tcc.toolName, check),
|
|
44
42
|
result: "allow",
|
|
@@ -82,9 +80,7 @@ export async function evaluateToolGate(
|
|
|
82
80
|
tcc.agentName ?? undefined,
|
|
83
81
|
tcc.input,
|
|
84
82
|
);
|
|
85
|
-
const toolCanConfirm = deps.
|
|
86
|
-
deps.runtime.runtimeContext!,
|
|
87
|
-
);
|
|
83
|
+
const toolCanConfirm = deps.canConfirm();
|
|
88
84
|
let toolDecisionAutoApproved = false;
|
|
89
85
|
const toolGate = await applyPermissionGate({
|
|
90
86
|
state: check.state,
|
|
@@ -94,23 +90,20 @@ export async function evaluateToolGate(
|
|
|
94
90
|
pattern: suggestion.pattern,
|
|
95
91
|
},
|
|
96
92
|
promptForApproval: async () => {
|
|
97
|
-
const decision = await deps.promptPermission(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
...permissionLogContext,
|
|
108
|
-
},
|
|
109
|
-
);
|
|
93
|
+
const decision = await deps.promptPermission({
|
|
94
|
+
requestId: tcc.toolCallId,
|
|
95
|
+
source: "tool_call",
|
|
96
|
+
agentName: tcc.agentName,
|
|
97
|
+
message: toolAskMessage,
|
|
98
|
+
toolCallId: tcc.toolCallId,
|
|
99
|
+
toolName: tcc.toolName,
|
|
100
|
+
sessionLabel: suggestion.label,
|
|
101
|
+
...permissionLogContext,
|
|
102
|
+
});
|
|
110
103
|
toolDecisionAutoApproved = decision.autoApproved === true;
|
|
111
104
|
return decision;
|
|
112
105
|
},
|
|
113
|
-
writeLog: deps.
|
|
106
|
+
writeLog: deps.writeReviewLog,
|
|
114
107
|
logContext: {
|
|
115
108
|
source: "tool_call",
|
|
116
109
|
toolCallId: tcc.toolCallId,
|
|
@@ -129,7 +122,7 @@ export async function evaluateToolGate(
|
|
|
129
122
|
|
|
130
123
|
const toolGateHasSession =
|
|
131
124
|
toolGate.action === "allow" && toolGate.sessionApproval !== undefined;
|
|
132
|
-
|
|
125
|
+
deps.emitDecision({
|
|
133
126
|
surface: tcc.toolName,
|
|
134
127
|
value: deriveDecisionValue(tcc.toolName, check),
|
|
135
128
|
result: toolGate.action === "allow" ? "allow" : "deny",
|
|
@@ -150,7 +143,7 @@ export async function evaluateToolGate(
|
|
|
150
143
|
}
|
|
151
144
|
|
|
152
145
|
if (toolGate.sessionApproval) {
|
|
153
|
-
deps.
|
|
146
|
+
deps.approveSessionRule(
|
|
154
147
|
toolGate.sessionApproval.surface,
|
|
155
148
|
toolGate.sessionApproval.pattern,
|
|
156
149
|
);
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
import type { PermissionPromptDecision } from "../../permission-dialog";
|
|
4
|
+
import type { PermissionDecisionEvent } from "../../permission-events";
|
|
5
|
+
import type { Rule } from "../../rule";
|
|
6
|
+
import type { SkillPromptEntry } from "../../skill-prompt-sanitizer";
|
|
7
|
+
import type { PermissionCheckResult } from "../../types";
|
|
8
|
+
import type { PromptPermissionDetails } from "../types";
|
|
9
|
+
|
|
3
10
|
/** Outcome of a single permission gate evaluation. */
|
|
4
11
|
export type GateOutcome =
|
|
5
12
|
| { action: "allow" }
|
|
@@ -13,3 +20,71 @@ export interface ToolCallContext {
|
|
|
13
20
|
toolCallId: string;
|
|
14
21
|
cwd: string | undefined;
|
|
15
22
|
}
|
|
23
|
+
|
|
24
|
+
// ── Per-gate narrow dependency interfaces ──────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Narrow deps for evaluateToolGate — every field is a leaf method. */
|
|
27
|
+
export interface ToolGateDeps {
|
|
28
|
+
checkPermission(
|
|
29
|
+
surface: string,
|
|
30
|
+
input: unknown,
|
|
31
|
+
agentName?: string,
|
|
32
|
+
sessionRules?: Rule[],
|
|
33
|
+
): PermissionCheckResult;
|
|
34
|
+
getSessionRuleset(): Rule[];
|
|
35
|
+
approveSessionRule(surface: string, pattern: string): void;
|
|
36
|
+
writeReviewLog(event: string, details: Record<string, unknown>): void;
|
|
37
|
+
emitDecision(event: PermissionDecisionEvent): void;
|
|
38
|
+
canConfirm(): boolean;
|
|
39
|
+
promptPermission(
|
|
40
|
+
details: PromptPermissionDetails,
|
|
41
|
+
): Promise<PermissionPromptDecision>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Narrow deps for evaluateExternalDirectoryGate. */
|
|
45
|
+
export interface ExternalDirectoryGateDeps {
|
|
46
|
+
checkPermission(
|
|
47
|
+
surface: string,
|
|
48
|
+
input: unknown,
|
|
49
|
+
agentName?: string,
|
|
50
|
+
sessionRules?: Rule[],
|
|
51
|
+
): PermissionCheckResult;
|
|
52
|
+
getSessionRuleset(): Rule[];
|
|
53
|
+
approveSessionRule(surface: string, pattern: string): void;
|
|
54
|
+
writeReviewLog(event: string, details: Record<string, unknown>): void;
|
|
55
|
+
emitDecision(event: PermissionDecisionEvent): void;
|
|
56
|
+
canConfirm(): boolean;
|
|
57
|
+
promptPermission(
|
|
58
|
+
details: PromptPermissionDetails,
|
|
59
|
+
): Promise<PermissionPromptDecision>;
|
|
60
|
+
/** Resolved infrastructure dirs (static + config-based). */
|
|
61
|
+
getInfrastructureDirs(): string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Narrow deps for evaluateBashExternalDirectoryGate. */
|
|
65
|
+
export interface BashExternalDirectoryGateDeps {
|
|
66
|
+
checkPermission(
|
|
67
|
+
surface: string,
|
|
68
|
+
input: unknown,
|
|
69
|
+
agentName?: string,
|
|
70
|
+
sessionRules?: Rule[],
|
|
71
|
+
): PermissionCheckResult;
|
|
72
|
+
getSessionRuleset(): Rule[];
|
|
73
|
+
approveSessionRule(surface: string, pattern: string): void;
|
|
74
|
+
writeReviewLog(event: string, details: Record<string, unknown>): void;
|
|
75
|
+
canConfirm(): boolean;
|
|
76
|
+
promptPermission(
|
|
77
|
+
details: PromptPermissionDetails,
|
|
78
|
+
): Promise<PermissionPromptDecision>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Narrow deps for evaluateSkillReadGate. */
|
|
82
|
+
export interface SkillReadGateDeps {
|
|
83
|
+
getActiveSkillEntries(): SkillPromptEntry[];
|
|
84
|
+
writeReviewLog(event: string, details: Record<string, unknown>): void;
|
|
85
|
+
emitDecision(event: PermissionDecisionEvent): void;
|
|
86
|
+
canConfirm(): boolean;
|
|
87
|
+
promptPermission(
|
|
88
|
+
details: PromptPermissionDetails,
|
|
89
|
+
): Promise<PermissionPromptDecision>;
|
|
90
|
+
}
|
package/src/handlers/input.ts
CHANGED
|
@@ -40,7 +40,7 @@ export async function handleInput(
|
|
|
40
40
|
event: InputPayload,
|
|
41
41
|
ctx: ExtensionContext,
|
|
42
42
|
): Promise<InputEventResult> {
|
|
43
|
-
deps.
|
|
43
|
+
deps.session.runtimeContext = ctx;
|
|
44
44
|
deps.startForwardedPermissionPolling(ctx);
|
|
45
45
|
|
|
46
46
|
const skillName = extractSkillNameFromInput(event.text);
|
|
@@ -49,7 +49,7 @@ export async function handleInput(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const agentName = deps.resolveAgentName(ctx);
|
|
52
|
-
const check = deps.
|
|
52
|
+
const check = deps.session.permissionManager.checkPermission(
|
|
53
53
|
"skill",
|
|
54
54
|
{ name: skillName },
|
|
55
55
|
agentName ?? undefined,
|
|
@@ -82,7 +82,7 @@ export async function handleInput(
|
|
|
82
82
|
skillInputAutoApproved = decision.autoApproved === true;
|
|
83
83
|
return decision;
|
|
84
84
|
},
|
|
85
|
-
writeLog: deps.
|
|
85
|
+
writeLog: deps.writeReviewLog,
|
|
86
86
|
logContext: {
|
|
87
87
|
source: "skill_input",
|
|
88
88
|
skillName,
|