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
|
@@ -8,6 +8,15 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { isAbsolute, join, normalize } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
assertNotKnownBrowserUserDataPath,
|
|
13
|
+
chromeUserAgentPlatformToken,
|
|
14
|
+
chromiumKeychainSupportedOnPlatform,
|
|
15
|
+
defaultCloneStrategyForPlatform,
|
|
16
|
+
detectDefaultBrowserProfileSource,
|
|
17
|
+
detectDefaultLinuxChromeExecutablePath,
|
|
18
|
+
sweetCookieSafeStoragePasswordScrubbedEnv,
|
|
19
|
+
} from "../shared/browser-profile-helpers.mjs";
|
|
11
20
|
import { getProjectId } from "./runtime.js";
|
|
12
21
|
|
|
13
22
|
export const ORACLE_PROVIDERS = ["chatgpt", "grok"] as const;
|
|
@@ -225,7 +234,6 @@ export type OracleCloneStrategy = (typeof CLONE_STRATEGIES)[number];
|
|
|
225
234
|
const ALLOWED_CHATGPT_ORIGINS = new Set(["https://chatgpt.com", "https://chat.openai.com"]);
|
|
226
235
|
const PROJECT_OVERRIDE_KEYS = new Set(["defaults", "worker", "poller", "artifacts", "cleanup"]);
|
|
227
236
|
const DEFAULT_MAC_CHROME_EXECUTABLE = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
228
|
-
const DEFAULT_MAC_CHROME_USER_DATA_DIR = join(homedir(), "Library", "Application Support", "Google", "Chrome");
|
|
229
237
|
|
|
230
238
|
export interface OracleConfig {
|
|
231
239
|
defaults: {
|
|
@@ -274,37 +282,36 @@ export interface OracleConfig {
|
|
|
274
282
|
}
|
|
275
283
|
|
|
276
284
|
function detectDefaultChromeExecutablePath(): string | undefined {
|
|
277
|
-
|
|
285
|
+
if (process.platform === "darwin") {
|
|
286
|
+
return existsSync(DEFAULT_MAC_CHROME_EXECUTABLE) ? DEFAULT_MAC_CHROME_EXECUTABLE : undefined;
|
|
287
|
+
}
|
|
288
|
+
if (process.platform === "linux") {
|
|
289
|
+
return detectDefaultLinuxChromeExecutablePath();
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
278
292
|
}
|
|
279
293
|
|
|
280
294
|
function detectDefaultChromeUserAgent(executablePath: string | undefined): string | undefined {
|
|
281
295
|
if (!executablePath) return undefined;
|
|
296
|
+
// Linux executable discovery is PATH-based, so avoid executing that discovered
|
|
297
|
+
// binary during config module initialization just to derive a user agent.
|
|
298
|
+
if (process.platform === "linux") return undefined;
|
|
299
|
+
const platformToken = chromeUserAgentPlatformToken(process.platform);
|
|
300
|
+
if (!platformToken) return undefined;
|
|
282
301
|
try {
|
|
283
|
-
const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8" }).trim();
|
|
302
|
+
const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv() }).trim();
|
|
284
303
|
const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+\.\d+)/);
|
|
285
304
|
if (!versionMatch) return undefined;
|
|
286
|
-
return `Mozilla/5.0 (
|
|
305
|
+
return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionMatch[1]} Safari/537.36`;
|
|
287
306
|
} catch {
|
|
288
307
|
return undefined;
|
|
289
308
|
}
|
|
290
309
|
}
|
|
291
310
|
|
|
292
|
-
function detectDefaultChromeProfileName(): string {
|
|
293
|
-
const localStatePath = join(DEFAULT_MAC_CHROME_USER_DATA_DIR, "Local State");
|
|
294
|
-
if (!existsSync(localStatePath)) return "Default";
|
|
295
|
-
try {
|
|
296
|
-
const localState = JSON.parse(readFileSync(localStatePath, "utf8")) as { profile?: { last_used?: string } };
|
|
297
|
-
const lastUsed = localState?.profile?.last_used;
|
|
298
|
-
return typeof lastUsed === "string" && lastUsed.trim() ? lastUsed.trim() : "Default";
|
|
299
|
-
} catch {
|
|
300
|
-
return "Default";
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
311
|
const detectedChromeExecutablePath = detectDefaultChromeExecutablePath();
|
|
305
312
|
const detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecutablePath);
|
|
306
313
|
const agentExtensionsDir = join(getAgentDir(), "extensions");
|
|
307
|
-
const detectedChromeProfileName =
|
|
314
|
+
const detectedChromeProfileName = detectDefaultBrowserProfileSource(process.platform);
|
|
308
315
|
|
|
309
316
|
export interface OracleConfigLoadDetails {
|
|
310
317
|
agentDir: string;
|
|
@@ -367,7 +374,7 @@ export const DEFAULT_CONFIG: OracleConfig = {
|
|
|
367
374
|
authSeedProfileDir: join(agentExtensionsDir, "oracle-auth-seed-profile"),
|
|
368
375
|
runtimeProfilesDir: join(agentExtensionsDir, "oracle-runtime-profiles"),
|
|
369
376
|
maxConcurrentJobs: 2,
|
|
370
|
-
cloneStrategy:
|
|
377
|
+
cloneStrategy: defaultCloneStrategyForPlatform(process.platform),
|
|
371
378
|
chatUrl: "https://chatgpt.com/",
|
|
372
379
|
authUrl: "https://chatgpt.com/auth/login",
|
|
373
380
|
runMode: "headless",
|
|
@@ -452,18 +459,28 @@ function expectAbsoluteNormalizedPath(value: unknown, path: string): string {
|
|
|
452
459
|
return normalize(expanded);
|
|
453
460
|
}
|
|
454
461
|
|
|
455
|
-
function expectSafeProfilePath(
|
|
462
|
+
function expectSafeProfilePath(
|
|
463
|
+
pathValue: string,
|
|
464
|
+
path: string,
|
|
465
|
+
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string },
|
|
466
|
+
): string {
|
|
456
467
|
if (pathValue === "/" || pathValue === homedir()) {
|
|
457
468
|
throw new Error(`Invalid oracle config: ${path} points to an unsafe directory`);
|
|
458
469
|
}
|
|
459
|
-
|
|
460
|
-
|
|
470
|
+
try {
|
|
471
|
+
assertNotKnownBrowserUserDataPath(pathValue, `Invalid oracle config: ${path}`, { cookieSources });
|
|
472
|
+
} catch (error) {
|
|
473
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
461
474
|
}
|
|
462
475
|
return pathValue;
|
|
463
476
|
}
|
|
464
477
|
|
|
465
|
-
function expectSafeProfileDir(
|
|
466
|
-
|
|
478
|
+
function expectSafeProfileDir(
|
|
479
|
+
value: unknown,
|
|
480
|
+
path: string,
|
|
481
|
+
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string },
|
|
482
|
+
): string {
|
|
483
|
+
return expectSafeProfilePath(expectAbsoluteNormalizedPath(value, path), path, cookieSources);
|
|
467
484
|
}
|
|
468
485
|
|
|
469
486
|
function expectBoolean(value: unknown, path: string): boolean {
|
|
@@ -584,17 +601,26 @@ function validateOracleConfig(value: unknown): OracleConfig {
|
|
|
584
601
|
const artifacts = expectObject(root.artifacts, "artifacts");
|
|
585
602
|
const cleanup = expectObject(root.cleanup, "cleanup");
|
|
586
603
|
|
|
587
|
-
const
|
|
588
|
-
const
|
|
604
|
+
const chromeProfile = expectString(auth.chromeProfile, "auth.chromeProfile");
|
|
605
|
+
const chromeCookiePath = expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath");
|
|
606
|
+
const cookieSources = { chromeProfile, chromeCookiePath };
|
|
607
|
+
const authSeedProfileDir = expectSafeProfileDir(browser.authSeedProfileDir, "browser.authSeedProfileDir", cookieSources);
|
|
608
|
+
const runtimeProfilesDir = expectSafeProfileDir(browser.runtimeProfilesDir, "browser.runtimeProfilesDir", cookieSources);
|
|
589
609
|
if (runtimeProfilesDir === authSeedProfileDir || runtimeProfilesDir.startsWith(`${authSeedProfileDir}/`)) {
|
|
590
610
|
throw new Error("Invalid oracle config: browser.runtimeProfilesDir must be separate from browser.authSeedProfileDir");
|
|
591
611
|
}
|
|
592
612
|
|
|
593
|
-
const chromeCookiePath = expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath");
|
|
594
613
|
const chromiumKeychain = expectOptionalChromiumKeychain(auth.chromiumKeychain, "auth.chromiumKeychain");
|
|
595
614
|
if (chromiumKeychain !== undefined && chromeCookiePath === undefined) {
|
|
596
615
|
throw new Error("Invalid oracle config: auth.chromiumKeychain requires auth.chromeCookiePath");
|
|
597
616
|
}
|
|
617
|
+
if (chromiumKeychain !== undefined && !chromiumKeychainSupportedOnPlatform(process.platform)) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
"Invalid oracle config: auth.chromiumKeychain is macOS-only. " +
|
|
620
|
+
"On Linux, set auth.chromeCookiePath/auth.chromeProfile without auth.chromiumKeychain and use @steipete/sweet-cookie's " +
|
|
621
|
+
"SWEET_COOKIE_LINUX_KEYRING, SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD, or SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD options for encrypted Chromium cookies.",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
598
624
|
|
|
599
625
|
return {
|
|
600
626
|
defaults: {
|
|
@@ -618,7 +644,7 @@ function validateOracleConfig(value: unknown): OracleConfig {
|
|
|
618
644
|
auth: {
|
|
619
645
|
pollMs: expectInteger(auth.pollMs, "auth.pollMs", 100),
|
|
620
646
|
bootstrapTimeoutMs: expectInteger(auth.bootstrapTimeoutMs, "auth.bootstrapTimeoutMs", 1000),
|
|
621
|
-
chromeProfile
|
|
647
|
+
chromeProfile,
|
|
622
648
|
chromeCookiePath,
|
|
623
649
|
chromiumKeychain,
|
|
624
650
|
},
|
|
@@ -197,9 +197,12 @@ export interface OracleRuntimeAllocation {
|
|
|
197
197
|
seedGeneration?: string;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
function hasSessionFileAccessor(value: unknown): value is { getSessionFile: () => string | undefined } {
|
|
201
|
+
return typeof value === "object" && value !== null && "getSessionFile" in value && typeof value.getSessionFile === "function";
|
|
202
|
+
}
|
|
203
|
+
|
|
200
204
|
export function getSessionFile(ctx: ExtensionContext): string | undefined {
|
|
201
|
-
|
|
202
|
-
return manager.getSessionFile?.();
|
|
205
|
+
return hasSessionFileAccessor(ctx.sessionManager) ? ctx.sessionManager.getSessionFile() : undefined;
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
export function getOracleJobsDir(): string {
|
|
@@ -989,13 +992,14 @@ export function resolveArchiveInputs(cwd: string, files: string[]): { absolute:
|
|
|
989
992
|
throw new Error("Archive input must use '.' exactly for a whole-repo archive");
|
|
990
993
|
}
|
|
991
994
|
const absolute = resolve(cwd, file);
|
|
992
|
-
|
|
995
|
+
const relativeFromCwd = relativePath(cwd, absolute);
|
|
996
|
+
if (relativeFromCwd === "" && file !== ".") {
|
|
993
997
|
throw new Error("Archive input must use '.' exactly for a whole-repo archive");
|
|
994
998
|
}
|
|
995
|
-
|
|
996
|
-
if (!relative) {
|
|
999
|
+
if (relativeFromCwd && (relativeFromCwd === ".." || relativeFromCwd.startsWith(`..${sep}`) || isAbsolute(relativeFromCwd))) {
|
|
997
1000
|
throw new Error(`Archive input must be inside the project cwd: ${file}`);
|
|
998
1001
|
}
|
|
1002
|
+
const relative = relativeFromCwd === "" ? "." : relativeFromCwd.split(sep).join("/");
|
|
999
1003
|
if (!existsSync(absolute)) {
|
|
1000
1004
|
throw new Error(`Archive input does not exist: ${file}`);
|
|
1001
1005
|
}
|
|
@@ -165,6 +165,7 @@ function getJobCountsForSession(sessionFile: string | undefined, cwd: string): {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function refreshOracleStatusSnapshot(snapshot: OraclePollerContextSnapshot): void {
|
|
168
|
+
if (!snapshot.hasUI) return;
|
|
168
169
|
if (!snapshot.sessionFile) {
|
|
169
170
|
snapshot.ui.setStatus("oracle", snapshot.ui.theme.fg("accent", "oracle: unavailable"));
|
|
170
171
|
return;
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { constants as fsConstants, existsSync, realpathSync, readFileSync } from "node:fs";
|
|
9
|
-
import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { access, cp as copyDirectory, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { delimiter, dirname, join } from "node:path";
|
|
11
|
+
import { assertNotKnownBrowserUserDataPath, sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
|
|
11
12
|
import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
|
|
12
13
|
import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
|
|
13
14
|
import type { OracleConfig } from "./config.js";
|
|
@@ -21,16 +22,44 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
|
|
|
21
22
|
) || "agent-browser";
|
|
22
23
|
const PROFILE_CLONE_TIMEOUT_MS = 120_000;
|
|
23
24
|
const ORACLE_SUBPROCESS_KILL_GRACE_MS = 2_000;
|
|
25
|
+
|
|
26
|
+
function killProcessTree(child: ReturnType<typeof spawn>): void {
|
|
27
|
+
if (process.platform === "win32" && child.pid) {
|
|
28
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true })
|
|
29
|
+
.on("error", () => undefined);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
child.kill("SIGTERM");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function killProcess(child: ReturnType<typeof spawn>): void {
|
|
36
|
+
if (process.platform === "win32" && child.pid) {
|
|
37
|
+
spawn("taskkill", ["/pid", String(child.pid), "/f"], { stdio: "ignore", windowsHide: true })
|
|
38
|
+
.on("error", () => undefined);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
child.kill("SIGKILL");
|
|
42
|
+
}
|
|
24
43
|
const WORKSPACE_ROOT_MARKERS = [
|
|
25
44
|
".pi/extensions/oracle.json",
|
|
26
45
|
".pi",
|
|
27
46
|
"AGENTS.md",
|
|
28
47
|
] as const;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
function cpCommand(): string {
|
|
49
|
+
return process.env.PI_ORACLE_CP_PATH?.trim() || "cp";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requiredOracleDependencies(config: OracleConfig): Array<{ name: string; command: string }> {
|
|
53
|
+
const dependencies = [
|
|
54
|
+
{ name: "agent-browser", command: AGENT_BROWSER_BIN },
|
|
55
|
+
{ name: "tar", command: "tar" },
|
|
56
|
+
{ name: "zstd", command: "zstd" },
|
|
57
|
+
];
|
|
58
|
+
if (config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
|
|
59
|
+
dependencies.push({ name: "cp", command: cpCommand() });
|
|
60
|
+
}
|
|
61
|
+
return dependencies;
|
|
62
|
+
}
|
|
34
63
|
|
|
35
64
|
export interface OracleRuntimeLeaseMetadata {
|
|
36
65
|
jobId: string;
|
|
@@ -152,30 +181,61 @@ function missingLocalDependencyMessage(name: string): string {
|
|
|
152
181
|
return `Oracle prerequisite not found on PATH: ${name}. Install ${name} and retry.`;
|
|
153
182
|
}
|
|
154
183
|
|
|
184
|
+
function unsafeOracleProfilePathMessage(label: string, path: string, error: unknown): string {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
return `Oracle ${label} path is unsafe: ${path}. ${message}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function assertSafeOracleProfilePath(
|
|
190
|
+
path: string,
|
|
191
|
+
label: "auth seed profile" | "runtime profile" | "runtime profiles",
|
|
192
|
+
config?: OracleConfig,
|
|
193
|
+
): void {
|
|
194
|
+
try {
|
|
195
|
+
assertNotKnownBrowserUserDataPath(path, `Oracle ${label}`, {
|
|
196
|
+
cookieSources: config ? { chromeProfile: config.auth.chromeProfile, chromeCookiePath: config.auth.chromeCookiePath } : undefined,
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new Error(unsafeOracleProfilePathMessage(label, path, error));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
155
203
|
function unwritableOracleDirectoryMessage(label: "runtime profiles" | "jobs", path: string): string {
|
|
156
204
|
return `Oracle ${label} directory is not writable: ${path}. Fix its permissions or configure a writable path, then retry.`;
|
|
157
205
|
}
|
|
158
206
|
|
|
207
|
+
async function isExecutableFile(path: string): Promise<boolean> {
|
|
208
|
+
try {
|
|
209
|
+
const stats = await stat(path);
|
|
210
|
+
if (!stats.isFile()) return false;
|
|
211
|
+
await access(path, fsConstants.X_OK);
|
|
212
|
+
return true;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function executableNameCandidates(command: string): string[] {
|
|
219
|
+
if (process.platform !== "win32" || /\.[^\\/]+$/.test(command)) return [command];
|
|
220
|
+
const extensions = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
|
|
221
|
+
return [command, ...extensions.map((extension) => `${command}${extension.toLowerCase()}`), ...extensions.map((extension) => `${command}${extension.toUpperCase()}`)];
|
|
222
|
+
}
|
|
223
|
+
|
|
159
224
|
async function resolveExecutableOnPath(command: string): Promise<string | undefined> {
|
|
160
225
|
if (!command) return undefined;
|
|
161
|
-
if (command.includes("/")) {
|
|
162
|
-
|
|
163
|
-
await
|
|
164
|
-
return command;
|
|
165
|
-
} catch {
|
|
166
|
-
return undefined;
|
|
226
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
227
|
+
for (const candidate of executableNameCandidates(command)) {
|
|
228
|
+
if (await isExecutableFile(candidate)) return candidate;
|
|
167
229
|
}
|
|
230
|
+
return undefined;
|
|
168
231
|
}
|
|
169
232
|
|
|
170
233
|
const pathValue = process.env.PATH ?? "";
|
|
171
234
|
for (const segment of pathValue.split(delimiter)) {
|
|
172
235
|
if (!segment) continue;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
await
|
|
176
|
-
return candidate;
|
|
177
|
-
} catch {
|
|
178
|
-
continue;
|
|
236
|
+
for (const name of executableNameCandidates(command)) {
|
|
237
|
+
const candidate = join(segment, name);
|
|
238
|
+
if (await isExecutableFile(candidate)) return candidate;
|
|
179
239
|
}
|
|
180
240
|
}
|
|
181
241
|
return undefined;
|
|
@@ -235,6 +295,7 @@ async function assertWritableDirectory(path: string, label: "runtime profiles" |
|
|
|
235
295
|
|
|
236
296
|
export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Promise<void> {
|
|
237
297
|
const seedDir = config.browser.authSeedProfileDir;
|
|
298
|
+
assertSafeOracleProfilePath(seedDir, "auth seed profile", config);
|
|
238
299
|
let seedStats;
|
|
239
300
|
try {
|
|
240
301
|
seedStats = await stat(seedDir);
|
|
@@ -257,9 +318,10 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
|
|
|
257
318
|
}
|
|
258
319
|
|
|
259
320
|
export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
|
|
321
|
+
assertSafeOracleProfilePath(config.browser.runtimeProfilesDir, "runtime profiles", config);
|
|
260
322
|
await assertOracleAuthSeedProfileReady(config);
|
|
261
323
|
await assertConfiguredBrowserExecutableReady(config.browser.executablePath);
|
|
262
|
-
for (const dependency of
|
|
324
|
+
for (const dependency of requiredOracleDependencies(config)) {
|
|
263
325
|
await assertRequiredLocalDependencyReady(dependency.name, dependency.command);
|
|
264
326
|
}
|
|
265
327
|
await assertWritableDirectory(config.browser.runtimeProfilesDir, "runtime profiles");
|
|
@@ -379,7 +441,7 @@ export async function releaseConversationLease(conversationId: string | undefine
|
|
|
379
441
|
}
|
|
380
442
|
|
|
381
443
|
function profileCloneArgs(config: OracleConfig, sourceDir: string, destinationDir: string): string[] {
|
|
382
|
-
if (config.browser.cloneStrategy === "apfs-clone") {
|
|
444
|
+
if (config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
|
|
383
445
|
return ["-cR", sourceDir, destinationDir];
|
|
384
446
|
}
|
|
385
447
|
return ["-R", sourceDir, destinationDir];
|
|
@@ -387,7 +449,7 @@ function profileCloneArgs(config: OracleConfig, sourceDir: string, destinationDi
|
|
|
387
449
|
|
|
388
450
|
async function spawnCp(args: string[], options?: { timeoutMs?: number }): Promise<void> {
|
|
389
451
|
await new Promise<void>((resolve, reject) => {
|
|
390
|
-
const child = spawn(
|
|
452
|
+
const child = spawn(cpCommand(), args, { env: sweetCookieSafeStoragePasswordScrubbedEnv(), stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
391
453
|
let stderr = "";
|
|
392
454
|
let timedOut = false;
|
|
393
455
|
let killTimer: NodeJS.Timeout | undefined;
|
|
@@ -401,9 +463,9 @@ async function spawnCp(args: string[], options?: { timeoutMs?: number }): Promis
|
|
|
401
463
|
if ((options?.timeoutMs ?? 0) > 0) {
|
|
402
464
|
killTimer = setTimeout(() => {
|
|
403
465
|
timedOut = true;
|
|
404
|
-
child
|
|
466
|
+
killProcessTree(child);
|
|
405
467
|
killGraceTimer = setTimeout(() => {
|
|
406
|
-
child
|
|
468
|
+
killProcess(child);
|
|
407
469
|
}, ORACLE_SUBPROCESS_KILL_GRACE_MS);
|
|
408
470
|
killGraceTimer.unref?.();
|
|
409
471
|
}, options?.timeoutMs);
|
|
@@ -445,11 +507,16 @@ export async function cloneSeedProfileToRuntime(
|
|
|
445
507
|
): Promise<string | undefined> {
|
|
446
508
|
const seedDir = config.browser.authSeedProfileDir;
|
|
447
509
|
await assertOracleAuthSeedProfileReady(config);
|
|
510
|
+
assertSafeOracleProfilePath(runtimeProfileDir, "runtime profile", config);
|
|
448
511
|
|
|
449
512
|
await withAuthLock({ runtimeProfileDir, seedDir }, async () => {
|
|
450
513
|
await rm(runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
|
|
451
514
|
await mkdir(dirname(runtimeProfileDir), { recursive: true, mode: 0o700 }).catch(() => undefined);
|
|
452
|
-
|
|
515
|
+
if (config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
|
|
516
|
+
await spawnCp(profileCloneArgs(config, seedDir, runtimeProfileDir), { timeoutMs: options?.cpTimeoutMs ?? PROFILE_CLONE_TIMEOUT_MS });
|
|
517
|
+
} else {
|
|
518
|
+
await copyDirectory(seedDir, runtimeProfileDir, { recursive: true, force: true, verbatimSymlinks: true });
|
|
519
|
+
}
|
|
453
520
|
await removeChromiumProcessSingletonArtifacts(runtimeProfileDir);
|
|
454
521
|
});
|
|
455
522
|
|
|
@@ -465,7 +532,7 @@ export interface OracleCleanupReport {
|
|
|
465
532
|
|
|
466
533
|
async function closeRuntimeBrowserSession(runtimeSessionName: string): Promise<string | undefined> {
|
|
467
534
|
return new Promise<string | undefined>((resolve) => {
|
|
468
|
-
const child = spawn(AGENT_BROWSER_BIN, ["--session", runtimeSessionName, "close"], { stdio: "ignore" });
|
|
535
|
+
const child = spawn(AGENT_BROWSER_BIN, ["--session", runtimeSessionName, "close"], { env: sweetCookieSafeStoragePasswordScrubbedEnv(), stdio: "ignore", shell: process.platform === "win32" });
|
|
469
536
|
let settled = false;
|
|
470
537
|
let timeout: NodeJS.Timeout | undefined;
|
|
471
538
|
let timedOut = false;
|
|
@@ -479,15 +546,21 @@ async function closeRuntimeBrowserSession(runtimeSessionName: string): Promise<s
|
|
|
479
546
|
|
|
480
547
|
timeout = setTimeout(() => {
|
|
481
548
|
timedOut = true;
|
|
482
|
-
child
|
|
549
|
+
killProcessTree(child);
|
|
483
550
|
setTimeout(() => {
|
|
484
|
-
child
|
|
551
|
+
killProcess(child);
|
|
485
552
|
finish(`Timed out closing agent-browser session ${runtimeSessionName} after ${AGENT_BROWSER_CLOSE_TIMEOUT_MS}ms`);
|
|
486
553
|
}, 2_000).unref?.();
|
|
487
554
|
}, AGENT_BROWSER_CLOSE_TIMEOUT_MS);
|
|
488
555
|
timeout.unref?.();
|
|
489
556
|
|
|
490
|
-
child.on("error", (error) =>
|
|
557
|
+
child.on("error", (error: NodeJS.ErrnoException) => {
|
|
558
|
+
if (error.code === "ENOENT") {
|
|
559
|
+
finish();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
finish(`Failed to close agent-browser session ${runtimeSessionName}: ${error.message}`);
|
|
563
|
+
});
|
|
491
564
|
child.on("close", (code) => {
|
|
492
565
|
if (timedOut || code === 0) finish();
|
|
493
566
|
else finish(`agent-browser close exited with code ${code} for session ${runtimeSessionName}`);
|
|
@@ -510,9 +583,12 @@ export async function cleanupRuntimeArtifacts(runtime: {
|
|
|
510
583
|
}
|
|
511
584
|
if (runtime.runtimeProfileDir) {
|
|
512
585
|
report.attempted.push("runtimeProfileDir");
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
586
|
+
try {
|
|
587
|
+
assertSafeOracleProfilePath(runtime.runtimeProfileDir, "runtime profile");
|
|
588
|
+
await rm(runtime.runtimeProfileDir, { recursive: true, force: true });
|
|
589
|
+
} catch (error) {
|
|
590
|
+
report.warnings.push(`Failed to remove runtime profile ${runtime.runtimeProfileDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
591
|
+
}
|
|
516
592
|
}
|
|
517
593
|
if (runtime.conversationId) {
|
|
518
594
|
report.attempted.push("conversationLease");
|
|
@@ -529,4 +605,3 @@ export async function cleanupRuntimeArtifacts(runtime: {
|
|
|
529
605
|
|
|
530
606
|
return report;
|
|
531
607
|
}
|
|
532
|
-
|