@trim21/personal-pi-extensions 0.0.156 → 0.0.160
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 -24
- package/package.json +1 -1
- package/src/bwrap/index.ts +86 -6
- package/src/workspace-guard.ts +50 -11
- package/src/agents-md-user-message.ts +0 -71
package/README.md
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
## 扩展概览
|
|
6
6
|
|
|
7
|
-
| 扩展
|
|
8
|
-
|
|
|
9
|
-
| [bwrap](#bwrap)
|
|
10
|
-
| [workspace-guard](#workspace-guard)
|
|
11
|
-
| [opencode-edit](#opencode-edit)
|
|
12
|
-
| [bash-default-timeout](#bash-default-timeout)
|
|
13
|
-
| [agents-md-user-message](#agents-md-user-message) | 将项目级 AGENTS.md 移至 user message,避免占用 system prompt |
|
|
7
|
+
| 扩展 | 描述 |
|
|
8
|
+
| --------------------------------------------- | ------------------------------------------------------ |
|
|
9
|
+
| [bwrap](#bwrap) | 基于 bubblewrap 的 OS 级沙箱,提供文件系统和网络隔离 |
|
|
10
|
+
| [workspace-guard](#workspace-guard) | 限制文件写入在 workspace 内,外部写入需用户审批 |
|
|
11
|
+
| [opencode-edit](#opencode-edit) | 替换内置 edit 工具,使用 opencode 的 schema 和匹配引擎 |
|
|
12
|
+
| [bash-default-timeout](#bash-default-timeout) | 为 bash 工具设置默认超时(180 秒) |
|
|
14
13
|
|
|
15
14
|
---
|
|
16
15
|
|
|
@@ -134,23 +133,6 @@ pi -e ./src/bash-default-timeout.ts
|
|
|
134
133
|
|
|
135
134
|
---
|
|
136
135
|
|
|
137
|
-
## agents-md-user-message
|
|
138
|
-
|
|
139
|
-
将项目级 `AGENTS.md` 从 system prompt 移至 user message,减少 system prompt 占用。
|
|
140
|
-
|
|
141
|
-
- 项目级 `AGENTS.md` → 放入 user message(仅首次消息注入一次)
|
|
142
|
-
- 全局 `~/.pi/agent/AGENTS.md` → 保留在 system prompt
|
|
143
|
-
|
|
144
|
-
需要配合 `--no-context-files` 参数禁用 pi 默认的上下文文件加载。
|
|
145
|
-
|
|
146
|
-
### 使用
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
pi -e ./src/agents-md-user-message.ts --no-context-files
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
136
|
## 安装
|
|
155
137
|
|
|
156
138
|
### 通过 npm/git 包
|
package/package.json
CHANGED
package/src/bwrap/index.ts
CHANGED
|
@@ -104,8 +104,29 @@ work inside the sandbox, run it WITHOUT full access first. If it fails with
|
|
|
104
104
|
and set \`request_full_access_reason\` to describe the failure.
|
|
105
105
|
`;
|
|
106
106
|
|
|
107
|
+
const SUBAGENT_SANDBOX_PROMPT = `
|
|
108
|
+
## Command Execution (subagent sandbox)
|
|
109
|
+
|
|
110
|
+
You are running inside a read-only sandbox in a subagent session.
|
|
111
|
+
|
|
112
|
+
- The bash tool is read-only: no filesystem writes, no network access, and
|
|
113
|
+
\`request_full_access\` is denied. Do not pass \`request_full_access\`.
|
|
114
|
+
- Use the write/edit tools for file changes inside your workspace.
|
|
115
|
+
- Writes outside the workspace are rejected. If a change outside the
|
|
116
|
+
workspace is needed, ask the parent session to apply it.
|
|
117
|
+
- If a command truly requires network or system-level access, stop and ask
|
|
118
|
+
the parent session to run it, where the user can approve it.
|
|
119
|
+
`;
|
|
120
|
+
|
|
107
121
|
const PROTECTED_DIRS = [".git", ".pi", ".agent"];
|
|
108
122
|
|
|
123
|
+
/**
|
|
124
|
+
* pi-subagents sets PI_SUBAGENT_CHILD=1 in every spawned child session
|
|
125
|
+
* (foreground and async alike). Subagent sessions are headless and must not
|
|
126
|
+
* be able to bypass the sandbox, so bwrap forces read-only bash there.
|
|
127
|
+
*/
|
|
128
|
+
const isSubagentChild = process.env.PI_SUBAGENT_CHILD === "1";
|
|
129
|
+
|
|
109
130
|
const bwrapPath = findDefaultBwrap();
|
|
110
131
|
|
|
111
132
|
function findDefaultBwrap(): string {
|
|
@@ -134,7 +155,7 @@ function findBwrap(override?: string): string {
|
|
|
134
155
|
|
|
135
156
|
type BwrapMode = "allow-all" | "workspace-write" | "readonly";
|
|
136
157
|
|
|
137
|
-
interface BwrapConfig {
|
|
158
|
+
export interface BwrapConfig {
|
|
138
159
|
mode: BwrapMode;
|
|
139
160
|
bwrapPath?: string;
|
|
140
161
|
writablePaths?: string[];
|
|
@@ -143,7 +164,7 @@ interface BwrapConfig {
|
|
|
143
164
|
extraArgs?: string[];
|
|
144
165
|
}
|
|
145
166
|
|
|
146
|
-
interface ResolvedBwrap {
|
|
167
|
+
export interface ResolvedBwrap {
|
|
147
168
|
mode: BwrapMode;
|
|
148
169
|
bwrapEnabled: boolean;
|
|
149
170
|
network: boolean;
|
|
@@ -176,6 +197,22 @@ function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
176
197
|
}
|
|
177
198
|
}
|
|
178
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Subagent sessions are forced read-only regardless of config: no writable
|
|
202
|
+
* paths (including configured extraWritablePaths), no network, and no
|
|
203
|
+
* user-supplied extra args that could add writable mounts.
|
|
204
|
+
*/
|
|
205
|
+
export function resolveSubagentBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
206
|
+
return resolveBwrap({
|
|
207
|
+
...config,
|
|
208
|
+
mode: "readonly",
|
|
209
|
+
writablePaths: [],
|
|
210
|
+
extraWritablePaths: [],
|
|
211
|
+
tmpfsPaths: [],
|
|
212
|
+
extraArgs: [],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
179
216
|
const DEFAULT_CONFIG: BwrapConfig = {
|
|
180
217
|
mode: "workspace-write",
|
|
181
218
|
writablePaths: [".", "/tmp"],
|
|
@@ -483,6 +520,36 @@ function notifyMode(
|
|
|
483
520
|
ctx.ui.notify(labels[mode], "info");
|
|
484
521
|
}
|
|
485
522
|
|
|
523
|
+
export type EscalationDecision = { kind: "dialog" } | { kind: "deny"; reason: string };
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Escalation (`request_full_access`) policy:
|
|
527
|
+
* - Subagent sessions are always denied: bash is read-only there and there is
|
|
528
|
+
* no approval path (headless), so escalation would silently bypass the sandbox.
|
|
529
|
+
* - Any other headless session is denied: there is no user to approve.
|
|
530
|
+
* - Interactive sessions require the user approval dialog.
|
|
531
|
+
*/
|
|
532
|
+
export function resolveEscalation(opts: {
|
|
533
|
+
hasUI: boolean;
|
|
534
|
+
isSubagentChild: boolean;
|
|
535
|
+
}): EscalationDecision {
|
|
536
|
+
if (opts.isSubagentChild) {
|
|
537
|
+
return {
|
|
538
|
+
kind: "deny",
|
|
539
|
+
reason:
|
|
540
|
+
"request_full_access is disabled in subagent sessions: the bash tool is read-only and cannot escalate. Ask the parent session to run this command.",
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (!opts.hasUI) {
|
|
544
|
+
return {
|
|
545
|
+
kind: "deny",
|
|
546
|
+
reason:
|
|
547
|
+
"request_full_access requires an interactive session with user approval; no UI is available in this session.",
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return { kind: "dialog" };
|
|
551
|
+
}
|
|
552
|
+
|
|
486
553
|
const sandboxedBashSchema = Type.Object({
|
|
487
554
|
command: Type.String({ description: "Bash command to execute" }),
|
|
488
555
|
timeout: Type.Optional(
|
|
@@ -517,6 +584,10 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
517
584
|
let resolved: ResolvedBwrap | null = null;
|
|
518
585
|
|
|
519
586
|
function getResolved(): ResolvedBwrap {
|
|
587
|
+
// Subagents ignore config and runtime switches: bash is always read-only.
|
|
588
|
+
if (isSubagentChild) {
|
|
589
|
+
return resolveSubagentBwrap(loadConfig(localCwd));
|
|
590
|
+
}
|
|
520
591
|
if (!resolved) {
|
|
521
592
|
resolved = resolveBwrap(loadConfig(localCwd));
|
|
522
593
|
}
|
|
@@ -524,6 +595,7 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
524
595
|
}
|
|
525
596
|
|
|
526
597
|
function setMode(mode: BwrapMode) {
|
|
598
|
+
if (isSubagentChild) return; // mode switching is not allowed in subagents
|
|
527
599
|
const config = loadConfig(localCwd);
|
|
528
600
|
config.mode = mode;
|
|
529
601
|
resolved = resolveBwrap(config);
|
|
@@ -550,6 +622,11 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
550
622
|
const escalate = params.request_full_access === true;
|
|
551
623
|
|
|
552
624
|
if (escalate) {
|
|
625
|
+
const policy = resolveEscalation({ hasUI: ctx?.hasUI ?? false, isSubagentChild });
|
|
626
|
+
if (policy.kind === "deny") {
|
|
627
|
+
throw new Error(policy.reason);
|
|
628
|
+
}
|
|
629
|
+
|
|
553
630
|
if (ctx?.hasUI) {
|
|
554
631
|
const reason = params.request_full_access_reason;
|
|
555
632
|
const reasonText = reason
|
|
@@ -595,7 +672,9 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
595
672
|
pi.on("session_start", (_event, ctx) => {
|
|
596
673
|
const noBwrap = pi.getFlag("no-bwrap") === true;
|
|
597
674
|
|
|
598
|
-
|
|
675
|
+
// Subagent sessions are always sandboxed read-only; --no-bwrap must not
|
|
676
|
+
// disable that (the flag is only honored in the parent session).
|
|
677
|
+
if (noBwrap && !isSubagentChild) {
|
|
599
678
|
resolved = null;
|
|
600
679
|
ctx.ui.notify("bwrap sandbox disabled via --no-bwrap", "warning");
|
|
601
680
|
return;
|
|
@@ -612,7 +691,7 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
612
691
|
}
|
|
613
692
|
|
|
614
693
|
const config = loadConfig(ctx.cwd);
|
|
615
|
-
resolved = resolveBwrap(config);
|
|
694
|
+
resolved = isSubagentChild ? resolveSubagentBwrap(config) : resolveBwrap(config);
|
|
616
695
|
|
|
617
696
|
if (resolved.bwrapEnabled) {
|
|
618
697
|
try {
|
|
@@ -644,8 +723,9 @@ export default function bwrapExtension(pi: ExtensionAPI) {
|
|
|
644
723
|
const r = getResolved();
|
|
645
724
|
|
|
646
725
|
return {
|
|
647
|
-
systemPrompt:
|
|
648
|
-
event.systemPrompt + "\n\n" +
|
|
726
|
+
systemPrompt: isSubagentChild
|
|
727
|
+
? event.systemPrompt + "\n\n" + SUBAGENT_SANDBOX_PROMPT
|
|
728
|
+
: event.systemPrompt + "\n\n" + SANDBOX_PROMPT + `\n\nCurrent mode: **${r.mode}**\n`,
|
|
649
729
|
};
|
|
650
730
|
});
|
|
651
731
|
|
package/src/workspace-guard.ts
CHANGED
|
@@ -27,6 +27,13 @@ import { normalizeForEdit, replace } from "./opencode-edit-engine.js";
|
|
|
27
27
|
const WRITE_TOOLS = new Set(["write", "edit"]);
|
|
28
28
|
const ALWAYS_ALLOW = ["/tmp"];
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* pi-subagents sets PI_SUBAGENT_CHILD=1 in every spawned child session.
|
|
32
|
+
* Subagents are headless, so there is no approval path: writes outside the
|
|
33
|
+
* workspace are rejected outright instead of asking for approval.
|
|
34
|
+
*/
|
|
35
|
+
const isSubagentChild = process.env.PI_SUBAGENT_CHILD === "1";
|
|
36
|
+
|
|
30
37
|
/** Maximum lines of the diff preview shown in the approval dialog. */
|
|
31
38
|
const MAX_PREVIEW_LINES = 100;
|
|
32
39
|
|
|
@@ -173,15 +180,48 @@ export async function buildDiffPreview(
|
|
|
173
180
|
return wrapDiff(generateUnifiedPatch(basename(resolvedPath), oldContent, newContent, 2));
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
export interface WriteGuardDecision {
|
|
184
|
+
block: boolean;
|
|
185
|
+
reason?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Decide how to handle a write outside the workspace.
|
|
190
|
+
* Subagent sessions (and any other headless session) have no approval path,
|
|
191
|
+
* so the write is rejected outright.
|
|
192
|
+
*/
|
|
193
|
+
export function decideOutsideWorkspaceWrite(
|
|
194
|
+
rawPath: string,
|
|
195
|
+
opts: { hasUI: boolean; isSubagentChild: boolean },
|
|
196
|
+
): WriteGuardDecision {
|
|
197
|
+
if (opts.isSubagentChild) {
|
|
198
|
+
return {
|
|
199
|
+
block: true,
|
|
200
|
+
reason: `Path "${rawPath}" is outside the subagent workspace. Writes outside the workspace are rejected in subagent sessions; ask the parent session to apply this change.`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (!opts.hasUI) {
|
|
204
|
+
return {
|
|
205
|
+
block: true,
|
|
206
|
+
reason: `Path "${rawPath}" is outside workspace. No UI available for approval.`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return { block: false };
|
|
210
|
+
}
|
|
211
|
+
|
|
176
212
|
export default function workspaceGuard(pi: ExtensionAPI) {
|
|
177
213
|
pi.on("before_agent_start", (event, ctx) => {
|
|
178
214
|
const currentCwd = ctx.cwd;
|
|
179
215
|
return {
|
|
180
|
-
systemPrompt:
|
|
181
|
-
event.systemPrompt +
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
systemPrompt: isSubagentChild
|
|
217
|
+
? event.systemPrompt +
|
|
218
|
+
`\nSubagent write protection is active. ` +
|
|
219
|
+
`write and edit are allowed inside "${currentCwd}" and /tmp. ` +
|
|
220
|
+
`Writes outside the workspace are rejected without approval — ask the parent session to make such changes.`
|
|
221
|
+
: event.systemPrompt +
|
|
222
|
+
`\nWorkspace write protection is active. ` +
|
|
223
|
+
`write and edit to paths inside the workspace "${currentCwd}" or /tmp are auto-allowed. ` +
|
|
224
|
+
`Paths outside require user approval before execution.`,
|
|
185
225
|
};
|
|
186
226
|
});
|
|
187
227
|
|
|
@@ -195,12 +235,11 @@ export default function workspaceGuard(pi: ExtensionAPI) {
|
|
|
195
235
|
|
|
196
236
|
if (isPathAllowed(resolved, ctx.cwd)) return;
|
|
197
237
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
238
|
+
const decision = decideOutsideWorkspaceWrite(rawPath, {
|
|
239
|
+
hasUI: ctx.hasUI,
|
|
240
|
+
isSubagentChild,
|
|
241
|
+
});
|
|
242
|
+
if (decision.block) return decision;
|
|
204
243
|
|
|
205
244
|
let choice: string | undefined;
|
|
206
245
|
while (!choice) {
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AGENTS.md User Message
|
|
3
|
-
*
|
|
4
|
-
* Moves project-level AGENTS.md from system prompt to user message.
|
|
5
|
-
* Global ~/.pi/agent/AGENTS.md stays in system prompt.
|
|
6
|
-
*
|
|
7
|
-
* Requires --no-context-files to disable pi's default context file loading.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* pi -e ./agents-md-user-message --no-context-files
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { getAgentDir, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
|
|
16
|
-
export default function agentsMdUserMessage(pi: ExtensionAPI) {
|
|
17
|
-
let messageInjected = false;
|
|
18
|
-
|
|
19
|
-
pi.on("before_agent_start", (event) => {
|
|
20
|
-
const contextFiles = event.systemPromptOptions.contextFiles ?? [];
|
|
21
|
-
if (contextFiles.length > 0) return; // pi already handled them
|
|
22
|
-
|
|
23
|
-
// --no-context-files was used, we handle everything
|
|
24
|
-
const files = loadProjectContextFiles({
|
|
25
|
-
cwd: event.systemPromptOptions.cwd,
|
|
26
|
-
agentDir: getAgentDir(),
|
|
27
|
-
});
|
|
28
|
-
if (files.length === 0) return;
|
|
29
|
-
|
|
30
|
-
const agentDir = getAgentDir();
|
|
31
|
-
const globalFiles = files.filter((f) => f.path.startsWith(agentDir));
|
|
32
|
-
const projectFiles = files.filter((f) => !f.path.startsWith(agentDir));
|
|
33
|
-
|
|
34
|
-
if (projectFiles.length === 0) return;
|
|
35
|
-
|
|
36
|
-
let systemPrompt = event.systemPrompt;
|
|
37
|
-
if (globalFiles.length > 0) {
|
|
38
|
-
let block = "\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n";
|
|
39
|
-
for (const { path, content } of globalFiles) {
|
|
40
|
-
block += `<project_instructions path="${path}">\n${content}\n</project_instructions>\n\n`;
|
|
41
|
-
}
|
|
42
|
-
block += "</project_context>\n";
|
|
43
|
-
systemPrompt += block;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!messageInjected) {
|
|
47
|
-
messageInjected = true;
|
|
48
|
-
|
|
49
|
-
const content = projectFiles
|
|
50
|
-
.map(
|
|
51
|
-
(f) => `<project_instructions path="${f.path}">\n${f.content}\n</project_instructions>`,
|
|
52
|
-
)
|
|
53
|
-
.join("\n\n");
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
systemPrompt,
|
|
57
|
-
message: {
|
|
58
|
-
customType: "agents-md-user",
|
|
59
|
-
content,
|
|
60
|
-
display: true,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return { systemPrompt };
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
pi.on("session_shutdown", () => {
|
|
69
|
-
messageInjected = false;
|
|
70
|
-
});
|
|
71
|
-
}
|