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 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
- [![Version](https://img.shields.io/badge/version-0.4.4-blue.svg)](package.json)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
3
+ [![npm version](https://img.shields.io/npm/v/pi-permission-system?style=flat-square)](https://www.npmjs.com/package/pi-permission-system) [![License](https://img.shields.io/github/license/MasuRii/pi-permission-system?style=flat-square)](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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "debugLog": false,
3
3
  "permissionReviewLog": true,
4
- "yoloMode": false
4
+ "yoloMode": true
5
5
  }
package/package.json CHANGED
@@ -1,65 +1,66 @@
1
- {
2
- "name": "pi-permission-system",
3
- "version": "0.4.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-coding-agent": "^0.70.2",
62
- "@mariozechner/pi-tui": "^0.70.2",
63
- "@sinclair/typebox": "^0.34.49"
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 formatJsonInputForPrompt(input: unknown): string {
394
- if (input === undefined || input === null) {
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
- const inline = serialized
414
- .replace(/\\r\\n|\\n|\\r|\\t/g, " ")
415
- .replace(/\s+/g, " ")
416
- .trim();
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 getPermissionLogContext(result: PermissionCheckResult): { command?: string; target?: string } {
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.getSystemPrompt()) || "unknown";
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
+ }
@@ -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 Promise.resolve(handler(event, createMockContext(harness.cwd, harness.prompts, options)));
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