pi-oracle 0.7.21 → 0.7.22

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 (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +49 -385
  3. package/docs/ORACLE_DESIGN.md +1 -1
  4. package/docs/development.md +99 -0
  5. package/docs/platform-smoke.md +3 -3
  6. package/docs/reference.md +331 -0
  7. package/extensions/oracle/index.ts +1 -0
  8. package/extensions/oracle/lib/poller.ts +3 -8
  9. package/extensions/oracle/lib/queue.ts +2 -4
  10. package/extensions/oracle/shared/browser-profile-helpers.d.mts +1 -0
  11. package/extensions/oracle/shared/browser-profile-helpers.mjs +32 -0
  12. package/extensions/oracle/worker/auth-bootstrap.mjs +35 -6
  13. package/extensions/oracle/worker/chromium-cookie-source.mjs +50 -6
  14. package/extensions/oracle/worker/linux-chromium-cookie-source.d.mts +6 -0
  15. package/extensions/oracle/worker/linux-chromium-cookie-source.mjs +98 -0
  16. package/extensions/oracle/worker/run-job.mjs +10 -5
  17. package/package.json +12 -11
  18. package/platform-smoke.config.mjs +1 -22
  19. package/scripts/oracle-auth.test.mjs +35 -0
  20. package/scripts/oracle-chatgpt-preset-proof.mjs +19 -12
  21. package/scripts/oracle-cookie-import.test.mjs +204 -0
  22. package/scripts/oracle-model.test.mjs +5 -1
  23. package/scripts/oracle-real-smoke.mjs +17 -7
  24. package/scripts/platform-smoke/Dockerfile.ubuntu +3 -2
  25. package/scripts/platform-smoke/crabbox-runner.mjs +3 -5
  26. package/scripts/platform-smoke/doctor.mjs +9 -24
  27. package/scripts/platform-smoke/invariants.mjs +234 -22
  28. package/scripts/platform-smoke/platform-build-windows.ps1 +4 -16
  29. package/scripts/platform-smoke/targets.mjs +35 -27
  30. package/scripts/platform-smoke.mjs +2 -2
@@ -0,0 +1,204 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { createCipheriv, createHash, pbkdf2Sync } from "node:crypto";
4
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join, relative } from "node:path";
7
+ import test from "node:test";
8
+
9
+ // Independent standard Chromium v24 SQLite/v11 fixture; the real auth worker
10
+ // reads/decrypts/filters it and replays into a fake browser, never a real one.
11
+ test("Linux auth imports native Chromium with its own key and preserves Chrome/Brave", {
12
+ skip: process.platform !== "linux", timeout: 90_000,
13
+ }, async (t) => {
14
+ const { DatabaseSync } = await import("node:sqlite");
15
+ const { detectDefaultLinuxCookieProfileSource } = await import("../extensions/oracle/shared/browser-profile-helpers.mjs");
16
+ const root = await mkdtemp(join(tmpdir(), "oracle-cookie-import-"));
17
+ t.after(() => rm(root, { recursive: true, force: true }));
18
+ const home = join(root, "home");
19
+ const configHome = join(home, ".config");
20
+ const bin = join(root, "bin");
21
+ const helperLog = join(root, "helpers.jsonl");
22
+ const browserLog = join(root, "browser.jsonl");
23
+ await mkdir(bin, { recursive: true });
24
+ const guard = `
25
+ import { appendFileSync, mkdirSync } from 'node:fs';
26
+ import { basename } from 'node:path';
27
+ for (const [name, value] of Object.entries(process.env)) {
28
+ if (name.startsWith('SWEET_COOKIE_') && name.endsWith('_PASSWORD') && value) {
29
+ throw Error('safe-storage password leaked to subprocess');
30
+ }
31
+ }
32
+ const args = process.argv.slice(2);
33
+ `;
34
+ const helper = `#!${process.execPath}
35
+ ${guard}
36
+ const command = basename(process.argv[1]);
37
+ appendFileSync(process.env.HELPER_LOG, JSON.stringify({ command, args }) + '\\n');
38
+ const mode = process.env.FIXTURE_MODE;
39
+ if (mode === 'timeout') { process.on('SIGTERM', () => {}); setInterval(() => {}, 1000); }
40
+ else if (mode === 'failure') { console.error('fixture-key-do-not-log'); process.exit(1); }
41
+ else if (command === 'dbus-send') {
42
+ if (mode === 'wallet-fallback') process.exit(1);
43
+ console.log('"fixture-wallet"');
44
+ } else if (command === 'kwallet-query') {
45
+ if (mode === 'wallet-empty') console.log('failed to read password');
46
+ else if (args.join('|') === '--read-password|Chromium Safe Storage|--folder|Chromium Keys|' + (mode === 'wallet-fallback' ? 'kdewallet' : 'fixture-wallet')) console.log('fixture-chromium-key');
47
+ else process.exit(1);
48
+ } else {
49
+ if (mode === 'application' && args.includes('service')) process.exit(1);
50
+ const browser = args.includes('Chromium') || args.includes('chromium') ? 'chromium'
51
+ : args.includes('Brave') || args.includes('brave') ? 'brave' : 'chrome';
52
+ console.log('fixture-' + browser + '-key');
53
+ }
54
+ `;
55
+ for (const command of ["secret-tool", "kwallet-query", "dbus-send"]) {
56
+ await writeFile(join(bin, command), helper, { mode: 0o700 });
57
+ }
58
+ await writeFile(join(bin, "agent-browser"), `#!${process.execPath}
59
+ ${guard}
60
+ appendFileSync(process.env.BROWSER_LOG, JSON.stringify(args) + '\\n');
61
+ if (args.includes('--profile')) mkdirSync(args[args.indexOf('--profile') + 1], { recursive: true });
62
+ if (args.includes('status')) console.log(JSON.stringify({ data: { connected: true } }));
63
+ else if (args.includes('snapshot')) console.log('- textbox "Chat with ChatGPT" [ref=e1]\\n- button "Add files and more" [ref=e2]');
64
+ else if (args.includes('eval')) { process.stdin.resume(); process.stdin.on('end', () => console.log(JSON.stringify({ ok: true, status: 200, bodyHasId: true, bodyHasEmail: true }))); }
65
+ else if (args.includes('url')) console.log('https://chatgpt.com/');
66
+ `, { mode: 0o700 });
67
+
68
+ const cases = [
69
+ { browser: "google-chrome", password: "fixture-chrome-key" },
70
+ { browser: "BraveSoftware/Brave-Browser", password: "fixture-brave-key" },
71
+ { browser: "chromium", password: "fixture-chromium-key" },
72
+ { browser: "chromium", password: "fixture-chromium-key", auto: true },
73
+ { browser: "chromium", password: "fixture-chromium-key", relative: true },
74
+ { browser: "chromium", password: "fixture-chromium-key", braveAncestor: true },
75
+ { browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true },
76
+ { browser: "chromium", password: "fixture-chromium-key", db: true, network: true },
77
+ { browser: "chromium-browser", password: "fixture-chromium-key", mode: "application" },
78
+ { browser: "chromium-browser", password: "fixture-chromium-key", db: true },
79
+ ...["6", "5", ""].map((kde) => ({ browser: "chromium", password: "fixture-chromium-key", backend: "kwallet", kde })),
80
+ { browser: "chromium", password: "fixture-chromium-key", backend: "kwallet", mode: "wallet-fallback" },
81
+ { browser: "chromium", password: "", backend: "kwallet", mode: "failure" },
82
+ { browser: "chromium", password: "", backend: "kwallet", mode: "wallet-empty" },
83
+ { browser: "chromium", password: "peanuts", backend: "basic", prefix: "v10" },
84
+ { browser: "chromium", password: "", backend: "basic" },
85
+ { browser: "chromium", password: "fixture-override-key", override: true },
86
+ { browser: "chromium", password: "fixture-chromium-key", backend: "invalid", desktop: "GNOME:KDE", kde: "6" },
87
+ { browser: "chromium", password: "", mode: "failure" },
88
+ { browser: "chromium", password: "", mode: "timeout" },
89
+ { browser: "google-chrome", password: "fixture-chrome-key", profileName: true },
90
+ { browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true, relative: true },
91
+ { browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true, tilde: true },
92
+ { browser: "chromium", password: "fixture-override-key", override: true, db: true, dotSegment: true },
93
+ ];
94
+ for (const [index, c] of cases.entries()) {
95
+ // Keep the release artifact scanner strict: fixture password values stay in
96
+ // this file, out of captured platform-smoke stdout (case flags stay unique).
97
+ const { password: _fixturePassword, ...label } = c;
98
+ await t.test(JSON.stringify(label), async () => {
99
+ const sourceHome = c.tilde ? join(root, "BraveSoftware", "home") : home;
100
+ const sourceConfigHome = c.tilde ? join(sourceHome, ".config")
101
+ : c.braveAncestor ? join(root, "BraveSoftware", "config")
102
+ : c.auto ? join(root, "xdg-native") : configHome;
103
+ const profile = join(sourceConfigHome, c.browser, "Default");
104
+ const dbPath = join(profile, ...(c.network ? ["Network"] : []), "Cookies");
105
+ await mkdir(join(profile, ...(c.network ? ["Network"] : [])), { recursive: true });
106
+ if (c.dotSegment) await mkdir(join(sourceConfigHome, "BraveSoftware"), { recursive: true });
107
+ const cookiePath = c.dotSegment ? `${sourceConfigHome}/BraveSoftware/../${c.browser}/Default/Cookies` : dbPath;
108
+ await rm(join(profile, "Cookies"), { force: true });
109
+ const db = new DatabaseSync(dbPath);
110
+ try {
111
+ db.exec(`CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);
112
+ INSERT INTO meta VALUES ('version', '24');
113
+ CREATE TABLE cookies (name TEXT, value TEXT, host_key TEXT, path TEXT,
114
+ expires_utc INTEGER, samesite INTEGER, encrypted_value BLOB,
115
+ is_secure INTEGER, is_httponly INTEGER);`);
116
+ const cipher = createCipheriv("aes-128-cbc", pbkdf2Sync(c.password, "saltysalt", 1, 16, "sha1"), Buffer.alloc(16, 32));
117
+ const plaintext = Buffer.concat([createHash("sha256").update(".chatgpt.com").digest(), Buffer.from("fixture-session-token")]);
118
+ const encrypted = Buffer.concat([Buffer.from(c.prefix ?? "v11"), cipher.update(plaintext), cipher.final()]);
119
+ db.prepare("INSERT INTO cookies VALUES (?, '', '.chatgpt.com', '/', 0, 1, ?, 1, 1)")
120
+ .run("__Secure-next-auth.session-token", encrypted);
121
+ } finally { db.close(); }
122
+ await writeFile(helperLog, "");
123
+ await writeFile(browserLog, "");
124
+ const config = {
125
+ defaults: { provider: "chatgpt", preset: "instant" },
126
+ browser: { authSeedProfileDir: join(root, `seed-${index}`), sessionPrefix: "fixture", chatUrl: "https://chatgpt.com/" },
127
+ auth: { chromeProfile: c.auto ? detectDefaultLinuxCookieProfileSource({ homeDir: sourceHome, env: { XDG_CONFIG_HOME: sourceConfigHome } })
128
+ : c.profileName ? "Default" : c.relative ? "./Default" : c.tilde ? `~/${relative(sourceHome, profile)}` : profile,
129
+ ...(c.db ? { chromeCookiePath: cookiePath } : {}), bootstrapTimeoutMs: 1000, pollMs: 1 },
130
+ };
131
+ const env = { HOME: sourceHome, XDG_CONFIG_HOME: sourceConfigHome, PATH: bin, TMPDIR: root,
132
+ AGENT_BROWSER_PATH: join(bin, "agent-browser"), PI_ORACLE_STATE_DIR: join(root, "state"),
133
+ SWEET_COOKIE_LINUX_KEYRING: c.backend ?? "gnome", KDE_SESSION_VERSION: c.kde ?? "",
134
+ XDG_CURRENT_DESKTOP: c.desktop ?? "",
135
+ ...(c.override ? { SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD: c.password } : {}),
136
+ FIXTURE_MODE: c.mode ?? "", HELPER_LOG: helperLog, BROWSER_LOG: browserLog };
137
+ const output = execFileSync(process.execPath, [new URL("../extensions/oracle/worker/auth-bootstrap.mjs", import.meta.url).pathname, JSON.stringify(config)], {
138
+ encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"], env,
139
+ ...(c.relative || c.profileName ? { cwd: join(sourceConfigHome, "chromium") } : {}),
140
+ });
141
+ assert.match(output, /Oracle auth synced/);
142
+ const replay = (await readFile(browserLog, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
143
+ assert(replay.some((args) => args.includes("__Secure-next-auth.session-token") && args.includes("fixture-session-token")), "production import must decrypt and replay the known cookie");
144
+ const calls = (await readFile(helperLog, "utf8")).trim();
145
+ if (c.override || c.backend === "basic") assert.equal(calls, "");
146
+ else if (c.browser.startsWith("chromium")) {
147
+ assert.doesNotMatch(calls, /Chrome Safe Storage|application","chrome"/);
148
+ if (c.mode === "application") assert.match(calls, /application","chromium/);
149
+ if (c.backend === "kwallet") {
150
+ assert.match(calls, new RegExp(`org.kde.kwalletd${c.kde ?? ""}`));
151
+ assert.match(calls, /Chromium Keys/);
152
+ assert.match(calls, ["wallet-fallback", "failure"].includes(c.mode) ? /kdewallet/ : /fixture-wallet/);
153
+ }
154
+ }
155
+ const diagnostics = output.match(/Diagnostics:\n([^\n]+)/)[1];
156
+ const log = await readFile(join(diagnostics, "oracle-auth.log"), "utf8");
157
+ assert.doesNotMatch(log + output, /fixture-(?:chromium|override)-key|fixture-key-do-not-log/);
158
+ if (["failure", "timeout"].includes(c.mode)) assert.match(log, /Failed to read Linux keyring via (secret-tool|kwallet-query)/);
159
+ if (index === 2 || (c.braveAncestor && !c.override)) {
160
+ // A prior successful Chrome read must not cache the wrong key. The
161
+ // adapter's legitimate API must restore env on success AND rejection.
162
+ execFileSync(process.execPath, ["--input-type=module", "-e", `
163
+ import assert from 'node:assert/strict';
164
+ import { getCookies } from '@steipete/sweet-cookie';
165
+ import { getCookiesFromNativeLinuxChromium } from './extensions/oracle/worker/linux-chromium-cookie-source.mjs';
166
+ const options = {url:'https://chatgpt.com/',browsers:['chrome'],mode:'merge',chromeProfile:process.argv[1]};
167
+ assert.equal((await getCookies({...options,chromeProfile:process.argv[2]})).cookies[0].value,'fixture-session-token');
168
+ process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD = ' ';
169
+ process.env.SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD = 'fixture-unused-brave-password';
170
+ const previous = {...process.env};
171
+ assert.equal((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies[0].value,'fixture-session-token');
172
+ assert.deepEqual({...process.env},previous);
173
+ await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
174
+ assert.deepEqual({...process.env},previous);
175
+ process.env.FIXTURE_MODE = 'failure';
176
+ const failureEnv = {...process.env};
177
+ const failedKeyring = await getCookiesFromNativeLinuxChromium(options, getCookies);
178
+ assert.deepEqual(failedKeyring.cookies,[]);
179
+ assert.match(failedKeyring.warnings.join('|'),/Failed to read Linux keyring via secret-tool/);
180
+ assert.deepEqual({...process.env},failureEnv);
181
+ await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
182
+ assert.deepEqual({...process.env},failureEnv);
183
+ process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD = ' fixture-chromium-key ';
184
+ const calibratedEnv = {...process.env};
185
+ assert.equal((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies[0].value,'fixture-session-token');
186
+ assert.deepEqual({...process.env},calibratedEnv);
187
+ await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
188
+ assert.deepEqual({...process.env},calibratedEnv);
189
+ delete process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD;
190
+ process.env.SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD = 'fixture-chromium-key';
191
+ process.env.SWEET_COOKIE_LINUX_KEYRING = 'basic';
192
+ const basicEnv = {...process.env};
193
+ assert.deepEqual((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies,[]);
194
+ assert.deepEqual({...process.env},basicEnv);
195
+ await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
196
+ assert.deepEqual({...process.env},basicEnv);
197
+ `, profile, join(configHome, "google-chrome", "Default")], {
198
+ cwd: new URL("../", import.meta.url), encoding: "utf8", timeout: 10_000, env,
199
+ });
200
+ }
201
+ await rm(join(profile, "Network"), { recursive: true, force: true });
202
+ });
203
+ }
204
+ });
@@ -38,7 +38,11 @@ function harness({ initial = 3, max = "4", wrongLabel = false, stuck = false, mo
38
38
  composerControlsVisible: ui.snapshotHasUsableComposerControls,
39
39
  log: async (line) => { if (line.startsWith("Verified ChatGPT Power tier")) verified = true; },
40
40
  agentBrowser: async (_job, command, arg) => {
41
- if (command === "focus") { assert(open); return; }
41
+ if (command === "focus") {
42
+ assert(open);
43
+ assert(mountingReads < 0, "wait for the Power slider to mount before focusing it");
44
+ return;
45
+ }
42
46
  if (command !== "press") return;
43
47
  if (arg === "Escape") open = false;
44
48
  else if (!stuck) value = Math.max(0, Math.min(4, value + (arg === "ArrowRight" ? 1 : -1)));
@@ -287,6 +287,11 @@ async function preparePackedProject({ runDir, provider, model, timeoutMs }) {
287
287
  writeFileSync(join(piProject, ".artifacts", "ignored", "artifact.txt"), "ignore me\n");
288
288
  mkdirSync(join(piProject, ".crabbox", "ignored"), { recursive: true });
289
289
  writeFileSync(join(piProject, ".crabbox", "ignored", "capture.txt"), "ignore me\n");
290
+ mkdirSync(join(piProject, ".pi"), { recursive: true });
291
+ writeFileSync(join(piProject, ".pi", "workspace-notes.txt"), "ignore me\n");
292
+ mkdirSync(join(piProject, ".oracle-context"), { recursive: true });
293
+ writeFileSync(join(piProject, ".oracle-context", "context.md"), "ignore me\n");
294
+ writeFileSync(join(piProject, ".scratchpad.md"), "ignore me\n");
290
295
 
291
296
  return { mode: "packed", cwd: piProject, installDir, provider, model, extensionPath: `./node_modules/${PACKAGE_NAME}` };
292
297
  }
@@ -369,11 +374,6 @@ async function run(mode = "packed") {
369
374
  const piVersion = await mustRun(runDir, "pi-version", piCommand(), ["--version"], { cwd: process.cwd(), env: process.env, timeoutMs: 30_000 });
370
375
  if (piVersion.stdout.trim() !== EXPECTED_PI_VERSION) throw new Error(`real smoke requires Pi ${EXPECTED_PI_VERSION}; found ${piVersion.stdout.trim() || "unknown"}`);
371
376
  const tmpRoot = mkdtempSync(join(tmpdir(), "pi-oracle-real-smoke-"));
372
- const prepared = mode === "packed"
373
- ? await preparePackedProject({ runDir, provider, model, timeoutMs })
374
- : prepareSourceProject({ provider, model });
375
- console.log(`Oracle real smoke mode=${prepared.mode} extension=${prepared.extensionPath}`);
376
- writeFileSync(join(runDir, "run.json"), `${JSON.stringify({ runId, mode: prepared.mode, provider, model, extensionPath: prepared.extensionPath, timeoutMs, startedAt: new Date().toISOString() }, null, 2)}\n`);
377
377
 
378
378
  function assert(id, condition, message) {
379
379
  assertions.push({ id, ok: Boolean(condition), ...(condition ? {} : { error: message }) });
@@ -381,6 +381,12 @@ async function run(mode = "packed") {
381
381
  }
382
382
 
383
383
  try {
384
+ const prepared = mode === "packed"
385
+ ? await preparePackedProject({ runDir, provider, model, timeoutMs })
386
+ : prepareSourceProject({ provider, model });
387
+ console.log(`Oracle real smoke mode=${prepared.mode} extension=${prepared.extensionPath}`);
388
+ writeFileSync(join(runDir, "run.json"), `${JSON.stringify({ runId, mode: prepared.mode, provider, model, extensionPath: prepared.extensionPath, timeoutMs, startedAt: new Date().toISOString() }, null, 2)}\n`);
389
+
384
390
  const test1 = join(runDir, "whole-repo-submit");
385
391
  const agent1 = join(runDir, "agent1");
386
392
  const sessions1 = join(runDir, "sessions1");
@@ -423,7 +429,7 @@ async function run(mode = "packed") {
423
429
  const entries = await tarList(archivePath);
424
430
  writeFileSync(join(test1, "archive-list.txt"), `${entries.join("\n")}\n`);
425
431
  assert("archive-includes-readme", entryExists(entries, "README.md"), "archive should include README.md");
426
- for (const excluded of [".pi", ".oracle-context", ".scratchpad.md", ".artifacts", ".crabbox", ".debug"]) {
432
+ for (const excluded of [".pi", ".oracle-context", ".scratchpad.md", ".artifacts", ".crabbox"]) {
427
433
  assert(`archive-excludes-${excluded.replace(/[^a-z0-9]+/gi, "-")}`, !entryExists(entries, excluded), `archive should exclude ${excluded}`);
428
434
  }
429
435
  } else {
@@ -466,7 +472,11 @@ async function run(mode = "packed") {
466
472
  const jobDir2 = latestJobDir(jobs2);
467
473
  writeFileSync(join(test2, "job-dir.txt"), `${jobDir2 ?? ""}\n`);
468
474
  const symlinkOutput = `${symlinkResult.stdout}\n${symlinkResult.stderr}`;
469
- assert("symlink-rejected", /resolve inside|symlink|outside|escape|must be inside/i.test(symlinkOutput), "symlink escape test output did not show the expected rejection");
475
+ assert(
476
+ "symlink-rejected",
477
+ symlinkOutput.includes("archive_input_symlink_escape") || symlinkOutput.includes("Archive input must resolve inside the project cwd without symlink escapes"),
478
+ "symlink escape test output did not show the actual oracle_submit rejection evidence",
479
+ );
470
480
  assert("symlink-no-job-created", !jobDir2, "symlink escape rejection should not create an oracle job");
471
481
  }
472
482
 
@@ -1,8 +1,9 @@
1
- FROM cimg/node:24.16
1
+ FROM cimg/node:24.21
2
2
 
3
3
  USER root
4
4
  RUN apt-get update \
5
5
  && apt-get install -y --no-install-recommends zstd rsync \
6
6
  && rm -rf /var/lib/apt/lists/*
7
- RUN npm install -g agent-browser@0.29.1
7
+ RUN npm install -g npm@11.21.0 \
8
+ && npm install -g agent-browser@0.29.1
8
9
  USER circleci
@@ -14,8 +14,7 @@ export function execCrabbox(args, opts = {}) {
14
14
  return new Promise((resolve) => {
15
15
  const child = spawn(CRABBOX_BIN, args, {
16
16
  stdio: ["ignore", "pipe", "pipe"],
17
- env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false", ...opts.env },
18
- ...opts.spawnOpts,
17
+ env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" },
19
18
  });
20
19
  const stdoutChunks = [];
21
20
  const stderrChunks = [];
@@ -122,10 +121,9 @@ export async function runOnLease(config, targetName, leaseId, command, opts = {}
122
121
  for (const name of opts.allowEnvNames ?? []) args.push("--allow-env", name);
123
122
  if (opts.sync === false) args.push("--no-sync");
124
123
  else args.push("--fresh-sync");
125
- if (opts.shell) args.push("--shell", command);
126
- else args.push("--", ...(Array.isArray(command) ? command : command.split(" ")));
124
+ args.push("--shell", command);
127
125
  console.log(` [crabbox] run ${targetName} ${opts.sync === false ? "--no-sync" : "--fresh-sync"} ...`);
128
- return execCrabbox(args, { timeout: opts.timeout ?? 900_000, env: opts.env });
126
+ return execCrabbox(args, { timeout: opts.timeout ?? 900_000 });
129
127
  }
130
128
 
131
129
  export async function stopLease(config, targetName, leaseId) {
@@ -3,6 +3,7 @@
3
3
  import { execFileSync, execSync } from "node:child_process";
4
4
  import { accessSync, constants, existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
+ import { buildTargetBaseArgs } from "./crabbox-runner.mjs";
6
7
 
7
8
  let failures = 0;
8
9
 
@@ -63,24 +64,6 @@ function parseJson(text) {
63
64
  try { return JSON.parse(text); } catch { return undefined; }
64
65
  }
65
66
 
66
- function targetBaseArgs(targetName, config) {
67
- if (targetName === "macos") {
68
- const host = env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
69
- const user = env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER");
70
- const workRoot = env("PI_ORACLE_SMOKE_MAC_WORK_ROOT") || env("PLATFORM_SMOKE_MAC_WORK_ROOT") || `/Users/${env("USER")}/crabbox/${config.packageName}`;
71
- return ["--provider", "ssh", "--target", "macos", "--static-host", host, "--static-user", user, "--static-port", "22", "--static-work-root", workRoot];
72
- }
73
- if (targetName === "ubuntu") {
74
- const image = env("PI_ORACLE_SMOKE_UBUNTU_IMAGE") || env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || "pi-oracle-platform-smoke:node24";
75
- return ["--provider", "local-container", "--target", "linux", "--local-container-image", image];
76
- }
77
- const vm = windowsVmName(config);
78
- const snapshot = windowsSnapshotName(config);
79
- const user = env("PI_ORACLE_SMOKE_WINDOWS_USER") || env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
80
- const workRoot = env("PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${config.packageName}`;
81
- return ["--provider", "parallels", "--target", "windows", "--windows-mode", "normal", "--parallels-source", vm, "--parallels-source-snapshot", snapshot, "--parallels-user", user, "--parallels-work-root", workRoot];
82
- }
83
-
84
67
  function runCrabboxDoctor(cbox, label, args, timeout = 120_000) {
85
68
  const output = silent(cbox, ["doctor", ...args, "--json"], { env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" }, timeout });
86
69
  if (!output) {
@@ -100,7 +83,7 @@ function runTargetToolProbe(cbox, targetName, config) {
100
83
  const command = targetName === "windows-native"
101
84
  ? `cmd.exe /c "where node && where npm && where git && where tar && where zstd && where agent-browser && zstd --version && agent-browser --version && echo tools-ok"`
102
85
  : `missing=""; for tool in ${required.join(" ")}; do command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"; done; if [ -n "$missing" ]; then echo "missing=$missing"; exit 1; fi; zstd --version >/dev/null && agent-browser --version >/dev/null && echo tools-ok`;
103
- const baseArgs = targetBaseArgs(targetName, config);
86
+ const baseArgs = buildTargetBaseArgs(targetName, config);
104
87
  if (targetName === "macos") baseArgs.push("--reclaim");
105
88
  const result = silent(cbox, ["run", ...baseArgs, "--no-sync", "--shell", command], {
106
89
  env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" },
@@ -137,11 +120,13 @@ function verifyPackageExclusions() {
137
120
  const forbiddenPatterns = [/^\.env(?:\.|$)/, /^\.artifacts(?:\/|$)/, /^\.crabbox(?:\/|$)/, /^\.debug(?:\/|$)/, /^\.platform-smoke-runs(?:\/|$)/, /\.tgz$/];
138
121
  const output = shell("npm pack --dry-run --json", { timeout: 120_000 });
139
122
  const parsed = parseJson(output);
140
- if (!Array.isArray(parsed) || !parsed[0]?.files) {
123
+ // npm 11 returns an array; npm 12 keys the same records by package name.
124
+ const [pack] = parsed && typeof parsed === "object" ? Object.values(parsed) : [];
125
+ if (!pack?.files) {
141
126
  warn("could not inspect npm pack file list");
142
127
  return;
143
128
  }
144
- const files = parsed[0].files.map((file) => file.path);
129
+ const files = pack.files.map((file) => file.path);
145
130
  const forbidden = files.filter((file) => forbiddenPatterns.some((pattern) => pattern.test(file)));
146
131
  forbidden.length ? fail(`forbidden files in package: ${forbidden.join(", ")}`) : ok("package excludes local env/artifact/debug state");
147
132
  }
@@ -184,9 +169,9 @@ export async function runDoctor(config) {
184
169
  for (const provider of ["ssh", "local-container", "parallels"]) {
185
170
  new RegExp(`^${provider}$`, "m").test(providers) ? ok(`provider available: ${provider}`) : fail(`crabbox providers missing ${provider}`);
186
171
  }
187
- runCrabboxDoctor(cbox, "macOS static SSH", targetBaseArgs("macos", config), 120_000);
188
- runCrabboxDoctor(cbox, "Windows native Parallels", targetBaseArgs("windows-native", config), 180_000);
189
- runCrabboxDoctor(cbox, "local-container", targetBaseArgs("ubuntu", config), 120_000);
172
+ runCrabboxDoctor(cbox, "macOS static SSH", buildTargetBaseArgs("macos", config), 120_000);
173
+ runCrabboxDoctor(cbox, "Windows native Parallels", buildTargetBaseArgs("windows-native", config), 180_000);
174
+ runCrabboxDoctor(cbox, "local-container", buildTargetBaseArgs("ubuntu", config), 120_000);
190
175
 
191
176
  console.log("\n── Host tools ──");
192
177
  for (const [name, command] of [["Docker", "docker info --format '{{.ServerVersion}}'"], ["Node", "node --version"], ["npm", "npm --version"], ["git", "git --version"], ["tar", "tar --version"], ["rsync", "rsync --version"]]) {