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
@@ -5,6 +5,7 @@
5
5
  // Invariants/Assumptions: Process identity is validated with `ps -o lstart=` to defend against PID reuse on macOS.
6
6
 
7
7
  import { spawn, execFileSync } from "node:child_process";
8
+ import { sweetCookieSafeStoragePasswordScrubbedEnv } from "./browser-profile-helpers.mjs";
8
9
 
9
10
  /** @typedef {import("./process-helpers.d.mts").OracleTrackedProcessOptions} OracleTrackedProcessOptions */
10
11
  /** @typedef {import("./process-helpers.d.mts").OracleDetachedProcessHandle} OracleDetachedProcessHandle */
@@ -20,7 +21,16 @@ function sleep(ms) {
20
21
  export function readProcessStartedAt(pid) {
21
22
  if (!pid || pid <= 0) return undefined;
22
23
  try {
23
- const startedAt = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" }).trim();
24
+ if (process.platform === "win32") {
25
+ const startedAt = execFileSync("powershell.exe", [
26
+ "-NoLogo",
27
+ "-NoProfile",
28
+ "-Command",
29
+ `$p = Get-Process -Id ${Number(pid)} -ErrorAction SilentlyContinue; if ($p) { $p.StartTime.ToUniversalTime().ToString('o') }`,
30
+ ], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv() }).trim();
31
+ return startedAt || undefined;
32
+ }
33
+ const startedAt = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv() }).trim();
24
34
  return startedAt || undefined;
25
35
  } catch {
26
36
  return undefined;
@@ -118,6 +128,7 @@ export async function terminateTrackedProcess(pid, startedAt, options = {}) {
118
128
  export async function spawnDetachedNodeProcess(scriptPath, args = []) {
119
129
  const child = spawn(process.execPath, [scriptPath, ...args], {
120
130
  detached: true,
131
+ env: sweetCookieSafeStoragePasswordScrubbedEnv(),
121
132
  stdio: "ignore",
122
133
  });
123
134
  child.unref();
@@ -167,7 +167,7 @@ function readLockProcessPid(path) {
167
167
  * @returns {boolean}
168
168
  */
169
169
  function isStateDirExistsError(error) {
170
- return Boolean(error && typeof error === "object" && "code" in error && (error.code === "EEXIST" || error.code === "ENOTEMPTY"));
170
+ return Boolean(error && typeof error === "object" && "code" in error && (error.code === "EEXIST" || error.code === "ENOTEMPTY" || (process.platform === "win32" && error.code === "EPERM")));
171
171
  }
172
172
 
173
173
  /**
@@ -248,7 +248,13 @@ export async function acquireStateLock(stateDir, kind, key, metadata, timeoutMs
248
248
  */
249
249
  export async function releaseStatePath(path) {
250
250
  if (!path) return;
251
- await rm(path, { recursive: true, force: true }).catch(() => undefined);
251
+ const deadline = Date.now() + (process.platform === "win32" ? 5_000 : 1_000);
252
+ while (true) {
253
+ await rm(path, { recursive: true, force: true }).catch(() => undefined);
254
+ if (!existsSync(path)) return;
255
+ if (Date.now() >= deadline) return;
256
+ await sleep(POLL_MS);
257
+ }
252
258
  }
253
259
 
254
260
  /**
@@ -2,14 +2,18 @@
2
2
  // Responsibilities: Copy/import cookies, classify auth pages, drive lightweight account-selection flows, and persist diagnostics for auth failures.
3
3
  // Scope: Auth bootstrap worker only; long-running oracle job execution stays in run-job.mjs and shared lifecycle/state helpers stay elsewhere.
4
4
  // Usage: Spawned by /oracle-auth to prepare the shared auth seed profile used by future oracle jobs.
5
- // Invariants/Assumptions: Runs against a local macOS Chromium-family profile, preserves private diagnostics, and must fail clearly when auth state cannot be verified.
5
+ // Invariants/Assumptions: Runs against a local Chromium-family profile, preserves private diagnostics, and must fail clearly when auth state cannot be verified.
6
6
  import { withLock } from "./state-locks.mjs";
7
7
  import { spawn } from "node:child_process";
8
8
  import { existsSync } from "node:fs";
9
9
  import { appendFile, chmod, lstat, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
10
10
  import { homedir, tmpdir } from "node:os";
11
- import { basename, dirname, join, resolve } from "node:path";
11
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
12
12
  import { getCookies } from "@steipete/sweet-cookie";
13
+ import {
14
+ assertNotKnownBrowserUserDataPath,
15
+ sweetCookieSafeStoragePasswordScrubbedEnv,
16
+ } from "../shared/browser-profile-helpers.mjs";
13
17
  import { ensureAccountCookie, filterImportableAuthCookies } from "./auth-cookie-policy.mjs";
14
18
  import { getCookiesFromConfiguredChromiumSource } from "./chromium-cookie-source.mjs";
15
19
  import { buildAllowedChatGptOrigins } from "./chatgpt-ui-helpers.mjs";
@@ -58,7 +62,6 @@ let URL_PATH = "(oracle-auth url path unavailable)";
58
62
  let SNAPSHOT_PATH = "(oracle-auth snapshot path unavailable)";
59
63
  let BODY_PATH = "(oracle-auth body path unavailable)";
60
64
  let SCREENSHOT_PATH = "(oracle-auth screenshot path unavailable)";
61
- const REAL_CHROME_USER_DATA_DIR = resolve(homedir(), "Library", "Application Support", "Google", "Chrome");
62
65
  const DEFAULT_ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
63
66
  const ORACLE_STATE_DIR = process.env.PI_ORACLE_STATE_DIR?.trim() || DEFAULT_ORACLE_STATE_DIR;
64
67
  const STALE_STAGING_PROFILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
@@ -140,12 +143,30 @@ async function log(message) {
140
143
  await chmod(LOG_PATH, 0o600).catch(() => undefined);
141
144
  }
142
145
 
146
+ function killProcessTree(child) {
147
+ if (process.platform === "win32" && child.pid) {
148
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true }).on("error", () => undefined);
149
+ return;
150
+ }
151
+ child.kill("SIGTERM");
152
+ }
153
+
154
+ function killProcess(child) {
155
+ if (process.platform === "win32" && child.pid) {
156
+ spawn("taskkill", ["/pid", String(child.pid), "/f"], { stdio: "ignore", windowsHide: true }).on("error", () => undefined);
157
+ return;
158
+ }
159
+ child.kill("SIGKILL");
160
+ }
161
+
143
162
  function spawnCommand(command, args, options = {}) {
144
163
  return new Promise((resolve, reject) => {
145
164
  const { timeoutMs = AGENT_BROWSER_COMMAND_TIMEOUT_MS, ...spawnOptions } = options;
146
165
  const child = spawn(command, args, {
147
166
  stdio: ["pipe", "pipe", "pipe"],
148
167
  ...spawnOptions,
168
+ env: sweetCookieSafeStoragePasswordScrubbedEnv(spawnOptions.env),
169
+ shell: spawnOptions.shell ?? process.platform === "win32",
149
170
  });
150
171
  let stdout = "";
151
172
  let stderr = "";
@@ -155,8 +176,8 @@ function spawnCommand(command, args, options = {}) {
155
176
  if (typeof timeoutMs === "number" && timeoutMs > 0) {
156
177
  killTimer = setTimeout(() => {
157
178
  timedOut = true;
158
- child.kill("SIGTERM");
159
- killGraceTimer = setTimeout(() => child.kill("SIGKILL"), AGENT_BROWSER_KILL_GRACE_MS);
179
+ killProcessTree(child);
180
+ killGraceTimer = setTimeout(() => killProcess(child), AGENT_BROWSER_KILL_GRACE_MS);
160
181
  killGraceTimer.unref?.();
161
182
  }, timeoutMs);
162
183
  killTimer.unref?.();
@@ -263,16 +284,15 @@ async function sweepStaleStagingProfiles(targetDir) {
263
284
 
264
285
  async function createProfilePlan(profileDir) {
265
286
  const targetDir = resolve(profileDir);
266
- if (!targetDir.startsWith("/")) {
287
+ if (!isAbsolute(targetDir)) {
267
288
  throw new Error(`Oracle profileDir must be an absolute path: ${profileDir}`);
268
289
  }
269
290
  if (targetDir === "/" || targetDir === homedir()) {
270
291
  throw new Error(`Oracle profileDir is unsafe: ${targetDir}`);
271
292
  }
272
- if (targetDir === REAL_CHROME_USER_DATA_DIR || targetDir.startsWith(`${REAL_CHROME_USER_DATA_DIR}/`)) {
273
- throw new Error(`Oracle profileDir must not point into the real Chrome user-data directory: ${targetDir}`);
274
- }
275
-
293
+ assertNotKnownBrowserUserDataPath(targetDir, "Oracle profileDir", {
294
+ cookieSources: { chromeProfile: config.auth.chromeProfile, chromeCookiePath: config.auth.chromeCookiePath },
295
+ });
276
296
  const stagingDir = `${targetDir}.staging-${Date.now()}`;
277
297
  const backupDir = `${targetDir}.prev`;
278
298
  await mkdir(dirname(targetDir), { recursive: true, mode: 0o700 });
@@ -552,6 +572,11 @@ function formatAuthFailureGuidance(error) {
552
572
  "3. Quit the browser fully.",
553
573
  "4. Re-run /oracle-auth.",
554
574
  );
575
+ if (process.platform === "linux") {
576
+ lines.push(
577
+ "5. If Chromium encrypted-cookie warnings mention the Linux keyring, install/configure secret-tool or kwallet-query, or set SWEET_COOKIE_LINUX_KEYRING / SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD / SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD for this run before rerunning.",
578
+ );
579
+ }
555
580
  }
556
581
 
557
582
  lines.push(
@@ -592,6 +617,10 @@ async function readRawSourceCookies() {
592
617
 
593
618
  async function readSourceCookies() {
594
619
  await log(`Reading ${providerName()} cookies from ${cookieSourceLabel()}`);
620
+ // Sweet Cookie reads Linux safe-storage overrides directly from process.env.
621
+ // Keep the worker's environment stable for the rest of this short-lived
622
+ // bootstrap process, but scrub every helper/browser subprocess via
623
+ // spawnCommand's sweetCookieSafeStoragePasswordScrubbedEnv().
595
624
  const { cookies, warnings } = await readRawSourceCookies();
596
625
 
597
626
  if (warnings.length) {
@@ -11,6 +11,8 @@ export declare const CHATGPT_CANONICAL_APP_ORIGINS: readonly string[];
11
11
 
12
12
  export declare function buildAllowedChatGptOrigins(chatUrl: string, authUrl?: string): string[];
13
13
  export declare function matchesModelFamilyLabel(label: string | undefined, family: OracleUiModelFamily): boolean;
14
+ export declare function matchesRequestedModelControlLabel(label: string | undefined, selection: OracleUiSelection): boolean;
15
+ export declare function matchesCompactIntelligenceOpenerLabel(label: string | undefined): boolean;
14
16
  export declare function requestedEffortLabel(selection: OracleUiSelection): string | undefined;
15
17
  export declare function effortSelectionVisible(snapshot: string, effortLabel: string | undefined): boolean;
16
18
  export declare function thinkingChipVisible(snapshot: string): boolean;
@@ -29,11 +29,15 @@ const AUTO_SWITCH_LABEL = "Auto-switch to Thinking";
29
29
  const THINKING_EFFORT_COMBOBOX_LABEL = "Thinking effort";
30
30
  const PRO_THINKING_EFFORT_COMBOBOX_LABEL = "Pro thinking effort";
31
31
  const EFFORT_LABELS = new Set(["Light", "Standard", "Extended", "Heavy"]);
32
+ const COMPACT_INTELLIGENCE_MENU_PATTERN = /Intelligence.*Instant.*Medium.*High.*Pro/i;
33
+ const COMPACT_INTELLIGENCE_CONTROL_PATTERN = /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Pro(?:\s+5\+\s*min)?)$/i;
34
+ const COMPACT_INTELLIGENCE_OPENER_PATTERN = /^(?:Instant|Medium|High|Pro)$/i;
32
35
  const BARE_EFFORT_PATTERN = /^(light|standard|extended|heavy)(?:, click to remove)?$/i;
33
36
  const INSTANT_CHIP_PATTERN = /^instant(?:, click to remove)?$/i;
34
37
  const THINKING_CHIP_PATTERN = /^(?:(light|standard|extended|heavy)\s+)?thinking(?:, click to remove)?$/i;
35
38
  const PRO_CHIP_PATTERN = /^(?:(light|standard|extended|heavy)\s+)?pro(?:, click to remove)?$/i;
36
39
  const MODEL_FAMILY_CONTROL_KINDS = new Set(["button", "radio", "menuitemradio"]);
40
+ const COMPACT_INTELLIGENCE_CONTROL_KINDS = new Set(["menuitemradio"]);
37
41
 
38
42
  /**
39
43
  * @param {string | undefined} url
@@ -151,9 +155,134 @@ function parseComposerChipSelection(label) {
151
155
  return undefined;
152
156
  }
153
157
 
158
+ function parseCompactIntelligenceSelection(label) {
159
+ if (/click to remove/i.test(String(label || ""))) return undefined;
160
+ const normalized = normalizeChipLabel(label);
161
+ if (!COMPACT_INTELLIGENCE_CONTROL_PATTERN.test(normalized)) return undefined;
162
+
163
+ if (/^Instant(?:\s+5s)?$/i.test(normalized)) {
164
+ return {
165
+ modelFamily: /** @type {OracleUiModelFamily} */ ("instant"),
166
+ compactTier: "instant",
167
+ };
168
+ }
169
+ if (/^Medium(?:\s+5\s*[–-]\s*30s)?$/i.test(normalized)) {
170
+ return {
171
+ modelFamily: /** @type {OracleUiModelFamily} */ ("thinking"),
172
+ effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ ("standard"),
173
+ compactTier: "medium",
174
+ };
175
+ }
176
+ if (/^High(?:\s+15\s*[–-]\s*60s)?$/i.test(normalized)) {
177
+ return {
178
+ modelFamily: /** @type {OracleUiModelFamily} */ ("thinking"),
179
+ effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ ("extended"),
180
+ compactTier: "high",
181
+ };
182
+ }
183
+ if (/^Pro(?:\s+5\+\s*min)?$/i.test(normalized)) {
184
+ return {
185
+ modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
186
+ compactTier: "pro",
187
+ };
188
+ }
189
+
190
+ return undefined;
191
+ }
192
+
193
+ function hasRemovableComposerModelChip(entries) {
194
+ return entries.some(
195
+ (entry) => entry.kind === "button" && /click to remove/i.test(String(entry.label || "")) && parseComposerChipSelection(entry.label),
196
+ );
197
+ }
198
+
199
+ function hasCompactIntelligenceMenuContext(entries) {
200
+ return entries.some((entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)))
201
+ || entries.some((entry) => !entry.disabled && entry.kind === "menuitemradio" && checkedState(entry) === true && /\d/.test(String(entry.label || "")) && parseCompactIntelligenceSelection(entry.label));
202
+ }
203
+
204
+ function hasLegacyEffortCombobox(entries) {
205
+ return entries.some((entry) => {
206
+ if (entry.disabled || entry.kind !== "combobox") return false;
207
+ const label = normalizeText(entry.label).toLowerCase();
208
+ return label === THINKING_EFFORT_COMBOBOX_LABEL.toLowerCase() || label === PRO_THINKING_EFFORT_COMBOBOX_LABEL.toLowerCase();
209
+ });
210
+ }
211
+
212
+ function compactSelectionFromEntry(entry, _entries, _options = {}) {
213
+ if (entry.disabled || !COMPACT_INTELLIGENCE_CONTROL_KINDS.has(entry.kind || "")) return undefined;
214
+ if (!/\d/.test(String(entry.label || ""))) return undefined;
215
+ return parseCompactIntelligenceSelection(entry.label);
216
+ }
217
+
218
+ function compactSelectionMatchesRequested(selection, compactSelection) {
219
+ if (!compactSelection || compactSelection.modelFamily !== selection.modelFamily) return false;
220
+
221
+ if (selection.modelFamily === "instant") {
222
+ // The compact Intelligence picker has no explicit auto-switch toggle. Treat
223
+ // Instant 5s as the closest available target for both instant presets.
224
+ return compactSelection.compactTier === "instant";
225
+ }
226
+
227
+ if (selection.modelFamily === "pro") {
228
+ // The compact picker exposes one Pro tier instead of separate Pro efforts.
229
+ return compactSelection.compactTier === "pro";
230
+ }
231
+
232
+ if (selection.modelFamily === "thinking") {
233
+ const requestedEffort = selection.effort || "standard";
234
+ if (compactSelection.compactTier === "medium") return requestedEffort === "light" || requestedEffort === "standard";
235
+ if (compactSelection.compactTier === "high") return requestedEffort === "extended" || requestedEffort === "heavy";
236
+ }
237
+
238
+ return false;
239
+ }
240
+
241
+ function compactSelectionMatchesRequestedInSnapshot(snapshot, selection, compactSelection, { weak = false } = {}) {
242
+ if (!compactSelectionMatchesRequested(selection, compactSelection)) return false;
243
+ if (selection.modelFamily !== "instant") return true;
244
+
245
+ const autoSwitchState = autoSwitchToThinkingSelectionVisible(snapshot);
246
+ if (autoSwitchState === undefined) return true;
247
+ if (weak) return selection.autoSwitchToThinking ? autoSwitchState !== false : autoSwitchState !== true;
248
+ return selection.autoSwitchToThinking ? autoSwitchState === true : autoSwitchState !== true;
249
+ }
250
+
251
+ function detectCompactIntelligenceSelection(entries) {
252
+ if (hasRemovableComposerModelChip(entries)) return undefined;
253
+ if (hasLegacyEffortCombobox(entries)) return undefined;
254
+
255
+ for (const entry of entries) {
256
+ if (entry.kind !== "menuitemradio" || checkedState(entry) !== true) continue;
257
+ const compactSelection = compactSelectionFromEntry(entry, entries, { allowClosedButtons: false });
258
+ if (compactSelection) return compactSelection;
259
+ }
260
+
261
+ if (hasCompactIntelligenceMenuContext(entries)) return undefined;
262
+
263
+ for (const entry of entries) {
264
+ if (entry.kind !== "button") continue;
265
+ const compactSelection = compactSelectionFromEntry(entry, entries);
266
+ if (!compactSelection) continue;
267
+ return compactSelection;
268
+ }
269
+ return undefined;
270
+ }
271
+
272
+ export function matchesRequestedModelControlLabel(label, selection) {
273
+ const compactSelection = parseCompactIntelligenceSelection(label);
274
+ if (compactSelection) return compactSelectionMatchesRequested(selection, compactSelection);
275
+ return matchesModelFamilyLabel(label, selection.modelFamily);
276
+ }
277
+
278
+ export function matchesCompactIntelligenceOpenerLabel(label) {
279
+ return COMPACT_INTELLIGENCE_OPENER_PATTERN.test(normalizeChipLabel(label));
280
+ }
281
+
154
282
  function detectComposerChipSelection(entries) {
155
283
  for (const entry of entries) {
156
284
  if (entry.disabled || entry.kind !== "button") continue;
285
+ if (/\bexpanded=true\b/.test(String(entry.line || "")) && !/click to remove/i.test(String(entry.label || ""))) continue;
157
286
  const selection = parseComposerChipSelection(entry.label);
158
287
  if (selection) return selection;
159
288
  }
@@ -168,6 +297,9 @@ function checkedState(entry) {
168
297
  }
169
298
 
170
299
  function detectSelectedModelFamily(entries) {
300
+ const compactSelection = detectCompactIntelligenceSelection(entries);
301
+ if (compactSelection) return compactSelection.modelFamily;
302
+
171
303
  for (const entry of entries) {
172
304
  if (entry.disabled || !MODEL_FAMILY_CONTROL_KINDS.has(entry.kind || "") || checkedState(entry) !== true) continue;
173
305
  for (const family of /** @type {OracleUiModelFamily[]} */ (["instant", "thinking", "pro"])) {
@@ -213,8 +345,15 @@ export function effortSelectionVisible(snapshot, effortLabel) {
213
345
  /** @type {SnapshotEntry[]} */
214
346
  const entries = parseSnapshotEntries(snapshot);
215
347
  const normalizedEffort = effortLabel.toLowerCase();
348
+ const compactClosedButtonsAllowed = !hasRemovableComposerModelChip(entries) && !hasLegacyEffortCombobox(entries) && !hasCompactIntelligenceMenuContext(entries);
216
349
  return entries.some((entry) => {
217
350
  if (entry.disabled) return false;
351
+ const compactSelection = compactSelectionFromEntry(entry, entries, { allowClosedButtons: compactClosedButtonsAllowed });
352
+ if (compactSelection && entry.kind === "menuitemradio" && checkedState(entry) !== true) return false;
353
+ if (compactSelection?.modelFamily === "thinking") {
354
+ return compactSelectionMatchesRequested({ modelFamily: "thinking", effort: /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiEffort} */ (normalizedEffort), autoSwitchToThinking: false }, compactSelection);
355
+ }
356
+ if (compactSelection?.modelFamily === "pro") return true;
218
357
  if (entry.kind === "combobox" && normalizeText(entry.value).toLowerCase() === normalizedEffort) return true;
219
358
  const chipSelection = entry.kind === "button" ? parseComposerChipSelection(entry.label) : undefined;
220
359
  if (chipSelection?.effort === normalizedEffort) return true;
@@ -255,12 +394,18 @@ export function snapshotHasModelConfigurationUi(snapshot) {
255
394
  .filter((family) => matchesModelFamilyLabel(entry.label, family)),
256
395
  ),
257
396
  );
397
+ const visibleCompactControls = entries.filter(
398
+ (entry) => !entry.disabled && entry.kind === "menuitemradio" && /\d/.test(String(entry.label || "")) && parseCompactIntelligenceSelection(entry.label),
399
+ );
400
+ const hasCompactIntelligenceMenu = entries.some(
401
+ (entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)),
402
+ );
258
403
  const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === "Close" && !entry.disabled);
259
404
  const hasIntelligenceHeading = entries.some((entry) => entry.kind === "heading" && normalizeText(entry.label) === "Intelligence" && !entry.disabled);
260
405
  const hasEffortCombobox = entries.some(
261
406
  (entry) => entry.kind === "combobox" && EFFORT_LABELS.has(entry.value || "") && !entry.disabled,
262
407
  );
263
- return visibleFamilies.size >= 2 || visibleRadioFamilies.size >= 2 || hasCloseButton || hasIntelligenceHeading || hasEffortCombobox;
408
+ return visibleFamilies.size >= 2 || visibleRadioFamilies.size >= 2 || visibleCompactControls.length >= 2 || hasCompactIntelligenceMenu || hasCloseButton || hasIntelligenceHeading || hasEffortCombobox;
264
409
  }
265
410
 
266
411
  /**
@@ -287,6 +432,7 @@ export function snapshotHasModelOpener(snapshot) {
287
432
  const label = normalizeChipLabel(entry.label);
288
433
  return label === "Model"
289
434
  || label === "Model selector"
435
+ || COMPACT_INTELLIGENCE_OPENER_PATTERN.test(label)
290
436
  || EFFORT_LABELS.has(label)
291
437
  || ["instant", "thinking", "pro"].some((family) => matchesModelFamilyLabel(label, /** @type {OracleUiModelFamily} */ (family)))
292
438
  || THINKING_CHIP_PATTERN.test(label)
@@ -325,6 +471,10 @@ export function autoSwitchToThinkingSelectionVisible(snapshot) {
325
471
  */
326
472
  export function snapshotCanSafelySkipModelConfiguration(snapshot, selection) {
327
473
  if (!snapshotStronglyMatchesRequestedModel(snapshot, selection)) return false;
474
+ const hasBareProPill = selection.modelFamily === "pro" && parseSnapshotEntries(snapshot).some(
475
+ (entry) => entry.kind === "button" && !entry.disabled && normalizeChipLabel(entry.label) === "Pro",
476
+ );
477
+ if (hasBareProPill && !snapshotHasModelConfigurationUi(snapshot)) return false;
328
478
  if (selection.modelFamily === "instant" && selection.autoSwitchToThinking) {
329
479
  return autoSwitchToThinkingSelectionVisible(snapshot) === true;
330
480
  }
@@ -339,6 +489,9 @@ export function snapshotCanSafelySkipModelConfiguration(snapshot, selection) {
339
489
  export function snapshotStronglyMatchesRequestedModel(snapshot, selection) {
340
490
  /** @type {SnapshotEntry[]} */
341
491
  const entries = parseSnapshotEntries(snapshot);
492
+ const compactSelection = detectCompactIntelligenceSelection(entries);
493
+ if (compactSelection) return compactSelectionMatchesRequestedInSnapshot(snapshot, selection, compactSelection);
494
+
342
495
  const chipSelection = detectComposerChipSelection(entries);
343
496
  if (chipSelection) return selectionMatchesChipSelection(selection, chipSelection);
344
497
 
@@ -366,6 +519,9 @@ export function snapshotStronglyMatchesRequestedModel(snapshot, selection) {
366
519
  export function snapshotWeaklyMatchesRequestedModel(snapshot, selection) {
367
520
  /** @type {SnapshotEntry[]} */
368
521
  const entries = parseSnapshotEntries(snapshot);
522
+ const compactSelection = detectCompactIntelligenceSelection(entries);
523
+ if (compactSelection) return compactSelectionMatchesRequestedInSnapshot(snapshot, selection, compactSelection, { weak: true });
524
+
369
525
  const chipSelection = detectComposerChipSelection(entries);
370
526
  if (chipSelection) return selectionMatchesChipSelection(selection, chipSelection);
371
527
 
@@ -4,6 +4,7 @@
4
4
  // Usage: auth-bootstrap.mjs uses this when auth.chromiumKeychain is configured alongside auth.chromeCookiePath.
5
5
  // Invariants/Assumptions: The configured cookie path points at a Chromium Cookies DB and the configured Keychain item is the browser's safe-storage secret.
6
6
  import { spawn } from "node:child_process";
7
+ import { sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
7
8
  import { createDecipheriv, pbkdf2Sync } from "node:crypto";
8
9
  import { copyFileSync, existsSync, mkdtempSync, rmSync } from "node:fs";
9
10
  import { tmpdir } from "node:os";
@@ -16,7 +17,7 @@ const MACOS_CHROMIUM_KEY_ITERATIONS = 1003;
16
17
 
17
18
  function spawnCapture(command, args, options = {}) {
18
19
  return new Promise((resolve) => {
19
- const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
20
+ const child = spawn(command, args, { env: sweetCookieSafeStoragePasswordScrubbedEnv(), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
20
21
  let stdout = "";
21
22
  let stderr = "";
22
23
  const timeoutMs = options.timeoutMs ?? 5_000;