pi-oracle 0.7.4 → 0.7.6

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 (31) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +53 -18
  3. package/docs/ORACLE_DESIGN.md +16 -8
  4. package/docs/platform-smoke.md +156 -0
  5. package/extensions/oracle/index.ts +10 -4
  6. package/extensions/oracle/lib/config.ts +53 -27
  7. package/extensions/oracle/lib/jobs.ts +9 -5
  8. package/extensions/oracle/lib/poller.ts +1 -0
  9. package/extensions/oracle/lib/runtime.ts +107 -32
  10. package/extensions/oracle/lib/tools.ts +138 -12
  11. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  12. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  13. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  14. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  15. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  16. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  17. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  18. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  19. package/extensions/oracle/worker/run-job.mjs +107 -25
  20. package/package.json +30 -9
  21. package/platform-smoke.config.mjs +66 -0
  22. package/scripts/oracle-real-smoke.mjs +500 -0
  23. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  24. package/scripts/platform-smoke/artifacts.mjs +87 -0
  25. package/scripts/platform-smoke/assertions.mjs +34 -0
  26. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  27. package/scripts/platform-smoke/doctor.mjs +239 -0
  28. package/scripts/platform-smoke/invariants.mjs +124 -0
  29. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  30. package/scripts/platform-smoke/targets.mjs +434 -0
  31. package/scripts/platform-smoke.mjs +152 -0
@@ -8,6 +8,15 @@ import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
10
10
  import { isAbsolute, join, normalize } from "node:path";
11
+ import {
12
+ assertNotKnownBrowserUserDataPath,
13
+ chromeUserAgentPlatformToken,
14
+ chromiumKeychainSupportedOnPlatform,
15
+ defaultCloneStrategyForPlatform,
16
+ detectDefaultBrowserProfileSource,
17
+ detectDefaultLinuxChromeExecutablePath,
18
+ sweetCookieSafeStoragePasswordScrubbedEnv,
19
+ } from "../shared/browser-profile-helpers.mjs";
11
20
  import { getProjectId } from "./runtime.js";
12
21
 
13
22
  export const ORACLE_PROVIDERS = ["chatgpt", "grok"] as const;
@@ -225,7 +234,6 @@ export type OracleCloneStrategy = (typeof CLONE_STRATEGIES)[number];
225
234
  const ALLOWED_CHATGPT_ORIGINS = new Set(["https://chatgpt.com", "https://chat.openai.com"]);
226
235
  const PROJECT_OVERRIDE_KEYS = new Set(["defaults", "worker", "poller", "artifacts", "cleanup"]);
227
236
  const DEFAULT_MAC_CHROME_EXECUTABLE = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
228
- const DEFAULT_MAC_CHROME_USER_DATA_DIR = join(homedir(), "Library", "Application Support", "Google", "Chrome");
229
237
 
230
238
  export interface OracleConfig {
231
239
  defaults: {
@@ -274,37 +282,36 @@ export interface OracleConfig {
274
282
  }
275
283
 
276
284
  function detectDefaultChromeExecutablePath(): string | undefined {
277
- return existsSync(DEFAULT_MAC_CHROME_EXECUTABLE) ? DEFAULT_MAC_CHROME_EXECUTABLE : undefined;
285
+ if (process.platform === "darwin") {
286
+ return existsSync(DEFAULT_MAC_CHROME_EXECUTABLE) ? DEFAULT_MAC_CHROME_EXECUTABLE : undefined;
287
+ }
288
+ if (process.platform === "linux") {
289
+ return detectDefaultLinuxChromeExecutablePath();
290
+ }
291
+ return undefined;
278
292
  }
279
293
 
280
294
  function detectDefaultChromeUserAgent(executablePath: string | undefined): string | undefined {
281
295
  if (!executablePath) return undefined;
296
+ // Linux executable discovery is PATH-based, so avoid executing that discovered
297
+ // binary during config module initialization just to derive a user agent.
298
+ if (process.platform === "linux") return undefined;
299
+ const platformToken = chromeUserAgentPlatformToken(process.platform);
300
+ if (!platformToken) return undefined;
282
301
  try {
283
- const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8" }).trim();
302
+ const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv() }).trim();
284
303
  const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+\.\d+)/);
285
304
  if (!versionMatch) return undefined;
286
- return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionMatch[1]} Safari/537.36`;
305
+ return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionMatch[1]} Safari/537.36`;
287
306
  } catch {
288
307
  return undefined;
289
308
  }
290
309
  }
291
310
 
292
- function detectDefaultChromeProfileName(): string {
293
- const localStatePath = join(DEFAULT_MAC_CHROME_USER_DATA_DIR, "Local State");
294
- if (!existsSync(localStatePath)) return "Default";
295
- try {
296
- const localState = JSON.parse(readFileSync(localStatePath, "utf8")) as { profile?: { last_used?: string } };
297
- const lastUsed = localState?.profile?.last_used;
298
- return typeof lastUsed === "string" && lastUsed.trim() ? lastUsed.trim() : "Default";
299
- } catch {
300
- return "Default";
301
- }
302
- }
303
-
304
311
  const detectedChromeExecutablePath = detectDefaultChromeExecutablePath();
305
312
  const detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecutablePath);
306
313
  const agentExtensionsDir = join(getAgentDir(), "extensions");
307
- const detectedChromeProfileName = detectDefaultChromeProfileName();
314
+ const detectedChromeProfileName = detectDefaultBrowserProfileSource(process.platform);
308
315
 
309
316
  export interface OracleConfigLoadDetails {
310
317
  agentDir: string;
@@ -367,7 +374,7 @@ export const DEFAULT_CONFIG: OracleConfig = {
367
374
  authSeedProfileDir: join(agentExtensionsDir, "oracle-auth-seed-profile"),
368
375
  runtimeProfilesDir: join(agentExtensionsDir, "oracle-runtime-profiles"),
369
376
  maxConcurrentJobs: 2,
370
- cloneStrategy: "apfs-clone",
377
+ cloneStrategy: defaultCloneStrategyForPlatform(process.platform),
371
378
  chatUrl: "https://chatgpt.com/",
372
379
  authUrl: "https://chatgpt.com/auth/login",
373
380
  runMode: "headless",
@@ -452,18 +459,28 @@ function expectAbsoluteNormalizedPath(value: unknown, path: string): string {
452
459
  return normalize(expanded);
453
460
  }
454
461
 
455
- function expectSafeProfilePath(pathValue: string, path: string): string {
462
+ function expectSafeProfilePath(
463
+ pathValue: string,
464
+ path: string,
465
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string },
466
+ ): string {
456
467
  if (pathValue === "/" || pathValue === homedir()) {
457
468
  throw new Error(`Invalid oracle config: ${path} points to an unsafe directory`);
458
469
  }
459
- if (pathValue === DEFAULT_MAC_CHROME_USER_DATA_DIR || pathValue.startsWith(`${DEFAULT_MAC_CHROME_USER_DATA_DIR}/`)) {
460
- throw new Error(`Invalid oracle config: ${path} must not point into the real Chrome user-data directory`);
470
+ try {
471
+ assertNotKnownBrowserUserDataPath(pathValue, `Invalid oracle config: ${path}`, { cookieSources });
472
+ } catch (error) {
473
+ throw new Error(error instanceof Error ? error.message : String(error));
461
474
  }
462
475
  return pathValue;
463
476
  }
464
477
 
465
- function expectSafeProfileDir(value: unknown, path: string): string {
466
- return expectSafeProfilePath(expectAbsoluteNormalizedPath(value, path), path);
478
+ function expectSafeProfileDir(
479
+ value: unknown,
480
+ path: string,
481
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string },
482
+ ): string {
483
+ return expectSafeProfilePath(expectAbsoluteNormalizedPath(value, path), path, cookieSources);
467
484
  }
468
485
 
469
486
  function expectBoolean(value: unknown, path: string): boolean {
@@ -584,17 +601,26 @@ function validateOracleConfig(value: unknown): OracleConfig {
584
601
  const artifacts = expectObject(root.artifacts, "artifacts");
585
602
  const cleanup = expectObject(root.cleanup, "cleanup");
586
603
 
587
- const authSeedProfileDir = expectSafeProfileDir(browser.authSeedProfileDir, "browser.authSeedProfileDir");
588
- const runtimeProfilesDir = expectSafeProfileDir(browser.runtimeProfilesDir, "browser.runtimeProfilesDir");
604
+ const chromeProfile = expectString(auth.chromeProfile, "auth.chromeProfile");
605
+ const chromeCookiePath = expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath");
606
+ const cookieSources = { chromeProfile, chromeCookiePath };
607
+ const authSeedProfileDir = expectSafeProfileDir(browser.authSeedProfileDir, "browser.authSeedProfileDir", cookieSources);
608
+ const runtimeProfilesDir = expectSafeProfileDir(browser.runtimeProfilesDir, "browser.runtimeProfilesDir", cookieSources);
589
609
  if (runtimeProfilesDir === authSeedProfileDir || runtimeProfilesDir.startsWith(`${authSeedProfileDir}/`)) {
590
610
  throw new Error("Invalid oracle config: browser.runtimeProfilesDir must be separate from browser.authSeedProfileDir");
591
611
  }
592
612
 
593
- const chromeCookiePath = expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath");
594
613
  const chromiumKeychain = expectOptionalChromiumKeychain(auth.chromiumKeychain, "auth.chromiumKeychain");
595
614
  if (chromiumKeychain !== undefined && chromeCookiePath === undefined) {
596
615
  throw new Error("Invalid oracle config: auth.chromiumKeychain requires auth.chromeCookiePath");
597
616
  }
617
+ if (chromiumKeychain !== undefined && !chromiumKeychainSupportedOnPlatform(process.platform)) {
618
+ throw new Error(
619
+ "Invalid oracle config: auth.chromiumKeychain is macOS-only. " +
620
+ "On Linux, set auth.chromeCookiePath/auth.chromeProfile without auth.chromiumKeychain and use @steipete/sweet-cookie's " +
621
+ "SWEET_COOKIE_LINUX_KEYRING, SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD, or SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD options for encrypted Chromium cookies.",
622
+ );
623
+ }
598
624
 
599
625
  return {
600
626
  defaults: {
@@ -618,7 +644,7 @@ function validateOracleConfig(value: unknown): OracleConfig {
618
644
  auth: {
619
645
  pollMs: expectInteger(auth.pollMs, "auth.pollMs", 100),
620
646
  bootstrapTimeoutMs: expectInteger(auth.bootstrapTimeoutMs, "auth.bootstrapTimeoutMs", 1000),
621
- chromeProfile: expectString(auth.chromeProfile, "auth.chromeProfile"),
647
+ chromeProfile,
622
648
  chromeCookiePath,
623
649
  chromiumKeychain,
624
650
  },
@@ -197,9 +197,12 @@ export interface OracleRuntimeAllocation {
197
197
  seedGeneration?: string;
198
198
  }
199
199
 
200
+ function hasSessionFileAccessor(value: unknown): value is { getSessionFile: () => string | undefined } {
201
+ return typeof value === "object" && value !== null && "getSessionFile" in value && typeof value.getSessionFile === "function";
202
+ }
203
+
200
204
  export function getSessionFile(ctx: ExtensionContext): string | undefined {
201
- const manager = ctx.sessionManager as unknown as { getSessionFile?: () => string | undefined };
202
- return manager.getSessionFile?.();
205
+ return hasSessionFileAccessor(ctx.sessionManager) ? ctx.sessionManager.getSessionFile() : undefined;
203
206
  }
204
207
 
205
208
  export function getOracleJobsDir(): string {
@@ -989,13 +992,14 @@ export function resolveArchiveInputs(cwd: string, files: string[]): { absolute:
989
992
  throw new Error("Archive input must use '.' exactly for a whole-repo archive");
990
993
  }
991
994
  const absolute = resolve(cwd, file);
992
- if (absolute === cwd && file !== ".") {
995
+ const relativeFromCwd = relativePath(cwd, absolute);
996
+ if (relativeFromCwd === "" && file !== ".") {
993
997
  throw new Error("Archive input must use '.' exactly for a whole-repo archive");
994
998
  }
995
- const relative = absolute.startsWith(`${cwd}/`) ? absolute.slice(cwd.length + 1) : absolute === cwd ? "." : "";
996
- if (!relative) {
999
+ if (relativeFromCwd && (relativeFromCwd === ".." || relativeFromCwd.startsWith(`..${sep}`) || isAbsolute(relativeFromCwd))) {
997
1000
  throw new Error(`Archive input must be inside the project cwd: ${file}`);
998
1001
  }
1002
+ const relative = relativeFromCwd === "" ? "." : relativeFromCwd.split(sep).join("/");
999
1003
  if (!existsSync(absolute)) {
1000
1004
  throw new Error(`Archive input does not exist: ${file}`);
1001
1005
  }
@@ -165,6 +165,7 @@ function getJobCountsForSession(sessionFile: string | undefined, cwd: string): {
165
165
  }
166
166
 
167
167
  function refreshOracleStatusSnapshot(snapshot: OraclePollerContextSnapshot): void {
168
+ if (!snapshot.hasUI) return;
168
169
  if (!snapshot.sessionFile) {
169
170
  snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("accent", "oracle: unavailable"));
170
171
  return;
@@ -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
-