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.
- package/CHANGELOG.md +32 -0
- package/README.md +53 -18
- package/docs/ORACLE_DESIGN.md +16 -8
- package/docs/platform-smoke.md +156 -0
- package/extensions/oracle/index.ts +10 -4
- package/extensions/oracle/lib/config.ts +53 -27
- package/extensions/oracle/lib/jobs.ts +9 -5
- package/extensions/oracle/lib/poller.ts +1 -0
- package/extensions/oracle/lib/runtime.ts +107 -32
- package/extensions/oracle/lib/tools.ts +138 -12
- package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
- package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
- package/extensions/oracle/shared/process-helpers.mjs +12 -1
- package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
- package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
- package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
- package/extensions/oracle/worker/run-job.mjs +107 -25
- package/package.json +30 -9
- package/platform-smoke.config.mjs +66 -0
- package/scripts/oracle-real-smoke.mjs +500 -0
- package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
- package/scripts/platform-smoke/artifacts.mjs +87 -0
- package/scripts/platform-smoke/assertions.mjs +34 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
- package/scripts/platform-smoke/doctor.mjs +239 -0
- package/scripts/platform-smoke/invariants.mjs +124 -0
- package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
- package/scripts/platform-smoke/targets.mjs +434 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
159
|
-
killGraceTimer = setTimeout(() => child
|
|
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
|
|
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
|
-
|
|
273
|
-
|
|
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;
|