pi-oracle 0.3.4 → 0.5.0
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 +38 -0
- package/README.md +27 -8
- package/docs/ORACLE_DESIGN.md +14 -8
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +276 -0
- package/extensions/oracle/index.ts +8 -1
- package/extensions/oracle/lib/commands.ts +25 -29
- package/extensions/oracle/lib/config.ts +56 -2
- package/extensions/oracle/lib/jobs.ts +134 -219
- package/extensions/oracle/lib/locks.ts +41 -209
- package/extensions/oracle/lib/poller.ts +38 -52
- package/extensions/oracle/lib/queue.ts +75 -112
- package/extensions/oracle/lib/runtime.ts +102 -19
- package/extensions/oracle/lib/tools.ts +663 -294
- package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
- package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +131 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +390 -0
- package/extensions/oracle/shared/job-observability-helpers.d.mts +60 -0
- package/extensions/oracle/shared/job-observability-helpers.mjs +161 -0
- package/extensions/oracle/shared/process-helpers.d.mts +20 -0
- package/extensions/oracle/shared/process-helpers.mjs +128 -0
- package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
- package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
- package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +125 -134
- package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
- package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
- package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
- package/extensions/oracle/worker/run-job.mjs +166 -274
- package/extensions/oracle/worker/state-locks.mjs +31 -216
- package/package.json +4 -3
- package/prompts/oracle.md +16 -10
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Purpose: Bootstrap isolated oracle browser auth by importing real Chrome cookies and validating ChatGPT session readiness.
|
|
2
|
+
// Responsibilities: Copy/import cookies, classify auth pages, drive lightweight account-selection flows, and persist diagnostics for auth failures.
|
|
3
|
+
// Scope: Auth bootstrap worker only; long-running oracle job execution stays in run-job.mjs and shared lifecycle/state helpers stay elsewhere.
|
|
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 Chrome profile, preserves private diagnostics, and must fail clearly when auth state cannot be verified.
|
|
1
6
|
import { withLock } from "./state-locks.mjs";
|
|
2
7
|
import { spawn } from "node:child_process";
|
|
3
8
|
import { existsSync } from "node:fs";
|
|
@@ -7,6 +12,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
7
12
|
import { getCookies } from "@steipete/sweet-cookie";
|
|
8
13
|
import { ensureAccountCookie, filterImportableAuthCookies } from "./auth-cookie-policy.mjs";
|
|
9
14
|
import { buildAllowedChatGptOrigins } from "./chatgpt-ui-helpers.mjs";
|
|
15
|
+
import { buildAccountChooserCandidateLabels, classifyChatAuthPage, normalizeLoginProbeResult } from "./auth-flow-helpers.mjs";
|
|
10
16
|
|
|
11
17
|
const rawConfig = process.argv[2];
|
|
12
18
|
if (!rawConfig) {
|
|
@@ -14,7 +20,23 @@ if (!rawConfig) {
|
|
|
14
20
|
process.exit(1);
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
const
|
|
23
|
+
const parsedConfigPayload = JSON.parse(rawConfig);
|
|
24
|
+
const config =
|
|
25
|
+
parsedConfigPayload &&
|
|
26
|
+
typeof parsedConfigPayload === "object" &&
|
|
27
|
+
!Array.isArray(parsedConfigPayload) &&
|
|
28
|
+
Object.hasOwn(parsedConfigPayload, "config")
|
|
29
|
+
? parsedConfigPayload.config
|
|
30
|
+
: parsedConfigPayload;
|
|
31
|
+
const configLoad =
|
|
32
|
+
parsedConfigPayload &&
|
|
33
|
+
typeof parsedConfigPayload === "object" &&
|
|
34
|
+
!Array.isArray(parsedConfigPayload) &&
|
|
35
|
+
Object.hasOwn(parsedConfigPayload, "configLoad") &&
|
|
36
|
+
parsedConfigPayload.configLoad &&
|
|
37
|
+
typeof parsedConfigPayload.configLoad === "object"
|
|
38
|
+
? parsedConfigPayload.configLoad
|
|
39
|
+
: undefined;
|
|
18
40
|
const CHATGPT_LABELS = {
|
|
19
41
|
composer: "Chat with ChatGPT",
|
|
20
42
|
addFiles: "Add files and more",
|
|
@@ -42,12 +64,51 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
|
|
|
42
64
|
(candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
|
|
43
65
|
) || "agent-browser";
|
|
44
66
|
|
|
67
|
+
function readPositiveIntEnv(name, fallback) {
|
|
68
|
+
const value = process.env[name]?.trim();
|
|
69
|
+
if (!value) return fallback;
|
|
70
|
+
const parsed = Number.parseInt(value, 10);
|
|
71
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const AGENT_BROWSER_COMMAND_TIMEOUT_MS = readPositiveIntEnv("PI_ORACLE_AUTH_AGENT_BROWSER_TIMEOUT_MS", 30_000);
|
|
75
|
+
const AGENT_BROWSER_CLOSE_TIMEOUT_MS = readPositiveIntEnv("PI_ORACLE_AUTH_CLOSE_TIMEOUT_MS", 10_000);
|
|
76
|
+
const AGENT_BROWSER_KILL_GRACE_MS = readPositiveIntEnv("PI_ORACLE_AUTH_KILL_GRACE_MS", 2_000);
|
|
77
|
+
|
|
45
78
|
let runtimeProfileDir = config.browser.authSeedProfileDir;
|
|
46
79
|
|
|
47
80
|
function authSessionName() {
|
|
48
81
|
return `${config.browser.sessionPrefix}-auth`;
|
|
49
82
|
}
|
|
50
83
|
|
|
84
|
+
function effectiveAuthConfigPath() {
|
|
85
|
+
return typeof configLoad?.effectiveAuthConfigPath === "string" && configLoad.effectiveAuthConfigPath
|
|
86
|
+
? configLoad.effectiveAuthConfigPath
|
|
87
|
+
: join(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"), "extensions", "oracle.json");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function authConfigRemediation() {
|
|
91
|
+
if (typeof configLoad?.remediation === "string" && configLoad.remediation) return configLoad.remediation;
|
|
92
|
+
if (typeof configLoad?.projectConfigPath === "string" && configLoad.projectConfigPath && configLoad.projectConfigExists) {
|
|
93
|
+
return (
|
|
94
|
+
`Set auth.chromeProfile / auth.chromeCookiePath in ${effectiveAuthConfigPath()}. ` +
|
|
95
|
+
`Project overrides are also read from ${configLoad.projectConfigPath}, but auth.* is loaded from ${effectiveAuthConfigPath()}.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return `Set auth.chromeProfile / auth.chromeCookiePath in ${effectiveAuthConfigPath()}.`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function authConfigSummary() {
|
|
102
|
+
if (typeof configLoad?.summary === "string" && configLoad.summary) return configLoad.summary;
|
|
103
|
+
const agentDirSuffix = typeof configLoad?.agentDir === "string" && configLoad.agentDir ? ` (agent dir: ${configLoad.agentDir})` : "";
|
|
104
|
+
const createSuffix = configLoad?.agentConfigExists === false ? " [create this file to override auth.*]" : "";
|
|
105
|
+
const lines = [`Effective oracle auth config: ${effectiveAuthConfigPath()}${agentDirSuffix}${createSuffix}`];
|
|
106
|
+
if (typeof configLoad?.projectConfigPath === "string" && configLoad.projectConfigPath && configLoad.projectConfigExists) {
|
|
107
|
+
lines.push(`Project oracle config also loaded: ${configLoad.projectConfigPath} (auth.* still comes from ${effectiveAuthConfigPath()}).`);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
51
112
|
function sleep(ms) {
|
|
52
113
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
114
|
}
|
|
@@ -78,12 +139,25 @@ async function log(message) {
|
|
|
78
139
|
|
|
79
140
|
function spawnCommand(command, args, options = {}) {
|
|
80
141
|
return new Promise((resolve, reject) => {
|
|
142
|
+
const { timeoutMs = AGENT_BROWSER_COMMAND_TIMEOUT_MS, ...spawnOptions } = options;
|
|
81
143
|
const child = spawn(command, args, {
|
|
82
144
|
stdio: ["pipe", "pipe", "pipe"],
|
|
83
|
-
...
|
|
145
|
+
...spawnOptions,
|
|
84
146
|
});
|
|
85
147
|
let stdout = "";
|
|
86
148
|
let stderr = "";
|
|
149
|
+
let timedOut = false;
|
|
150
|
+
let killTimer;
|
|
151
|
+
let killGraceTimer;
|
|
152
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
153
|
+
killTimer = setTimeout(() => {
|
|
154
|
+
timedOut = true;
|
|
155
|
+
child.kill("SIGTERM");
|
|
156
|
+
killGraceTimer = setTimeout(() => child.kill("SIGKILL"), AGENT_BROWSER_KILL_GRACE_MS);
|
|
157
|
+
killGraceTimer.unref?.();
|
|
158
|
+
}, timeoutMs);
|
|
159
|
+
killTimer.unref?.();
|
|
160
|
+
}
|
|
87
161
|
if (options.input) child.stdin.end(options.input);
|
|
88
162
|
else child.stdin.end();
|
|
89
163
|
child.stdout.on("data", (data) => {
|
|
@@ -92,8 +166,20 @@ function spawnCommand(command, args, options = {}) {
|
|
|
92
166
|
child.stderr.on("data", (data) => {
|
|
93
167
|
stderr += String(data);
|
|
94
168
|
});
|
|
95
|
-
child.on("error",
|
|
169
|
+
child.on("error", (error) => {
|
|
170
|
+
if (killTimer) clearTimeout(killTimer);
|
|
171
|
+
if (killGraceTimer) clearTimeout(killGraceTimer);
|
|
172
|
+
reject(error);
|
|
173
|
+
});
|
|
96
174
|
child.on("close", (code) => {
|
|
175
|
+
if (killTimer) clearTimeout(killTimer);
|
|
176
|
+
if (killGraceTimer) clearTimeout(killGraceTimer);
|
|
177
|
+
if (timedOut) {
|
|
178
|
+
const error = new Error(stderr || stdout || `${command} timed out after ${timeoutMs}ms`);
|
|
179
|
+
if (options.allowFailure) resolve({ code, stdout: stdout.trim(), stderr: error.message });
|
|
180
|
+
else reject(error);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
97
183
|
if (code === 0 || options.allowFailure) resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
98
184
|
else reject(new Error(stderr || stdout || `${command} exited with code ${code}`));
|
|
99
185
|
});
|
|
@@ -114,7 +200,10 @@ function targetBrowserBaseArgs(options = {}) {
|
|
|
114
200
|
|
|
115
201
|
async function closeTargetBrowser() {
|
|
116
202
|
await log(`Closing target browser session ${authSessionName()} if present`);
|
|
117
|
-
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "close"], {
|
|
203
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "close"], {
|
|
204
|
+
allowFailure: true,
|
|
205
|
+
timeoutMs: AGENT_BROWSER_CLOSE_TIMEOUT_MS,
|
|
206
|
+
});
|
|
118
207
|
await log(`close result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
119
208
|
}
|
|
120
209
|
|
|
@@ -131,7 +220,10 @@ async function ensureNotSymlink(path, label) {
|
|
|
131
220
|
}
|
|
132
221
|
|
|
133
222
|
async function isAuthBrowserConnected() {
|
|
134
|
-
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], {
|
|
223
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], {
|
|
224
|
+
allowFailure: true,
|
|
225
|
+
timeoutMs: AGENT_BROWSER_COMMAND_TIMEOUT_MS,
|
|
226
|
+
});
|
|
135
227
|
try {
|
|
136
228
|
const parsed = JSON.parse(result.stdout || "{}");
|
|
137
229
|
return parsed?.data?.connected === true;
|
|
@@ -223,7 +315,7 @@ async function launchTargetBrowser() {
|
|
|
223
315
|
await closeTargetBrowser();
|
|
224
316
|
const args = [...targetBrowserBaseArgs({ withLaunchOptions: true, mode: "headed" }), "open", "about:blank"];
|
|
225
317
|
await log(`Launching isolated browser: agent-browser ${JSON.stringify(args)}`);
|
|
226
|
-
const result = await spawnCommand(AGENT_BROWSER_BIN, args, { allowFailure: true });
|
|
318
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, args, { allowFailure: true, timeoutMs: AGENT_BROWSER_COMMAND_TIMEOUT_MS });
|
|
227
319
|
await log(`launch result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
228
320
|
if (result.code !== 0) {
|
|
229
321
|
throw new Error(result.stderr || result.stdout || "Failed to launch isolated oracle browser");
|
|
@@ -231,7 +323,10 @@ async function launchTargetBrowser() {
|
|
|
231
323
|
}
|
|
232
324
|
|
|
233
325
|
async function streamStatus() {
|
|
234
|
-
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], {
|
|
326
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], {
|
|
327
|
+
allowFailure: true,
|
|
328
|
+
timeoutMs: AGENT_BROWSER_COMMAND_TIMEOUT_MS,
|
|
329
|
+
});
|
|
235
330
|
await log(`stream status: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
236
331
|
try {
|
|
237
332
|
const parsed = JSON.parse(result.stdout || "{}");
|
|
@@ -255,7 +350,11 @@ async function targetCommand(...args) {
|
|
|
255
350
|
maybeOptions &&
|
|
256
351
|
typeof maybeOptions === "object" &&
|
|
257
352
|
!Array.isArray(maybeOptions) &&
|
|
258
|
-
(Object.hasOwn(maybeOptions, "allowFailure") ||
|
|
353
|
+
(Object.hasOwn(maybeOptions, "allowFailure") ||
|
|
354
|
+
Object.hasOwn(maybeOptions, "input") ||
|
|
355
|
+
Object.hasOwn(maybeOptions, "cwd") ||
|
|
356
|
+
Object.hasOwn(maybeOptions, "logLabel") ||
|
|
357
|
+
Object.hasOwn(maybeOptions, "timeoutMs"))
|
|
259
358
|
) {
|
|
260
359
|
options = args.pop();
|
|
261
360
|
}
|
|
@@ -402,7 +501,7 @@ async function readSourceCookies() {
|
|
|
402
501
|
|
|
403
502
|
if (!hasSessionToken) {
|
|
404
503
|
throw new Error(
|
|
405
|
-
`No ChatGPT session-token cookies were found in ${cookieSourceLabel()}. Make sure ChatGPT is logged into that Chrome profile
|
|
504
|
+
`No ChatGPT session-token cookies were found in ${cookieSourceLabel()}. Make sure ChatGPT is logged into that Chrome profile. ${authConfigRemediation()}`,
|
|
406
505
|
);
|
|
407
506
|
}
|
|
408
507
|
|
|
@@ -545,22 +644,7 @@ function buildLoginProbeScript(timeoutMs) {
|
|
|
545
644
|
|
|
546
645
|
async function loginProbe() {
|
|
547
646
|
const result = await evalPage(buildLoginProbeScript(LOGIN_PROBE_TIMEOUT_MS), "login probe eval");
|
|
548
|
-
|
|
549
|
-
return { ok: false, status: 0, error: "invalid-probe-result" };
|
|
550
|
-
}
|
|
551
|
-
return {
|
|
552
|
-
ok: result.ok === true,
|
|
553
|
-
status: typeof result.status === "number" ? result.status : 0,
|
|
554
|
-
pageUrl: typeof result.pageUrl === "string" ? result.pageUrl : undefined,
|
|
555
|
-
domLoginCta: result.domLoginCta === true,
|
|
556
|
-
onAuthPage: result.onAuthPage === true,
|
|
557
|
-
error: typeof result.error === "string" ? result.error : undefined,
|
|
558
|
-
bodyKeys: Array.isArray(result.bodyKeys) ? result.bodyKeys : [],
|
|
559
|
-
bodyHasId: result.bodyHasId === true,
|
|
560
|
-
bodyHasEmail: result.bodyHasEmail === true,
|
|
561
|
-
name: typeof result.name === "string" ? result.name : undefined,
|
|
562
|
-
responsePreview: typeof result.responsePreview === "string" ? result.responsePreview : undefined,
|
|
563
|
-
};
|
|
647
|
+
return normalizeLoginProbeResult(result);
|
|
564
648
|
}
|
|
565
649
|
|
|
566
650
|
async function captureDiagnostics(reason) {
|
|
@@ -580,116 +664,22 @@ async function captureDiagnostics(reason) {
|
|
|
580
664
|
}
|
|
581
665
|
|
|
582
666
|
function classifyChatPage({ url, snapshot, body, probe }) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
state: "challenge_blocking",
|
|
597
|
-
message:
|
|
598
|
-
`ChatGPT challenge detected after syncing cookies from ${cookieSourceLabel()}. ` +
|
|
599
|
-
`The isolated oracle browser was left open on profile ${runtimeProfileDir}; complete the challenge there, then rerun /oracle-auth. Logs: ${LOG_PATH}`,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (/http error 431|request header or cookie too large/i.test(text)) {
|
|
604
|
-
return {
|
|
605
|
-
state: "login_required",
|
|
606
|
-
message:
|
|
607
|
-
`Imported auth hit HTTP 431 during ChatGPT auth resolution, which usually means the imported cookie set is too large or stale. ` +
|
|
608
|
-
`Inspect ${LOG_PATH}.`,
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const outagePatterns = [
|
|
613
|
-
/something went wrong/i,
|
|
614
|
-
/a network error occurred/i,
|
|
615
|
-
/an error occurred while connecting to the websocket/i,
|
|
616
|
-
/try again later/i,
|
|
617
|
-
];
|
|
618
|
-
if (outagePatterns.some((pattern) => pattern.test(text))) {
|
|
619
|
-
return { state: "transient_outage_error", message: `ChatGPT is showing a transient outage/error page. Logs: ${LOG_PATH}` };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const onAllowedOrigin = allowedOrigins.some((origin) => url.startsWith(origin));
|
|
623
|
-
const hasComposer = snapshot.includes(`textbox \"${CHATGPT_LABELS.composer}\"`);
|
|
624
|
-
const hasAddFiles = snapshot.includes(`button \"${CHATGPT_LABELS.addFiles}\"`);
|
|
625
|
-
const hasModelControl =
|
|
626
|
-
snapshot.includes('button "Model selector"') ||
|
|
627
|
-
/button "(Instant|Thinking|Pro)(?: [^"]*)?"/.test(snapshot);
|
|
628
|
-
|
|
629
|
-
if (probe?.status === 401 || probe?.status === 403) {
|
|
630
|
-
return {
|
|
631
|
-
state: "login_required",
|
|
632
|
-
message:
|
|
633
|
-
`Synced cookies from ${cookieSourceLabel()}, but ChatGPT still rejected the session ` +
|
|
634
|
-
`(status=${probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${LOG_PATH}.`,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if (probe?.onAuthPage) {
|
|
639
|
-
if (probe?.bodyHasId || probe?.bodyHasEmail) {
|
|
640
|
-
return {
|
|
641
|
-
state: "auth_transitioning",
|
|
642
|
-
message:
|
|
643
|
-
`ChatGPT is on /auth/login, but /backend-api/me returned a partial authenticated session. ` +
|
|
644
|
-
`Trying to drive the login resolution flow. Logs: ${LOG_PATH}`,
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
return {
|
|
648
|
-
state: "login_required",
|
|
649
|
-
message:
|
|
650
|
-
`Synced cookies from ${cookieSourceLabel()}, but ChatGPT still rejected the session ` +
|
|
651
|
-
`(status=${probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${LOG_PATH}.`,
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (onAllowedOrigin && probe?.status === 200 && hasComposer && hasAddFiles && hasModelControl) {
|
|
656
|
-
if (!probe?.domLoginCta) {
|
|
657
|
-
return {
|
|
658
|
-
state: "authenticated_and_ready",
|
|
659
|
-
message: `Imported ChatGPT auth from ${cookieSourceLabel()} into the isolated oracle profile. Logs: ${LOG_PATH}`,
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return {
|
|
664
|
-
state: "auth_transitioning",
|
|
665
|
-
message:
|
|
666
|
-
probe?.bodyHasId || probe?.bodyHasEmail
|
|
667
|
-
? `ChatGPT backend session is authenticated but the shell still shows public CTA chrome. Logs: ${LOG_PATH}`
|
|
668
|
-
: `ChatGPT accepted cookies but is still hydrating/auth-selecting. Logs: ${LOG_PATH}`,
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (onAllowedOrigin && probe?.ok && hasComposer && hasAddFiles && hasModelControl) {
|
|
673
|
-
return {
|
|
674
|
-
state: "authenticated_and_ready",
|
|
675
|
-
message: `Imported ChatGPT auth from ${cookieSourceLabel()} into the isolated oracle profile. Logs: ${LOG_PATH}`,
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (url && !onAllowedOrigin) {
|
|
680
|
-
return { state: "login_required", message: `Imported auth redirected away from the expected ChatGPT origin. Logs: ${LOG_PATH}` };
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
return { state: "unknown", message: `ChatGPT page state is not yet ready. Logs: ${LOG_PATH}` };
|
|
667
|
+
return classifyChatAuthPage({
|
|
668
|
+
url,
|
|
669
|
+
snapshot,
|
|
670
|
+
body,
|
|
671
|
+
probe,
|
|
672
|
+
allowedOrigins: buildAllowedChatGptOrigins(config.browser.chatUrl, config.browser.authUrl),
|
|
673
|
+
cookieSourceLabel: cookieSourceLabel(),
|
|
674
|
+
runtimeProfileDir,
|
|
675
|
+
logPath: LOG_PATH,
|
|
676
|
+
composerLabel: CHATGPT_LABELS.composer,
|
|
677
|
+
addFilesLabel: CHATGPT_LABELS.addFiles,
|
|
678
|
+
});
|
|
684
679
|
}
|
|
685
680
|
|
|
686
681
|
async function maybeSelectAccountIdentity(snapshot, probe) {
|
|
687
|
-
const candidates =
|
|
688
|
-
if (typeof probe?.name === "string" && probe.name.trim()) {
|
|
689
|
-
candidates.push(probe.name.trim());
|
|
690
|
-
const firstToken = probe.name.trim().split(/\s+/)[0];
|
|
691
|
-
if (firstToken && firstToken !== probe.name.trim()) candidates.push(firstToken);
|
|
692
|
-
}
|
|
682
|
+
const candidates = buildAccountChooserCandidateLabels(probe?.name);
|
|
693
683
|
|
|
694
684
|
for (const label of candidates) {
|
|
695
685
|
const entry = findEntry(
|
|
@@ -785,7 +775,7 @@ async function waitForImportedAuthReady() {
|
|
|
785
775
|
}
|
|
786
776
|
if (classification.state === "login_required") {
|
|
787
777
|
await captureDiagnostics("login-required");
|
|
788
|
-
throw new Error(classification.message);
|
|
778
|
+
throw new Error(`${classification.message} ${authConfigRemediation()}`);
|
|
789
779
|
}
|
|
790
780
|
await sleep(config.auth.pollMs);
|
|
791
781
|
}
|
|
@@ -805,6 +795,7 @@ async function run() {
|
|
|
805
795
|
await log(
|
|
806
796
|
`Config summary: session=${authSessionName()} seedProfileDir=${profilePlan.targetDir} stagingProfileDir=${profilePlan.stagingDir} executable=${config.browser.executablePath || "(default)"} source=${cookieSourceLabel()}`,
|
|
807
797
|
);
|
|
798
|
+
await log(authConfigSummary());
|
|
808
799
|
const cookies = await readSourceCookies();
|
|
809
800
|
await prepareStagedProfile(profilePlan);
|
|
810
801
|
await launchTargetBrowser();
|
|
@@ -837,7 +828,7 @@ async function run() {
|
|
|
837
828
|
|
|
838
829
|
run().catch((error) => {
|
|
839
830
|
process.stderr.write(
|
|
840
|
-
`${error instanceof Error ? error.message : String(error)}\nSee ${LOG_PATH} and diagnostics in ${DIAGNOSTICS_DIR || "(oracle-auth diagnostics dir unavailable)"}\nIf needed, ensure the configured real Chrome profile is already logged into ChatGPT and grant macOS Keychain access when prompted.`,
|
|
831
|
+
`${error instanceof Error ? error.message : String(error)}\nSee ${LOG_PATH} and diagnostics in ${DIAGNOSTICS_DIR || "(oracle-auth diagnostics dir unavailable)"}\n${authConfigSummary()}\nIf needed, ensure the configured real Chrome profile is already logged into ChatGPT and grant macOS Keychain access when prompted.`,
|
|
841
832
|
);
|
|
842
833
|
process.exit(1);
|
|
843
834
|
});
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Purpose: Define the allowlist/drop policy for importing ChatGPT/OpenAI auth cookies into the isolated oracle browser profile.
|
|
2
|
+
// Responsibilities: Recognize required auth cookies, drop noisy/irrelevant cookies, and normalize cookie import decisions.
|
|
3
|
+
// Scope: Pure cookie-policy logic only; reading cookies from Chrome and writing them into the isolated profile happen elsewhere.
|
|
4
|
+
// Usage: Imported by auth-bootstrap and sanity tests to keep cookie import behavior deterministic and reviewable.
|
|
5
|
+
// Invariants/Assumptions: Security-sensitive auth cookies are allowlisted intentionally, and analytics/ambient cookies should be excluded by default.
|
|
1
6
|
const AUTH_COOKIE_NAME_PATTERNS = [
|
|
2
7
|
/^__Secure-next-auth\.session-token(?:\.|$)/,
|
|
3
8
|
/^__Secure-next-auth\.callback-url$/,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface OracleAuthLoginProbe {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
status: number;
|
|
4
|
+
pageUrl?: string;
|
|
5
|
+
domLoginCta?: boolean;
|
|
6
|
+
onAuthPage?: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
bodyKeys?: string[];
|
|
9
|
+
bodyHasId?: boolean;
|
|
10
|
+
bodyHasEmail?: boolean;
|
|
11
|
+
name?: string;
|
|
12
|
+
responsePreview?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type OracleAuthPageState =
|
|
16
|
+
| "challenge_blocking"
|
|
17
|
+
| "login_required"
|
|
18
|
+
| "transient_outage_error"
|
|
19
|
+
| "auth_transitioning"
|
|
20
|
+
| "authenticated_and_ready"
|
|
21
|
+
| "unknown";
|
|
22
|
+
|
|
23
|
+
export interface OracleAuthPageClassification {
|
|
24
|
+
state: OracleAuthPageState;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export declare function normalizeLoginProbeResult(result: unknown): OracleAuthLoginProbe;
|
|
29
|
+
export declare function buildAccountChooserCandidateLabels(name?: string): string[];
|
|
30
|
+
export declare function classifyChatAuthPage(args: {
|
|
31
|
+
url: string;
|
|
32
|
+
snapshot: string;
|
|
33
|
+
body: string;
|
|
34
|
+
probe?: OracleAuthLoginProbe;
|
|
35
|
+
allowedOrigins: readonly string[];
|
|
36
|
+
cookieSourceLabel: string;
|
|
37
|
+
runtimeProfileDir: string;
|
|
38
|
+
logPath: string;
|
|
39
|
+
composerLabel?: string;
|
|
40
|
+
addFilesLabel?: string;
|
|
41
|
+
}): OracleAuthPageClassification;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Purpose: Provide pure auth-bootstrap classification helpers for ChatGPT browser state handling.
|
|
2
|
+
// Responsibilities: Normalize login-probe payloads, classify ChatGPT auth page states, and derive account chooser candidate labels.
|
|
3
|
+
// Scope: Pure worker/auth decision logic only; browser I/O, logging, and side effects stay in auth-bootstrap.mjs.
|
|
4
|
+
// Usage: Imported by auth-bootstrap.mjs and sanity tests to exercise auth classification behavior without driving a browser.
|
|
5
|
+
// Invariants/Assumptions: Inputs are already captured snapshots/probe results from the live browser session; outputs are deterministic and side-effect free.
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./auth-flow-helpers.d.mts").OracleAuthLoginProbe} OracleAuthLoginProbe */
|
|
8
|
+
/** @typedef {import("./auth-flow-helpers.d.mts").OracleAuthPageClassification} OracleAuthPageClassification */
|
|
9
|
+
|
|
10
|
+
const DEFAULT_COMPOSER_LABEL = "Chat with ChatGPT";
|
|
11
|
+
const DEFAULT_ADD_FILES_LABEL = "Add files and more";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {unknown} result
|
|
15
|
+
* @returns {OracleAuthLoginProbe}
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeLoginProbeResult(result) {
|
|
18
|
+
if (!result || typeof result !== "object") {
|
|
19
|
+
return { ok: false, status: 0, error: "invalid-probe-result" };
|
|
20
|
+
}
|
|
21
|
+
const value = /** @type {Record<string, unknown>} */ (result);
|
|
22
|
+
return {
|
|
23
|
+
ok: value.ok === true,
|
|
24
|
+
status: typeof value.status === "number" ? value.status : 0,
|
|
25
|
+
pageUrl: typeof value.pageUrl === "string" ? value.pageUrl : undefined,
|
|
26
|
+
domLoginCta: value.domLoginCta === true,
|
|
27
|
+
onAuthPage: value.onAuthPage === true,
|
|
28
|
+
error: typeof value.error === "string" ? value.error : undefined,
|
|
29
|
+
bodyKeys: Array.isArray(value.bodyKeys) ? value.bodyKeys.filter((entry) => typeof entry === "string") : [],
|
|
30
|
+
bodyHasId: value.bodyHasId === true,
|
|
31
|
+
bodyHasEmail: value.bodyHasEmail === true,
|
|
32
|
+
name: typeof value.name === "string" ? value.name : undefined,
|
|
33
|
+
responsePreview: typeof value.responsePreview === "string" ? value.responsePreview : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string | undefined} name
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
export function buildAccountChooserCandidateLabels(name) {
|
|
42
|
+
const normalized = typeof name === "string" ? name.trim() : "";
|
|
43
|
+
if (!normalized) return [];
|
|
44
|
+
const firstToken = normalized.split(/\s+/)[0] || "";
|
|
45
|
+
return firstToken && firstToken !== normalized ? [normalized, firstToken] : [normalized];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {{
|
|
50
|
+
* url: string;
|
|
51
|
+
* snapshot: string;
|
|
52
|
+
* body: string;
|
|
53
|
+
* probe?: OracleAuthLoginProbe;
|
|
54
|
+
* allowedOrigins: readonly string[];
|
|
55
|
+
* cookieSourceLabel: string;
|
|
56
|
+
* runtimeProfileDir: string;
|
|
57
|
+
* logPath: string;
|
|
58
|
+
* composerLabel?: string;
|
|
59
|
+
* addFilesLabel?: string;
|
|
60
|
+
* }} args
|
|
61
|
+
* @returns {OracleAuthPageClassification}
|
|
62
|
+
*/
|
|
63
|
+
export function classifyChatAuthPage(args) {
|
|
64
|
+
const text = `${args.snapshot}\n${args.body}`;
|
|
65
|
+
const composerLabel = args.composerLabel || DEFAULT_COMPOSER_LABEL;
|
|
66
|
+
const addFilesLabel = args.addFilesLabel || DEFAULT_ADD_FILES_LABEL;
|
|
67
|
+
const onAllowedOrigin = args.allowedOrigins.some((origin) => args.url.startsWith(origin));
|
|
68
|
+
const hasComposer = args.snapshot.includes(`textbox "${composerLabel}"`);
|
|
69
|
+
const hasAddFiles = args.snapshot.includes(`button "${addFilesLabel}"`);
|
|
70
|
+
const hasModelControl =
|
|
71
|
+
args.snapshot.includes('button "Model selector"') ||
|
|
72
|
+
/button "(Instant|Thinking|Pro)(?: [^"]*)?"/.test(args.snapshot);
|
|
73
|
+
|
|
74
|
+
const challengePatterns = [
|
|
75
|
+
/just a moment/i,
|
|
76
|
+
/verify you are human/i,
|
|
77
|
+
/cloudflare/i,
|
|
78
|
+
/captcha|turnstile|hcaptcha/i,
|
|
79
|
+
/unusual activity detected/i,
|
|
80
|
+
/we detect suspicious activity/i,
|
|
81
|
+
];
|
|
82
|
+
if (challengePatterns.some((pattern) => pattern.test(text))) {
|
|
83
|
+
return {
|
|
84
|
+
state: "challenge_blocking",
|
|
85
|
+
message:
|
|
86
|
+
`ChatGPT challenge detected after syncing cookies from ${args.cookieSourceLabel}. ` +
|
|
87
|
+
`The isolated oracle browser was left open on profile ${args.runtimeProfileDir}; complete the challenge there, then rerun /oracle-auth. Logs: ${args.logPath}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (/http error 431|request header or cookie too large/i.test(text)) {
|
|
92
|
+
return {
|
|
93
|
+
state: "login_required",
|
|
94
|
+
message:
|
|
95
|
+
`Imported auth hit HTTP 431 during ChatGPT auth resolution, which usually means the imported cookie set is too large or stale. ` +
|
|
96
|
+
`Inspect ${args.logPath}.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const outagePatterns = [
|
|
101
|
+
/something went wrong/i,
|
|
102
|
+
/a network error occurred/i,
|
|
103
|
+
/an error occurred while connecting to the websocket/i,
|
|
104
|
+
/try again later/i,
|
|
105
|
+
];
|
|
106
|
+
if (outagePatterns.some((pattern) => pattern.test(text))) {
|
|
107
|
+
return { state: "transient_outage_error", message: `ChatGPT is showing a transient outage/error page. Logs: ${args.logPath}` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (args.probe?.status === 401 || args.probe?.status === 403) {
|
|
111
|
+
return {
|
|
112
|
+
state: "login_required",
|
|
113
|
+
message:
|
|
114
|
+
`Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
|
|
115
|
+
`(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (args.probe?.onAuthPage) {
|
|
120
|
+
if (args.probe?.bodyHasId || args.probe?.bodyHasEmail) {
|
|
121
|
+
return {
|
|
122
|
+
state: "auth_transitioning",
|
|
123
|
+
message:
|
|
124
|
+
`ChatGPT is on /auth/login, but /backend-api/me returned a partial authenticated session. ` +
|
|
125
|
+
`Trying to drive the login resolution flow. Logs: ${args.logPath}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
state: "login_required",
|
|
130
|
+
message:
|
|
131
|
+
`Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
|
|
132
|
+
`(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (onAllowedOrigin && args.probe?.status === 200 && hasComposer && hasAddFiles && hasModelControl) {
|
|
137
|
+
if (!args.probe?.domLoginCta) {
|
|
138
|
+
return {
|
|
139
|
+
state: "authenticated_and_ready",
|
|
140
|
+
message: `Imported ChatGPT auth from ${args.cookieSourceLabel} into the isolated oracle profile. Logs: ${args.logPath}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
state: "auth_transitioning",
|
|
146
|
+
message:
|
|
147
|
+
args.probe?.bodyHasId || args.probe?.bodyHasEmail
|
|
148
|
+
? `ChatGPT backend session is authenticated but the shell still shows public CTA chrome. Logs: ${args.logPath}`
|
|
149
|
+
: `ChatGPT accepted cookies but is still hydrating/auth-selecting. Logs: ${args.logPath}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (onAllowedOrigin && args.probe?.ok && hasComposer && hasAddFiles && hasModelControl) {
|
|
154
|
+
return {
|
|
155
|
+
state: "authenticated_and_ready",
|
|
156
|
+
message: `Imported ChatGPT auth from ${args.cookieSourceLabel} into the isolated oracle profile. Logs: ${args.logPath}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (args.url && !onAllowedOrigin) {
|
|
161
|
+
return { state: "login_required", message: `Imported auth redirected away from the expected ChatGPT origin. Logs: ${args.logPath}` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { state: "unknown", message: `ChatGPT page state is not yet ready. Logs: ${args.logPath}` };
|
|
165
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface OracleStableValueState {
|
|
2
|
+
lastValue: string;
|
|
3
|
+
stableCount: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export declare function assistantSnapshotSlice(snapshot: string, composerLabel: string, responseIndex: number): string | undefined;
|
|
7
|
+
export declare function stripUrlQueryAndHash(url: string | undefined): string;
|
|
8
|
+
export declare function isConversationPathUrl(url: string): boolean;
|
|
9
|
+
export declare function resolveStableConversationUrlCandidate(url: string, previousChatUrl?: string): string | undefined;
|
|
10
|
+
export declare function nextStableValueState(
|
|
11
|
+
state: Partial<OracleStableValueState> | undefined,
|
|
12
|
+
nextValue: string,
|
|
13
|
+
): OracleStableValueState;
|