@erichll/pi-sandbox 0.21.0 → 0.22.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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.0 - 2026-09-30
4
+
5
+ - Raise the `@earendil-works/pi-coding-agent` peer floor to `^0.99.1` and the
6
+ development baseline to `^0.99.1`. Validated against Pi 0.99.1 with
7
+ `tsc --noEmit` and the test suite (92 pass, 1 macOS-only skip); the shipped
8
+ `src/` uses no API that changed between 0.87 and 0.99.
9
+ - Adapt two test fixtures to the 0.99 extension surface: `ToolInfo` now requires
10
+ `exposure`, and tool handlers receive `ExtensionToolContext` (`tools`,
11
+ `executeTool`). The shipped extension uses neither field, so runtime behavior
12
+ is unchanged.
13
+ - Raise the `pi-subagents` development dependency to `^0.72.1` and revalidate
14
+ the native-background loader against the published 0.72.1 package:
15
+ `discoverAgents` and `resolveAgentName` keep their signatures,
16
+ `src/agents/agents` only gains an optional `options.globalNpmRoot` argument,
17
+ `typebox` moved from a hard dependency to an optional host peer, and the
18
+ `./capability-ceiling` export still resolves to `.js`.
19
+ - Depend on `@erichll/pi-auto-review` `^0.22.0`.
20
+ - Document the three-way `subagents.provider` choice (`builtin` / `off` /
21
+ `pi-subagents`) with copy-ready example configs, including the duplicate
22
+ `subagent` tool that makes `builtin` unavailable whenever the pi-subagents
23
+ extension is loaded, and the scheduled-runs-disabled pi-subagents config that
24
+ protected mode requires.
25
+ - Bump the development `typescript` baseline to `^7.0.2`.
26
+
27
+ ## 0.21.1 - 2026-09-26
28
+
29
+ - Alias `@earendil-works/pi-tui` for the `jiti` instance that loads the
30
+ `pi-subagents` internals. `pi-subagents` imports that host peer from its
31
+ internal modules but does not ship it, and Pi's own extension loader aliases
32
+ it to the copy inside the running Pi package. The protected-mode loader
33
+ created its own `jiti` instance without that alias, so installs where the peer
34
+ is not hoisted next to the extension (for example the global
35
+ `~/.pi/agent/npm` tree) failed to load as soon as `subagents.provider` was
36
+ `pi-subagents` with `pi-subagents compatibility failure: Cannot find module
37
+ '@earendil-works/pi-tui'`. The alias is computed host-first from the running Pi
38
+ package (`process.argv[1]`, `PI_PACKAGE_DIR`) and falls back to plain
39
+ resolution from the extension tree; an unresolvable host peer still fails
40
+ closed with that explicit error.
41
+ - Add regression coverage for host alias resolution, including a `jiti` load in
42
+ a tree where `@earendil-works/pi-tui` is unreachable without the alias.
43
+
3
44
  ## 0.21.0 - 2026-09-25
4
45
 
5
46
  - Support project-level configuration at `.pi/extensions/pi-sandbox/config.json`.
package/README.md CHANGED
@@ -92,9 +92,12 @@ Supported modes:
92
92
  ### `pi-subagents` native-background tool boundary
93
93
 
94
94
  This provider supports the `pi-subagents >=0.66.0` line (validated through
95
- 0.71.0) and fails closed if its public ceiling API, module layout, or internal
95
+ 0.72.1) and fails closed if its public ceiling API, module layout, or internal
96
96
  discovery layout drifts. The peer dependency is a floor with no upper pin; the
97
- loader's explicit export and layout checks are the real gate.
97
+ loader's explicit export and layout checks are the real gate. The loader also
98
+ aliases `@earendil-works/pi-tui`, a host peer that the `pi-subagents` internals
99
+ import but do not ship, to the copy inside the running Pi package, so installs
100
+ where it is not hoisted next to the extension still load.
98
101
  Configure both the protection mode and a non-empty canonical whitelist:
99
102
 
100
103
  ```json
@@ -293,7 +296,7 @@ directory. It never reads or updates production Pi configuration. Model-backed
293
296
  acceptance requires `PI_SUBAGENTS_GATE_MODEL` and an already-exported matching
294
297
  credential; missing prerequisites are reported as `SKIP`. Protected mode with
295
298
  pi-subagents 0.68.0 or newer needs Pi 0.86.0 or newer. The development
296
- baseline is validated against Pi 0.87.1.
299
+ baseline is validated against Pi 0.99.1.
297
300
 
298
301
  The test suite covers real Linux Sandbox Runtime enforcement when its native
299
302
  dependencies are installed, plus deterministic broker, network approval,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-sandbox",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Linux (bubblewrap) and macOS (Seatbelt) process sandbox for Pi backed by Anthropic's @anthropic-ai/sandbox-runtime. Fail-closed static filesystem policy plus per-connection network approval from pi-auto-review; sandboxes main-agent Bash and built-in subagent process trees.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -51,11 +51,11 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@anthropic-ai/sandbox-runtime": "^0.0.77",
54
- "@erichll/pi-auto-review": "^0.21.0",
54
+ "@erichll/pi-auto-review": "^0.22.0",
55
55
  "jiti": "2.7.0"
56
56
  },
57
57
  "peerDependencies": {
58
- "@earendil-works/pi-coding-agent": "^0.87.1",
58
+ "@earendil-works/pi-coding-agent": "^0.99.1",
59
59
  "pi-subagents": ">=0.66.0",
60
60
  "typebox": ">=1.0.0"
61
61
  },
@@ -65,12 +65,12 @@
65
65
  }
66
66
  },
67
67
  "devDependencies": {
68
- "@earendil-works/pi-ai": "^0.87.1",
69
- "@earendil-works/pi-coding-agent": "^0.87.1",
70
- "@earendil-works/pi-server": "^0.87.1",
68
+ "@earendil-works/pi-ai": "^0.99.1",
69
+ "@earendil-works/pi-coding-agent": "^0.99.1",
70
+ "@earendil-works/pi-server": "^0.99.1",
71
71
  "@types/node": "^26.6.2",
72
- "pi-subagents": "^0.71.0",
72
+ "pi-subagents": "^0.72.1",
73
73
  "typebox": "^1.3.34",
74
- "typescript": "^6.0.3"
74
+ "typescript": "^7.0.2"
75
75
  }
76
76
  }
@@ -1,4 +1,5 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
2
3
  import { dirname, join } from "node:path";
3
4
  import { fileURLToPath, pathToFileURL } from "node:url";
4
5
  import { createJiti } from "jiti";
@@ -15,15 +16,25 @@ import { createJiti } from "jiti";
15
16
  * 2026-09-23 against the published 0.71.0 package: the ceiling implementation
16
17
  * and the config loader are byte-identical to 0.70.0, `discoverAgents` and
17
18
  * `resolveAgentName` keep their signatures, and the `./capability-ceiling`
18
- * export still resolves to `.js`). Protected mode needs Pi 0.86.0 or newer
19
+ * export still resolves to `.js`), and revalidated 2026-09-27 against the
20
+ * published 0.72.1 package: the ceiling implementation and the config loader
21
+ * are still byte-identical to 0.70.0, `discoverAgents` and `resolveAgentName`
22
+ * keep their signatures, `src/agents/agents` only gains an optional
23
+ * `options.globalNpmRoot` argument, `typebox` moved from a hard dependency to
24
+ * an optional host peer, and the `./capability-ceiling` export still resolves
25
+ * to `.js`). Protected mode needs Pi 0.86.0 or newer
19
26
  * (the pi-subagents 0.68.0+ floor): 0.85.0 does not ship
20
27
  * `@earendil-works/pi-server`, so background children fail to launch there.
21
- * This package's own peer floor is Pi 0.87.1 (validated 2026-09-23).
28
+ * This package's own peer floor is Pi 0.99.1 (validated 2026-09-30).
22
29
  */
23
30
  export const PI_SUBAGENTS_COMPAT_RANGE = ">=0.66.0";
24
31
  export const NATIVE_CHILD_TOOLS = ["bash", "read", "grep", "find", "ls"] as const;
25
32
  export const PI_SANDBOX_ACKNOWLEDGEMENT = "@erichll:pi-sandbox";
26
33
 
34
+ const HOST_PI_PACKAGE = "@earendil-works/pi-coding-agent";
35
+ /** Host peer that pi-subagents' internal modules import but that is not installed next to it. */
36
+ const HOST_PI_TUI_PACKAGE = "@earendil-works/pi-tui";
37
+
27
38
  /**
28
39
  * Module extension of the installed pi-subagents layout. Source checkouts (and
29
40
  * the 0.66.0-0.69.0 npm line) ship `.ts` modules that need a TypeScript loader;
@@ -142,6 +153,89 @@ export function piSubagentsInternalModulePath(
142
153
  return `${relativePath.replace(/\.(?:ts|js)$/u, "")}${extension}`;
143
154
  }
144
155
 
156
+ async function readPackageName(dir: string): Promise<string | undefined> {
157
+ try {
158
+ const manifest = JSON.parse(
159
+ await readFile(join(dir, "package.json"), "utf8"),
160
+ ) as { name?: unknown };
161
+ return typeof manifest?.name === "string" ? manifest.name : undefined;
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Walk up from a Pi entry script (or a Pi package directory) to the running
169
+ * `@earendil-works/pi-coding-agent` root. Returns undefined for launchers whose
170
+ * entry is not a real file (compiled binaries, embedded SDK hosts).
171
+ */
172
+ export async function findPiPackageRootFromEntry(
173
+ entryPath: string,
174
+ ): Promise<string | undefined> {
175
+ let dir = entryPath;
176
+ for (;;) {
177
+ if ((await readPackageName(dir)) === HOST_PI_PACKAGE) return dir;
178
+ const parent = dirname(dir);
179
+ if (parent === dir) return undefined;
180
+ dir = parent;
181
+ }
182
+ }
183
+
184
+ function resolveFrom(base: string, specifier: string): string | undefined {
185
+ try {
186
+ return createRequire(base).resolve(specifier);
187
+ } catch {
188
+ return undefined;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Alias map for the jiti instance that loads pi-subagents internals.
194
+ *
195
+ * pi-subagents imports `@earendil-works/pi-tui` from its internal modules but,
196
+ * as a peer dependency, does not ship it. Pi's own extension loader aliases that
197
+ * specifier to the copy inside the running Pi package (`getAliases()` in the
198
+ * host loader), so extensions loaded by Pi resolve it; our jiti instance is a
199
+ * separate resolver and would otherwise fall back to plain Node resolution,
200
+ * which fails for installs where pi-tui is neither hoisted next to the
201
+ * extension nor installed beside `@earendil-works/pi-subagents`.
202
+ *
203
+ * The alias is computed host-first so the loaded modules share the host's
204
+ * pi-tui instance, and degrades to an empty map when no copy is reachable: the
205
+ * hard failure then stays the explicit compatibility error raised on load.
206
+ */
207
+ export async function resolvePiSubagentsHostAliases(
208
+ options: { entry?: string; moduleUrl?: string; env?: Record<string, string | undefined> } = {},
209
+ ): Promise<Record<string, string>> {
210
+ const bases: string[] = [];
211
+ const entry = options.entry ?? process.argv[1];
212
+ if (entry) {
213
+ const realEntry = await realpath(entry).catch(() => undefined);
214
+ const root = realEntry
215
+ ? await findPiPackageRootFromEntry(realEntry)
216
+ : undefined;
217
+ if (root) bases.push(join(root, "package.json"));
218
+ }
219
+ const packageDir = (options.env ?? process.env).PI_PACKAGE_DIR?.trim();
220
+ if (packageDir && (await readPackageName(packageDir)) === HOST_PI_PACKAGE) {
221
+ bases.push(join(packageDir, "package.json"));
222
+ }
223
+ // Extension-local resolution: the development checkout and installs that
224
+ // provide pi-tui beside the extension or beside pi-subagents.
225
+ bases.push(options.moduleUrl ?? import.meta.url);
226
+
227
+ const seen = new Set<string>();
228
+ for (const base of bases) {
229
+ if (seen.has(base)) continue;
230
+ seen.add(base);
231
+ const resolved = resolveFrom(base, HOST_PI_TUI_PACKAGE);
232
+ if (!resolved) continue;
233
+ const target = await realpath(resolved).catch(() => resolved);
234
+ return { [HOST_PI_TUI_PACKAGE]: target };
235
+ }
236
+ return {};
237
+ }
238
+
145
239
  /** Load the public ceiling API and the discovery/config internals.
146
240
  * This intentionally fails closed on package version, export, or layout drift.
147
241
  */
@@ -163,6 +257,7 @@ export async function loadPiSubagentsNativeRuntime(
163
257
  const jiti = createJiti(import.meta.url, {
164
258
  interopDefault: false,
165
259
  fsCache: false,
260
+ alias: await resolvePiSubagentsHostAliases(),
166
261
  });
167
262
  const internal = (relative: string): string =>
168
263
  pathToFileURL(join(root, piSubagentsInternalModulePath(relative, extension))).href;