pi-permission-system 0.4.4 → 0.4.6
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 +14 -0
- package/README.md +11 -3
- package/config.json +1 -1
- package/package.json +66 -65
- package/src/index.ts +55 -24
- package/src/logging.ts +4 -1
- package/src/model-option-compatibility.ts +165 -0
- package/src/types-shims.d.ts +20 -0
- package/tests/permission-system.test.ts +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.6] - 2026-04-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added bounded, sanitized tool input previews to permission review logs for non-bash/non-MCP tool calls, inspired by PR #10 from @DevkumarPatel.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Reused the extension's safe JSON serialization path for generic tool approval previews so circular values and BigInts are summarized without raw full-input logging.
|
|
17
|
+
- Updated `@mariozechner/pi-ai`, `@mariozechner/pi-coding-agent`, and `@mariozechner/pi-tui` peer dependencies to `^0.70.5`.
|
|
18
|
+
|
|
19
|
+
## [0.4.5] - 2026-04-27
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Added a model option compatibility guard for OpenAI Responses/Codex streams so unsupported `temperature` values are removed from stream options and outgoing payloads before provider calls.
|
|
23
|
+
|
|
10
24
|
## [0.4.4] - 2026-04-25
|
|
11
25
|
|
|
12
26
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# 🔐 pi-permission-system
|
|
2
2
|
|
|
3
|
-
[](LICENSE)
|
|
3
|
+
[](https://www.npmjs.com/package/pi-permission-system) [](LICENSE)
|
|
5
4
|
|
|
6
5
|
Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
|
|
7
6
|
|
|
@@ -25,6 +24,14 @@ Permission enforcement extension for the Pi coding agent that provides centraliz
|
|
|
25
24
|
|
|
26
25
|
## Installation
|
|
27
26
|
|
|
27
|
+
### npm package
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install npm:pi-permission-system
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Local extension folder
|
|
34
|
+
|
|
28
35
|
Place this folder in one of the following locations:
|
|
29
36
|
|
|
30
37
|
| Scope | Path |
|
|
@@ -86,6 +93,7 @@ The extension integrates via Pi's lifecycle hooks:
|
|
|
86
93
|
- Extension-provided tools like `task`, `mcp`, and third-party tools are handled by exact registered name instead of private built-in hardcodes
|
|
87
94
|
- When a subagent hits an `ask` permission without direct UI access, the request can be forwarded to the main interactive session for confirmation
|
|
88
95
|
- Generic extension-tool approval prompts include a bounded input preview; built-in file tools use concise human-readable summaries instead of raw multiline JSON
|
|
96
|
+
- Permission review logs include bounded `toolInputPreview` values for non-bash/non-MCP tool calls so approvals can be audited without writing raw full payloads
|
|
89
97
|
- Path-bearing file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) evaluate `special.external_directory` before their normal tool permission when an explicit path points outside `ctx.cwd`
|
|
90
98
|
|
|
91
99
|
## Configuration
|
|
@@ -425,7 +433,7 @@ Default global logs directory: ~/.pi/agent/extensions/pi-permission-system/logs/
|
|
|
425
433
|
Actual global logs directory: $PI_CODING_AGENT_DIR/extensions/pi-permission-system/logs when PI_CODING_AGENT_DIR is set
|
|
426
434
|
```
|
|
427
435
|
|
|
428
|
-
- `pi-permission-system-permission-review.jsonl` — enabled by default for permission review/audit history
|
|
436
|
+
- `pi-permission-system-permission-review.jsonl` — enabled by default for permission review/audit history, including bounded `toolInputPreview` values for non-bash/non-MCP tool calls
|
|
429
437
|
- `pi-permission-system-debug.jsonl` — disabled by default and intended for troubleshooting
|
|
430
438
|
|
|
431
439
|
### Architecture
|
package/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,65 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-permission-system",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.ts"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"index.ts",
|
|
12
|
-
"src",
|
|
13
|
-
"tests",
|
|
14
|
-
"config.json",
|
|
15
|
-
"config/config.example.json",
|
|
16
|
-
"schemas/permissions.schema.json",
|
|
17
|
-
"README.md",
|
|
18
|
-
"CHANGELOG.md",
|
|
19
|
-
"LICENSE"
|
|
20
|
-
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
23
|
-
"lint": "npm run build",
|
|
24
|
-
"test": "bun ./tests/permission-system.test.ts && bun ./tests/config-modal.test.ts",
|
|
25
|
-
"check": "npm run lint && npm run test"
|
|
26
|
-
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
"pi-package",
|
|
29
|
-
"pi",
|
|
30
|
-
"pi-extension",
|
|
31
|
-
"pi-coding-agent",
|
|
32
|
-
"coding-agent",
|
|
33
|
-
"permissions",
|
|
34
|
-
"policy",
|
|
35
|
-
"access-control",
|
|
36
|
-
"authorization",
|
|
37
|
-
"security"
|
|
38
|
-
],
|
|
39
|
-
"author": "MasuRii",
|
|
40
|
-
"license": "MIT",
|
|
41
|
-
"repository": {
|
|
42
|
-
"type": "git",
|
|
43
|
-
"url": "git+https://github.com/MasuRii/pi-permission-system.git"
|
|
44
|
-
},
|
|
45
|
-
"homepage": "https://github.com/MasuRii/pi-permission-system#readme",
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/MasuRii/pi-permission-system/issues"
|
|
48
|
-
},
|
|
49
|
-
"engines": {
|
|
50
|
-
"node": ">=20"
|
|
51
|
-
},
|
|
52
|
-
"publishConfig": {
|
|
53
|
-
"access": "public"
|
|
54
|
-
},
|
|
55
|
-
"pi": {
|
|
56
|
-
"extensions": [
|
|
57
|
-
"./index.ts"
|
|
58
|
-
]
|
|
59
|
-
},
|
|
60
|
-
"peerDependencies": {
|
|
61
|
-
"@mariozechner/pi-
|
|
62
|
-
"@mariozechner/pi-
|
|
63
|
-
"@
|
|
64
|
-
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-permission-system",
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src",
|
|
13
|
+
"tests",
|
|
14
|
+
"config.json",
|
|
15
|
+
"config/config.example.json",
|
|
16
|
+
"schemas/permissions.schema.json",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
23
|
+
"lint": "npm run build",
|
|
24
|
+
"test": "bun ./tests/permission-system.test.ts && bun ./tests/config-modal.test.ts",
|
|
25
|
+
"check": "npm run lint && npm run test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"pi-package",
|
|
29
|
+
"pi",
|
|
30
|
+
"pi-extension",
|
|
31
|
+
"pi-coding-agent",
|
|
32
|
+
"coding-agent",
|
|
33
|
+
"permissions",
|
|
34
|
+
"policy",
|
|
35
|
+
"access-control",
|
|
36
|
+
"authorization",
|
|
37
|
+
"security"
|
|
38
|
+
],
|
|
39
|
+
"author": "MasuRii",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/MasuRii/pi-permission-system.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/MasuRii/pi-permission-system#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/MasuRii/pi-permission-system/issues"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"pi": {
|
|
56
|
+
"extensions": [
|
|
57
|
+
"./index.ts"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@mariozechner/pi-ai": "^0.70.5",
|
|
62
|
+
"@mariozechner/pi-coding-agent": "^0.70.5",
|
|
63
|
+
"@mariozechner/pi-tui": "^0.70.5",
|
|
64
|
+
"@sinclair/typebox": "^0.34.49"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
savePermissionSystemConfig,
|
|
23
23
|
type PermissionSystemExtensionConfig,
|
|
24
24
|
} from "./extension-config.js";
|
|
25
|
-
import { createPermissionSystemLogger } from "./logging.js";
|
|
25
|
+
import { createPermissionSystemLogger, safeJsonStringify } from "./logging.js";
|
|
26
26
|
import { registerPermissionSystemCommand } from "./config-modal.js";
|
|
27
27
|
import {
|
|
28
28
|
createPermissionForwardingLocation,
|
|
@@ -46,6 +46,7 @@ import { checkRequestedToolRegistration, getToolNameFromValue } from "./tool-reg
|
|
|
46
46
|
import type { PermissionCheckResult } from "./types.js";
|
|
47
47
|
import { PERMISSION_SYSTEM_STATUS_KEY, syncPermissionSystemStatus } from "./status.js";
|
|
48
48
|
import { canResolveAskPermissionRequest, shouldAutoApprovePermissionState } from "./yolo-mode.js";
|
|
49
|
+
import { registerModelOptionCompatibilityGuard } from "./model-option-compatibility.js";
|
|
49
50
|
|
|
50
51
|
const PI_AGENT_DIR = getAgentDir();
|
|
51
52
|
const SESSIONS_DIR = join(PI_AGENT_DIR, "sessions");
|
|
@@ -68,6 +69,7 @@ type PermissionRequestEvent = {
|
|
|
68
69
|
path?: string;
|
|
69
70
|
command?: string;
|
|
70
71
|
target?: string;
|
|
72
|
+
toolInputPreview?: string;
|
|
71
73
|
agentName?: string | null;
|
|
72
74
|
};
|
|
73
75
|
|
|
@@ -237,6 +239,21 @@ function getActiveAgentNameFromSystemPrompt(systemPrompt: string | undefined): s
|
|
|
237
239
|
return normalizeAgentName(match[1]);
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
function getContextSystemPrompt(ctx: ExtensionContext): string | undefined {
|
|
243
|
+
const getSystemPrompt = toRecord(ctx).getSystemPrompt;
|
|
244
|
+
if (typeof getSystemPrompt !== "function") {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const systemPrompt = getSystemPrompt.call(ctx);
|
|
250
|
+
return typeof systemPrompt === "string" ? systemPrompt : undefined;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logPermissionForwardingWarning("Failed to read context system prompt for forwarded permission metadata", error);
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
240
257
|
function formatMissingToolNameReason(): string {
|
|
241
258
|
return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
|
|
242
259
|
}
|
|
@@ -297,6 +314,7 @@ function formatUserDeniedReason(result: PermissionCheckResult, denialReason?: st
|
|
|
297
314
|
}
|
|
298
315
|
|
|
299
316
|
const TOOL_INPUT_PREVIEW_MAX_LENGTH = 200;
|
|
317
|
+
const TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH = 1000;
|
|
300
318
|
const TOOL_TEXT_SUMMARY_MAX_LENGTH = 80;
|
|
301
319
|
|
|
302
320
|
function truncateInlineText(value: string, maxLength: number): string {
|
|
@@ -390,30 +408,17 @@ function formatSearchInputForPrompt(toolName: string, input: Record<string, unkn
|
|
|
390
408
|
return parts.length > 0 ? `for ${parts.join(", ")}` : "";
|
|
391
409
|
}
|
|
392
410
|
|
|
393
|
-
function
|
|
394
|
-
|
|
395
|
-
return "";
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (typeof input === "object" && !Array.isArray(input) && Object.keys(input as Record<string, unknown>).length === 0) {
|
|
399
|
-
return "";
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
let serialized: string;
|
|
403
|
-
try {
|
|
404
|
-
serialized = JSON.stringify(input);
|
|
405
|
-
} catch {
|
|
406
|
-
return "";
|
|
407
|
-
}
|
|
408
|
-
|
|
411
|
+
function serializeToolInputPreview(input: unknown): string {
|
|
412
|
+
const serialized = safeJsonStringify(input);
|
|
409
413
|
if (!serialized || serialized === "{}" || serialized === "null") {
|
|
410
414
|
return "";
|
|
411
415
|
}
|
|
412
416
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
+
return serialized.replace(/\s+/g, " ").trim();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function formatJsonInputForPrompt(input: unknown): string {
|
|
421
|
+
const inline = serializeToolInputPreview(input);
|
|
417
422
|
return inline ? `with input ${truncateInlineText(inline, TOOL_INPUT_PREVIEW_MAX_LENGTH)}` : "";
|
|
418
423
|
}
|
|
419
424
|
|
|
@@ -503,10 +508,29 @@ function formatExternalDirectoryUserDeniedReason(
|
|
|
503
508
|
return `User denied external directory access for tool '${toolName}' path '${pathValue}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
|
|
504
509
|
}
|
|
505
510
|
|
|
506
|
-
function
|
|
511
|
+
function formatGenericToolInputForLog(input: unknown): string | undefined {
|
|
512
|
+
const inline = serializeToolInputPreview(input);
|
|
513
|
+
return inline ? `input ${truncateInlineText(inline, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)}` : undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function getToolInputPreviewForLog(result: PermissionCheckResult, input: unknown): string | undefined {
|
|
517
|
+
if (result.toolName === "bash" || result.toolName === "mcp" || result.source === "mcp") {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (PATH_BEARING_TOOLS.has(result.toolName)) {
|
|
522
|
+
const inputPreview = formatToolInputForPrompt(result.toolName, input);
|
|
523
|
+
return inputPreview ? truncateInlineText(inputPreview, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH) : undefined;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return formatGenericToolInputForLog(input);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function getPermissionLogContext(result: PermissionCheckResult, input: unknown): { command?: string; target?: string; toolInputPreview?: string } {
|
|
507
530
|
return {
|
|
508
531
|
command: result.command,
|
|
509
532
|
target: result.target,
|
|
533
|
+
toolInputPreview: getToolInputPreviewForLog(result, input),
|
|
510
534
|
};
|
|
511
535
|
}
|
|
512
536
|
|
|
@@ -784,7 +808,7 @@ async function waitForForwardedPermissionApproval(
|
|
|
784
808
|
}
|
|
785
809
|
|
|
786
810
|
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
787
|
-
const requesterAgentName = getActiveAgentName(ctx) || getActiveAgentNameFromSystemPrompt(ctx
|
|
811
|
+
const requesterAgentName = getActiveAgentName(ctx) || getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx)) || "unknown";
|
|
788
812
|
const request: ForwardedPermissionRequest = {
|
|
789
813
|
id: requestId,
|
|
790
814
|
createdAt: Date.now(),
|
|
@@ -1060,6 +1084,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1060
1084
|
|
|
1061
1085
|
setLoggingWarningReporter(notifyWarning);
|
|
1062
1086
|
refreshExtensionConfig();
|
|
1087
|
+
registerModelOptionCompatibilityGuard(pi);
|
|
1063
1088
|
|
|
1064
1089
|
registerPermissionSystemCommand(pi, {
|
|
1065
1090
|
getConfig: () => extensionConfig,
|
|
@@ -1097,6 +1122,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1097
1122
|
path?: string;
|
|
1098
1123
|
command?: string;
|
|
1099
1124
|
target?: string;
|
|
1125
|
+
toolInputPreview?: string;
|
|
1100
1126
|
resolution?: string;
|
|
1101
1127
|
denialReason?: string;
|
|
1102
1128
|
},
|
|
@@ -1112,6 +1138,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1112
1138
|
path: details.path ?? null,
|
|
1113
1139
|
command: details.command ?? null,
|
|
1114
1140
|
target: details.target ?? null,
|
|
1141
|
+
toolInputPreview: details.toolInputPreview ?? null,
|
|
1115
1142
|
resolution: details.resolution ?? null,
|
|
1116
1143
|
denialReason: details.denialReason ?? null,
|
|
1117
1144
|
});
|
|
@@ -1130,6 +1157,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1130
1157
|
path?: string;
|
|
1131
1158
|
command?: string;
|
|
1132
1159
|
target?: string;
|
|
1160
|
+
toolInputPreview?: string;
|
|
1133
1161
|
},
|
|
1134
1162
|
): Promise<PermissionPromptDecision> => {
|
|
1135
1163
|
if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
|
|
@@ -1145,6 +1173,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1145
1173
|
path: details.path,
|
|
1146
1174
|
command: details.command,
|
|
1147
1175
|
target: details.target,
|
|
1176
|
+
toolInputPreview: details.toolInputPreview,
|
|
1148
1177
|
agentName: details.agentName,
|
|
1149
1178
|
});
|
|
1150
1179
|
return { approved: true, state: "approved" };
|
|
@@ -1162,6 +1191,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1162
1191
|
path: details.path,
|
|
1163
1192
|
command: details.command,
|
|
1164
1193
|
target: details.target,
|
|
1194
|
+
toolInputPreview: details.toolInputPreview,
|
|
1165
1195
|
agentName: details.agentName,
|
|
1166
1196
|
});
|
|
1167
1197
|
|
|
@@ -1182,6 +1212,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1182
1212
|
path: details.path,
|
|
1183
1213
|
command: details.command,
|
|
1184
1214
|
target: details.target,
|
|
1215
|
+
toolInputPreview: details.toolInputPreview,
|
|
1185
1216
|
agentName: details.agentName,
|
|
1186
1217
|
});
|
|
1187
1218
|
|
|
@@ -1546,7 +1577,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1546
1577
|
}
|
|
1547
1578
|
|
|
1548
1579
|
const check = permissionManager.checkPermission(toolName, input, agentName ?? undefined);
|
|
1549
|
-
const permissionLogContext = getPermissionLogContext(check);
|
|
1580
|
+
const permissionLogContext = getPermissionLogContext(check, input);
|
|
1550
1581
|
|
|
1551
1582
|
if (check.state === "deny") {
|
|
1552
1583
|
writeReviewLog("permission_request.blocked", {
|
package/src/logging.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type PermissionSystemExtensionConfig,
|
|
10
10
|
} from "./extension-config.js";
|
|
11
11
|
|
|
12
|
-
function safeJsonStringify(value: unknown): string {
|
|
12
|
+
export function safeJsonStringify(value: unknown): string | undefined {
|
|
13
13
|
const seen = new WeakSet<object>();
|
|
14
14
|
return JSON.stringify(value, (_key, currentValue) => {
|
|
15
15
|
if (currentValue instanceof Error) {
|
|
@@ -66,6 +66,9 @@ export function createPermissionSystemLogger(options: PermissionSystemLoggerOpti
|
|
|
66
66
|
event,
|
|
67
67
|
...details,
|
|
68
68
|
});
|
|
69
|
+
if (!line) {
|
|
70
|
+
return `Failed to write permission-system ${stream} log '${path}': event could not be serialized.`;
|
|
71
|
+
}
|
|
69
72
|
appendFileSync(path, `${line}\n`, "utf-8");
|
|
70
73
|
return undefined;
|
|
71
74
|
} catch (error) {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getApiProvider,
|
|
3
|
+
type Api,
|
|
4
|
+
type AssistantMessageEventStream,
|
|
5
|
+
type Context as LlmContext,
|
|
6
|
+
type Model,
|
|
7
|
+
type SimpleStreamOptions,
|
|
8
|
+
} from "@mariozechner/pi-ai";
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
const GUARDED_TEMPERATURE_APIS = [
|
|
12
|
+
"openai-codex-responses",
|
|
13
|
+
"openai-responses",
|
|
14
|
+
"azure-openai-responses",
|
|
15
|
+
] as const satisfies readonly Api[];
|
|
16
|
+
const OPENAI_RESPONSES_APIS = new Set<Api>([
|
|
17
|
+
"openai-responses",
|
|
18
|
+
"azure-openai-responses",
|
|
19
|
+
]);
|
|
20
|
+
const TEMPERATURE_UNSUPPORTED_APIS = new Set<Api>([
|
|
21
|
+
"openai-codex-responses",
|
|
22
|
+
]);
|
|
23
|
+
const TEMPERATURE_UNSUPPORTED_PROVIDERS = new Set<string>([
|
|
24
|
+
"openai-codex",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export type ApiStreamSimpleDelegate = (
|
|
28
|
+
model: Model<Api>,
|
|
29
|
+
context: LlmContext,
|
|
30
|
+
options?: SimpleStreamOptions,
|
|
31
|
+
) => AssistantMessageEventStream;
|
|
32
|
+
|
|
33
|
+
type GlobalWithPermissionSystemProviderGuard = typeof globalThis & {
|
|
34
|
+
__piPermissionSystemModelOptionBaseStreams?: Map<string, ApiStreamSimpleDelegate>;
|
|
35
|
+
__piPermissionSystemModelOptionGuardedApis?: Set<string>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function getBaseApiStreams(): Map<string, ApiStreamSimpleDelegate> {
|
|
39
|
+
const globalScope = globalThis as GlobalWithPermissionSystemProviderGuard;
|
|
40
|
+
if (!globalScope.__piPermissionSystemModelOptionBaseStreams) {
|
|
41
|
+
globalScope.__piPermissionSystemModelOptionBaseStreams = new Map<string, ApiStreamSimpleDelegate>();
|
|
42
|
+
}
|
|
43
|
+
return globalScope.__piPermissionSystemModelOptionBaseStreams;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getGuardedApis(): Set<string> {
|
|
47
|
+
const globalScope = globalThis as GlobalWithPermissionSystemProviderGuard;
|
|
48
|
+
if (!globalScope.__piPermissionSystemModelOptionGuardedApis) {
|
|
49
|
+
globalScope.__piPermissionSystemModelOptionGuardedApis = new Set<string>();
|
|
50
|
+
}
|
|
51
|
+
return globalScope.__piPermissionSystemModelOptionGuardedApis;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeIdentifier(value: string | undefined): string {
|
|
55
|
+
return (value ?? "").trim().toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasModelToken(modelId: string, token: string): boolean {
|
|
59
|
+
return normalizeIdentifier(modelId).split(/[^a-z0-9]+/).includes(token);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getUnsupportedTemperatureReason(
|
|
63
|
+
model: Pick<Model<Api>, "api" | "id" | "provider" | "reasoning">,
|
|
64
|
+
): string | undefined {
|
|
65
|
+
if (TEMPERATURE_UNSUPPORTED_APIS.has(model.api)) {
|
|
66
|
+
return `api '${model.api}' does not support temperature`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const provider = normalizeIdentifier(model.provider);
|
|
70
|
+
if (TEMPERATURE_UNSUPPORTED_PROVIDERS.has(provider)) {
|
|
71
|
+
return `provider '${model.provider}' does not support temperature`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (OPENAI_RESPONSES_APIS.has(model.api) && hasModelToken(model.id, "codex")) {
|
|
75
|
+
return `model '${model.id}' does not support temperature`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (OPENAI_RESPONSES_APIS.has(model.api) && model.reasoning) {
|
|
79
|
+
return `reasoning model '${model.id}' accepts only the provider default temperature`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function stripUnsupportedTemperatureFromPayload(payload: unknown): unknown {
|
|
90
|
+
if (!isRecord(payload) || !("temperature" in payload)) {
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { temperature: _temperature, ...rest } = payload;
|
|
95
|
+
return rest;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function composeTemperatureSanitizer(
|
|
99
|
+
options: SimpleStreamOptions | undefined,
|
|
100
|
+
model: Model<Api>,
|
|
101
|
+
): SimpleStreamOptions | undefined {
|
|
102
|
+
const reason = getUnsupportedTemperatureReason(model);
|
|
103
|
+
if (!reason && options?.temperature === undefined) {
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!reason) {
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const existingOnPayload = options?.onPayload;
|
|
112
|
+
const nextOptions: SimpleStreamOptions = options
|
|
113
|
+
? { ...options, temperature: undefined }
|
|
114
|
+
: {};
|
|
115
|
+
|
|
116
|
+
nextOptions.onPayload = async (payload, payloadModel) => {
|
|
117
|
+
const transformedPayload = existingOnPayload
|
|
118
|
+
? await existingOnPayload(payload, payloadModel)
|
|
119
|
+
: undefined;
|
|
120
|
+
return stripUnsupportedTemperatureFromPayload(transformedPayload ?? payload);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return nextOptions;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ensureModelOptionGuardForApi(pi: ExtensionAPI, api: Api): boolean {
|
|
127
|
+
const guardedApis = getGuardedApis();
|
|
128
|
+
if (guardedApis.has(api)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const baseStreams = getBaseApiStreams();
|
|
133
|
+
let baseStream = baseStreams.get(api);
|
|
134
|
+
if (!baseStream) {
|
|
135
|
+
const currentProvider = getApiProvider(api);
|
|
136
|
+
if (!currentProvider) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
baseStream = currentProvider.streamSimple as ApiStreamSimpleDelegate;
|
|
140
|
+
baseStreams.set(api, baseStream);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const providerName = `pi-permission-system-model-option-compatibility-${api.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`;
|
|
144
|
+
pi.registerProvider(providerName, {
|
|
145
|
+
api,
|
|
146
|
+
streamSimple: (model, context, options) => {
|
|
147
|
+
const typedModel = model as Model<Api>;
|
|
148
|
+
const delegate = baseStreams.get(typedModel.api);
|
|
149
|
+
if (!delegate) {
|
|
150
|
+
throw new Error(`No base stream provider available for api '${typedModel.api}'.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return delegate(typedModel, context, composeTemperatureSanitizer(options, typedModel));
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
guardedApis.add(api);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function registerModelOptionCompatibilityGuard(pi: ExtensionAPI): void {
|
|
162
|
+
for (const api of GUARDED_TEMPERATURE_APIS) {
|
|
163
|
+
ensureModelOptionGuardForApi(pi, api);
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/types-shims.d.ts
CHANGED
|
@@ -106,6 +106,7 @@ declare module "@mariozechner/pi-coding-agent" {
|
|
|
106
106
|
on(event: string, handler: (...args: any[]) => any): void;
|
|
107
107
|
getAllTools(): any[];
|
|
108
108
|
setActiveTools(toolNames: string[]): void;
|
|
109
|
+
registerProvider?(...args: any[]): void;
|
|
109
110
|
registerCommand(
|
|
110
111
|
name: string,
|
|
111
112
|
definition: {
|
|
@@ -124,6 +125,25 @@ declare module "@mariozechner/pi-coding-agent" {
|
|
|
124
125
|
export function isToolCallEventType(toolName: string, event: unknown): boolean;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
declare module "@mariozechner/pi-ai" {
|
|
129
|
+
export type Api = string;
|
|
130
|
+
export type AssistantMessageEventStream = any;
|
|
131
|
+
export type Context = any;
|
|
132
|
+
export type SimpleStreamOptions = {
|
|
133
|
+
temperature?: number;
|
|
134
|
+
onPayload?: (payload: unknown, model: Model<Api>) => unknown | Promise<unknown | undefined> | undefined;
|
|
135
|
+
[key: string]: any;
|
|
136
|
+
};
|
|
137
|
+
export interface Model<TApi extends Api> {
|
|
138
|
+
id: string;
|
|
139
|
+
api: TApi;
|
|
140
|
+
provider: string;
|
|
141
|
+
reasoning: boolean;
|
|
142
|
+
[key: string]: any;
|
|
143
|
+
}
|
|
144
|
+
export function getApiProvider(api: Api): { streamSimple: (...args: any[]) => AssistantMessageEventStream } | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
127
147
|
declare module "@mariozechner/pi-tui" {
|
|
128
148
|
export interface SettingItem {
|
|
129
149
|
id: string;
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
createPermissionForwardingLocation,
|
|
16
16
|
isForwardedPermissionRequestForSession,
|
|
17
17
|
resolvePermissionForwardingTargetSessionId,
|
|
18
|
+
SUBAGENT_ENV_HINT_KEYS,
|
|
19
|
+
SUBAGENT_PARENT_SESSION_ENV_KEY,
|
|
18
20
|
} from "../src/permission-forwarding.js";
|
|
19
21
|
import piPermissionSystemExtension from "../src/index.js";
|
|
20
22
|
import { PermissionManager } from "../src/permission-manager.js";
|
|
@@ -85,6 +87,31 @@ type ExtensionHarnessOptions = {
|
|
|
85
87
|
inputResponse?: string;
|
|
86
88
|
};
|
|
87
89
|
|
|
90
|
+
const INHERITED_SUBAGENT_ENV_KEYS = [
|
|
91
|
+
...SUBAGENT_ENV_HINT_KEYS,
|
|
92
|
+
SUBAGENT_PARENT_SESSION_ENV_KEY,
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
95
|
+
async function withIsolatedSubagentEnv<T>(operation: () => Promise<T>): Promise<T> {
|
|
96
|
+
const originalValues = new Map<string, string | undefined>();
|
|
97
|
+
for (const key of INHERITED_SUBAGENT_ENV_KEYS) {
|
|
98
|
+
originalValues.set(key, process.env[key]);
|
|
99
|
+
delete process.env[key];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return await operation();
|
|
104
|
+
} finally {
|
|
105
|
+
for (const [key, value] of originalValues.entries()) {
|
|
106
|
+
if (value === undefined) {
|
|
107
|
+
delete process.env[key];
|
|
108
|
+
} else {
|
|
109
|
+
process.env[key] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
function createToolCallHarness(
|
|
89
116
|
config: GlobalPermissionConfig,
|
|
90
117
|
toolNames: readonly string[],
|
|
@@ -111,6 +138,7 @@ function createToolCallHarness(
|
|
|
111
138
|
registerCommand: (): void => {},
|
|
112
139
|
getAllTools: (): Array<{ name: string }> => toolNames.map((name) => ({ name })),
|
|
113
140
|
setActiveTools: (): void => {},
|
|
141
|
+
registerProvider: (): void => {},
|
|
114
142
|
events: {
|
|
115
143
|
emit: (): void => {},
|
|
116
144
|
},
|
|
@@ -175,7 +203,9 @@ async function runToolCall(
|
|
|
175
203
|
const handler = harness.handlers.tool_call;
|
|
176
204
|
assert.equal(typeof handler, "function");
|
|
177
205
|
|
|
178
|
-
const result = await
|
|
206
|
+
const result = await withIsolatedSubagentEnv(async () => Promise.resolve(
|
|
207
|
+
handler(event, createMockContext(harness.cwd, harness.prompts, options)),
|
|
208
|
+
));
|
|
179
209
|
return (result ?? {}) as Record<string, unknown>;
|
|
180
210
|
}
|
|
181
211
|
|