@erichll/pi-sandbox 0.21.0 → 0.21.1

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.1 - 2026-09-26
4
+
5
+ - Alias `@earendil-works/pi-tui` for the `jiti` instance that loads the
6
+ `pi-subagents` internals. `pi-subagents` imports that host peer from its
7
+ internal modules but does not ship it, and Pi's own extension loader aliases
8
+ it to the copy inside the running Pi package. The protected-mode loader
9
+ created its own `jiti` instance without that alias, so installs where the peer
10
+ is not hoisted next to the extension (for example the global
11
+ `~/.pi/agent/npm` tree) failed to load as soon as `subagents.provider` was
12
+ `pi-subagents` with `pi-subagents compatibility failure: Cannot find module
13
+ '@earendil-works/pi-tui'`. The alias is computed host-first from the running Pi
14
+ package (`process.argv[1]`, `PI_PACKAGE_DIR`) and falls back to plain
15
+ resolution from the extension tree; an unresolvable host peer still fails
16
+ closed with that explicit error.
17
+ - Add regression coverage for host alias resolution, including a `jiti` load in
18
+ a tree where `@earendil-works/pi-tui` is unreachable without the alias.
19
+
3
20
  ## 0.21.0 - 2026-09-25
4
21
 
5
22
  - Support project-level configuration at `.pi/extensions/pi-sandbox/config.json`.
package/README.md CHANGED
@@ -94,7 +94,10 @@ Supported modes:
94
94
  This provider supports the `pi-subagents >=0.66.0` line (validated through
95
95
  0.71.0) 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-sandbox",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
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",
@@ -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";
@@ -24,6 +25,10 @@ export const PI_SUBAGENTS_COMPAT_RANGE = ">=0.66.0";
24
25
  export const NATIVE_CHILD_TOOLS = ["bash", "read", "grep", "find", "ls"] as const;
25
26
  export const PI_SANDBOX_ACKNOWLEDGEMENT = "@erichll:pi-sandbox";
26
27
 
28
+ const HOST_PI_PACKAGE = "@earendil-works/pi-coding-agent";
29
+ /** Host peer that pi-subagents' internal modules import but that is not installed next to it. */
30
+ const HOST_PI_TUI_PACKAGE = "@earendil-works/pi-tui";
31
+
27
32
  /**
28
33
  * Module extension of the installed pi-subagents layout. Source checkouts (and
29
34
  * the 0.66.0-0.69.0 npm line) ship `.ts` modules that need a TypeScript loader;
@@ -142,6 +147,89 @@ export function piSubagentsInternalModulePath(
142
147
  return `${relativePath.replace(/\.(?:ts|js)$/u, "")}${extension}`;
143
148
  }
144
149
 
150
+ async function readPackageName(dir: string): Promise<string | undefined> {
151
+ try {
152
+ const manifest = JSON.parse(
153
+ await readFile(join(dir, "package.json"), "utf8"),
154
+ ) as { name?: unknown };
155
+ return typeof manifest?.name === "string" ? manifest.name : undefined;
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Walk up from a Pi entry script (or a Pi package directory) to the running
163
+ * `@earendil-works/pi-coding-agent` root. Returns undefined for launchers whose
164
+ * entry is not a real file (compiled binaries, embedded SDK hosts).
165
+ */
166
+ export async function findPiPackageRootFromEntry(
167
+ entryPath: string,
168
+ ): Promise<string | undefined> {
169
+ let dir = entryPath;
170
+ for (;;) {
171
+ if ((await readPackageName(dir)) === HOST_PI_PACKAGE) return dir;
172
+ const parent = dirname(dir);
173
+ if (parent === dir) return undefined;
174
+ dir = parent;
175
+ }
176
+ }
177
+
178
+ function resolveFrom(base: string, specifier: string): string | undefined {
179
+ try {
180
+ return createRequire(base).resolve(specifier);
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Alias map for the jiti instance that loads pi-subagents internals.
188
+ *
189
+ * pi-subagents imports `@earendil-works/pi-tui` from its internal modules but,
190
+ * as a peer dependency, does not ship it. Pi's own extension loader aliases that
191
+ * specifier to the copy inside the running Pi package (`getAliases()` in the
192
+ * host loader), so extensions loaded by Pi resolve it; our jiti instance is a
193
+ * separate resolver and would otherwise fall back to plain Node resolution,
194
+ * which fails for installs where pi-tui is neither hoisted next to the
195
+ * extension nor installed beside `@earendil-works/pi-subagents`.
196
+ *
197
+ * The alias is computed host-first so the loaded modules share the host's
198
+ * pi-tui instance, and degrades to an empty map when no copy is reachable: the
199
+ * hard failure then stays the explicit compatibility error raised on load.
200
+ */
201
+ export async function resolvePiSubagentsHostAliases(
202
+ options: { entry?: string; moduleUrl?: string; env?: Record<string, string | undefined> } = {},
203
+ ): Promise<Record<string, string>> {
204
+ const bases: string[] = [];
205
+ const entry = options.entry ?? process.argv[1];
206
+ if (entry) {
207
+ const realEntry = await realpath(entry).catch(() => undefined);
208
+ const root = realEntry
209
+ ? await findPiPackageRootFromEntry(realEntry)
210
+ : undefined;
211
+ if (root) bases.push(join(root, "package.json"));
212
+ }
213
+ const packageDir = (options.env ?? process.env).PI_PACKAGE_DIR?.trim();
214
+ if (packageDir && (await readPackageName(packageDir)) === HOST_PI_PACKAGE) {
215
+ bases.push(join(packageDir, "package.json"));
216
+ }
217
+ // Extension-local resolution: the development checkout and installs that
218
+ // provide pi-tui beside the extension or beside pi-subagents.
219
+ bases.push(options.moduleUrl ?? import.meta.url);
220
+
221
+ const seen = new Set<string>();
222
+ for (const base of bases) {
223
+ if (seen.has(base)) continue;
224
+ seen.add(base);
225
+ const resolved = resolveFrom(base, HOST_PI_TUI_PACKAGE);
226
+ if (!resolved) continue;
227
+ const target = await realpath(resolved).catch(() => resolved);
228
+ return { [HOST_PI_TUI_PACKAGE]: target };
229
+ }
230
+ return {};
231
+ }
232
+
145
233
  /** Load the public ceiling API and the discovery/config internals.
146
234
  * This intentionally fails closed on package version, export, or layout drift.
147
235
  */
@@ -163,6 +251,7 @@ export async function loadPiSubagentsNativeRuntime(
163
251
  const jiti = createJiti(import.meta.url, {
164
252
  interopDefault: false,
165
253
  fsCache: false,
254
+ alias: await resolvePiSubagentsHostAliases(),
166
255
  });
167
256
  const internal = (relative: string): string =>
168
257
  pathToFileURL(join(root, piSubagentsInternalModulePath(relative, extension))).href;