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 +5 -0
- package/README.md +9 -2
- package/config.json +1 -1
- package/package.json +2 -1
- package/src/index.ts +18 -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,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
|
-
[](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 |
|
package/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-permission-system",
|
|
3
|
-
"version": "0.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
|
|
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
|
+
}
|
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
|
|