@trim21/personal-pi-extensions 0.0.167 → 0.0.168

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/spawn-agent.ts +51 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.167",
3
+ "version": "0.0.168",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -23,6 +23,7 @@ import { existsSync } from "node:fs";
23
23
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
24
24
  import { tmpdir } from "node:os";
25
25
  import { basename, dirname, join } from "node:path";
26
+ import { fileURLToPath } from "node:url";
26
27
 
27
28
  import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core";
28
29
  import {
@@ -46,6 +47,24 @@ const DEFAULT_TOOLS = ["read", "grep", "find", "ls"];
46
47
  /** Progress log keeps only the most recent lines (rolling window). */
47
48
  const MAX_PROGRESS_LINES = 5;
48
49
 
50
+ /**
51
+ * Extensions loaded unconditionally into every subagent: the workspace write
52
+ * guard and the bwrap sandbox. Both are protection layers and must not depend
53
+ * on the agent's declared toolset.
54
+ */
55
+ const UNCONDITIONAL_EXTENSIONS = ["workspace-guard.ts", "bwrap/index.ts"] as const;
56
+
57
+ /**
58
+ * Tool → extension override map: when a subagent's frontmatter enables a
59
+ * built-in tool, the matching opencode extension is loaded via `-e` so the
60
+ * subagent uses the enhanced implementation instead of the built-in one.
61
+ */
62
+ const TOOL_EXTENSION_OVERRIDES: Record<string, string> = {
63
+ read: "opencode-read.ts",
64
+ edit: "opencode-edit.ts",
65
+ write: "opencode-write.ts",
66
+ };
67
+
49
68
  // ── schema ───────────────────────────────────────────────────────────────────
50
69
 
51
70
  const spawnAgentSchema = Type.Object({
@@ -131,6 +150,20 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
131
150
  return { command: "pi", args };
132
151
  }
133
152
 
153
+ /**
154
+ * Resolve a sibling extension file (relative to this module) to an absolute
155
+ * path, so `-e` works both when running from the source tree and from an
156
+ * installed pi package (node_modules). A missing extension is fatal: silently
157
+ * skipping a guard (e.g. bwrap) would leave the subagent unprotected.
158
+ */
159
+ function extensionPath(fileName: string): string {
160
+ const abs = fileURLToPath(new URL(fileName, import.meta.url));
161
+ if (!existsSync(abs)) {
162
+ throw new Error(`Extension file not found: ${abs}`);
163
+ }
164
+ return abs;
165
+ }
166
+
134
167
  async function writePromptToTempFile(agentName: string, prompt: string): Promise<string> {
135
168
  const dir = await mkdtemp(join(tmpdir(), "pi-spawn-agent-"));
136
169
  const safeName = agentName.replaceAll(/[^\w.-]+/g, "_");
@@ -147,9 +180,18 @@ export function buildSubagentArgs(
147
180
  systemPromptPath: string | undefined,
148
181
  ): string[] {
149
182
  // --mode json: emit events as JSON lines; -p: single-shot answer;
150
- // --no-session: ephemeral, do not persist. --no-extensions keeps the
151
- // subagent clean (no recursive spawn_agent, no sandbox surprises).
183
+ // --no-session: ephemeral, do not persist. --no-extensions disables
184
+ // extension discovery; only the extensions explicitly loaded below (the
185
+ // unconditional guards plus per-tool overrides) run inside the subagent.
152
186
  const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions"];
187
+
188
+ // Protection layers that must be present in every subagent regardless of
189
+ // its declared toolset: the workspace write guard and the bwrap sandbox
190
+ // (forced read-only for subagents via PI_SUBAGENT_CHILD=1).
191
+ for (const ext of UNCONDITIONAL_EXTENSIONS) {
192
+ args.push("-e", extensionPath(ext));
193
+ }
194
+
153
195
  // Thinking level rides on the model shorthand ("model:level"); it cannot be
154
196
  // set without a model, so a level without a model is ignored.
155
197
  const model =
@@ -159,6 +201,13 @@ export function buildSubagentArgs(
159
201
  if (model) args.push("--model", model);
160
202
  // Read-only default unless the agent explicitly declares a toolset.
161
203
  const tools = agent.tools ?? DEFAULT_TOOLS;
204
+ // Load the opencode override for each built-in tool the agent declares
205
+ // (read/edit/write), so the subagent uses the enhanced implementation
206
+ // instead of the built-in one.
207
+ for (const tool of tools) {
208
+ const ext = TOOL_EXTENSION_OVERRIDES[tool];
209
+ if (ext) args.push("-e", extensionPath(ext));
210
+ }
162
211
  args.push("--tools", tools.join(","));
163
212
  if (systemPromptPath) args.push("--append-system-prompt", systemPromptPath);
164
213
  args.push(`Task: ${task}`);