pi-oracle 0.7.8 → 0.7.9

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.9 - 2026-06-11
6
+
7
+ ### Fixed
8
+ - made oracle workers launch their isolated Chrome runtime directly and attach `agent-browser` via DevTools, avoiding failures when unrelated `agent-browser` sessions or daemons are already running
9
+ - tightened worker-owned browser cleanup so runtime profiles are deleted only after the isolated Chrome process has been closed or terminated
10
+ - rejected `browser.args` overrides that would bypass oracle-managed Chrome profile or DevTools isolation
11
+
12
+ ### Validation
13
+ - verified ChatGPT and Grok oracle smoke tests against the local source extension after the worker-owned browser launch fix
14
+
5
15
  ## 0.7.8 - 2026-06-11
6
16
 
7
17
  ### Changed
@@ -85,12 +85,15 @@ const POST_SEND_SETTLE_MS = 15_000;
85
85
  const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
86
86
  (candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
87
87
  ) || "agent-browser";
88
+ const CHROME_DEVTOOLS_READY_TIMEOUT_MS = 15_000;
88
89
  const CP_BIN = process.env.PI_ORACLE_CP_PATH?.trim() || "cp";
89
90
  scrubSweetCookieSafeStoragePasswordEnv();
90
91
 
91
92
  let cpSupportsApfsCloneFlag;
92
93
  let currentJob;
93
94
  let browserStarted = false;
95
+ let browserProcess;
96
+ let browserProcessError;
94
97
  let cleaningUpBrowser = false;
95
98
  let cleaningUpRuntime = false;
96
99
  let shuttingDown = false;
@@ -355,16 +358,24 @@ async function cleanupRuntime(job) {
355
358
  cleaningUpRuntime = true;
356
359
  const warnings = [];
357
360
  try {
361
+ let browserClosed = true;
358
362
  await closeBrowser(job).catch(async (error) => {
363
+ browserClosed = false;
359
364
  const message = `Browser close warning during cleanup: ${error instanceof Error ? error.message : String(error)}`;
360
365
  warnings.push(message);
361
366
  await log(message).catch(() => undefined);
362
367
  });
363
- try {
364
- assertSafeRuntimeProfilePath(job.runtimeProfileDir, "runtime profile", job.config);
365
- await rm(job.runtimeProfileDir, { recursive: true, force: true });
366
- } catch (error) {
367
- const message = `Runtime profile cleanup warning: ${error instanceof Error ? error.message : String(error)}`;
368
+ if (browserClosed) {
369
+ try {
370
+ assertSafeRuntimeProfilePath(job.runtimeProfileDir, "runtime profile", job.config);
371
+ await rm(job.runtimeProfileDir, { recursive: true, force: true });
372
+ } catch (error) {
373
+ const message = `Runtime profile cleanup warning: ${error instanceof Error ? error.message : String(error)}`;
374
+ warnings.push(message);
375
+ await log(message).catch(() => undefined);
376
+ }
377
+ } else {
378
+ const message = `Runtime profile cleanup skipped because isolated browser close did not complete: ${job.runtimeProfileDir}`;
368
379
  warnings.push(message);
369
380
  await log(message).catch(() => undefined);
370
381
  }
@@ -542,6 +553,39 @@ function browserBaseArgs(job, options = {}) {
542
553
  return args;
543
554
  }
544
555
 
556
+ function waitForChildClose(child, timeoutMs) {
557
+ if (!child || child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
558
+ return new Promise((resolve) => {
559
+ let settled = false;
560
+ const timer = setTimeout(() => {
561
+ if (settled) return;
562
+ settled = true;
563
+ resolve(false);
564
+ }, timeoutMs);
565
+ timer.unref?.();
566
+ child.once("close", () => {
567
+ if (settled) return;
568
+ settled = true;
569
+ clearTimeout(timer);
570
+ resolve(true);
571
+ });
572
+ });
573
+ }
574
+
575
+ async function terminateBrowserProcess() {
576
+ if (!browserProcess) return;
577
+ const child = browserProcess;
578
+ browserProcess = undefined;
579
+ browserProcessError = undefined;
580
+ if (child.exitCode !== null || child.signalCode !== null) return;
581
+ killProcessTree(child);
582
+ if (await waitForChildClose(child, 2_000)) return;
583
+ killProcess(child);
584
+ if (!(await waitForChildClose(child, 2_000))) {
585
+ throw new Error(`Timed out terminating isolated Chrome process ${child.pid ?? "(unknown pid)"}`);
586
+ }
587
+ }
588
+
545
589
  async function closeBrowser(job) {
546
590
  if (cleaningUpBrowser) return;
547
591
  cleaningUpBrowser = true;
@@ -554,15 +598,107 @@ async function closeBrowser(job) {
554
598
  throw new Error(result.stderr || result.stdout || `agent-browser close exited with code ${result.code}`);
555
599
  }
556
600
  } finally {
601
+ await terminateBrowserProcess();
557
602
  browserStarted = false;
558
603
  cleaningUpBrowser = false;
559
604
  }
560
605
  }
561
606
 
607
+ function assertSafeBrowserLaunchArg(arg) {
608
+ const value = String(arg).trim().toLowerCase();
609
+ const managedFlags = [
610
+ "--user-data-dir",
611
+ "--remote-debugging-port",
612
+ "--remote-debugging-pipe",
613
+ "--remote-debugging-address",
614
+ "--remote-allow-origins",
615
+ ];
616
+ const flag = managedFlags.find((candidate) => value === candidate || value.startsWith(`${candidate}=`) || value.startsWith(`${candidate} `));
617
+ if (flag) {
618
+ throw new Error(`browser.args cannot override oracle-managed Chrome launch isolation flag ${flag}`);
619
+ }
620
+ }
621
+
622
+ function safeBrowserLaunchArgs(job) {
623
+ if (!Array.isArray(job.config.browser.args)) return [];
624
+ for (const arg of job.config.browser.args) assertSafeBrowserLaunchArg(arg);
625
+ return job.config.browser.args;
626
+ }
627
+
628
+ function chromeLaunchArgs(job, url) {
629
+ const args = [
630
+ "--remote-debugging-port=0",
631
+ "--remote-allow-origins=*",
632
+ "--no-first-run",
633
+ "--no-default-browser-check",
634
+ "--disable-background-networking",
635
+ "--disable-backgrounding-occluded-windows",
636
+ "--disable-component-update",
637
+ "--disable-default-apps",
638
+ "--disable-hang-monitor",
639
+ "--disable-popup-blocking",
640
+ "--disable-prompt-on-repost",
641
+ "--disable-sync",
642
+ "--disable-features=Translate",
643
+ "--enable-features=NetworkService,NetworkServiceInProcess",
644
+ "--metrics-recording-only",
645
+ "--password-store=basic",
646
+ "--use-mock-keychain",
647
+ "--enable-unsafe-swiftshader",
648
+ "--window-size=1280,720",
649
+ `--user-data-dir=${job.runtimeProfileDir}`,
650
+ ];
651
+ if (job.config.browser.runMode !== "headed") args.push("--headless=new", "--hide-scrollbars");
652
+ if (job.config.browser.userAgent) args.push(`--user-agent=${job.config.browser.userAgent}`);
653
+ args.push(...safeBrowserLaunchArgs(job));
654
+ args.push(url);
655
+ return args;
656
+ }
657
+
658
+ async function waitForDevToolsEndpoint(job) {
659
+ const path = join(job.runtimeProfileDir, "DevToolsActivePort");
660
+ const startedAt = Date.now();
661
+ while (Date.now() - startedAt < CHROME_DEVTOOLS_READY_TIMEOUT_MS) {
662
+ if (browserProcessError) {
663
+ throw new Error(`Chrome failed before DevTools became available: ${browserProcessError instanceof Error ? browserProcessError.message : String(browserProcessError)}`);
664
+ }
665
+ if (browserProcess?.exitCode !== null && browserProcess?.exitCode !== undefined) {
666
+ throw new Error(`Chrome exited before DevTools became available (exit code ${browserProcess.exitCode}).`);
667
+ }
668
+ if (existsSync(path)) {
669
+ const lines = (await readFile(path, "utf8")).trim().split(/\r?\n/);
670
+ const port = lines[0]?.trim();
671
+ const browserPath = lines[1]?.trim();
672
+ if (/^\d+$/.test(port)) {
673
+ return browserPath ? `ws://127.0.0.1:${port}${browserPath}` : `http://127.0.0.1:${port}`;
674
+ }
675
+ }
676
+ await sleep(100);
677
+ }
678
+ throw new Error(`Timed out waiting for Chrome DevTools endpoint at ${path}.`);
679
+ }
680
+
562
681
  async function launchBrowser(job, url) {
563
682
  await closeBrowser(job);
564
- const mode = job.config.browser.runMode;
565
- await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job, { withLaunchOptions: true, mode }), "open", url]);
683
+ const executablePath = job.config.browser.executablePath;
684
+ if (!executablePath) throw new Error("Oracle requires browser.executablePath when launching isolated browser runtimes without owning the global agent-browser daemon.");
685
+ const args = chromeLaunchArgs(job, url);
686
+ await log(`Launching isolated Chrome directly for agent-browser attach: ${JSON.stringify([executablePath, ...args])}`);
687
+ browserProcessError = undefined;
688
+ browserProcess = spawn(executablePath, args, {
689
+ env: sweetCookieSafeStoragePasswordScrubbedEnv(),
690
+ stdio: "ignore",
691
+ detached: false,
692
+ shell: false,
693
+ });
694
+ browserProcess.on("error", (error) => {
695
+ browserProcessError = error;
696
+ log(`Chrome process error: ${error instanceof Error ? error.message : String(error)}`).catch(() => undefined);
697
+ });
698
+ const endpoint = await waitForDevToolsEndpoint(job);
699
+ await log(`Connecting agent-browser session ${job.runtimeSessionName} to isolated Chrome DevTools endpoint`);
700
+ await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "connect", endpoint]);
701
+ await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "open", url]);
566
702
  browserStarted = true;
567
703
  }
568
704
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",