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
@@ -0,0 +1,395 @@
1
+ // Purpose: Centralize platform-specific browser profile paths, executable discovery, and profile-safety checks for oracle auth/runtime code.
2
+ // Responsibilities: Resolve Chromium-family user-data roots, choose platform defaults, find executable files safely, and block oracle profile paths that point into real browser data.
3
+ // Scope: Local filesystem/path policy only; cookie import, browser automation, and config loading stay in higher-level modules.
4
+ // Usage: Imported by config.ts, runtime.ts, auth-bootstrap.mjs, run-job.mjs, and sanity tests.
5
+ // Invariants/Assumptions: Real browser profile roots must never be used as oracle seed/runtime profile destinations, even through symlinked ancestors.
6
+
7
+ import { accessSync, constants as fsConstants, existsSync, realpathSync, readFileSync, statSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { basename, delimiter, dirname, isAbsolute, join, normalize, resolve } from "node:path";
10
+
11
+ /** @typedef {import("./browser-profile-helpers.d.mts").OraclePlatform} OraclePlatform */
12
+ /** @typedef {import("./browser-profile-helpers.d.mts").BrowserPathOptions} BrowserPathOptions */
13
+ /** @typedef {import("./browser-profile-helpers.d.mts").ExecutableSearchOptions} ExecutableSearchOptions */
14
+
15
+ export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES = Object.freeze([
16
+ "SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
17
+ "SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
18
+ "SWEET_COOKIE_EDGE_SAFE_STORAGE_PASSWORD",
19
+ ]);
20
+
21
+ const LINUX_COOKIE_IMPORT_USER_DATA_RELATIVE_DIRS = Object.freeze([
22
+ ["google-chrome"],
23
+ ["chromium"],
24
+ ["chromium-browser"],
25
+ ["BraveSoftware", "Brave-Browser"],
26
+ ]);
27
+
28
+ const LINUX_SAFETY_EXTRA_USER_DATA_RELATIVE_DIRS = Object.freeze([
29
+ ["google-chrome-beta"],
30
+ ["google-chrome-unstable"],
31
+ ["microsoft-edge"],
32
+ ["microsoft-edge-beta"],
33
+ ["microsoft-edge-dev"],
34
+ ["vivaldi"],
35
+ ["opera"],
36
+ ]);
37
+
38
+ const MAC_CHROMIUM_USER_DATA_RELATIVE_DIRS = Object.freeze([
39
+ ["Library", "Application Support", "Google", "Chrome"],
40
+ ["Library", "Application Support", "Chromium"],
41
+ ["Library", "Application Support", "BraveSoftware", "Brave-Browser"],
42
+ ["Library", "Application Support", "Microsoft Edge"],
43
+ ["Library", "Application Support", "Arc", "User Data"],
44
+ ["Library", "Application Support", "Vivaldi"],
45
+ ["Library", "Application Support", "com.operasoftware.Opera"],
46
+ ["Library", "Application Support", "Google", "Chrome for Testing"],
47
+ ]);
48
+
49
+ const WINDOWS_CHROMIUM_USER_DATA_RELATIVE_DIRS = Object.freeze([
50
+ ["AppData", "Local", "Google", "Chrome", "User Data"],
51
+ ["AppData", "Local", "Chromium", "User Data"],
52
+ ["AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data"],
53
+ ["AppData", "Local", "Microsoft", "Edge", "User Data"],
54
+ ["AppData", "Local", "Vivaldi", "User Data"],
55
+ ["AppData", "Roaming", "Opera Software", "Opera Stable"],
56
+ ]);
57
+
58
+ const LINUX_CHROME_EXECUTABLE_NAMES = Object.freeze(["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "brave-browser", "brave"]);
59
+
60
+ /**
61
+ * @param {string} value
62
+ * @param {string} [homeDir]
63
+ * @returns {string}
64
+ */
65
+ export function expandHomePath(value, homeDir = homedir()) {
66
+ if (value === "~") return homeDir;
67
+ if (value.startsWith("~/")) return join(homeDir, value.slice(2));
68
+ return value;
69
+ }
70
+
71
+ /**
72
+ * @param {string} value
73
+ * @param {BrowserPathOptions} [options]
74
+ * @returns {string}
75
+ */
76
+ export function normalizedAbsolutePath(value, options = {}) {
77
+ const expanded = expandHomePath(value, options.homeDir ?? homedir());
78
+ return normalize(isAbsolute(expanded) ? expanded : resolve(expanded));
79
+ }
80
+
81
+ /**
82
+ * @param {BrowserPathOptions} [options]
83
+ * @returns {string}
84
+ */
85
+ export function linuxConfigHome(options = {}) {
86
+ const env = options.env ?? process.env;
87
+ const configured = env.XDG_CONFIG_HOME?.trim();
88
+ return configured ? normalizedAbsolutePath(configured, options) : join(options.homeDir ?? homedir(), ".config");
89
+ }
90
+
91
+ /**
92
+ * @param {BrowserPathOptions} [options]
93
+ * @returns {string[]}
94
+ */
95
+ export function linuxChromiumCookieImportUserDataDirs(options = {}) {
96
+ const configHome = linuxConfigHome(options);
97
+ return LINUX_COOKIE_IMPORT_USER_DATA_RELATIVE_DIRS.map((segments) => join(configHome, ...segments));
98
+ }
99
+
100
+ /**
101
+ * @param {BrowserPathOptions} [options]
102
+ * @returns {string[]}
103
+ */
104
+ export function linuxBrowserSafetyUserDataDirs(options = {}) {
105
+ const configHome = linuxConfigHome(options);
106
+ return [
107
+ ...LINUX_COOKIE_IMPORT_USER_DATA_RELATIVE_DIRS,
108
+ ...LINUX_SAFETY_EXTRA_USER_DATA_RELATIVE_DIRS,
109
+ ].map((segments) => join(configHome, ...segments));
110
+ }
111
+
112
+ /**
113
+ * @param {OraclePlatform} [platform]
114
+ * @param {BrowserPathOptions & { includeUnsupported?: boolean }} [options]
115
+ * @returns {string[]}
116
+ */
117
+ export function browserUserDataDirsForPlatform(platform = process.platform, options = {}) {
118
+ const homeDir = options.homeDir ?? homedir();
119
+ if (platform === "darwin") return MAC_CHROMIUM_USER_DATA_RELATIVE_DIRS.map((segments) => join(homeDir, ...segments));
120
+ if (platform === "linux") return options.includeUnsupported === false ? linuxChromiumCookieImportUserDataDirs({ ...options, homeDir }) : linuxBrowserSafetyUserDataDirs({ ...options, homeDir });
121
+ if (platform === "win32") return WINDOWS_CHROMIUM_USER_DATA_RELATIVE_DIRS.map((segments) => join(homeDir, ...segments));
122
+ return [];
123
+ }
124
+
125
+ /**
126
+ * @param {OraclePlatform} [platform]
127
+ * @returns {"apfs-clone" | "copy"}
128
+ */
129
+ export function defaultCloneStrategyForPlatform(platform = process.platform) {
130
+ return platform === "darwin" ? "apfs-clone" : "copy";
131
+ }
132
+
133
+ /**
134
+ * @param {OraclePlatform} [platform]
135
+ * @returns {boolean}
136
+ */
137
+ export function chromiumKeychainSupportedOnPlatform(platform = process.platform) {
138
+ return platform === "darwin";
139
+ }
140
+
141
+ /**
142
+ * @param {OraclePlatform} [platform]
143
+ * @returns {string | undefined}
144
+ */
145
+ export function chromeUserAgentPlatformToken(platform = process.platform) {
146
+ if (platform === "darwin") return "Macintosh; Intel Mac OS X 10_15_7";
147
+ if (platform === "linux") return "X11; Linux x86_64";
148
+ return undefined;
149
+ }
150
+
151
+ /**
152
+ * @param {string} childPath
153
+ * @param {string} parentPath
154
+ * @returns {boolean}
155
+ */
156
+ export function pathInsideOrEqual(childPath, parentPath) {
157
+ const child = normalize(childPath);
158
+ const parent = normalize(parentPath);
159
+ if (child === parent) return true;
160
+ if (!parent) return false;
161
+ const parentWithSeparator = /[/\\]$/.test(parent) ? parent : `${parent}/`;
162
+ const alternateParentWithSeparator = parentWithSeparator.includes("/")
163
+ ? parentWithSeparator.replaceAll("/", "\\")
164
+ : parentWithSeparator.replaceAll("\\", "/");
165
+ return child.startsWith(parentWithSeparator) || child.startsWith(alternateParentWithSeparator);
166
+ }
167
+
168
+ /**
169
+ * Resolve a path as far as its existing ancestors allow. If the final path does
170
+ * not exist yet, any non-existing suffix is appended to the nearest existing
171
+ * ancestor's realpath so symlinked ancestors are still accounted for.
172
+ *
173
+ * @param {string} pathValue
174
+ * @returns {string | undefined}
175
+ */
176
+ export function resolvePathThroughExistingAncestorsSync(pathValue) {
177
+ const absolute = normalizedAbsolutePath(pathValue);
178
+ const suffix = [];
179
+ let current = absolute;
180
+ while (true) {
181
+ if (existsSync(current)) {
182
+ try {
183
+ return normalize(join(realpathSync(current), ...suffix.reverse()));
184
+ } catch {
185
+ return undefined;
186
+ }
187
+ }
188
+ const parent = dirname(current);
189
+ if (parent === current) return undefined;
190
+ suffix.push(basename(current));
191
+ current = parent;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * @param {string} pathValue
197
+ * @returns {boolean}
198
+ */
199
+ function looksLikeFilesystemPath(pathValue) {
200
+ return pathValue.startsWith("/") || pathValue.startsWith("~/") || pathValue === "~" || pathValue.startsWith(".");
201
+ }
202
+
203
+ /**
204
+ * @param {string} pathValue
205
+ * @returns {boolean}
206
+ */
207
+ function isCookiesDbPath(pathValue) {
208
+ return basename(pathValue) === "Cookies";
209
+ }
210
+
211
+ /**
212
+ * @param {string} cookiePath
213
+ * @returns {string[]}
214
+ */
215
+ function protectedPathsForCookieDb(cookiePath) {
216
+ const normalized = normalizedAbsolutePath(cookiePath);
217
+ const cookieParent = dirname(normalized);
218
+ const profileDir = basename(cookieParent) === "Network" ? dirname(cookieParent) : cookieParent;
219
+ return [profileDir, dirname(profileDir)];
220
+ }
221
+
222
+ /**
223
+ * @param {{ chromeProfile?: string; chromeCookiePath?: string } | undefined} cookieSources
224
+ * @returns {string[]}
225
+ */
226
+ export function protectedCookieSourcePaths(cookieSources) {
227
+ if (!cookieSources) return [];
228
+ const roots = [];
229
+ const cookiePath = typeof cookieSources.chromeCookiePath === "string" && cookieSources.chromeCookiePath.trim()
230
+ ? cookieSources.chromeCookiePath.trim()
231
+ : undefined;
232
+ if (cookiePath) roots.push(...protectedPathsForCookieDb(cookiePath));
233
+
234
+ const profile = typeof cookieSources.chromeProfile === "string" && cookieSources.chromeProfile.trim()
235
+ ? cookieSources.chromeProfile.trim()
236
+ : undefined;
237
+ if (profile && looksLikeFilesystemPath(profile)) {
238
+ const normalizedProfile = normalizedAbsolutePath(profile);
239
+ if (isCookiesDbPath(normalizedProfile)) roots.push(...protectedPathsForCookieDb(normalizedProfile));
240
+ else roots.push(normalizedProfile, dirname(normalizedProfile));
241
+ }
242
+
243
+ return [...new Set(roots.map((root) => normalize(root)))];
244
+ }
245
+
246
+ /**
247
+ * @param {string} pathValue
248
+ * @param {BrowserPathOptions & { platform?: OraclePlatform; includeUnsupported?: boolean; extraProtectedPaths?: string[]; cookieSources?: { chromeProfile?: string; chromeCookiePath?: string } }} [options]
249
+ * @returns {string | undefined}
250
+ */
251
+ export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
252
+ const platform = options.platform ?? process.platform;
253
+ const normalizedPath = normalizedAbsolutePath(pathValue, options);
254
+ const resolvedPath = resolvePathThroughExistingAncestorsSync(normalizedPath);
255
+ const roots = [
256
+ ...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true }),
257
+ ...protectedCookieSourcePaths(options.cookieSources),
258
+ ...((options.extraProtectedPaths ?? [])),
259
+ ];
260
+ for (const root of roots) {
261
+ const normalizedRoot = normalizedAbsolutePath(root, options);
262
+ if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return normalizedRoot;
263
+ const resolvedRoot = resolvePathThroughExistingAncestorsSync(normalizedRoot) ?? normalizedRoot;
264
+ if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return resolvedRoot;
265
+ }
266
+ return undefined;
267
+ }
268
+
269
+ /**
270
+ * @param {string} pathValue
271
+ * @param {string} label
272
+ * @param {BrowserPathOptions & { platform?: OraclePlatform; includeUnsupported?: boolean; extraProtectedPaths?: string[]; cookieSources?: { chromeProfile?: string; chromeCookiePath?: string } }} [options]
273
+ * @returns {void}
274
+ */
275
+ export function assertNotKnownBrowserUserDataPath(pathValue, label, options = {}) {
276
+ const match = knownBrowserUserDataPathMatch(pathValue, options);
277
+ if (match) {
278
+ throw new Error(`${label} must not point into a real browser user-data directory (${match}): ${pathValue}`);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * @param {string} pathValue
284
+ * @returns {boolean}
285
+ */
286
+ export function isExecutableFileSync(pathValue) {
287
+ try {
288
+ const stats = statSync(pathValue);
289
+ if (!stats.isFile()) return false;
290
+ accessSync(pathValue, fsConstants.X_OK);
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * @param {readonly string[]} names
299
+ * @param {ExecutableSearchOptions} [options]
300
+ * @returns {string | undefined}
301
+ */
302
+ export function findExecutableOnPathSync(names, options = {}) {
303
+ const pathValue = options.pathValue ?? process.env.PATH ?? "";
304
+ const pathDelimiter = options.pathDelimiter ?? delimiter;
305
+ for (const name of names) {
306
+ for (const dir of pathValue.split(pathDelimiter).filter(Boolean)) {
307
+ const candidate = join(dir, name);
308
+ if (isExecutableFileSync(candidate)) return candidate;
309
+ }
310
+ }
311
+ return undefined;
312
+ }
313
+
314
+ /**
315
+ * @param {ExecutableSearchOptions} [options]
316
+ * @returns {string | undefined}
317
+ */
318
+ export function detectDefaultLinuxChromeExecutablePath(options = {}) {
319
+ return findExecutableOnPathSync(LINUX_CHROME_EXECUTABLE_NAMES, options);
320
+ }
321
+
322
+ /**
323
+ * @param {string} userDataDir
324
+ * @returns {string | undefined}
325
+ */
326
+ function readLastUsedProfileName(userDataDir) {
327
+ const localStatePath = join(userDataDir, "Local State");
328
+ if (!existsSync(localStatePath)) return undefined;
329
+ try {
330
+ const localState = JSON.parse(readFileSync(localStatePath, "utf8"));
331
+ const lastUsed = localState?.profile?.last_used;
332
+ if (typeof lastUsed !== "string") return undefined;
333
+ const trimmed = lastUsed.trim();
334
+ if (!trimmed || trimmed === "." || trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) return undefined;
335
+ return trimmed;
336
+ } catch {
337
+ return undefined;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Return an absolute Linux Chrome/Chromium-family profile directory for the
343
+ * default cookie importer. Sweet Cookie's Linux `chrome` backend only resolves
344
+ * non-path profile names under google-chrome, so non-Google roots must be
345
+ * passed as absolute paths.
346
+ *
347
+ * @param {BrowserPathOptions} [options]
348
+ * @returns {string | undefined}
349
+ */
350
+ export function detectDefaultLinuxCookieProfileSource(options = {}) {
351
+ for (const userDataDir of linuxChromiumCookieImportUserDataDirs(options)) {
352
+ const lastUsed = readLastUsedProfileName(userDataDir);
353
+ if (lastUsed) {
354
+ const profilePath = join(userDataDir, lastUsed);
355
+ if (pathInsideOrEqual(profilePath, userDataDir) && existsSync(profilePath)) return profilePath;
356
+ }
357
+ const defaultProfile = join(userDataDir, "Default");
358
+ if (existsSync(defaultProfile)) return defaultProfile;
359
+ }
360
+ return undefined;
361
+ }
362
+
363
+ /**
364
+ * @param {OraclePlatform} [platform]
365
+ * @param {BrowserPathOptions} [options]
366
+ * @returns {string}
367
+ */
368
+ export function detectDefaultBrowserProfileSource(platform = process.platform, options = {}) {
369
+ if (platform === "linux") return detectDefaultLinuxCookieProfileSource(options) ?? "Default";
370
+ if (platform === "darwin") {
371
+ const userDataDir = browserUserDataDirsForPlatform("darwin", options)[0];
372
+ return readLastUsedProfileName(userDataDir) ?? "Default";
373
+ }
374
+ return "Default";
375
+ }
376
+
377
+ /**
378
+ * @param {NodeJS.ProcessEnv} [env]
379
+ * @returns {void}
380
+ */
381
+ export function scrubSweetCookieSafeStoragePasswordEnv(env = process.env) {
382
+ for (const name of SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES) {
383
+ delete env[name];
384
+ }
385
+ }
386
+
387
+ /**
388
+ * @param {NodeJS.ProcessEnv} [env]
389
+ * @returns {NodeJS.ProcessEnv}
390
+ */
391
+ export function sweetCookieSafeStoragePasswordScrubbedEnv(env = process.env) {
392
+ const childEnv = { ...env };
393
+ scrubSweetCookieSafeStoragePasswordEnv(childEnv);
394
+ return childEnv;
395
+ }
@@ -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;