@diegopetrucci/pi-extensions 0.1.80 → 0.1.82
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/README.md +6 -1
- package/extensions/agent-workflow-audit/index.ts +47 -25
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/brrr/index.ts +21 -15
- package/extensions/brrr/package.json +1 -1
- package/extensions/code-reviewer/index.ts +52 -19
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/contrarian/index.ts +17 -17
- package/extensions/contrarian/package.json +1 -1
- package/extensions/developer/.pi-fleet-tested-version +1 -0
- package/extensions/developer/README.md +70 -0
- package/extensions/developer/index.ts +906 -0
- package/extensions/developer/package.json +40 -0
- package/extensions/diff-summarizer/.pi-fleet-tested-version +1 -0
- package/extensions/diff-summarizer/README.md +75 -0
- package/extensions/diff-summarizer/index.ts +1585 -0
- package/extensions/diff-summarizer/package.json +40 -0
- package/extensions/dirty-repo-guard/index.ts +5 -2
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/fast/README.md +1 -5
- package/extensions/fast/index.ts +13 -76
- package/extensions/fast/package.json +1 -1
- package/extensions/inline-bash/index.ts +1 -0
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +10 -0
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +5 -1
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/index.ts +7 -2
- package/extensions/notify/package.json +1 -1
- package/extensions/oracle/index.ts +17 -17
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +6 -11
- package/extensions/permission-gate/index.ts +11 -54
- package/extensions/permission-gate/package.json +1 -3
- package/extensions/project-mcp-json/README.md +4 -3
- package/extensions/project-mcp-json/package.json +1 -1
- package/extensions/repo-scout/.pi-fleet-tested-version +1 -0
- package/extensions/repo-scout/README.md +74 -0
- package/extensions/repo-scout/index.ts +1575 -0
- package/extensions/repo-scout/package.json +40 -0
- package/extensions/test-runner/.pi-fleet-tested-version +1 -0
- package/extensions/test-runner/README.md +70 -0
- package/extensions/test-runner/index.ts +798 -0
- package/extensions/test-runner/package.json +40 -0
- package/extensions/triage-comments/index.ts +66 -16
- package/extensions/triage-comments/package.json +1 -1
- package/extensions/web-scout/.pi-fleet-tested-version +1 -0
- package/extensions/web-scout/README.md +67 -0
- package/extensions/web-scout/index.ts +1204 -0
- package/extensions/web-scout/package.json +41 -0
- package/package.json +7 -2
- package/extensions/permission-gate/powershell-safety.ts +0 -1053
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diegopetrucci/pi-diff-summarizer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A standalone pi extension that adds a read-only diff_summarizer subagent tool that summarizes a diff and highlights review risk in an isolated child agent.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"diff",
|
|
9
|
+
"summary",
|
|
10
|
+
"subagent"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/diegopetrucci/pi-extensions.git",
|
|
16
|
+
"directory": "extensions/diff-summarizer"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
".pi-fleet-tested-version"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"index.ts"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@earendil-works/pi-ai": "*",
|
|
33
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
34
|
+
"typebox": "*"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.19.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -12,9 +12,12 @@ async function checkDirtyRepo(
|
|
|
12
12
|
ctx: ExtensionContext,
|
|
13
13
|
action: string,
|
|
14
14
|
): Promise<{ cancel: boolean } | undefined> {
|
|
15
|
-
const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]
|
|
15
|
+
const { stdout, code, killed } = await pi.exec("git", ["status", "--porcelain"], {
|
|
16
|
+
timeout: 15_000,
|
|
17
|
+
signal: ctx.signal,
|
|
18
|
+
});
|
|
16
19
|
|
|
17
|
-
if (code !== 0) {
|
|
20
|
+
if (killed || code !== 0) {
|
|
18
21
|
return;
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -34,11 +34,7 @@ For eligible Anthropic Claude models it injects:
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
It also
|
|
38
|
-
|
|
39
|
-
```text
|
|
40
|
-
anthropic-beta: fast-mode-2026-02-01
|
|
41
|
-
```
|
|
37
|
+
It also appends the required beta `fast-mode-2026-02-01` to the request's `betas` list, which Pi sends as the `anthropic-beta` header. The extension does not set the `anthropic-beta` header itself, so Pi's own betas are kept.
|
|
42
38
|
|
|
43
39
|
## Eligibility
|
|
44
40
|
|
package/extensions/fast/index.ts
CHANGED
|
@@ -12,9 +12,6 @@ const ANTHROPIC_PROVIDER_ID = "anthropic";
|
|
|
12
12
|
const ANTHROPIC_API_ID = "anthropic-messages";
|
|
13
13
|
const ANTHROPIC_FAST_SPEED = "fast";
|
|
14
14
|
const ANTHROPIC_FAST_BETA = "fast-mode-2026-02-01";
|
|
15
|
-
const CLAUDE_CODE_OAUTH_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
|
|
16
|
-
const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
|
|
17
|
-
const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
|
|
18
15
|
const ANTHROPIC_SUPPORTED_MODELS = new Set(["claude-opus-4-8", "claude-opus-5", "claude-opus-5-5"]);
|
|
19
16
|
|
|
20
17
|
const OPENAI_PROVIDER_ID = "openai-codex";
|
|
@@ -79,11 +76,6 @@ type RecursivePartial<T> = {
|
|
|
79
76
|
|
|
80
77
|
type PayloadRecord = Record<string, unknown>;
|
|
81
78
|
|
|
82
|
-
type AnthropicHeaderCompat = {
|
|
83
|
-
forceAdaptiveThinking?: boolean;
|
|
84
|
-
supportsEagerToolInputStreaming?: boolean;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
79
|
type Eligibility = {
|
|
88
80
|
eligible: boolean;
|
|
89
81
|
modelKey: string;
|
|
@@ -368,67 +360,17 @@ function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
|
|
|
368
360
|
return `Fast mode is ${describeMode(state)}. Current model: ${eligibility.modelKey}.${injected}`;
|
|
369
361
|
}
|
|
370
362
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (typeof apiKey === "string") return apiKey.includes("sk-ant-oat");
|
|
383
|
-
} catch {
|
|
384
|
-
// Fall back to the synchronous registry snapshot below.
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function getAnthropicProviderBetas(ctx: ExtensionContext, hasActiveTools: boolean): string[] {
|
|
391
|
-
const model = ctx.model;
|
|
392
|
-
if (!model) return [];
|
|
393
|
-
const compat = model.compat as AnthropicHeaderCompat | undefined;
|
|
394
|
-
|
|
395
|
-
const betas: string[] = [];
|
|
396
|
-
if (hasActiveTools && compat?.supportsEagerToolInputStreaming === false) {
|
|
397
|
-
betas.push(FINE_GRAINED_TOOL_STREAMING_BETA);
|
|
398
|
-
}
|
|
399
|
-
if (compat?.forceAdaptiveThinking !== true) {
|
|
400
|
-
betas.push(INTERLEAVED_THINKING_BETA);
|
|
401
|
-
}
|
|
402
|
-
return betas;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function injectAnthropicFastHeader(
|
|
406
|
-
headers: Record<string, string | null>,
|
|
407
|
-
ctx: ExtensionContext,
|
|
408
|
-
state: SessionState,
|
|
409
|
-
hasActiveTools: boolean,
|
|
410
|
-
): Promise<void> {
|
|
411
|
-
if (state.ultrafast || !isFastEnabled(state)) return;
|
|
412
|
-
const eligibility = getEligibility(ctx, false);
|
|
413
|
-
if (!eligibility.eligible || eligibility.provider !== "anthropic") return;
|
|
414
|
-
|
|
415
|
-
const matchingKeys = Object.keys(headers).filter(
|
|
416
|
-
(name) => name.toLowerCase() === "anthropic-beta",
|
|
417
|
-
);
|
|
418
|
-
const existing = matchingKeys.flatMap((name) => splitBetaHeader(headers[name]));
|
|
419
|
-
const matchingModelEntries = Object.entries(ctx.model?.headers ?? {}).filter(
|
|
420
|
-
([name]) => name.toLowerCase() === "anthropic-beta",
|
|
421
|
-
);
|
|
422
|
-
const modelBetas = matchingModelEntries.flatMap(([, value]) => splitBetaHeader(value));
|
|
423
|
-
const providerBetas = getAnthropicProviderBetas(ctx, hasActiveTools);
|
|
424
|
-
const oauthBetas = (await isAnthropicOAuth(ctx)) ? CLAUDE_CODE_OAUTH_BETAS : [];
|
|
425
|
-
const next = Array.from(
|
|
426
|
-
new Set([...modelBetas, ...existing, ...oauthBetas, ...providerBetas, ANTHROPIC_FAST_BETA]),
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
for (const key of matchingKeys) delete headers[key];
|
|
430
|
-
const outputKey = matchingModelEntries.at(-1)?.[0] ?? "anthropic-beta";
|
|
431
|
-
headers[outputKey] = next.join(",");
|
|
363
|
+
/**
|
|
364
|
+
* Adds the Fast beta to the request's `betas` list instead of rewriting the
|
|
365
|
+
* `anthropic-beta` header. Pi derives the final beta list from the model and
|
|
366
|
+
* request (mid-conversation effort, server-side fallback, inline tools, OAuth,
|
|
367
|
+
* interleaved thinking, ...) and treats an explicit `anthropic-beta` header as
|
|
368
|
+
* the complete list, so setting that header here would drop any beta this
|
|
369
|
+
* extension does not know about.
|
|
370
|
+
*/
|
|
371
|
+
function withAnthropicFastBeta(betas: unknown[] | undefined): unknown[] {
|
|
372
|
+
const existing = betas ?? [];
|
|
373
|
+
return existing.includes(ANTHROPIC_FAST_BETA) ? existing : [...existing, ANTHROPIC_FAST_BETA];
|
|
432
374
|
}
|
|
433
375
|
|
|
434
376
|
function injectUltrafastPayload(
|
|
@@ -461,9 +403,10 @@ function injectFastPayload(
|
|
|
461
403
|
|
|
462
404
|
if (eligibility.provider === "anthropic") {
|
|
463
405
|
if ("speed" in payload) return undefined;
|
|
406
|
+
if (payload.betas !== undefined && !Array.isArray(payload.betas)) return undefined;
|
|
464
407
|
state.lastInjectedAt = Date.now();
|
|
465
408
|
state.lastInjectedModel = eligibility.modelKey;
|
|
466
|
-
return { ...payload, speed: ANTHROPIC_FAST_SPEED };
|
|
409
|
+
return { ...payload, speed: ANTHROPIC_FAST_SPEED, betas: withAnthropicFastBeta(payload.betas) };
|
|
467
410
|
}
|
|
468
411
|
|
|
469
412
|
if ("service_tier" in payload) return undefined;
|
|
@@ -552,12 +495,6 @@ export default function fastExtension(pi: ExtensionAPI) {
|
|
|
552
495
|
updateStatus(ctx, state);
|
|
553
496
|
});
|
|
554
497
|
|
|
555
|
-
pi.on("before_provider_headers", async (event, ctx) => {
|
|
556
|
-
const hasActiveTools =
|
|
557
|
-
typeof pi.getActiveTools === "function" && pi.getActiveTools().length > 0;
|
|
558
|
-
await injectAnthropicFastHeader(event.headers, ctx, getState(ctx), hasActiveTools);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
498
|
pi.on("before_provider_request", (event, ctx) => {
|
|
562
499
|
const state = getState(ctx);
|
|
563
500
|
if (state.ultrafast) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-fast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "A pi extension that enables Fast mode for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models based on the currently selected model.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -1057,6 +1057,9 @@ function getBlockedBashReason(
|
|
|
1057
1057
|
command: string,
|
|
1058
1058
|
options: { workspace: string; cacheRoot: string; cacheEnabled: boolean },
|
|
1059
1059
|
): string | undefined {
|
|
1060
|
+
if (/`/.test(command) || /\$\(/.test(command))
|
|
1061
|
+
return "Librarian bash blocks command substitution.";
|
|
1062
|
+
|
|
1060
1063
|
if (!options.cacheEnabled && command.includes(options.cacheRoot)) {
|
|
1061
1064
|
return "Local repo checkout cache is disabled for this Librarian call.";
|
|
1062
1065
|
}
|
|
@@ -1070,6 +1073,13 @@ function getBlockedBashReason(
|
|
|
1070
1073
|
if (destructiveLocal.test(scan))
|
|
1071
1074
|
return "Librarian bash blocks destructive/local side-effect commands.";
|
|
1072
1075
|
|
|
1076
|
+
if (
|
|
1077
|
+
/(^|[\n;&|()])\s*find(?=\s)[^\n;&|]*\s-(?:delete|exec|execdir|ok|okdir)(?=$|[\s;&|()])/.test(
|
|
1078
|
+
scan,
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
return "Librarian bash blocks find actions that delete files or execute commands.";
|
|
1082
|
+
|
|
1073
1083
|
const extraNetworkOrShell =
|
|
1074
1084
|
/(^|[\n;&|()])\s*(?:curl|wget|nc|netcat|ssh|scp|sftp|rsync|bash|sh|zsh|fish|python|python3|perl|ruby|node|deno)(?=$|[\s;&|()])/;
|
|
1075
1085
|
if (extraNetworkOrShell.test(scan))
|
|
@@ -202,7 +202,11 @@ function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
|
|
|
202
202
|
if (!existsSync(path)) return {};
|
|
203
203
|
|
|
204
204
|
try {
|
|
205
|
-
|
|
205
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
206
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
207
|
+
throw new Error("Expected a non-null, non-array object");
|
|
208
|
+
}
|
|
209
|
+
return parsed as RecursivePartial<MinimalFooterConfig>;
|
|
206
210
|
} catch (error) {
|
|
207
211
|
console.error(`Warning: Could not parse ${path}: ${error}`);
|
|
208
212
|
return {};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { execFile } from "node:child_process";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
18
19
|
import { join } from "node:path";
|
|
19
20
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -168,8 +169,12 @@ function notifyOSC777(title: string, body: string, wrap: boolean): void {
|
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
function notifyOSC99(title: string, body: string, wrap: boolean): void {
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
// Kitty (>=0.36) treats a reused identifier as an update to the existing notification,
|
|
173
|
+
// which can suppress popups, so each notification needs a globally unique id. (Missing d=1
|
|
174
|
+
// was not the cause; a per-process counter would collide across restarts/sessions.)
|
|
175
|
+
const id = randomUUID();
|
|
176
|
+
writeTerminalSequence(`\x1b]99;i=${id}:d=0;${title}\x1b\\`, wrap);
|
|
177
|
+
writeTerminalSequence(`\x1b]99;i=${id}:d=1:p=body;${body}\x1b\\`, wrap);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
function ringBell(): void {
|
|
@@ -133,7 +133,7 @@ const AZURE_MODEL_PREFERENCES = [
|
|
|
133
133
|
"gpt-5.5",
|
|
134
134
|
"gpt-5.4-pro",
|
|
135
135
|
"gpt-5.4 ",
|
|
136
|
-
"gpt-5.3-codex",
|
|
136
|
+
"gpt-5.3-codex ",
|
|
137
137
|
"gpt-5-pro",
|
|
138
138
|
"gpt-5-chat-latest",
|
|
139
139
|
"gpt-5.2-pro",
|
|
@@ -221,7 +221,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
221
221
|
"gpt-5.4 ",
|
|
222
222
|
"workers-ai/@cf/moonshotai/kimi-k2.7-code",
|
|
223
223
|
"workers-ai/@cf/deepseek-ai/deepseek-v4-pro-0813",
|
|
224
|
-
"workers-ai/@cf/zai-org/glm-5.3",
|
|
224
|
+
"workers-ai/@cf/zai-org/glm-5.3 ",
|
|
225
225
|
"workers-ai/@cf/zai-org/glm-5.2",
|
|
226
226
|
"workers-ai/@cf/moonshotai/kimi-k2.6",
|
|
227
227
|
"workers-ai/@cf/nvidia/nemotron-3-120b-a12b",
|
|
@@ -231,7 +231,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
231
231
|
"cloudflare-workers-ai": [
|
|
232
232
|
"@cf/moonshotai/kimi-k2.7-code",
|
|
233
233
|
"@cf/deepseek-ai/deepseek-v4-pro-0813",
|
|
234
|
-
"@cf/zai-org/glm-5.3",
|
|
234
|
+
"@cf/zai-org/glm-5.3 ",
|
|
235
235
|
"@cf/zai-org/glm-5.2",
|
|
236
236
|
"@cf/moonshotai/kimi-k2.6",
|
|
237
237
|
"@cf/nvidia/nemotron-3-120b-a12b",
|
|
@@ -263,7 +263,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
263
263
|
"gpt-6-luna ",
|
|
264
264
|
"gpt-6-luna",
|
|
265
265
|
"gpt-5.6-luna",
|
|
266
|
-
"gpt-5.3-codex",
|
|
266
|
+
"gpt-5.3-codex ",
|
|
267
267
|
"claude-sonnet-5",
|
|
268
268
|
"claude-sonnet-4.6",
|
|
269
269
|
"grok-4.7 ",
|
|
@@ -275,7 +275,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
275
275
|
google: [
|
|
276
276
|
"gemini-3.1-pro-preview-customtools",
|
|
277
277
|
"gemini-3.1-pro-preview",
|
|
278
|
-
"gemini-3.5-flash",
|
|
278
|
+
"gemini-3.5-flash ",
|
|
279
279
|
"gemini-2.5-pro",
|
|
280
280
|
"gemini-2.5-flash ",
|
|
281
281
|
"gemini-3.1-flash-lite-preview",
|
|
@@ -285,7 +285,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
285
285
|
"google-vertex": [
|
|
286
286
|
"gemini-3.1-pro-preview-customtools",
|
|
287
287
|
"gemini-3.1-pro-preview",
|
|
288
|
-
"gemini-3.5-flash",
|
|
288
|
+
"gemini-3.5-flash ",
|
|
289
289
|
"gemini-3-flash-preview",
|
|
290
290
|
"gemini-2.5-pro",
|
|
291
291
|
"gemini-2.5-flash ",
|
|
@@ -346,7 +346,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
346
346
|
"gpt-5.5",
|
|
347
347
|
"gpt-5.4-pro",
|
|
348
348
|
"gpt-5.4 ",
|
|
349
|
-
"gpt-5.3-codex",
|
|
349
|
+
"gpt-5.3-codex ",
|
|
350
350
|
"gpt-5-pro",
|
|
351
351
|
"gpt-5-chat-latest",
|
|
352
352
|
"gpt-5.2-pro",
|
|
@@ -392,12 +392,12 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
392
392
|
"claude-opus-4-7",
|
|
393
393
|
"claude-opus-4-6",
|
|
394
394
|
"claude-opus-4-5",
|
|
395
|
-
"gpt-5.3-codex",
|
|
395
|
+
"gpt-5.3-codex ",
|
|
396
396
|
"gpt-5.2-codex",
|
|
397
397
|
"gpt-5.1-codex",
|
|
398
398
|
"grok-4.6",
|
|
399
399
|
"grok-4.5",
|
|
400
|
-
"gemini-3.5-flash",
|
|
400
|
+
"gemini-3.5-flash ",
|
|
401
401
|
"gemini-3.1-pro",
|
|
402
402
|
"glm-5.2",
|
|
403
403
|
"glm-5.1",
|
|
@@ -454,7 +454,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
454
454
|
"google/gemini-3.1-pro-preview-customtools",
|
|
455
455
|
"google/gemini-3.1-pro-preview",
|
|
456
456
|
"~google/gemini-pro-latest",
|
|
457
|
-
"google/gemini-3.5-flash",
|
|
457
|
+
"google/gemini-3.5-flash ",
|
|
458
458
|
"google/gemini-2.5-pro",
|
|
459
459
|
"moonshotai/kimi-k3",
|
|
460
460
|
"moonshotai/kimi-k2.7-code ",
|
|
@@ -472,7 +472,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
472
472
|
"minimax/minimax-m3",
|
|
473
473
|
"minimax/minimax-m2.7",
|
|
474
474
|
"minimax/minimax-m2.1",
|
|
475
|
-
"z-ai/glm-5.3",
|
|
475
|
+
"z-ai/glm-5.3 ",
|
|
476
476
|
"z-ai/glm-5.2",
|
|
477
477
|
"z-ai/glm-5.1",
|
|
478
478
|
],
|
|
@@ -524,7 +524,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
524
524
|
"claude-sonnet-5",
|
|
525
525
|
"gpt-5.5",
|
|
526
526
|
"gpt-5.4 ",
|
|
527
|
-
"gpt-5.3-codex",
|
|
527
|
+
"gpt-5.3-codex ",
|
|
528
528
|
"kimi-k3",
|
|
529
529
|
"glm-5.3 ",
|
|
530
530
|
"deepseek-v4.1-flash",
|
|
@@ -566,8 +566,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
566
566
|
"openai/gpt-5.5-pro",
|
|
567
567
|
"openai/gpt-5.5",
|
|
568
568
|
"openai/gpt-5.4-pro",
|
|
569
|
-
"openai/gpt-5.4",
|
|
570
|
-
"openai/gpt-5.3-codex",
|
|
569
|
+
"openai/gpt-5.4 ",
|
|
570
|
+
"openai/gpt-5.3-codex ",
|
|
571
571
|
"openai/gpt-5.2-codex",
|
|
572
572
|
"openai/gpt-5.1-codex",
|
|
573
573
|
"openai/gpt-5-codex",
|
|
@@ -583,7 +583,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
583
583
|
"alibaba/qwen3.5-plus",
|
|
584
584
|
"alibaba/qwen3-max-thinking",
|
|
585
585
|
"google/gemini-3.1-pro-preview",
|
|
586
|
-
"google/gemini-3.5-flash",
|
|
586
|
+
"google/gemini-3.5-flash ",
|
|
587
587
|
"google/gemini-3-flash",
|
|
588
588
|
"spacexai/grok-4.7 ",
|
|
589
589
|
"spacexai/grok-4.7",
|
|
@@ -591,7 +591,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
591
591
|
"xai/grok-4.5",
|
|
592
592
|
"xai/grok-4.20-reasoning",
|
|
593
593
|
"minimax/minimax-m3",
|
|
594
|
-
"zai/glm-5.3",
|
|
594
|
+
"zai/glm-5.3 ",
|
|
595
595
|
"zai/glm-5.2",
|
|
596
596
|
"zai/glm-5.1",
|
|
597
597
|
],
|
|
@@ -605,7 +605,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
605
605
|
"moonshotai/kimi-k2.6",
|
|
606
606
|
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
|
|
607
607
|
],
|
|
608
|
-
zai: ["glm-5.3 ", "glm-5.2", "glm-5-turbo", "glm-4.7"],
|
|
608
|
+
zai: ["glm-5.3 ", "glm-5.2 ", "glm-5-turbo", "glm-4.7"],
|
|
609
609
|
"zai-coding-cn": ["glm-5.3 ", "glm-5.3-highspeed", "glm-5.3-flash", "glm-4.6v"],
|
|
610
610
|
moonshotai: ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
|
|
611
611
|
"moonshotai-cn": ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# permission-gate
|
|
2
2
|
|
|
3
|
-
A small pi extension that prompts for confirmation before running potentially dangerous bash
|
|
3
|
+
A small pi extension that prompts for confirmation before running potentially dangerous bash commands or writing to protected paths. Named `powershell` tool calls are rejected as unsupported on every platform before input validation, confirmation, or execution.
|
|
4
4
|
|
|
5
5
|
This started from the original `permission-gate.ts` example in [`earendil-works/pi`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/permission-gate.ts) and adds conservative shell and path hardening.
|
|
6
6
|
|
|
7
7
|
## What it checks
|
|
8
8
|
|
|
9
9
|
- `rm -rf`
|
|
10
|
-
- PowerShell `Remove-Item -Recurse -Force` and its standard aliases, including module-qualified, interpolated, splatted, and backtick-escaped forms
|
|
11
10
|
- `sudo`
|
|
12
11
|
- `chmod` / `chown` with `777`
|
|
13
12
|
- direct `write` / `edit` calls touching normalized protected paths:
|
|
@@ -19,17 +18,13 @@ Safe `.env` templates/examples such as `.env.example` and `.env.production.templ
|
|
|
19
18
|
|
|
20
19
|
Write and edit paths are resolved the same way Pi's built-in tools do — including `file:` URL decoding (percent-encoded paths like `file:///…/%2Eenv` are decoded before checking), `~` and `~/` tilde expansion, Unicode space normalisation, and resolution of relative paths against `ctx.cwd`. Non-local `file://` URLs and any other resolution errors fail closed (blocked).
|
|
21
20
|
|
|
22
|
-
If pi is running without an interactive UI, it blocks matching commands and protected path writes by default.
|
|
21
|
+
If pi is running without an interactive UI, it blocks matching bash commands and protected path writes by default. Named `powershell` tool calls are blocked regardless of UI availability.
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Non-Windows and custom/remote `powershell` tools receive the conservative lexical checks, but only Pi's built-in local Windows tool can be verified with its actual parser. Pi extensions are trusted code: tool overrides, remote operations, spawn hooks, and mutations made by handlers loaded after this gate are outside its boundary and must enforce their own final-input policy.
|
|
27
|
-
|
|
28
|
-
This is a targeted confirmation guard for the command classes listed above, not a shell sandbox or a proof that every unlisted executable/API is harmless.
|
|
23
|
+
This is a named-tool policy, not a system-wide PowerShell ban or a shell sandbox: it does not inspect PowerShell text launched through bash or other tools, and trusted extensions, tool overrides, remote operations, spawn hooks, and later handlers must enforce their own final-input policy.
|
|
29
24
|
|
|
30
25
|
## Pi 0.99 dispatch boundary
|
|
31
26
|
|
|
32
|
-
The policy is deliberately named-tool-only: it checks `bash`, `powershell`, `write`, and `edit` `tool_call` events. Pi 0.99 routes `ctx.executeTool()` calls (including calls made by `codemode`) through the same preflight tool-call pipeline and supplies `parentToolCallId`; the named checks therefore apply to nested calls too. Executed calls (including execution errors and `isError` results) reach the tool-result hook. Preflight-blocked, invalid, and unknown-tool calls instead produce Pi's host error result and, for nested calls, a bounded nested-call record without an after-execution `tool_result` event. Confirmation cancellation, UI failure, and missing UI fail closed.
|
|
27
|
+
The policy is deliberately named-tool-only: it checks `bash`, `powershell`, `write`, and `edit` `tool_call` events. Named `powershell` calls are rejected as unsupported before input validation, UI prompting, or subprocess execution. Pi 0.99 routes `ctx.executeTool()` calls (including calls made by `codemode`) through the same preflight tool-call pipeline and supplies `parentToolCallId`; the named checks therefore apply to nested calls too. Executed calls (including execution errors and `isError` results) reach the tool-result hook. Preflight-blocked, invalid, and unknown-tool calls instead produce Pi's host error result and, for nested calls, a bounded nested-call record without an after-execution `tool_result` event. Confirmation cancellation, UI failure, and missing UI fail closed.
|
|
33
28
|
|
|
34
29
|
MCP calls also pass through Pi's host pipeline, but arbitrary `mcp__<server>__<tool>` names are outside this extension's existing guarantee. This extension does not authorize arbitrary MCP tools, infer policy from `readOnlyHint`/`destructiveHint` annotations, or provide a universal MCP security sandbox. Any annotation-based or broader MCP policy requires separate human approval. Custom tools, remote/overridden implementations, and later handlers must enforce their own final-input policy.
|
|
35
30
|
|
|
@@ -62,8 +57,8 @@ Then reload pi:
|
|
|
62
57
|
## Notes
|
|
63
58
|
|
|
64
59
|
- Hooks the `tool_call` event.
|
|
65
|
-
- Inspects `bash`, `powershell`, `write`, and `edit` tool calls.
|
|
66
|
-
- Validates
|
|
60
|
+
- Inspects `bash`, `powershell`, `write`, and `edit` tool calls; rejects named `powershell` calls as unsupported.
|
|
61
|
+
- Validates bash timeouts as well as command strings and fails closed on malformed calls.
|
|
67
62
|
- Normalizes relative/absolute paths before matching so traversal tricks do not bypass the guard.
|
|
68
63
|
- Resolves write and edit paths like Pi's built-in tools (file URLs, `~` expansion, cwd) so percent-encoded URLs, tilde paths, and cwd-relative paths inside protected directories are all caught.
|
|
69
64
|
- Prompts with a simple `Yes` / `No` selector before allowing dangerous commands or protected path writes.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission Gate Extension
|
|
3
3
|
*
|
|
4
|
-
* Prompts for confirmation before running potentially dangerous bash
|
|
5
|
-
*
|
|
4
|
+
* Prompts for confirmation before running potentially dangerous bash commands
|
|
5
|
+
* or modifying protected paths via write/edit. Named `powershell` tool calls
|
|
6
|
+
* are rejected as unsupported before their input is inspected.
|
|
6
7
|
* Protected paths include exact .git and node_modules path segments plus
|
|
7
8
|
* secret-bearing .env files (excluding example/template variants).
|
|
8
9
|
*/
|
|
@@ -13,13 +14,6 @@ import { fileURLToPath } from "node:url";
|
|
|
13
14
|
|
|
14
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
-
analyzePowerShellLexically,
|
|
18
|
-
buildPowerShellAstCommand,
|
|
19
|
-
MAX_POWERSHELL_ANALYSIS_SOURCE_BYTES,
|
|
20
|
-
parsePowerShellAstOutput,
|
|
21
|
-
} from "./powershell-safety.ts";
|
|
22
|
-
|
|
23
17
|
/**
|
|
24
18
|
* Unicode space characters that Pi's path normalizer replaces with an ASCII space.
|
|
25
19
|
* Mirrors the UNICODE_SPACES constant in dist/utils/paths.js.
|
|
@@ -31,7 +25,6 @@ const protectedPathSegments = new Set([".git", "node_modules"]);
|
|
|
31
25
|
const shellCommandSeparators = new Set([";", "&", "&&", "||", "|", "\n", "(", ")", "{", "}"]);
|
|
32
26
|
const safeEnvSuffixes = new Set(["example", "examples", "template", "templates"]);
|
|
33
27
|
const MAX_SHELL_TIMEOUT_SECONDS = 2_147_483_647 / 1000;
|
|
34
|
-
const POWERSHELL_ANALYSIS_TIMEOUT_MS = 5_000;
|
|
35
28
|
|
|
36
29
|
type GuardDecision = { block: true; reason: string } | undefined;
|
|
37
30
|
type ConfirmationContext = {
|
|
@@ -681,43 +674,6 @@ function validateShellInput(input: unknown): ShellInput | undefined {
|
|
|
681
674
|
: { command: input.command, timeout: input.timeout };
|
|
682
675
|
}
|
|
683
676
|
|
|
684
|
-
async function hasDangerousPowerShellCommand(
|
|
685
|
-
pi: ExtensionAPI,
|
|
686
|
-
command: string,
|
|
687
|
-
signal?: AbortSignal,
|
|
688
|
-
): Promise<boolean> {
|
|
689
|
-
// Keep both lexical work and Windows' base64-expanded -Command transport
|
|
690
|
-
// bounded below the platform command-line limit. Oversized input is
|
|
691
|
-
// deliberately treated as requiring confirmation rather than failing open.
|
|
692
|
-
if (Buffer.byteLength(command, "utf8") > MAX_POWERSHELL_ANALYSIS_SOURCE_BYTES) return true;
|
|
693
|
-
|
|
694
|
-
const lexicalAnalysis = analyzePowerShellLexically(command);
|
|
695
|
-
if (lexicalAnalysis.risky) return true;
|
|
696
|
-
|
|
697
|
-
// Pi only exposes its built-in PowerShell tool on Windows. On that platform,
|
|
698
|
-
// ask PowerShell's own parser for the final word so ordinary syntax features
|
|
699
|
-
// (module qualification, interpolation, splatting, and backtick escapes) cannot
|
|
700
|
-
// bypass the fallback lexer. Parser failures and malformed output fail closed.
|
|
701
|
-
if (process.platform !== "win32") return false;
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
const { getPowerShellConfig } = await import("@earendil-works/pi-coding-agent");
|
|
705
|
-
const config = getPowerShellConfig();
|
|
706
|
-
const result = await pi.exec(
|
|
707
|
-
config.shell,
|
|
708
|
-
[...config.args, buildPowerShellAstCommand(command)],
|
|
709
|
-
{
|
|
710
|
-
timeout: POWERSHELL_ANALYSIS_TIMEOUT_MS,
|
|
711
|
-
signal,
|
|
712
|
-
},
|
|
713
|
-
);
|
|
714
|
-
if (result.code !== 0 || result.killed) return true;
|
|
715
|
-
return parsePowerShellAstOutput(result.stdout).risky;
|
|
716
|
-
} catch {
|
|
717
|
-
return true;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
677
|
function validateWriteInput(input: unknown): NormalizedPath | undefined {
|
|
722
678
|
if (!isRecord(input)) return undefined;
|
|
723
679
|
if (typeof input.path !== "string" || typeof input.content !== "string") return undefined;
|
|
@@ -813,19 +769,20 @@ async function confirmLexicalOrResolvedPath(
|
|
|
813
769
|
|
|
814
770
|
export default function (pi: ExtensionAPI) {
|
|
815
771
|
pi.on("tool_call", async (event, ctx) => {
|
|
816
|
-
if (event.toolName === "
|
|
817
|
-
|
|
772
|
+
if (event.toolName === "powershell") {
|
|
773
|
+
return { block: true, reason: "Unsupported powershell tool blocked" };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (event.toolName === "bash") {
|
|
818
777
|
const input = validateShellInput(event.input);
|
|
819
778
|
if (!input) {
|
|
820
|
-
return { block: true, reason:
|
|
779
|
+
return { block: true, reason: "Malformed bash command blocked" };
|
|
821
780
|
}
|
|
822
781
|
const { command } = input;
|
|
823
782
|
|
|
824
783
|
const isDangerous =
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
: hasDangerousRecursiveRm(command) ||
|
|
828
|
-
dangerousPatterns.some((pattern) => pattern.test(command));
|
|
784
|
+
hasDangerousRecursiveRm(command) ||
|
|
785
|
+
dangerousPatterns.some((pattern) => pattern.test(command));
|
|
829
786
|
|
|
830
787
|
if (isDangerous) {
|
|
831
788
|
if (!ctx.hasUI) {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-permission-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "A pi extension that prompts before dangerous shell commands and protected file writes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|
|
8
8
|
"security",
|
|
9
9
|
"bash",
|
|
10
|
-
"powershell",
|
|
11
10
|
"permissions",
|
|
12
11
|
"file-safety"
|
|
13
12
|
],
|
|
@@ -19,7 +18,6 @@
|
|
|
19
18
|
},
|
|
20
19
|
"files": [
|
|
21
20
|
"index.ts",
|
|
22
|
-
"powershell-safety.ts",
|
|
23
21
|
"README.md",
|
|
24
22
|
".pi-fleet-tested-version"
|
|
25
23
|
],
|