@gotgenes/pi-permission-system 7.4.1 → 8.1.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 +29 -0
- package/config/config.example.json +3 -0
- package/package.json +1 -1
- package/schemas/permissions.schema.json +12 -0
- package/src/extension-config.ts +23 -0
- package/src/handlers/gates/tool.ts +4 -2
- package/src/handlers/permission-gate-handler.ts +106 -138
- package/src/index.ts +0 -6
- package/src/permission-prompts.ts +5 -2
- package/src/service.ts +1 -23
- package/src/subagent-registry.ts +3 -3
- package/src/tool-input-preview.ts +0 -116
- package/src/tool-preview-formatter.ts +188 -0
- package/test/extension-config.test.ts +93 -0
- package/test/handlers/external-directory-integration.test.ts +2 -0
- package/test/handlers/external-directory-session-dedup.test.ts +2 -0
- package/test/handlers/gates/tool.test.ts +29 -2
- package/test/handlers/tool-call-events.test.ts +2 -1
- package/test/handlers/tool-call.test.ts +2 -1
- package/test/handlers/validate-requested-tool.test.ts +92 -0
- package/test/permission-prompts.test.ts +66 -38
- package/test/service.test.ts +0 -54
- package/test/tool-input-preview.test.ts +0 -244
- package/test/tool-preview-formatter.test.ts +385 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ 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
|
+
## [8.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v8.0.0...pi-permission-system-v8.1.0) (2026-05-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add toolInputPreviewMaxLength and toolTextSummaryMaxLength config fields ([#266](https://github.com/gotgenes/pi-packages/issues/266)) ([3a7dafb](https://github.com/gotgenes/pi-packages/commit/3a7dafbb0bb8534dabda7eeba6c4d35ba2e8708b))
|
|
14
|
+
* use configured preview limits in permission prompts ([#266](https://github.com/gotgenes/pi-packages/issues/266)) ([83e2829](https://github.com/gotgenes/pi-packages/commit/83e2829175a55f2f0436c742e19e3753ee171e47))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* document configurable tool-preview length knobs ([#266](https://github.com/gotgenes/pi-packages/issues/266)) ([6d0b134](https://github.com/gotgenes/pi-packages/commit/6d0b134be4ef4c90ddf582b32058c3ec9d2eb13f))
|
|
20
|
+
|
|
21
|
+
## [8.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.4.1...pi-permission-system-v8.0.0) (2026-05-30)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### ⚠ BREAKING CHANGES
|
|
25
|
+
|
|
26
|
+
* `registerSubagentSession` and `unregisterSubagentSession` are removed from the `PermissionsService` interface and its implementation. The `SubagentSessionInfo` type is no longer re-exported from the public service module.
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
* remove inbound subagent-registration methods from PermissionsService ([#267](https://github.com/gotgenes/pi-packages/issues/267)) ([552735a](https://github.com/gotgenes/pi-packages/commit/552735a97eec939fc06130bce059c78f03eb8e58))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
|
|
35
|
+
* **pi-permission-system:** describe event-driven subagent registration ([#267](https://github.com/gotgenes/pi-packages/issues/267)) ([8c39b87](https://github.com/gotgenes/pi-packages/commit/8c39b8785aa389d96b5f38996711d8aa3dbeb284))
|
|
36
|
+
|
|
8
37
|
## [7.4.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.4.0...pi-permission-system-v7.4.1) (2026-05-30)
|
|
9
38
|
|
|
10
39
|
|
package/package.json
CHANGED
|
@@ -29,6 +29,18 @@
|
|
|
29
29
|
"type": "boolean",
|
|
30
30
|
"default": false
|
|
31
31
|
},
|
|
32
|
+
"toolInputPreviewMaxLength": {
|
|
33
|
+
"description": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts. Omit to use the default (200). Set to a large value to disable truncation.",
|
|
34
|
+
"markdownDescription": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts.\n\nOmit to use the default (200). Set to a large value (e.g. `10000`) to effectively disable truncation and see the full input.",
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"minimum": 1
|
|
37
|
+
},
|
|
38
|
+
"toolTextSummaryMaxLength": {
|
|
39
|
+
"description": "Maximum character length of inline pattern/path summaries (e.g. grep patterns, find globs, ls paths) in permission prompts. Omit to use the default (80).",
|
|
40
|
+
"markdownDescription": "Maximum character length of inline pattern/path summaries (e.g. grep patterns, find globs, ls paths) shown in permission prompts.\n\nOmit to use the default (80). Increase this when working with long regexes or deep paths that are being cut off.",
|
|
41
|
+
"type": "integer",
|
|
42
|
+
"minimum": 1
|
|
43
|
+
},
|
|
32
44
|
"piInfrastructureReadPaths": {
|
|
33
45
|
"description": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the external_directory gate. Supports ~ expansion and wildcard patterns (* and ?).",
|
|
34
46
|
"markdownDescription": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the `external_directory` gate.\n\nThe extension auto-discovers the global node_modules root (walks up from the extension's install path; falls back to `npm root -g` from a dev checkout), `agentDir`, `agentDir/git`, and project-local `.pi/npm/` and `.pi/git/`. Add entries here for edge cases where auto-discovery is insufficient (e.g. custom `npmCommand` pointing to pnpm).\n\nSupports `~`/`$HOME` expansion. Entries may be plain directory prefixes or wildcard patterns using `*` (matches any characters, including `/`) and `?` (matches exactly one character). `**` and `*` are equivalent — both cross directory boundaries.",
|
package/src/extension-config.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface PermissionSystemExtensionConfig {
|
|
|
12
12
|
yoloMode: boolean;
|
|
13
13
|
/** Additional directories to auto-allow for reads as Pi infrastructure. */
|
|
14
14
|
piInfrastructureReadPaths?: string[];
|
|
15
|
+
/** Max length of the inline-JSON input preview shown in permission prompts. Defaults to 200. */
|
|
16
|
+
toolInputPreviewMaxLength?: number;
|
|
17
|
+
/** Max length of inline pattern/path summaries (grep/find/ls) in permission prompts. Defaults to 80. */
|
|
18
|
+
toolTextSummaryMaxLength?: number;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
|
|
@@ -42,6 +46,13 @@ export function detectMisplacedPermissionKeys(
|
|
|
42
46
|
return Object.keys(raw).filter((key) => PERMISSION_POLICY_KEYS.has(key));
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
/** Returns `raw` if it is a positive integer; otherwise `undefined`. */
|
|
50
|
+
export function normalizeOptionalPositiveInt(raw: unknown): number | undefined {
|
|
51
|
+
return typeof raw === "number" && Number.isInteger(raw) && raw > 0
|
|
52
|
+
? raw
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
export function normalizePermissionSystemConfig(
|
|
46
57
|
raw: unknown,
|
|
47
58
|
): PermissionSystemExtensionConfig {
|
|
@@ -60,6 +71,18 @@ export function normalizePermissionSystemConfig(
|
|
|
60
71
|
if (piInfrastructureReadPaths !== undefined) {
|
|
61
72
|
result.piInfrastructureReadPaths = piInfrastructureReadPaths;
|
|
62
73
|
}
|
|
74
|
+
const toolInputPreviewMaxLength = normalizeOptionalPositiveInt(
|
|
75
|
+
record.toolInputPreviewMaxLength,
|
|
76
|
+
);
|
|
77
|
+
if (toolInputPreviewMaxLength !== undefined) {
|
|
78
|
+
result.toolInputPreviewMaxLength = toolInputPreviewMaxLength;
|
|
79
|
+
}
|
|
80
|
+
const toolTextSummaryMaxLength = normalizeOptionalPositiveInt(
|
|
81
|
+
record.toolTextSummaryMaxLength,
|
|
82
|
+
);
|
|
83
|
+
if (toolTextSummaryMaxLength !== undefined) {
|
|
84
|
+
result.toolTextSummaryMaxLength = toolTextSummaryMaxLength;
|
|
85
|
+
}
|
|
63
86
|
return result;
|
|
64
87
|
}
|
|
65
88
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getPathBearingToolPath, PATH_BEARING_TOOLS } from "#src/path-utils";
|
|
2
2
|
import { suggestSessionPattern } from "#src/pattern-suggest";
|
|
3
3
|
import { formatAskPrompt } from "#src/permission-prompts";
|
|
4
|
-
import {
|
|
4
|
+
import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
|
|
5
5
|
import type { PermissionCheckResult } from "#src/types";
|
|
6
6
|
import type { GateDescriptor } from "./descriptor";
|
|
7
7
|
import { deriveDecisionValue } from "./helpers";
|
|
@@ -31,8 +31,9 @@ function deriveSuggestionValue(
|
|
|
31
31
|
export function describeToolGate(
|
|
32
32
|
tcc: ToolCallContext,
|
|
33
33
|
check: PermissionCheckResult,
|
|
34
|
+
formatter: ToolPreviewFormatter,
|
|
34
35
|
): GateDescriptor {
|
|
35
|
-
const permissionLogContext = getPermissionLogContext(
|
|
36
|
+
const permissionLogContext = formatter.getPermissionLogContext(
|
|
36
37
|
check,
|
|
37
38
|
tcc.input,
|
|
38
39
|
PATH_BEARING_TOOLS,
|
|
@@ -48,6 +49,7 @@ export function describeToolGate(
|
|
|
48
49
|
check,
|
|
49
50
|
tcc.agentName ?? undefined,
|
|
50
51
|
tcc.input,
|
|
52
|
+
formatter,
|
|
51
53
|
);
|
|
52
54
|
|
|
53
55
|
return {
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
formatUnknownToolReason,
|
|
17
17
|
} from "#src/permission-prompts";
|
|
18
18
|
import type { PermissionSession } from "#src/permission-session";
|
|
19
|
+
import {
|
|
20
|
+
resolveToolPreviewLimits,
|
|
21
|
+
ToolPreviewFormatter,
|
|
22
|
+
} from "#src/tool-preview-formatter";
|
|
19
23
|
import {
|
|
20
24
|
checkRequestedToolRegistration,
|
|
21
25
|
getToolNameFromValue,
|
|
@@ -23,7 +27,7 @@ import {
|
|
|
23
27
|
} from "#src/tool-registry";
|
|
24
28
|
import { describeBashExternalDirectoryGate } from "./gates/bash-external-directory";
|
|
25
29
|
import { describeBashPathGate } from "./gates/bash-path";
|
|
26
|
-
import type { GateRunnerDeps } from "./gates/descriptor";
|
|
30
|
+
import type { GateResult, GateRunnerDeps } from "./gates/descriptor";
|
|
27
31
|
import { isGateBypass } from "./gates/descriptor";
|
|
28
32
|
import { describeExternalDirectoryGate } from "./gates/external-directory";
|
|
29
33
|
import { describePathGate } from "./gates/path";
|
|
@@ -58,30 +62,13 @@ export class PermissionGateHandler {
|
|
|
58
62
|
): Promise<{ block?: true; reason?: string }> {
|
|
59
63
|
this.session.activate(ctx);
|
|
60
64
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!toolName) {
|
|
65
|
-
return { block: true, reason: formatMissingToolNameReason() };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const registrationCheck = checkRequestedToolRegistration(
|
|
69
|
-
toolName,
|
|
70
|
-
this.toolRegistry.getAll(),
|
|
71
|
-
);
|
|
72
|
-
if (registrationCheck.status === "missing-tool-name") {
|
|
73
|
-
return { block: true, reason: formatMissingToolNameReason() };
|
|
65
|
+
const validation = validateRequestedTool(event, this.toolRegistry.getAll());
|
|
66
|
+
if (validation.status === "block") {
|
|
67
|
+
return { block: true, reason: validation.reason };
|
|
74
68
|
}
|
|
69
|
+
const toolName = validation.toolName;
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
block: true,
|
|
79
|
-
reason: formatUnknownToolReason(
|
|
80
|
-
registrationCheck.requestedToolName,
|
|
81
|
-
registrationCheck.availableToolNames,
|
|
82
|
-
),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
71
|
+
const agentName = this.session.resolveAgentName(ctx);
|
|
85
72
|
|
|
86
73
|
const input = getEventInput(event);
|
|
87
74
|
const toolCallId =
|
|
@@ -126,136 +113,78 @@ export class PermissionGateHandler {
|
|
|
126
113
|
promptPermission,
|
|
127
114
|
};
|
|
128
115
|
|
|
129
|
-
// ──
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
116
|
+
// ── Unified gate executor ─────────────────────────────────────────────
|
|
117
|
+
// Handles the bypass log/emit branch, calls runGateCheck for descriptors,
|
|
118
|
+
// and returns a block result or undefined (allow / no-op).
|
|
119
|
+
const runGate = async (
|
|
120
|
+
gate: GateResult,
|
|
121
|
+
): Promise<{ block: true; reason: string } | undefined> => {
|
|
122
|
+
if (!gate) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (isGateBypass(gate)) {
|
|
126
|
+
if (gate.log) {
|
|
127
|
+
writeReviewLog(gate.log.event, gate.log.details);
|
|
128
|
+
}
|
|
129
|
+
if (gate.decision) {
|
|
130
|
+
emitDecision(gate.decision);
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const result = await runGateCheck(
|
|
135
|
+
gate,
|
|
136
136
|
tcc.agentName,
|
|
137
137
|
tcc.toolCallId,
|
|
138
138
|
runnerDeps,
|
|
139
139
|
);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
140
|
+
return result.action === "block"
|
|
141
|
+
? { block: true, reason: result.reason }
|
|
142
|
+
: undefined;
|
|
143
|
+
};
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (isGateBypass(pathDesc)) {
|
|
149
|
-
if (pathDesc.log) {
|
|
150
|
-
writeReviewLog(pathDesc.log.event, pathDesc.log.details);
|
|
151
|
-
}
|
|
152
|
-
} else {
|
|
153
|
-
const pathResult = await runGateCheck(
|
|
154
|
-
pathDesc,
|
|
155
|
-
tcc.agentName,
|
|
156
|
-
tcc.toolCallId,
|
|
157
|
-
runnerDeps,
|
|
158
|
-
);
|
|
159
|
-
if (pathResult.action === "block") {
|
|
160
|
-
return { block: true, reason: pathResult.reason };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
145
|
+
const formatter = new ToolPreviewFormatter(
|
|
146
|
+
resolveToolPreviewLimits(this.session.config),
|
|
147
|
+
);
|
|
164
148
|
|
|
165
|
-
// ──
|
|
149
|
+
// ── Ordered gate pipeline ─────────────────────────────────────────────
|
|
150
|
+
// infraDirs is computed once, outside the pipeline, exactly as before.
|
|
166
151
|
const infraDirs = [
|
|
167
152
|
...this.session.getInfrastructureDirs(),
|
|
168
153
|
...this.session.getInfrastructureReadPaths(),
|
|
169
154
|
];
|
|
170
|
-
const extDirDesc = describeExternalDirectoryGate(tcc, infraDirs);
|
|
171
|
-
if (extDirDesc) {
|
|
172
|
-
if (isGateBypass(extDirDesc)) {
|
|
173
|
-
if (extDirDesc.log) {
|
|
174
|
-
writeReviewLog(extDirDesc.log.event, extDirDesc.log.details);
|
|
175
|
-
}
|
|
176
|
-
if (extDirDesc.decision) {
|
|
177
|
-
emitDecision(extDirDesc.decision);
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
const extDirResult = await runGateCheck(
|
|
181
|
-
extDirDesc,
|
|
182
|
-
tcc.agentName,
|
|
183
|
-
tcc.toolCallId,
|
|
184
|
-
runnerDeps,
|
|
185
|
-
);
|
|
186
|
-
if (extDirResult.action === "block") {
|
|
187
|
-
return { block: true, reason: extDirResult.reason };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
155
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
checkPermission,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
tcc.
|
|
207
|
-
tcc.
|
|
208
|
-
|
|
156
|
+
const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
|
|
157
|
+
() =>
|
|
158
|
+
describeSkillReadGate(tcc, () => this.session.getActiveSkillEntries()),
|
|
159
|
+
() => describePathGate(tcc, checkPermission, getSessionRuleset),
|
|
160
|
+
() => describeExternalDirectoryGate(tcc, infraDirs),
|
|
161
|
+
() =>
|
|
162
|
+
describeBashExternalDirectoryGate(
|
|
163
|
+
tcc,
|
|
164
|
+
checkPermission,
|
|
165
|
+
getSessionRuleset,
|
|
166
|
+
),
|
|
167
|
+
() => describeBashPathGate(tcc, checkPermission, getSessionRuleset),
|
|
168
|
+
() => {
|
|
169
|
+
const toolCheck = checkPermission(
|
|
170
|
+
tcc.toolName,
|
|
171
|
+
tcc.input,
|
|
172
|
+
tcc.agentName ?? undefined,
|
|
173
|
+
getSessionRuleset(),
|
|
209
174
|
);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
175
|
+
const toolDescriptor = describeToolGate(tcc, toolCheck, formatter);
|
|
176
|
+
toolDescriptor.preCheck = toolCheck;
|
|
177
|
+
return toolDescriptor;
|
|
178
|
+
},
|
|
179
|
+
];
|
|
215
180
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
getSessionRuleset,
|
|
221
|
-
);
|
|
222
|
-
if (bashPathDesc) {
|
|
223
|
-
if (isGateBypass(bashPathDesc)) {
|
|
224
|
-
if (bashPathDesc.log) {
|
|
225
|
-
writeReviewLog(bashPathDesc.log.event, bashPathDesc.log.details);
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
const bashPathResult = await runGateCheck(
|
|
229
|
-
bashPathDesc,
|
|
230
|
-
tcc.agentName,
|
|
231
|
-
tcc.toolCallId,
|
|
232
|
-
runnerDeps,
|
|
233
|
-
);
|
|
234
|
-
if (bashPathResult.action === "block") {
|
|
235
|
-
return { block: true, reason: bashPathResult.reason };
|
|
236
|
-
}
|
|
181
|
+
for (const produce of gateProducers) {
|
|
182
|
+
const blocked = await runGate(await produce());
|
|
183
|
+
if (blocked) {
|
|
184
|
+
return blocked;
|
|
237
185
|
}
|
|
238
186
|
}
|
|
239
187
|
|
|
240
|
-
// ── Normal tool permission gate (descriptor + runner) ────────────────────
|
|
241
|
-
const toolCheck = checkPermission(
|
|
242
|
-
tcc.toolName,
|
|
243
|
-
tcc.input,
|
|
244
|
-
tcc.agentName ?? undefined,
|
|
245
|
-
getSessionRuleset(),
|
|
246
|
-
);
|
|
247
|
-
const toolDescriptor = describeToolGate(tcc, toolCheck);
|
|
248
|
-
toolDescriptor.preCheck = toolCheck;
|
|
249
|
-
const toolResult = await runGateCheck(
|
|
250
|
-
toolDescriptor,
|
|
251
|
-
tcc.agentName,
|
|
252
|
-
tcc.toolCallId,
|
|
253
|
-
runnerDeps,
|
|
254
|
-
);
|
|
255
|
-
if (toolResult.action === "block") {
|
|
256
|
-
return { block: true, reason: toolResult.reason };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
188
|
return {};
|
|
260
189
|
}
|
|
261
190
|
|
|
@@ -352,7 +281,46 @@ export class PermissionGateHandler {
|
|
|
352
281
|
}
|
|
353
282
|
}
|
|
354
283
|
|
|
355
|
-
// ── Pure helpers
|
|
284
|
+
// ── Pure helpers ─────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/** Discriminated result of validating a tool-call event's name and registration. */
|
|
287
|
+
export type RequestedToolValidation =
|
|
288
|
+
| { status: "ok"; toolName: string }
|
|
289
|
+
| { status: "block"; reason: string };
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Validate the tool name from a raw event against the registered tool list.
|
|
293
|
+
*
|
|
294
|
+
* Composes `getToolNameFromValue` + `checkRequestedToolRegistration` + the
|
|
295
|
+
* two reason formatters and returns a discriminated result so `handleToolCall`
|
|
296
|
+
* reads as a straight validate → proceed path without nested early-returns.
|
|
297
|
+
*
|
|
298
|
+
* Returns the **raw** tool name (not the normalised form) so that
|
|
299
|
+
* `ToolCallContext.toolName` stays identical to the pre-extraction behaviour.
|
|
300
|
+
*/
|
|
301
|
+
export function validateRequestedTool(
|
|
302
|
+
event: unknown,
|
|
303
|
+
availableTools: readonly unknown[],
|
|
304
|
+
): RequestedToolValidation {
|
|
305
|
+
const toolName = getToolNameFromValue(event);
|
|
306
|
+
if (!toolName) {
|
|
307
|
+
return { status: "block", reason: formatMissingToolNameReason() };
|
|
308
|
+
}
|
|
309
|
+
const check = checkRequestedToolRegistration(toolName, availableTools);
|
|
310
|
+
if (check.status === "missing-tool-name") {
|
|
311
|
+
return { status: "block", reason: formatMissingToolNameReason() };
|
|
312
|
+
}
|
|
313
|
+
if (check.status === "unregistered") {
|
|
314
|
+
return {
|
|
315
|
+
status: "block",
|
|
316
|
+
reason: formatUnknownToolReason(
|
|
317
|
+
check.requestedToolName,
|
|
318
|
+
check.availableToolNames,
|
|
319
|
+
),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return { status: "ok", toolName };
|
|
323
|
+
}
|
|
356
324
|
|
|
357
325
|
/**
|
|
358
326
|
* Extract the tool input from an event, checking both `input` and `arguments`
|
package/src/index.ts
CHANGED
|
@@ -118,12 +118,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
118
118
|
sessionRules,
|
|
119
119
|
);
|
|
120
120
|
},
|
|
121
|
-
registerSubagentSession(sessionKey, info) {
|
|
122
|
-
subagentRegistry.register(sessionKey, info);
|
|
123
|
-
},
|
|
124
|
-
unregisterSubagentSession(sessionKey) {
|
|
125
|
-
subagentRegistry.unregister(sessionKey);
|
|
126
|
-
},
|
|
127
121
|
getToolPermission(toolName, agentName) {
|
|
128
122
|
return runtime.permissionManager.getToolPermission(toolName, agentName);
|
|
129
123
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
2
|
-
import {
|
|
2
|
+
import type { ToolPreviewFormatter } from "./tool-preview-formatter";
|
|
3
3
|
import type { PermissionCheckResult } from "./types";
|
|
4
4
|
|
|
5
5
|
// NOTE: formatDenyReason, formatUserDeniedReason, and
|
|
@@ -31,6 +31,7 @@ export function formatAskPrompt(
|
|
|
31
31
|
result: PermissionCheckResult,
|
|
32
32
|
agentName?: string,
|
|
33
33
|
input?: unknown,
|
|
34
|
+
formatter?: ToolPreviewFormatter,
|
|
34
35
|
): string {
|
|
35
36
|
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
36
37
|
|
|
@@ -51,7 +52,9 @@ export function formatAskPrompt(
|
|
|
51
52
|
const patternInfo = result.matchedPattern
|
|
52
53
|
? ` (matched '${result.matchedPattern}')`
|
|
53
54
|
: "";
|
|
54
|
-
const inputPreview =
|
|
55
|
+
const inputPreview = formatter
|
|
56
|
+
? formatter.formatToolInputForPrompt(result.toolName, input)
|
|
57
|
+
: "";
|
|
55
58
|
const inputSuffix = inputPreview ? ` ${inputPreview}` : "";
|
|
56
59
|
return `${subject} requested tool '${result.toolName}'${patternInfo}${inputSuffix}. Allow this call?`;
|
|
57
60
|
}
|
package/src/service.ts
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
* reference — this ensures resilience across `/reload` and load-order edge cases.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { SubagentSessionInfo } from "./subagent-registry";
|
|
15
14
|
import type { PermissionCheckResult, PermissionState } from "./types";
|
|
16
15
|
|
|
17
|
-
export type { PermissionCheckResult, PermissionState
|
|
16
|
+
export type { PermissionCheckResult, PermissionState };
|
|
18
17
|
|
|
19
18
|
/** Process-global key for the service slot. */
|
|
20
19
|
const SERVICE_KEY = Symbol.for("@gotgenes/pi-permission-system:service");
|
|
@@ -44,27 +43,6 @@ export interface PermissionsService {
|
|
|
44
43
|
agentName?: string,
|
|
45
44
|
): PermissionCheckResult;
|
|
46
45
|
|
|
47
|
-
/**
|
|
48
|
-
* Register an in-process subagent session.
|
|
49
|
-
*
|
|
50
|
-
* Call this before `bindExtensions()` so that `isSubagentExecutionContext()`
|
|
51
|
-
* and permission-forwarding target resolution can detect the child session.
|
|
52
|
-
* Always pair with `unregisterSubagentSession()` in a `finally` block.
|
|
53
|
-
*
|
|
54
|
-
* @param sessionKey - Unique session identifier (use the session directory path).
|
|
55
|
-
* @param info - Agent name and optional parent session ID.
|
|
56
|
-
*/
|
|
57
|
-
registerSubagentSession(sessionKey: string, info: SubagentSessionInfo): void;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Remove a previously registered in-process subagent session.
|
|
61
|
-
*
|
|
62
|
-
* Safe to call even if `registerSubagentSession` was never called for this key.
|
|
63
|
-
*
|
|
64
|
-
* @param sessionKey - The same key passed to `registerSubagentSession`.
|
|
65
|
-
*/
|
|
66
|
-
unregisterSubagentSession(sessionKey: string): void;
|
|
67
|
-
|
|
68
46
|
/**
|
|
69
47
|
* Query the tool-level permission state for pre-filtering tools before
|
|
70
48
|
* creating a child session.
|
package/src/subagent-registry.ts
CHANGED
|
@@ -23,9 +23,9 @@ export interface SubagentSessionInfo {
|
|
|
23
23
|
/**
|
|
24
24
|
* Registry of active in-process subagent sessions.
|
|
25
25
|
*
|
|
26
|
-
* Owned by `ExtensionRuntime`;
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
26
|
+
* Owned by `ExtensionRuntime`; written exclusively by `subscribeSubagentLifecycle`
|
|
27
|
+
* via the `subagents:child:session-created` / `subagents:child:disposed` event
|
|
28
|
+
* subscription (ADR 0002 — the core publishes, consumers observe).
|
|
29
29
|
*
|
|
30
30
|
* Concurrent background agents are safe because each session has a unique
|
|
31
31
|
* directory path as its key — no scalar global flag is needed.
|