@trim21/personal-pi-extensions 0.0.0
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 +185 -0
- package/package.json +59 -0
- package/src/agents-md-user-message.ts +71 -0
- package/src/bash-default-timeout.ts +9 -0
- package/src/bwrap/index.ts +733 -0
- package/src/bwrap/seccomp-aarch64.bpf +0 -0
- package/src/bwrap/seccomp-x86_64.bpf +0 -0
- package/src/gh-readonly.ts +1264 -0
- package/src/opencode-edit.ts +556 -0
- package/src/opencode-read.ts +485 -0
- package/src/opencode-write.ts +63 -0
- package/src/todo-pendant.ts +82 -0
- package/src/workspace-guard.ts +112 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Guard Extension
|
|
3
|
+
*
|
|
4
|
+
* File-modifying tools (write, edit) are gated:
|
|
5
|
+
* - Paths inside the workspace or /tmp are auto-allowed.
|
|
6
|
+
* - Paths outside require user approval via confirmation dialog.
|
|
7
|
+
*
|
|
8
|
+
* Read tools (read, ls, find, grep) are unrestricted.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pi -e workspace-guard
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isAbsolute, join, resolve, relative, sep } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
const WRITE_TOOLS = new Set(["write", "edit"]);
|
|
19
|
+
const ALWAYS_ALLOW = ["/tmp"];
|
|
20
|
+
|
|
21
|
+
function resolvePath(filePath: string, cwd: string): string {
|
|
22
|
+
let p = filePath;
|
|
23
|
+
if (p.startsWith("~")) {
|
|
24
|
+
const rest = p.slice(1);
|
|
25
|
+
if (rest === "" || rest.startsWith("/")) {
|
|
26
|
+
p = join(homedir(), rest.slice(1));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return isAbsolute(p) ? resolve(p) : resolve(cwd, p);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isInside(dir: string, filePath: string): boolean {
|
|
33
|
+
const rel = relative(dir, filePath);
|
|
34
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isPathAllowed(resolvedPath: string, cwd: string): boolean {
|
|
38
|
+
if (isInside(cwd, resolvedPath)) return true;
|
|
39
|
+
|
|
40
|
+
for (const allowed of ALWAYS_ALLOW) {
|
|
41
|
+
const resolvedAllowed = resolvePath(allowed, cwd);
|
|
42
|
+
if (isInside(resolvedAllowed, resolvedPath)) return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function (pi: ExtensionAPI) {
|
|
49
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
50
|
+
const currentCwd = ctx.cwd;
|
|
51
|
+
return {
|
|
52
|
+
systemPrompt:
|
|
53
|
+
event.systemPrompt +
|
|
54
|
+
`\nWorkspace write protection is active. ` +
|
|
55
|
+
`write and edit to paths inside the workspace "${currentCwd}" or /tmp are auto-allowed. ` +
|
|
56
|
+
`Paths outside require user approval before execution.`,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
61
|
+
if (!WRITE_TOOLS.has(event.toolName)) return;
|
|
62
|
+
|
|
63
|
+
const input = event.input as { path?: string; filePath?: string };
|
|
64
|
+
const rawPath = input.path ?? input.filePath;
|
|
65
|
+
if (!rawPath) return;
|
|
66
|
+
|
|
67
|
+
const resolved = resolvePath(rawPath, ctx.cwd);
|
|
68
|
+
|
|
69
|
+
if (isPathAllowed(resolved, ctx.cwd)) return;
|
|
70
|
+
|
|
71
|
+
if (!ctx.hasUI) {
|
|
72
|
+
return {
|
|
73
|
+
block: true,
|
|
74
|
+
reason: `Path "${rawPath}" is outside workspace. No UI available for approval.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let choice: string | undefined;
|
|
79
|
+
while (!choice) {
|
|
80
|
+
choice = await ctx.ui.select(
|
|
81
|
+
`Model requests write access outside workspace:\n\n` +
|
|
82
|
+
` Tool: ${event.toolName}\n` +
|
|
83
|
+
` Path: ${rawPath}\n` +
|
|
84
|
+
` Resolved: ${resolved}\n\nAllow?`,
|
|
85
|
+
["Approve once", "Block", "Block with reason"],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (typeof choice === "undefined") {
|
|
89
|
+
ctx.abort();
|
|
90
|
+
return { block: true, reason: "Write outside workspace cancelled by user." };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (choice === "Block with reason") {
|
|
94
|
+
const feedback = await ctx.ui.input("Why was this write denied?");
|
|
95
|
+
if (feedback === undefined) {
|
|
96
|
+
choice = undefined; // cancelled input, retry select
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
block: true,
|
|
101
|
+
reason: feedback
|
|
102
|
+
? `Write outside workspace denied: ${feedback}`
|
|
103
|
+
: "Write outside workspace denied by user.",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (choice !== "Approve once") {
|
|
109
|
+
return { block: true, reason: "Write outside workspace denied by user." };
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|