@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;
|
|
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.
|
|
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
|
-
|
|
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() {} }); }',
|