@diegopetrucci/pi-extensions 0.1.55 → 0.1.57
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/.pi-fleet-tested-version +1 -1
- package/README.md +2 -2
- package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-git-diff/package.json +2 -2
- package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/brrr/.pi-fleet-tested-version +1 -1
- package/extensions/brrr/README.md +3 -3
- package/extensions/brrr/index.ts +26 -6
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
- package/extensions/claude-fast/README.md +3 -3
- package/extensions/claude-fast/index.ts +1 -2
- package/extensions/claude-fast/package.json +1 -1
- package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
- package/extensions/code-reviewer/index.ts +27 -19
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/.pi-fleet-tested-version +1 -1
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
- package/extensions/context-inspector/README.md +4 -2
- package/extensions/context-inspector/index.ts +10 -4
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/.pi-fleet-tested-version +1 -1
- package/extensions/contrarian/index.ts +96 -92
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/git-footer/.pi-fleet-tested-version +1 -1
- package/extensions/git-footer/package.json +1 -1
- package/extensions/gnosis/.pi-fleet-tested-version +1 -1
- package/extensions/gnosis/package.json +1 -1
- package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
- package/extensions/illustrations-to-explain-things/package.json +1 -1
- package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/.pi-fleet-tested-version +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +39 -8
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
- package/extensions/minimal-footer/README.md +3 -3
- package/extensions/minimal-footer/index.ts +1 -1
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/.pi-fleet-tested-version +1 -1
- package/extensions/notify/README.md +3 -3
- package/extensions/notify/index.ts +4 -6
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
- package/extensions/openai-fast/README.md +5 -5
- package/extensions/openai-fast/index.ts +11 -5
- package/extensions/openai-fast/package.json +2 -2
- package/extensions/oracle/.pi-fleet-tested-version +1 -1
- package/extensions/oracle/index.ts +97 -90
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
- package/extensions/permission-gate/README.md +11 -4
- package/extensions/permission-gate/index.ts +156 -17
- package/extensions/permission-gate/package.json +5 -3
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/package.json +1 -1
- package/extensions/review/.pi-fleet-tested-version +1 -1
- package/extensions/review/README.md +1 -1
- package/extensions/review/index.ts +4 -4
- package/extensions/review/package.json +1 -1
- package/extensions/todo/.pi-fleet-tested-version +1 -1
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +9 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
0.80.
|
|
1
|
+
0.80.6
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# permission-gate
|
|
2
2
|
|
|
3
|
-
A small pi extension that prompts for confirmation before running potentially dangerous bash commands.
|
|
3
|
+
A small pi extension that prompts for confirmation before running potentially dangerous bash commands or writing to protected paths.
|
|
4
4
|
|
|
5
5
|
This is adapted from the original `permission-gate.ts` example in [`earendil-works/pi-mono`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/examples/extensions/permission-gate.ts) and kept basically the same.
|
|
6
6
|
|
|
@@ -9,8 +9,14 @@ This is adapted from the original `permission-gate.ts` example in [`earendil-wor
|
|
|
9
9
|
- `rm -rf`
|
|
10
10
|
- `sudo`
|
|
11
11
|
- `chmod` / `chown` with `777`
|
|
12
|
+
- direct `write` / `edit` calls touching normalized protected paths:
|
|
13
|
+
- exact `.git` path segments
|
|
14
|
+
- exact `node_modules` path segments
|
|
15
|
+
- secret-bearing `.env` files such as `.env` and `.env.production`
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
Safe `.env` templates/examples such as `.env.example` and `.env.production.template` are allowed.
|
|
18
|
+
|
|
19
|
+
If pi is running without an interactive UI, it blocks matching commands and protected path writes by default.
|
|
14
20
|
|
|
15
21
|
## Install
|
|
16
22
|
|
|
@@ -41,5 +47,6 @@ Then reload pi:
|
|
|
41
47
|
## Notes
|
|
42
48
|
|
|
43
49
|
- Hooks the `tool_call` event.
|
|
44
|
-
-
|
|
45
|
-
-
|
|
50
|
+
- Inspects `bash`, `write`, and `edit` tool calls.
|
|
51
|
+
- Normalizes relative/absolute paths before matching so traversal tricks do not bypass the guard.
|
|
52
|
+
- Prompts with a simple `Yes` / `No` selector before allowing dangerous commands or protected path writes.
|
|
@@ -1,36 +1,175 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission Gate Extension
|
|
3
3
|
*
|
|
4
|
-
* Prompts for confirmation before running potentially dangerous bash commands
|
|
5
|
-
*
|
|
4
|
+
* Prompts for confirmation before running potentially dangerous bash commands
|
|
5
|
+
* or modifying protected paths via write/edit.
|
|
6
|
+
* Protected paths include exact .git and node_modules path segments plus
|
|
7
|
+
* secret-bearing .env files (excluding example/template variants).
|
|
6
8
|
*/
|
|
7
9
|
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
8
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
|
|
15
|
+
const protectedPathSegments = new Set([".git", "node_modules"]);
|
|
16
|
+
const safeEnvSuffixes = new Set(["example", "examples", "template", "templates"]);
|
|
17
|
+
|
|
18
|
+
type GuardDecision = { block: true; reason: string } | undefined;
|
|
19
|
+
type NormalizedPath = {
|
|
20
|
+
original: string;
|
|
21
|
+
displayPath: string;
|
|
22
|
+
segments: string[];
|
|
23
|
+
basename: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type WriteInput = { path: string; content: string };
|
|
27
|
+
type EditInput = { path: string; edits: Array<{ oldText: string; newText: string }> };
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeToolPath(rawPath: string): NormalizedPath | undefined {
|
|
34
|
+
const trimmed = rawPath.trim();
|
|
35
|
+
if (!trimmed) return undefined;
|
|
36
|
+
|
|
37
|
+
// Pi's built-in path tools treat a leading @ as path syntax and strip it
|
|
38
|
+
// before execution, so inspect the same effective target.
|
|
39
|
+
const withoutPathSigil = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
40
|
+
if (!withoutPathSigil) return undefined;
|
|
41
|
+
|
|
42
|
+
const withPosixSeparators = withoutPathSigil.replace(/\\+/g, "/");
|
|
43
|
+
const normalizedPath = path.posix.normalize(
|
|
44
|
+
withPosixSeparators.startsWith("/") ? withPosixSeparators : path.posix.join("/", withPosixSeparators),
|
|
45
|
+
);
|
|
46
|
+
const segments = normalizedPath.split("/").filter(Boolean);
|
|
47
|
+
const basename = segments.at(-1) ?? "";
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
original: rawPath,
|
|
51
|
+
displayPath: withPosixSeparators,
|
|
52
|
+
segments,
|
|
53
|
+
basename,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSafeEnvTemplate(basename: string): boolean {
|
|
58
|
+
const normalizedBasename = basename.toLowerCase();
|
|
59
|
+
if (!normalizedBasename.startsWith(".env.")) return false;
|
|
60
|
+
const terminalSuffix = normalizedBasename.split(".").at(-1) ?? "";
|
|
61
|
+
return safeEnvSuffixes.has(terminalSuffix);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isProtectedEnvFile(basename: string): boolean {
|
|
65
|
+
const normalizedBasename = basename.toLowerCase();
|
|
66
|
+
if (normalizedBasename === ".env") return true;
|
|
67
|
+
if (!normalizedBasename.startsWith(".env.")) return false;
|
|
68
|
+
return !isSafeEnvTemplate(normalizedBasename);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isProtectedPath(normalizedPath: NormalizedPath): boolean {
|
|
72
|
+
if (normalizedPath.segments.some((segment) => protectedPathSegments.has(segment.toLowerCase()))) return true;
|
|
73
|
+
return isProtectedEnvFile(normalizedPath.basename);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateWriteInput(input: unknown): WriteInput | undefined {
|
|
77
|
+
if (!isRecord(input)) return undefined;
|
|
78
|
+
if (typeof input.path !== "string" || typeof input.content !== "string") return undefined;
|
|
79
|
+
if (!normalizeToolPath(input.path)) return undefined;
|
|
80
|
+
return { path: input.path, content: input.content };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateEditInput(input: unknown): EditInput | undefined {
|
|
84
|
+
if (!isRecord(input)) return undefined;
|
|
85
|
+
if (typeof input.path !== "string" || !Array.isArray(input.edits)) return undefined;
|
|
86
|
+
if (!normalizeToolPath(input.path)) return undefined;
|
|
12
87
|
|
|
88
|
+
const edits = input.edits;
|
|
89
|
+
if (
|
|
90
|
+
edits.some(
|
|
91
|
+
(edit) =>
|
|
92
|
+
!isRecord(edit) || typeof edit.oldText !== "string" || typeof edit.newText !== "string",
|
|
93
|
+
)
|
|
94
|
+
) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
path: input.path,
|
|
100
|
+
edits: edits as Array<{ oldText: string; newText: string }>,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function confirmProtectedPathAction(
|
|
105
|
+
toolName: "write" | "edit",
|
|
106
|
+
normalizedPath: NormalizedPath,
|
|
107
|
+
ctx: { hasUI?: boolean; ui?: { select(prompt: string, options: string[]): Promise<string | undefined> } },
|
|
108
|
+
): Promise<GuardDecision> {
|
|
109
|
+
if (!isProtectedPath(normalizedPath)) return undefined;
|
|
110
|
+
if (!ctx.hasUI || !ctx.ui) {
|
|
111
|
+
return { block: true, reason: `Protected path blocked (${toolName} without UI confirmation): ${normalizedPath.displayPath}` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const choice = await ctx.ui.select(
|
|
115
|
+
`⚠️ Protected path ${toolName} request:\n\n ${normalizedPath.displayPath}\n\nAllow?`,
|
|
116
|
+
["Yes", "No"],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (choice !== "Yes") {
|
|
120
|
+
return { block: true, reason: "Blocked by user" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default function (pi: ExtensionAPI) {
|
|
13
127
|
pi.on("tool_call", async (event, ctx) => {
|
|
14
|
-
if (event.toolName
|
|
128
|
+
if (event.toolName === "bash") {
|
|
129
|
+
const command = event.input?.command;
|
|
130
|
+
if (typeof command !== "string" || command.trim() === "") {
|
|
131
|
+
return { block: true, reason: "Malformed bash command blocked" };
|
|
132
|
+
}
|
|
15
133
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
134
|
+
const isDangerous = dangerousPatterns.some((p) => p.test(command));
|
|
135
|
+
|
|
136
|
+
if (isDangerous) {
|
|
137
|
+
if (!ctx.hasUI) {
|
|
138
|
+
return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" };
|
|
139
|
+
}
|
|
20
140
|
|
|
21
|
-
|
|
141
|
+
const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]);
|
|
22
142
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" };
|
|
143
|
+
if (choice !== "Yes") {
|
|
144
|
+
return { block: true, reason: "Blocked by user" };
|
|
145
|
+
}
|
|
27
146
|
}
|
|
28
147
|
|
|
29
|
-
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
30
150
|
|
|
31
|
-
|
|
32
|
-
|
|
151
|
+
if (event.toolName === "write") {
|
|
152
|
+
const input = validateWriteInput(event.input);
|
|
153
|
+
if (!input) {
|
|
154
|
+
return { block: true, reason: "Malformed write input blocked" };
|
|
155
|
+
}
|
|
156
|
+
const normalizedPath = normalizeToolPath(input.path);
|
|
157
|
+
if (!normalizedPath) {
|
|
158
|
+
return { block: true, reason: "Malformed write input blocked" };
|
|
159
|
+
}
|
|
160
|
+
return confirmProtectedPathAction("write", normalizedPath, ctx);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (event.toolName === "edit") {
|
|
164
|
+
const input = validateEditInput(event.input);
|
|
165
|
+
if (!input) {
|
|
166
|
+
return { block: true, reason: "Malformed edit input blocked" };
|
|
167
|
+
}
|
|
168
|
+
const normalizedPath = normalizeToolPath(input.path);
|
|
169
|
+
if (!normalizedPath) {
|
|
170
|
+
return { block: true, reason: "Malformed edit input blocked" };
|
|
33
171
|
}
|
|
172
|
+
return confirmProtectedPathAction("edit", normalizedPath, ctx);
|
|
34
173
|
}
|
|
35
174
|
|
|
36
175
|
return undefined;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-permission-gate",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A pi extension that prompts before dangerous bash commands.",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "A pi extension that prompts before dangerous bash commands and protected file writes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|
|
8
8
|
"security",
|
|
9
|
-
"bash"
|
|
9
|
+
"bash",
|
|
10
|
+
"permissions",
|
|
11
|
+
"file-safety"
|
|
10
12
|
],
|
|
11
13
|
"license": "MIT",
|
|
12
14
|
"repository": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.80.
|
|
1
|
+
0.80.6
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.80.
|
|
1
|
+
0.80.6
|
|
@@ -31,7 +31,7 @@ Then reload pi:
|
|
|
31
31
|
|
|
32
32
|
- Behavior is intentionally kept equivalent to the upstream source, with only packaging/attribution changes for this repository.
|
|
33
33
|
- PR review requires `gh` access and a clean working tree for tracked files.
|
|
34
|
-
- If the project is trusted and a `REVIEW_GUIDELINES.md` file exists next to the repo's
|
|
34
|
+
- If the project is trusted and a `REVIEW_GUIDELINES.md` file exists next to the repo's `<pi-config-dir>` directory, its contents are appended to the review prompt. `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default).
|
|
35
35
|
|
|
36
36
|
## License and attribution
|
|
37
37
|
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
* - `/review --extra "focus on performance regressions"` - add extra review instruction (works with any mode)
|
|
30
30
|
*
|
|
31
31
|
* Project-specific review guidelines:
|
|
32
|
-
* - If the project is trusted and a REVIEW_GUIDELINES.md file exists
|
|
33
|
-
*
|
|
32
|
+
* - If the project is trusted and a REVIEW_GUIDELINES.md file exists next to
|
|
33
|
+
* the project config directory, its contents are appended to the review prompt.
|
|
34
34
|
*
|
|
35
35
|
* Note: PR review requires a clean working tree (no uncommitted changes to tracked files).
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
39
|
-
import { DynamicBorder, BorderedLoader } from "@earendil-works/pi-coding-agent";
|
|
39
|
+
import { CONFIG_DIR_NAME, DynamicBorder, BorderedLoader } from "@earendil-works/pi-coding-agent";
|
|
40
40
|
import {
|
|
41
41
|
Container,
|
|
42
42
|
fuzzyFilter,
|
|
@@ -49,7 +49,6 @@ import {
|
|
|
49
49
|
import path from "node:path";
|
|
50
50
|
import { promises as fs } from "node:fs";
|
|
51
51
|
|
|
52
|
-
const CONFIG_DIR_NAME = ".pi";
|
|
53
52
|
|
|
54
53
|
// State to track fresh session review (where we branched from).
|
|
55
54
|
// Module-level state means only one review can be active at a time.
|
|
@@ -1089,6 +1088,7 @@ export const __test__ = {
|
|
|
1089
1088
|
parsePrReference,
|
|
1090
1089
|
parseReviewPaths,
|
|
1091
1090
|
REVIEW_SUMMARY_PROMPT,
|
|
1091
|
+
loadProjectReviewGuidelines,
|
|
1092
1092
|
resetReviewRuntimeState,
|
|
1093
1093
|
tokenizeArgs,
|
|
1094
1094
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.80.
|
|
1
|
+
0.80.6
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.80.
|
|
1
|
+
0.80.6
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
4
4
|
"description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, contrarian review, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -71,17 +71,20 @@
|
|
|
71
71
|
]
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@earendil-works/pi-ai": "^0.80.
|
|
75
|
-
"@earendil-works/pi-coding-agent": "^0.80.
|
|
76
|
-
"@earendil-works/pi-tui": "^0.80.
|
|
74
|
+
"@earendil-works/pi-ai": "^0.80.6",
|
|
75
|
+
"@earendil-works/pi-coding-agent": "^0.80.6",
|
|
76
|
+
"@earendil-works/pi-tui": "^0.80.6",
|
|
77
77
|
"@types/node": "^25.9.1",
|
|
78
78
|
"husky": "^9.1.7",
|
|
79
|
-
"typebox": "^1.
|
|
79
|
+
"typebox": "^1.3.6",
|
|
80
80
|
"typescript": "^6.0.3"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@tailwindcss/browser": "^4.3.
|
|
83
|
+
"@tailwindcss/browser": "^4.3.2",
|
|
84
84
|
"glimpseui": "^0.8.1",
|
|
85
85
|
"monaco-editor": "0.52.2"
|
|
86
|
+
},
|
|
87
|
+
"overrides": {
|
|
88
|
+
"protobufjs": "^7.6.4"
|
|
86
89
|
}
|
|
87
90
|
}
|