pi-oracle 0.7.3 → 0.7.5

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 (29) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +51 -17
  3. package/docs/ORACLE_DESIGN.md +12 -5
  4. package/docs/platform-smoke.md +153 -0
  5. package/extensions/oracle/lib/config.ts +53 -27
  6. package/extensions/oracle/lib/jobs.ts +9 -5
  7. package/extensions/oracle/lib/runtime.ts +107 -32
  8. package/extensions/oracle/lib/tools.ts +138 -12
  9. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  10. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  11. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  12. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  13. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  14. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  15. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  16. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  17. package/extensions/oracle/worker/run-job.mjs +107 -25
  18. package/package.json +40 -11
  19. package/platform-smoke.config.mjs +59 -0
  20. package/scripts/oracle-real-smoke.mjs +497 -0
  21. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  22. package/scripts/platform-smoke/artifacts.mjs +87 -0
  23. package/scripts/platform-smoke/assertions.mjs +34 -0
  24. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  25. package/scripts/platform-smoke/doctor.mjs +239 -0
  26. package/scripts/platform-smoke/invariants.mjs +108 -0
  27. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  28. package/scripts/platform-smoke/targets.mjs +434 -0
  29. package/scripts/platform-smoke.mjs +149 -0
@@ -6,8 +6,9 @@
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { spawn } from "node:child_process";
8
8
  import { constants as fsConstants, existsSync, realpathSync, readFileSync } from "node:fs";
9
- import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
9
+ import { access, cp as copyDirectory, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
10
10
  import { delimiter, dirname, join } from "node:path";
11
+ import { assertNotKnownBrowserUserDataPath, sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
11
12
  import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
12
13
  import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
13
14
  import type { OracleConfig } from "./config.js";
@@ -21,16 +22,44 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
21
22
  ) || "agent-browser";
22
23
  const PROFILE_CLONE_TIMEOUT_MS = 120_000;
23
24
  const ORACLE_SUBPROCESS_KILL_GRACE_MS = 2_000;
25
+
26
+ function killProcessTree(child: ReturnType<typeof spawn>): void {
27
+ if (process.platform === "win32" && child.pid) {
28
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true })
29
+ .on("error", () => undefined);
30
+ return;
31
+ }
32
+ child.kill("SIGTERM");
33
+ }
34
+
35
+ function killProcess(child: ReturnType<typeof spawn>): void {
36
+ if (process.platform === "win32" && child.pid) {
37
+ spawn("taskkill", ["/pid", String(child.pid), "/f"], { stdio: "ignore", windowsHide: true })
38
+ .on("error", () => undefined);
39
+ return;
40
+ }
41
+ child.kill("SIGKILL");
42
+ }
24
43
  const WORKSPACE_ROOT_MARKERS = [
25
44
  ".pi/extensions/oracle.json",
26
45
  ".pi",
27
46
  "AGENTS.md",
28
47
  ] as const;
29
- const REQUIRED_ORACLE_DEPENDENCIES = [
30
- { name: "agent-browser", command: AGENT_BROWSER_BIN },
31
- { name: "tar", command: "tar" },
32
- { name: "zstd", command: "zstd" },
33
- ] as const;
48
+ function cpCommand(): string {
49
+ return process.env.PI_ORACLE_CP_PATH?.trim() || "cp";
50
+ }
51
+
52
+ function requiredOracleDependencies(config: OracleConfig): Array<{ name: string; command: string }> {
53
+ const dependencies = [
54
+ { name: "agent-browser", command: AGENT_BROWSER_BIN },
55
+ { name: "tar", command: "tar" },
56
+ { name: "zstd", command: "zstd" },
57
+ ];
58
+ if (config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
59
+ dependencies.push({ name: "cp", command: cpCommand() });
60
+ }
61
+ return dependencies;
62
+ }
34
63
 
35
64
  export interface OracleRuntimeLeaseMetadata {
36
65
  jobId: string;
@@ -152,30 +181,61 @@ function missingLocalDependencyMessage(name: string): string {
152
181
  return `Oracle prerequisite not found on PATH: ${name}. Install ${name} and retry.`;
153
182
  }
154
183
 
184
+ function unsafeOracleProfilePathMessage(label: string, path: string, error: unknown): string {
185
+ const message = error instanceof Error ? error.message : String(error);
186
+ return `Oracle ${label} path is unsafe: ${path}. ${message}`;
187
+ }
188
+
189
+ function assertSafeOracleProfilePath(
190
+ path: string,
191
+ label: "auth seed profile" | "runtime profile" | "runtime profiles",
192
+ config?: OracleConfig,
193
+ ): void {
194
+ try {
195
+ assertNotKnownBrowserUserDataPath(path, `Oracle ${label}`, {
196
+ cookieSources: config ? { chromeProfile: config.auth.chromeProfile, chromeCookiePath: config.auth.chromeCookiePath } : undefined,
197
+ });
198
+ } catch (error) {
199
+ throw new Error(unsafeOracleProfilePathMessage(label, path, error));
200
+ }
201
+ }
202
+
155
203
  function unwritableOracleDirectoryMessage(label: "runtime profiles" | "jobs", path: string): string {
156
204
  return `Oracle ${label} directory is not writable: ${path}. Fix its permissions or configure a writable path, then retry.`;
157
205
  }
158
206
 
207
+ async function isExecutableFile(path: string): Promise<boolean> {
208
+ try {
209
+ const stats = await stat(path);
210
+ if (!stats.isFile()) return false;
211
+ await access(path, fsConstants.X_OK);
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ function executableNameCandidates(command: string): string[] {
219
+ if (process.platform !== "win32" || /\.[^\\/]+$/.test(command)) return [command];
220
+ const extensions = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
221
+ return [command, ...extensions.map((extension) => `${command}${extension.toLowerCase()}`), ...extensions.map((extension) => `${command}${extension.toUpperCase()}`)];
222
+ }
223
+
159
224
  async function resolveExecutableOnPath(command: string): Promise<string | undefined> {
160
225
  if (!command) return undefined;
161
- if (command.includes("/")) {
162
- try {
163
- await access(command, fsConstants.X_OK);
164
- return command;
165
- } catch {
166
- return undefined;
226
+ if (command.includes("/") || command.includes("\\")) {
227
+ for (const candidate of executableNameCandidates(command)) {
228
+ if (await isExecutableFile(candidate)) return candidate;
167
229
  }
230
+ return undefined;
168
231
  }
169
232
 
170
233
  const pathValue = process.env.PATH ?? "";
171
234
  for (const segment of pathValue.split(delimiter)) {
172
235
  if (!segment) continue;
173
- const candidate = join(segment, command);
174
- try {
175
- await access(candidate, fsConstants.X_OK);
176
- return candidate;
177
- } catch {
178
- continue;
236
+ for (const name of executableNameCandidates(command)) {
237
+ const candidate = join(segment, name);
238
+ if (await isExecutableFile(candidate)) return candidate;
179
239
  }
180
240
  }
181
241
  return undefined;
@@ -235,6 +295,7 @@ async function assertWritableDirectory(path: string, label: "runtime profiles" |
235
295
 
236
296
  export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Promise<void> {
237
297
  const seedDir = config.browser.authSeedProfileDir;
298
+ assertSafeOracleProfilePath(seedDir, "auth seed profile", config);
238
299
  let seedStats;
239
300
  try {
240
301
  seedStats = await stat(seedDir);
@@ -257,9 +318,10 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
257
318
  }
258
319
 
259
320
  export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
321
+ assertSafeOracleProfilePath(config.browser.runtimeProfilesDir, "runtime profiles", config);
260
322
  await assertOracleAuthSeedProfileReady(config);
261
323
  await assertConfiguredBrowserExecutableReady(config.browser.executablePath);
262
- for (const dependency of REQUIRED_ORACLE_DEPENDENCIES) {
324
+ for (const dependency of requiredOracleDependencies(config)) {
263
325
  await assertRequiredLocalDependencyReady(dependency.name, dependency.command);
264
326
  }
265
327
  await assertWritableDirectory(config.browser.runtimeProfilesDir, "runtime profiles");
@@ -379,7 +441,7 @@ export async function releaseConversationLease(conversationId: string | undefine
379
441
  }
380
442
 
381
443
  function profileCloneArgs(config: OracleConfig, sourceDir: string, destinationDir: string): string[] {
382
- if (config.browser.cloneStrategy === "apfs-clone") {
444
+ if (config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
383
445
  return ["-cR", sourceDir, destinationDir];
384
446
  }
385
447
  return ["-R", sourceDir, destinationDir];
@@ -387,7 +449,7 @@ function profileCloneArgs(config: OracleConfig, sourceDir: string, destinationDi
387
449
 
388
450
  async function spawnCp(args: string[], options?: { timeoutMs?: number }): Promise<void> {
389
451
  await new Promise<void>((resolve, reject) => {
390
- const child = spawn("cp", args, { stdio: ["ignore", "pipe", "pipe"] });
452
+ const child = spawn(cpCommand(), args, { env: sweetCookieSafeStoragePasswordScrubbedEnv(), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
391
453
  let stderr = "";
392
454
  let timedOut = false;
393
455
  let killTimer: NodeJS.Timeout | undefined;
@@ -401,9 +463,9 @@ async function spawnCp(args: string[], options?: { timeoutMs?: number }): Promis
401
463
  if ((options?.timeoutMs ?? 0) > 0) {
402
464
  killTimer = setTimeout(() => {
403
465
  timedOut = true;
404
- child.kill("SIGTERM");
466
+ killProcessTree(child);
405
467
  killGraceTimer = setTimeout(() => {
406
- child.kill("SIGKILL");
468
+ killProcess(child);
407
469
  }, ORACLE_SUBPROCESS_KILL_GRACE_MS);
408
470
  killGraceTimer.unref?.();
409
471
  }, options?.timeoutMs);
@@ -445,11 +507,16 @@ export async function cloneSeedProfileToRuntime(
445
507
  ): Promise<string | undefined> {
446
508
  const seedDir = config.browser.authSeedProfileDir;
447
509
  await assertOracleAuthSeedProfileReady(config);
510
+ assertSafeOracleProfilePath(runtimeProfileDir, "runtime profile", config);
448
511
 
449
512
  await withAuthLock({ runtimeProfileDir, seedDir }, async () => {
450
513
  await rm(runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
451
514
  await mkdir(dirname(runtimeProfileDir), { recursive: true, mode: 0o700 }).catch(() => undefined);
452
- await spawnCp(profileCloneArgs(config, seedDir, runtimeProfileDir), { timeoutMs: options?.cpTimeoutMs ?? PROFILE_CLONE_TIMEOUT_MS });
515
+ if (config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
516
+ await spawnCp(profileCloneArgs(config, seedDir, runtimeProfileDir), { timeoutMs: options?.cpTimeoutMs ?? PROFILE_CLONE_TIMEOUT_MS });
517
+ } else {
518
+ await copyDirectory(seedDir, runtimeProfileDir, { recursive: true, force: true, verbatimSymlinks: true });
519
+ }
453
520
  await removeChromiumProcessSingletonArtifacts(runtimeProfileDir);
454
521
  });
455
522
 
@@ -465,7 +532,7 @@ export interface OracleCleanupReport {
465
532
 
466
533
  async function closeRuntimeBrowserSession(runtimeSessionName: string): Promise<string | undefined> {
467
534
  return new Promise<string | undefined>((resolve) => {
468
- const child = spawn(AGENT_BROWSER_BIN, ["--session", runtimeSessionName, "close"], { stdio: "ignore" });
535
+ const child = spawn(AGENT_BROWSER_BIN, ["--session", runtimeSessionName, "close"], { env: sweetCookieSafeStoragePasswordScrubbedEnv(), stdio: "ignore", shell: process.platform === "win32" });
469
536
  let settled = false;
470
537
  let timeout: NodeJS.Timeout | undefined;
471
538
  let timedOut = false;
@@ -479,15 +546,21 @@ async function closeRuntimeBrowserSession(runtimeSessionName: string): Promise<s
479
546
 
480
547
  timeout = setTimeout(() => {
481
548
  timedOut = true;
482
- child.kill("SIGTERM");
549
+ killProcessTree(child);
483
550
  setTimeout(() => {
484
- child.kill("SIGKILL");
551
+ killProcess(child);
485
552
  finish(`Timed out closing agent-browser session ${runtimeSessionName} after ${AGENT_BROWSER_CLOSE_TIMEOUT_MS}ms`);
486
553
  }, 2_000).unref?.();
487
554
  }, AGENT_BROWSER_CLOSE_TIMEOUT_MS);
488
555
  timeout.unref?.();
489
556
 
490
- child.on("error", (error) => finish(`Failed to close agent-browser session ${runtimeSessionName}: ${error.message}`));
557
+ child.on("error", (error: NodeJS.ErrnoException) => {
558
+ if (error.code === "ENOENT") {
559
+ finish();
560
+ return;
561
+ }
562
+ finish(`Failed to close agent-browser session ${runtimeSessionName}: ${error.message}`);
563
+ });
491
564
  child.on("close", (code) => {
492
565
  if (timedOut || code === 0) finish();
493
566
  else finish(`agent-browser close exited with code ${code} for session ${runtimeSessionName}`);
@@ -510,9 +583,12 @@ export async function cleanupRuntimeArtifacts(runtime: {
510
583
  }
511
584
  if (runtime.runtimeProfileDir) {
512
585
  report.attempted.push("runtimeProfileDir");
513
- await rm(runtime.runtimeProfileDir, { recursive: true, force: true }).catch((error: Error) => {
514
- report.warnings.push(`Failed to remove runtime profile ${runtime.runtimeProfileDir}: ${error.message}`);
515
- });
586
+ try {
587
+ assertSafeOracleProfilePath(runtime.runtimeProfileDir, "runtime profile");
588
+ await rm(runtime.runtimeProfileDir, { recursive: true, force: true });
589
+ } catch (error) {
590
+ report.warnings.push(`Failed to remove runtime profile ${runtime.runtimeProfileDir}: ${error instanceof Error ? error.message : String(error)}`);
591
+ }
516
592
  }
517
593
  if (runtime.conversationId) {
518
594
  report.attempted.push("conversationLease");
@@ -529,4 +605,3 @@ export async function cleanupRuntimeArtifacts(runtime: {
529
605
 
530
606
  return report;
531
607
  }
532
-
@@ -4,9 +4,13 @@
4
4
  // Usage: Imported by the oracle extension entrypoint and sanity tests to register tools against the pi API.
5
5
  // Invariants/Assumptions: The pi runtime validates TypeBox schemas before execute, while execute owns semantic normalization.
6
6
  import { randomUUID } from "node:crypto";
7
- import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
+ import { spawn } from "node:child_process";
8
+ import { once } from "node:events";
9
+ import { createReadStream } from "node:fs";
10
+ import { lstat, mkdtemp, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
8
11
  import { tmpdir } from "node:os";
9
- import { basename, join, posix } from "node:path";
12
+ import { basename, dirname, join, posix } from "node:path";
13
+ import { sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
10
14
  import { runOracleAuthBootstrap } from "./auth.js";
11
15
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
16
  import { Type } from "typebox";
@@ -125,6 +129,8 @@ const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_ANYWHERE = new Set([
125
129
  ".pi",
126
130
  ".oracle-context",
127
131
  ".cursor",
132
+ ".artifacts",
133
+ ".crabbox",
128
134
  "node_modules",
129
135
  "target",
130
136
  ".venv",
@@ -386,6 +392,99 @@ function formatArchiveOversizeError(args: {
386
392
  .join("\n");
387
393
  }
388
394
 
395
+ function writeOctal(value: number, width: number): Buffer {
396
+ const text = Math.max(0, Math.floor(value)).toString(8).slice(-(width - 1)).padStart(width - 1, "0") + "\0";
397
+ return Buffer.from(text, "ascii");
398
+ }
399
+
400
+ function writeTarName(header: Buffer, name: string): void {
401
+ const normalized = name.replaceAll("\\", "/");
402
+ const nameBytes = Buffer.byteLength(normalized);
403
+ if (nameBytes <= 100) {
404
+ header.write(normalized, 0, 100, "utf8");
405
+ return;
406
+ }
407
+ const parts = normalized.split("/");
408
+ const fileName = parts.pop() || "";
409
+ const prefix = parts.join("/");
410
+ if (Buffer.byteLength(fileName) > 100 || Buffer.byteLength(prefix) > 155) {
411
+ throw new Error(`archive path is too long for portable tar header: ${normalized}`);
412
+ }
413
+ header.write(fileName, 0, 100, "utf8");
414
+ header.write(prefix, 345, 155, "utf8");
415
+ }
416
+
417
+ function buildTarHeader(name: string, options: { mode: number; size: number; mtimeMs: number; type: "file" | "directory" | "symlink"; linkName?: string }): Buffer {
418
+ const header = Buffer.alloc(512);
419
+ writeTarName(header, options.type === "directory" && !name.endsWith("/") ? `${name}/` : name);
420
+ writeOctal(options.mode & 0o7777, 8).copy(header, 100);
421
+ writeOctal(0, 8).copy(header, 108);
422
+ writeOctal(0, 8).copy(header, 116);
423
+ writeOctal(options.size, 12).copy(header, 124);
424
+ writeOctal(Math.floor(options.mtimeMs / 1000), 12).copy(header, 136);
425
+ Buffer.from(" ", "ascii").copy(header, 148);
426
+ header[156] = options.type === "directory" ? 53 : options.type === "symlink" ? 50 : 48;
427
+ if (options.linkName) header.write(options.linkName.replaceAll("\\", "/"), 157, 100, "utf8");
428
+ header.write("ustar", 257, 6, "ascii");
429
+ header.write("00", 263, 2, "ascii");
430
+ let checksum = 0;
431
+ for (const byte of header) checksum += byte;
432
+ const checksumText = checksum.toString(8).padStart(6, "0");
433
+ header.write(`${checksumText}\0 `, 148, 8, "ascii");
434
+ return header;
435
+ }
436
+
437
+ async function writeChunk(stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> {
438
+ if (!stream.write(chunk)) await once(stream, "drain");
439
+ }
440
+
441
+ async function writeWindowsTarArchiveToZstd(cwd: string, entries: string[], archivePath: string, timeout: AbortSignal): Promise<void> {
442
+ const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv(process.env);
443
+ const zstd = spawn("zstd", ["-19", "-T0", "-f", "-o", archivePath], {
444
+ cwd,
445
+ env: scrubbedEnv,
446
+ stdio: ["pipe", "ignore", "pipe"],
447
+ signal: timeout,
448
+ });
449
+ let stderr = "";
450
+ zstd.stderr?.on("data", (chunk) => {
451
+ stderr += String(chunk);
452
+ });
453
+ try {
454
+ for (const entry of entries) {
455
+ const normalizedEntry = entry.replaceAll("\\", "/");
456
+ const absolutePath = join(cwd, normalizedEntry);
457
+ const info = await lstat(absolutePath);
458
+ if (info.isSymbolicLink()) {
459
+ await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "symlink", linkName: await readlink(absolutePath) }));
460
+ continue;
461
+ }
462
+ if (info.isDirectory()) {
463
+ await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "directory" }));
464
+ continue;
465
+ }
466
+ if (!info.isFile()) continue;
467
+ await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: info.size, mtimeMs: info.mtimeMs, type: "file" }));
468
+ for await (const chunk of createReadStream(absolutePath)) {
469
+ await writeChunk(zstd.stdin, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
470
+ }
471
+ const padding = info.size % 512 === 0 ? 0 : 512 - (info.size % 512);
472
+ if (padding > 0) await writeChunk(zstd.stdin, Buffer.alloc(padding));
473
+ }
474
+ await writeChunk(zstd.stdin, Buffer.alloc(1024));
475
+ zstd.stdin.end();
476
+ } catch (error) {
477
+ zstd.stdin.destroy();
478
+ zstd.kill();
479
+ throw error;
480
+ }
481
+ const code = await new Promise<number | null>((resolve, reject) => {
482
+ zstd.once("error", reject);
483
+ zstd.once("close", resolve);
484
+ });
485
+ if (code !== 0) throw new Error(`zstd archive compression failed with status ${code}: ${stderr.trim()}`);
486
+ }
487
+
389
488
  async function writeArchiveFile(
390
489
  cwd: string,
391
490
  entries: string[],
@@ -396,13 +495,32 @@ async function writeArchiveFile(
396
495
  await writeFile(listPath, Buffer.from(`${entries.join("\0")}\0`), { mode: 0o600 });
397
496
  await rm(archivePath, { force: true }).catch(() => undefined);
398
497
 
399
- const { spawn } = await import("node:child_process");
498
+ if (process.platform === "win32") {
499
+ const timeoutController = new AbortController();
500
+ const timeout = setTimeout(() => timeoutController.abort(), options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS);
501
+ try {
502
+ await writeWindowsTarArchiveToZstd(cwd, entries, archivePath, timeoutController.signal);
503
+ return (await stat(archivePath)).size;
504
+ } catch (error) {
505
+ if (timeoutController.signal.aborted) {
506
+ throw new Error(`Oracle archive subprocess timed out after ${options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS}ms`);
507
+ }
508
+ throw error;
509
+ } finally {
510
+ clearTimeout(timeout);
511
+ }
512
+ }
513
+
400
514
  await new Promise<void>((resolvePromise, rejectPromise) => {
401
- const tar = spawn("tar", ["--null", "-cf", "-", "-T", listPath], {
402
- cwd,
515
+ const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv();
516
+ const tarArgs = ["--null", "-cf", "-", "-C", cwd, "-T", basename(listPath)];
517
+ const tar = spawn(process.env.PI_ORACLE_TEST_TAR_BIN ?? "tar", tarArgs, {
518
+ cwd: dirname(listPath),
519
+ env: scrubbedEnv,
403
520
  stdio: ["ignore", "pipe", "pipe"],
404
521
  });
405
- const zstd = spawn("zstd", ["-19", "-T0", "-f", "-o", archivePath], {
522
+ const zstd = spawn(process.env.PI_ORACLE_TEST_ZSTD_BIN ?? "zstd", ["-19", "-T0", "-f", "-o", archivePath], {
523
+ env: scrubbedEnv,
406
524
  stdio: ["pipe", "ignore", "pipe"],
407
525
  });
408
526
 
@@ -832,6 +950,14 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
832
950
  };
833
951
  }
834
952
 
953
+ if (/^Oracle (auth seed profile|runtime profile|runtime profiles) path is unsafe: /.test(message)) {
954
+ return {
955
+ code: "oracle_profile_path_unsafe",
956
+ message,
957
+ suggestedNextStep: "Move browser.authSeedProfileDir/browser.runtimeProfilesDir outside real browser profile directories and retry.",
958
+ };
959
+ }
960
+
835
961
  if (message.startsWith("Oracle runtime profiles directory is not writable: ")) {
836
962
  return {
837
963
  code: "runtime_profiles_dir_unwritable",
@@ -964,10 +1090,10 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
964
1090
  function buildOracleToolErrorResult(
965
1091
  toolName: OracleToolName,
966
1092
  error: unknown,
967
- params: Record<string, unknown>,
1093
+ params: unknown,
968
1094
  options?: { job?: NonNullable<ReturnType<typeof readJob>>; jobDetails?: OracleToolJobDetailsOptions },
969
1095
  ) {
970
- const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
1096
+ const errorDetails = buildOracleToolErrorDetails(toolName, error, asRecord(params) ?? {});
971
1097
  return {
972
1098
  content: [{
973
1099
  type: "text" as const,
@@ -1430,7 +1556,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1430
1556
  await appendCleanupWarnings(job.id, cleanupReport.warnings).catch(() => undefined);
1431
1557
  }
1432
1558
  if (ctx.hasUI) refreshOracleStatus(ctx);
1433
- return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>, {
1559
+ return buildOracleToolErrorResult("oracle_submit", error, params, {
1434
1560
  job: latest ?? job,
1435
1561
  jobDetails: {
1436
1562
  queue: latest ? buildOracleQueueSnapshot(latest, latest.status === "queued" ? getQueuePosition(latest.id) : undefined) : undefined,
@@ -1443,7 +1569,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1443
1569
  await rm(tempArchivePath, { force: true }).catch(() => undefined);
1444
1570
  }
1445
1571
  } catch (error) {
1446
- return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>);
1572
+ return buildOracleToolErrorResult("oracle_submit", error, params);
1447
1573
  }
1448
1574
  },
1449
1575
  });
@@ -1503,7 +1629,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1503
1629
  },
1504
1630
  };
1505
1631
  } catch (error) {
1506
- return buildOracleToolErrorResult("oracle_read", error, params as unknown as Record<string, unknown>);
1632
+ return buildOracleToolErrorResult("oracle_read", error, params);
1507
1633
  }
1508
1634
  },
1509
1635
  });
@@ -1538,7 +1664,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1538
1664
  details: { job: redactJobDetails(cancelled, { queue: buildOracleQueueSnapshot(cancelled, cancelled.status === "queued" ? getQueuePosition(cancelled.id) : undefined) }) },
1539
1665
  };
1540
1666
  } catch (error) {
1541
- return buildOracleToolErrorResult("oracle_cancel", error, params as unknown as Record<string, unknown>);
1667
+ return buildOracleToolErrorResult("oracle_cancel", error, params);
1542
1668
  }
1543
1669
  },
1544
1670
  });
@@ -0,0 +1,59 @@
1
+ export type OraclePlatform = NodeJS.Platform;
2
+
3
+ export interface BrowserPathOptions {
4
+ env?: NodeJS.ProcessEnv;
5
+ homeDir?: string;
6
+ }
7
+
8
+ export interface ExecutableSearchOptions {
9
+ pathValue?: string;
10
+ pathDelimiter?: string;
11
+ }
12
+
13
+ export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES: readonly [
14
+ "SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
15
+ "SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
16
+ "SWEET_COOKIE_EDGE_SAFE_STORAGE_PASSWORD",
17
+ ];
18
+
19
+ export function expandHomePath(value: string, homeDir?: string): string;
20
+ export function normalizedAbsolutePath(value: string, options?: BrowserPathOptions): string;
21
+ export function linuxConfigHome(options?: BrowserPathOptions): string;
22
+ export function linuxChromiumCookieImportUserDataDirs(options?: BrowserPathOptions): string[];
23
+ export function linuxBrowserSafetyUserDataDirs(options?: BrowserPathOptions): string[];
24
+ export function browserUserDataDirsForPlatform(
25
+ platform?: OraclePlatform,
26
+ options?: BrowserPathOptions & { includeUnsupported?: boolean },
27
+ ): string[];
28
+ export function defaultCloneStrategyForPlatform(platform?: OraclePlatform): "apfs-clone" | "copy";
29
+ export function chromiumKeychainSupportedOnPlatform(platform?: OraclePlatform): boolean;
30
+ export function chromeUserAgentPlatformToken(platform?: OraclePlatform): string | undefined;
31
+ export function pathInsideOrEqual(childPath: string, parentPath: string): boolean;
32
+ export function resolvePathThroughExistingAncestorsSync(pathValue: string): string | undefined;
33
+ export function protectedCookieSourcePaths(cookieSources?: { chromeProfile?: string; chromeCookiePath?: string }): string[];
34
+ export function knownBrowserUserDataPathMatch(
35
+ pathValue: string,
36
+ options?: BrowserPathOptions & {
37
+ platform?: OraclePlatform;
38
+ includeUnsupported?: boolean;
39
+ extraProtectedPaths?: string[];
40
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
41
+ },
42
+ ): string | undefined;
43
+ export function assertNotKnownBrowserUserDataPath(
44
+ pathValue: string,
45
+ label: string,
46
+ options?: BrowserPathOptions & {
47
+ platform?: OraclePlatform;
48
+ includeUnsupported?: boolean;
49
+ extraProtectedPaths?: string[];
50
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
51
+ },
52
+ ): void;
53
+ export function isExecutableFileSync(pathValue: string): boolean;
54
+ export function findExecutableOnPathSync(names: readonly string[], options?: ExecutableSearchOptions): string | undefined;
55
+ export function detectDefaultLinuxChromeExecutablePath(options?: ExecutableSearchOptions): string | undefined;
56
+ export function detectDefaultLinuxCookieProfileSource(options?: BrowserPathOptions): string | undefined;
57
+ export function detectDefaultBrowserProfileSource(platform?: OraclePlatform, options?: BrowserPathOptions): string;
58
+ export function scrubSweetCookieSafeStoragePasswordEnv(env?: NodeJS.ProcessEnv): void;
59
+ export function sweetCookieSafeStoragePasswordScrubbedEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;