pi-oracle 0.7.4 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +51 -17
- package/docs/ORACLE_DESIGN.md +12 -5
- package/docs/platform-smoke.md +153 -0
- package/extensions/oracle/lib/config.ts +53 -27
- package/extensions/oracle/lib/jobs.ts +9 -5
- 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 +27 -6
- package/platform-smoke.config.mjs +59 -0
- package/scripts/oracle-real-smoke.mjs +497 -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 +108 -0
- package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
- package/scripts/platform-smoke/targets.mjs +434 -0
- 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
|
-
|
|
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;
|