@danypops/pi-packed 0.28.1 → 0.28.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"smoke.d.ts","sourceRoot":"","sources":["../../service/src/adoption/smoke.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,mBAAmB,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAEpH,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,kBAAkB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAuDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO7F;AAmJD,wBAAsB,iBAAiB,CACtC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAoF/B"}
1
+ {"version":3,"file":"smoke.d.ts","sourceRoot":"","sources":["../../service/src/adoption/smoke.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,mBAAmB,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAEpH,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,kBAAkB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAuDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO7F;AAsLD,wBAAsB,iBAAiB,CACtC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAoF/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.28.1",
3
+ "version": "0.28.3",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,8 @@
42
42
  "@danypops/pi-tui-harness": "^0.0.1",
43
43
  "@danypops/vehicle-client-pi": "^0.43.1",
44
44
  "@danypops/vehicle-conformance": "^0.4.0",
45
- "@types/semver": "^7.7.1"
45
+ "@types/semver": "^7.7.1",
46
+ "follow-redirects": "^1.16.0"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "@danypops/vehicle-client-pi": "^0.43.1",
@@ -99,7 +99,16 @@ async function main(): Promise<void> {
99
99
  const extensionPath = process.argv[2];
100
100
  if (!extensionPath) throw new Error("extension path is required");
101
101
  try {
102
- const jiti = createJiti(import.meta.url, { interopDefault: true, tryNative: false });
102
+ // tryNative matches loader.ts's own real, non-Bun-binary Pi extension loading convention:
103
+ // prefer Bun's native TS/ESM transform, falling back to jiti's own Babel transform only for
104
+ // what native genuinely can't handle. Confirmed live: tryNative:false forced EVERY
105
+ // transitive dependency (including plain CJS libraries that never needed a Babel transform,
106
+ // e.g. follow-redirects) through jiti's Babel path -- which has its own real, reproducible
107
+ // bug interacting with a library that calls Error.captureStackTrace on a not-yet-initialized
108
+ // Error instance during module-level prototype setup (confirmed outside this sandbox too,
109
+ // given a cold jiti transform cache), surfacing as an opaque "First argument must be an
110
+ // Error object" crash unrelated to the extension's own code.
111
+ const jiti = createJiti(import.meta.url, { interopDefault: true, tryNative: true });
103
112
  const factory = (await jiti.import(extensionPath, { default: true })) as unknown;
104
113
  if (typeof factory !== "function") throw new Error("extension has no default factory export");
105
114
  await factory(api);
@@ -1,4 +1,4 @@
1
- import { existsSync, realpathSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -105,6 +105,41 @@ export function resolveDependencyModulesDir(fromDir: string, name: string): stri
105
105
  }
106
106
  }
107
107
 
108
+ const MAX_PROCESS_SCAN_ENTRIES = 100_000;
109
+
110
+ /**
111
+ * RLIMIT_NPROC (prlimit's own --nproc) is a per-real-UID, system-wide limit -- NOT scoped to
112
+ * this sandbox's own process tree. A small absolute constant breaks the instant the invoking
113
+ * user's REAL total process count already exceeds it (routine on a heavily-used workstation
114
+ * with several long-running agent sessions): any further thread/fork Bun's own runtime needs
115
+ * internally (confirmed live: its native TS transpiler spawns one) immediately fails, surfacing
116
+ * as an unrecoverable SIGABRT (exit 134) with no output at all, not a catchable error. Correct
117
+ * enforcement of "at most N NEW processes the sandboxed extension can spawn" requires an offset
118
+ * from the CURRENT real count, not an absolute cap. Returns 0 (never throws) if /proc can't be
119
+ * read at all -- the caller's own maxProcesses then applies as an absolute floor, same as before
120
+ * this existed.
121
+ */
122
+ function currentProcessCountForUid(uid: number): number {
123
+ let entries: string[];
124
+ try {
125
+ entries = readdirSync("/proc");
126
+ } catch {
127
+ return 0;
128
+ }
129
+ let count = 0;
130
+ for (const entry of entries.slice(0, MAX_PROCESS_SCAN_ENTRIES)) {
131
+ if (!/^\d+$/.test(entry)) continue;
132
+ try {
133
+ const status = readFileSync(`/proc/${entry}/status`, "utf8");
134
+ const match = status.match(/^Uid:\s+(\d+)/m);
135
+ if (match && Number(match[1]) === uid) count++;
136
+ } catch {
137
+ // The process exited mid-scan, or is otherwise unreadable -- not this sandbox's concern.
138
+ }
139
+ }
140
+ return count;
141
+ }
142
+
108
143
  function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses: number): string[] | undefined {
109
144
  if (process.platform !== "linux" || !existsSync("/usr/bin/bwrap") || !existsSync("/usr/bin/prlimit")) return undefined;
110
145
  const serviceRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
@@ -185,7 +220,7 @@ function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses
185
220
  packageRoot,
186
221
  "--",
187
222
  "/usr/bin/prlimit",
188
- `--nproc=${maxProcesses}`,
223
+ `--nproc=${(typeof process.getuid === "function" ? currentProcessCountForUid(process.getuid()) : 0) + maxProcesses}`,
189
224
  "--nofile=64",
190
225
  "--fsize=16777216",
191
226
  "--as=8589934592",
@@ -1,8 +1,9 @@
1
1
  import { afterEach, describe, expect, it } from "bun:test";
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
- import { join } from "node:path";
5
+ import { dirname, join } from "node:path";
6
+ import { createRequire } from "node:module";
6
7
  import { checkPackage } from "../src/adoption/check.ts";
7
8
  import { resolveDependencyModulesDir, runExtensionSmoke } from "../src/adoption/smoke.ts";
8
9
 
@@ -180,6 +181,56 @@ describeIfSandboxed("isolated extension smoke runner", () => {
180
181
  expect(result.registrations.commands).toEqual(["hoisted-sibling"]);
181
182
  });
182
183
 
184
+ it("never routes a plain CJS dependency through jiti's own Babel transform path -- confirmed live: forcing every transitive dependency through Babel (tryNative:false) hit a real, reproducible upstream bug in jiti's own Babel-installed Error.prepareStackTrace hook, triggered by follow-redirects' own module-level Error.captureStackTrace call during its chained error-type prototype setup, surfacing as an opaque 'First argument must be an Error object' crash unrelated to the scanned extension's own code -- reproduces with a cold jiti transform cache regardless of this sandbox specifically, so a real npm package (not a synthetic fixture) is the only faithful way to prove it stays fixed", async () => {
185
+ const require = createRequire(import.meta.url);
186
+ const followRedirectsDir = dirname(require.resolve("follow-redirects/package.json"));
187
+
188
+ const workspaceRoot = mkdtempSync(join(tmpdir(), "packed-smoke-follow-redirects-"));
189
+ roots.push(workspaceRoot);
190
+ const sharedNodeModules = join(workspaceRoot, "node_modules");
191
+ cpSync(followRedirectsDir, join(sharedNodeModules, "follow-redirects"), { recursive: true });
192
+ const packageDir = join(sharedNodeModules, "@fixture", "pi-smoke-follow-redirects");
193
+ mkdirSync(join(packageDir, "extension"), { recursive: true });
194
+ const extensionPath = join(packageDir, "extension", "index.ts");
195
+ writeFileSync(
196
+ extensionPath,
197
+ 'import { http } from "follow-redirects";\nexport default function (pi: any) { pi.registerCommand(String(typeof http), { handler() {} }); }\n',
198
+ );
199
+ writeFileSync(
200
+ join(packageDir, "package.json"),
201
+ JSON.stringify({
202
+ name: "@fixture/pi-smoke-follow-redirects",
203
+ version: "1.0.0",
204
+ keywords: ["pi-package"],
205
+ files: ["extension"],
206
+ pi: { extensions: ["extension/index.ts"] },
207
+ }),
208
+ );
209
+
210
+ const result = await runExtensionSmoke(packageDir, extensionPath);
211
+
212
+ expect(result.status).toBe("ok");
213
+ expect(result.registrations.commands).toEqual(["object"]);
214
+ });
215
+
216
+ it("never breaks under a real per-UID process count that already exceeds the sandbox's own maxProcesses budget -- confirmed live: prlimit's --nproc is a per-real-UID, system-wide limit, not scoped to this sandbox's own process tree, so a small absolute cap unconditionally breaks the instant the invoking user's REAL total process count exceeds it (routine on a busy multi-session workstation) -- any further thread Bun's own runtime needs internally (its native TS transpiler spawns one) immediately fails, aborting with SIGABRT and zero output, not a catchable error", async () => {
217
+ const dummyProcessCount = 40;
218
+ const dummies = Array.from({ length: dummyProcessCount }, () => Bun.spawn(["/usr/bin/sleep", "20"], { stdin: "ignore", stdout: "ignore", stderr: "ignore" }));
219
+ try {
220
+ const fixture = extension(`export default function (pi: any) {
221
+ pi.registerCommand("hello", { handler() {} });
222
+ }`);
223
+
224
+ const result = await runExtensionSmoke(fixture.root, fixture.path, { maxProcesses: 5 });
225
+
226
+ expect(result.status).toBe("ok");
227
+ expect(result.registrations.commands).toEqual(["hello"]);
228
+ } finally {
229
+ for (const dummy of dummies) dummy.kill("SIGKILL");
230
+ await Promise.all(dummies.map((dummy) => dummy.exited));
231
+ }
232
+ });
233
+
183
234
  it("keeps default package checks static and adds smoke results only when requested", async () => {
184
235
  const fixture = extension(
185
236
  'import { writeFileSync } from "node:fs"; export default function (pi: any) { writeFileSync("executed", "yes"); pi.registerCommand("x", { handler() {} }); }',