pi-permission-system 0.4.4 → 0.4.5

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,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.5] - 2026-04-27
11
+
12
+ ### Fixed
13
+ - 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.
14
+
10
15
  ## [0.4.4] - 2026-04-25
11
16
 
12
17
  ### 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 |
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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-permission-system",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -58,6 +58,7 @@
58
58
  ]
59
59
  },
60
60
  "peerDependencies": {
61
+ "@mariozechner/pi-ai": "^0.70.2",
61
62
  "@mariozechner/pi-coding-agent": "^0.70.2",
62
63
  "@mariozechner/pi-tui": "^0.70.2",
63
64
  "@sinclair/typebox": "^0.34.49"
package/src/index.ts CHANGED
@@ -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");
@@ -237,6 +238,21 @@ function getActiveAgentNameFromSystemPrompt(systemPrompt: string | undefined): s
237
238
  return normalizeAgentName(match[1]);
238
239
  }
239
240
 
241
+ function getContextSystemPrompt(ctx: ExtensionContext): string | undefined {
242
+ const getSystemPrompt = toRecord(ctx).getSystemPrompt;
243
+ if (typeof getSystemPrompt !== "function") {
244
+ return undefined;
245
+ }
246
+
247
+ try {
248
+ const systemPrompt = getSystemPrompt.call(ctx);
249
+ return typeof systemPrompt === "string" ? systemPrompt : undefined;
250
+ } catch (error) {
251
+ logPermissionForwardingWarning("Failed to read context system prompt for forwarded permission metadata", error);
252
+ return undefined;
253
+ }
254
+ }
255
+
240
256
  function formatMissingToolNameReason(): string {
241
257
  return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
242
258
  }
@@ -784,7 +800,7 @@ async function waitForForwardedPermissionApproval(
784
800
  }
785
801
 
786
802
  const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
787
- const requesterAgentName = getActiveAgentName(ctx) || getActiveAgentNameFromSystemPrompt(ctx.getSystemPrompt()) || "unknown";
803
+ const requesterAgentName = getActiveAgentName(ctx) || getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx)) || "unknown";
788
804
  const request: ForwardedPermissionRequest = {
789
805
  id: requestId,
790
806
  createdAt: Date.now(),
@@ -1060,6 +1076,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1060
1076
 
1061
1077
  setLoggingWarningReporter(notifyWarning);
1062
1078
  refreshExtensionConfig();
1079
+ registerModelOptionCompatibilityGuard(pi);
1063
1080
 
1064
1081
  registerPermissionSystemCommand(pi, {
1065
1082
  getConfig: () => extensionConfig,
@@ -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