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.
- package/CHANGELOG.md +27 -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 +40 -11
- 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
|
@@ -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
|
-
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
// Usage: Imported by the oracle extension entrypoint and sanity tests to register tools against the pi API.
|
|
5
5
|
// Invariants/Assumptions: The pi runtime validates TypeBox schemas before execute, while execute owns semantic normalization.
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { once } from "node:events";
|
|
9
|
+
import { createReadStream } from "node:fs";
|
|
10
|
+
import { lstat, mkdtemp, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
8
11
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { basename, join, posix } from "node:path";
|
|
12
|
+
import { basename, dirname, join, posix } from "node:path";
|
|
13
|
+
import { sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
|
|
10
14
|
import { runOracleAuthBootstrap } from "./auth.js";
|
|
11
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
16
|
import { Type } from "typebox";
|
|
@@ -125,6 +129,8 @@ const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_ANYWHERE = new Set([
|
|
|
125
129
|
".pi",
|
|
126
130
|
".oracle-context",
|
|
127
131
|
".cursor",
|
|
132
|
+
".artifacts",
|
|
133
|
+
".crabbox",
|
|
128
134
|
"node_modules",
|
|
129
135
|
"target",
|
|
130
136
|
".venv",
|
|
@@ -386,6 +392,99 @@ function formatArchiveOversizeError(args: {
|
|
|
386
392
|
.join("\n");
|
|
387
393
|
}
|
|
388
394
|
|
|
395
|
+
function writeOctal(value: number, width: number): Buffer {
|
|
396
|
+
const text = Math.max(0, Math.floor(value)).toString(8).slice(-(width - 1)).padStart(width - 1, "0") + "\0";
|
|
397
|
+
return Buffer.from(text, "ascii");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function writeTarName(header: Buffer, name: string): void {
|
|
401
|
+
const normalized = name.replaceAll("\\", "/");
|
|
402
|
+
const nameBytes = Buffer.byteLength(normalized);
|
|
403
|
+
if (nameBytes <= 100) {
|
|
404
|
+
header.write(normalized, 0, 100, "utf8");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const parts = normalized.split("/");
|
|
408
|
+
const fileName = parts.pop() || "";
|
|
409
|
+
const prefix = parts.join("/");
|
|
410
|
+
if (Buffer.byteLength(fileName) > 100 || Buffer.byteLength(prefix) > 155) {
|
|
411
|
+
throw new Error(`archive path is too long for portable tar header: ${normalized}`);
|
|
412
|
+
}
|
|
413
|
+
header.write(fileName, 0, 100, "utf8");
|
|
414
|
+
header.write(prefix, 345, 155, "utf8");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function buildTarHeader(name: string, options: { mode: number; size: number; mtimeMs: number; type: "file" | "directory" | "symlink"; linkName?: string }): Buffer {
|
|
418
|
+
const header = Buffer.alloc(512);
|
|
419
|
+
writeTarName(header, options.type === "directory" && !name.endsWith("/") ? `${name}/` : name);
|
|
420
|
+
writeOctal(options.mode & 0o7777, 8).copy(header, 100);
|
|
421
|
+
writeOctal(0, 8).copy(header, 108);
|
|
422
|
+
writeOctal(0, 8).copy(header, 116);
|
|
423
|
+
writeOctal(options.size, 12).copy(header, 124);
|
|
424
|
+
writeOctal(Math.floor(options.mtimeMs / 1000), 12).copy(header, 136);
|
|
425
|
+
Buffer.from(" ", "ascii").copy(header, 148);
|
|
426
|
+
header[156] = options.type === "directory" ? 53 : options.type === "symlink" ? 50 : 48;
|
|
427
|
+
if (options.linkName) header.write(options.linkName.replaceAll("\\", "/"), 157, 100, "utf8");
|
|
428
|
+
header.write("ustar", 257, 6, "ascii");
|
|
429
|
+
header.write("00", 263, 2, "ascii");
|
|
430
|
+
let checksum = 0;
|
|
431
|
+
for (const byte of header) checksum += byte;
|
|
432
|
+
const checksumText = checksum.toString(8).padStart(6, "0");
|
|
433
|
+
header.write(`${checksumText}\0 `, 148, 8, "ascii");
|
|
434
|
+
return header;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function writeChunk(stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> {
|
|
438
|
+
if (!stream.write(chunk)) await once(stream, "drain");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function writeWindowsTarArchiveToZstd(cwd: string, entries: string[], archivePath: string, timeout: AbortSignal): Promise<void> {
|
|
442
|
+
const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv(process.env);
|
|
443
|
+
const zstd = spawn("zstd", ["-19", "-T0", "-f", "-o", archivePath], {
|
|
444
|
+
cwd,
|
|
445
|
+
env: scrubbedEnv,
|
|
446
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
447
|
+
signal: timeout,
|
|
448
|
+
});
|
|
449
|
+
let stderr = "";
|
|
450
|
+
zstd.stderr?.on("data", (chunk) => {
|
|
451
|
+
stderr += String(chunk);
|
|
452
|
+
});
|
|
453
|
+
try {
|
|
454
|
+
for (const entry of entries) {
|
|
455
|
+
const normalizedEntry = entry.replaceAll("\\", "/");
|
|
456
|
+
const absolutePath = join(cwd, normalizedEntry);
|
|
457
|
+
const info = await lstat(absolutePath);
|
|
458
|
+
if (info.isSymbolicLink()) {
|
|
459
|
+
await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "symlink", linkName: await readlink(absolutePath) }));
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (info.isDirectory()) {
|
|
463
|
+
await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "directory" }));
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (!info.isFile()) continue;
|
|
467
|
+
await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: info.size, mtimeMs: info.mtimeMs, type: "file" }));
|
|
468
|
+
for await (const chunk of createReadStream(absolutePath)) {
|
|
469
|
+
await writeChunk(zstd.stdin, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
470
|
+
}
|
|
471
|
+
const padding = info.size % 512 === 0 ? 0 : 512 - (info.size % 512);
|
|
472
|
+
if (padding > 0) await writeChunk(zstd.stdin, Buffer.alloc(padding));
|
|
473
|
+
}
|
|
474
|
+
await writeChunk(zstd.stdin, Buffer.alloc(1024));
|
|
475
|
+
zstd.stdin.end();
|
|
476
|
+
} catch (error) {
|
|
477
|
+
zstd.stdin.destroy();
|
|
478
|
+
zstd.kill();
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
const code = await new Promise<number | null>((resolve, reject) => {
|
|
482
|
+
zstd.once("error", reject);
|
|
483
|
+
zstd.once("close", resolve);
|
|
484
|
+
});
|
|
485
|
+
if (code !== 0) throw new Error(`zstd archive compression failed with status ${code}: ${stderr.trim()}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
389
488
|
async function writeArchiveFile(
|
|
390
489
|
cwd: string,
|
|
391
490
|
entries: string[],
|
|
@@ -396,13 +495,32 @@ async function writeArchiveFile(
|
|
|
396
495
|
await writeFile(listPath, Buffer.from(`${entries.join("\0")}\0`), { mode: 0o600 });
|
|
397
496
|
await rm(archivePath, { force: true }).catch(() => undefined);
|
|
398
497
|
|
|
399
|
-
|
|
498
|
+
if (process.platform === "win32") {
|
|
499
|
+
const timeoutController = new AbortController();
|
|
500
|
+
const timeout = setTimeout(() => timeoutController.abort(), options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS);
|
|
501
|
+
try {
|
|
502
|
+
await writeWindowsTarArchiveToZstd(cwd, entries, archivePath, timeoutController.signal);
|
|
503
|
+
return (await stat(archivePath)).size;
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (timeoutController.signal.aborted) {
|
|
506
|
+
throw new Error(`Oracle archive subprocess timed out after ${options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS}ms`);
|
|
507
|
+
}
|
|
508
|
+
throw error;
|
|
509
|
+
} finally {
|
|
510
|
+
clearTimeout(timeout);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
400
514
|
await new Promise<void>((resolvePromise, rejectPromise) => {
|
|
401
|
-
const
|
|
402
|
-
|
|
515
|
+
const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv();
|
|
516
|
+
const tarArgs = ["--null", "-cf", "-", "-C", cwd, "-T", basename(listPath)];
|
|
517
|
+
const tar = spawn(process.env.PI_ORACLE_TEST_TAR_BIN ?? "tar", tarArgs, {
|
|
518
|
+
cwd: dirname(listPath),
|
|
519
|
+
env: scrubbedEnv,
|
|
403
520
|
stdio: ["ignore", "pipe", "pipe"],
|
|
404
521
|
});
|
|
405
|
-
const zstd = spawn("zstd", ["-19", "-T0", "-f", "-o", archivePath], {
|
|
522
|
+
const zstd = spawn(process.env.PI_ORACLE_TEST_ZSTD_BIN ?? "zstd", ["-19", "-T0", "-f", "-o", archivePath], {
|
|
523
|
+
env: scrubbedEnv,
|
|
406
524
|
stdio: ["pipe", "ignore", "pipe"],
|
|
407
525
|
});
|
|
408
526
|
|
|
@@ -832,6 +950,14 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
832
950
|
};
|
|
833
951
|
}
|
|
834
952
|
|
|
953
|
+
if (/^Oracle (auth seed profile|runtime profile|runtime profiles) path is unsafe: /.test(message)) {
|
|
954
|
+
return {
|
|
955
|
+
code: "oracle_profile_path_unsafe",
|
|
956
|
+
message,
|
|
957
|
+
suggestedNextStep: "Move browser.authSeedProfileDir/browser.runtimeProfilesDir outside real browser profile directories and retry.",
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
835
961
|
if (message.startsWith("Oracle runtime profiles directory is not writable: ")) {
|
|
836
962
|
return {
|
|
837
963
|
code: "runtime_profiles_dir_unwritable",
|
|
@@ -964,10 +1090,10 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
964
1090
|
function buildOracleToolErrorResult(
|
|
965
1091
|
toolName: OracleToolName,
|
|
966
1092
|
error: unknown,
|
|
967
|
-
params:
|
|
1093
|
+
params: unknown,
|
|
968
1094
|
options?: { job?: NonNullable<ReturnType<typeof readJob>>; jobDetails?: OracleToolJobDetailsOptions },
|
|
969
1095
|
) {
|
|
970
|
-
const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
|
|
1096
|
+
const errorDetails = buildOracleToolErrorDetails(toolName, error, asRecord(params) ?? {});
|
|
971
1097
|
return {
|
|
972
1098
|
content: [{
|
|
973
1099
|
type: "text" as const,
|
|
@@ -1430,7 +1556,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1430
1556
|
await appendCleanupWarnings(job.id, cleanupReport.warnings).catch(() => undefined);
|
|
1431
1557
|
}
|
|
1432
1558
|
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
1433
|
-
return buildOracleToolErrorResult("oracle_submit", error, params
|
|
1559
|
+
return buildOracleToolErrorResult("oracle_submit", error, params, {
|
|
1434
1560
|
job: latest ?? job,
|
|
1435
1561
|
jobDetails: {
|
|
1436
1562
|
queue: latest ? buildOracleQueueSnapshot(latest, latest.status === "queued" ? getQueuePosition(latest.id) : undefined) : undefined,
|
|
@@ -1443,7 +1569,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1443
1569
|
await rm(tempArchivePath, { force: true }).catch(() => undefined);
|
|
1444
1570
|
}
|
|
1445
1571
|
} catch (error) {
|
|
1446
|
-
return buildOracleToolErrorResult("oracle_submit", error, params
|
|
1572
|
+
return buildOracleToolErrorResult("oracle_submit", error, params);
|
|
1447
1573
|
}
|
|
1448
1574
|
},
|
|
1449
1575
|
});
|
|
@@ -1503,7 +1629,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1503
1629
|
},
|
|
1504
1630
|
};
|
|
1505
1631
|
} catch (error) {
|
|
1506
|
-
return buildOracleToolErrorResult("oracle_read", error, params
|
|
1632
|
+
return buildOracleToolErrorResult("oracle_read", error, params);
|
|
1507
1633
|
}
|
|
1508
1634
|
},
|
|
1509
1635
|
});
|
|
@@ -1538,7 +1664,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
1538
1664
|
details: { job: redactJobDetails(cancelled, { queue: buildOracleQueueSnapshot(cancelled, cancelled.status === "queued" ? getQueuePosition(cancelled.id) : undefined) }) },
|
|
1539
1665
|
};
|
|
1540
1666
|
} catch (error) {
|
|
1541
|
-
return buildOracleToolErrorResult("oracle_cancel", error, params
|
|
1667
|
+
return buildOracleToolErrorResult("oracle_cancel", error, params);
|
|
1542
1668
|
}
|
|
1543
1669
|
},
|
|
1544
1670
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type OraclePlatform = NodeJS.Platform;
|
|
2
|
+
|
|
3
|
+
export interface BrowserPathOptions {
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
homeDir?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ExecutableSearchOptions {
|
|
9
|
+
pathValue?: string;
|
|
10
|
+
pathDelimiter?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES: readonly [
|
|
14
|
+
"SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
|
|
15
|
+
"SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
|
|
16
|
+
"SWEET_COOKIE_EDGE_SAFE_STORAGE_PASSWORD",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function expandHomePath(value: string, homeDir?: string): string;
|
|
20
|
+
export function normalizedAbsolutePath(value: string, options?: BrowserPathOptions): string;
|
|
21
|
+
export function linuxConfigHome(options?: BrowserPathOptions): string;
|
|
22
|
+
export function linuxChromiumCookieImportUserDataDirs(options?: BrowserPathOptions): string[];
|
|
23
|
+
export function linuxBrowserSafetyUserDataDirs(options?: BrowserPathOptions): string[];
|
|
24
|
+
export function browserUserDataDirsForPlatform(
|
|
25
|
+
platform?: OraclePlatform,
|
|
26
|
+
options?: BrowserPathOptions & { includeUnsupported?: boolean },
|
|
27
|
+
): string[];
|
|
28
|
+
export function defaultCloneStrategyForPlatform(platform?: OraclePlatform): "apfs-clone" | "copy";
|
|
29
|
+
export function chromiumKeychainSupportedOnPlatform(platform?: OraclePlatform): boolean;
|
|
30
|
+
export function chromeUserAgentPlatformToken(platform?: OraclePlatform): string | undefined;
|
|
31
|
+
export function pathInsideOrEqual(childPath: string, parentPath: string): boolean;
|
|
32
|
+
export function resolvePathThroughExistingAncestorsSync(pathValue: string): string | undefined;
|
|
33
|
+
export function protectedCookieSourcePaths(cookieSources?: { chromeProfile?: string; chromeCookiePath?: string }): string[];
|
|
34
|
+
export function knownBrowserUserDataPathMatch(
|
|
35
|
+
pathValue: string,
|
|
36
|
+
options?: BrowserPathOptions & {
|
|
37
|
+
platform?: OraclePlatform;
|
|
38
|
+
includeUnsupported?: boolean;
|
|
39
|
+
extraProtectedPaths?: string[];
|
|
40
|
+
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
|
|
41
|
+
},
|
|
42
|
+
): string | undefined;
|
|
43
|
+
export function assertNotKnownBrowserUserDataPath(
|
|
44
|
+
pathValue: string,
|
|
45
|
+
label: string,
|
|
46
|
+
options?: BrowserPathOptions & {
|
|
47
|
+
platform?: OraclePlatform;
|
|
48
|
+
includeUnsupported?: boolean;
|
|
49
|
+
extraProtectedPaths?: string[];
|
|
50
|
+
cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
|
|
51
|
+
},
|
|
52
|
+
): void;
|
|
53
|
+
export function isExecutableFileSync(pathValue: string): boolean;
|
|
54
|
+
export function findExecutableOnPathSync(names: readonly string[], options?: ExecutableSearchOptions): string | undefined;
|
|
55
|
+
export function detectDefaultLinuxChromeExecutablePath(options?: ExecutableSearchOptions): string | undefined;
|
|
56
|
+
export function detectDefaultLinuxCookieProfileSource(options?: BrowserPathOptions): string | undefined;
|
|
57
|
+
export function detectDefaultBrowserProfileSource(platform?: OraclePlatform, options?: BrowserPathOptions): string;
|
|
58
|
+
export function scrubSweetCookieSafeStoragePasswordEnv(env?: NodeJS.ProcessEnv): void;
|
|
59
|
+
export function sweetCookieSafeStoragePasswordScrubbedEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|