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.
Files changed (35) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +27 -8
  3. package/docs/ORACLE_DESIGN.md +14 -8
  4. package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +276 -0
  5. package/extensions/oracle/index.ts +8 -1
  6. package/extensions/oracle/lib/commands.ts +25 -29
  7. package/extensions/oracle/lib/config.ts +56 -2
  8. package/extensions/oracle/lib/jobs.ts +134 -219
  9. package/extensions/oracle/lib/locks.ts +41 -209
  10. package/extensions/oracle/lib/poller.ts +38 -52
  11. package/extensions/oracle/lib/queue.ts +75 -112
  12. package/extensions/oracle/lib/runtime.ts +102 -19
  13. package/extensions/oracle/lib/tools.ts +663 -294
  14. package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
  15. package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
  16. package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +131 -0
  17. package/extensions/oracle/shared/job-lifecycle-helpers.mjs +390 -0
  18. package/extensions/oracle/shared/job-observability-helpers.d.mts +60 -0
  19. package/extensions/oracle/shared/job-observability-helpers.mjs +161 -0
  20. package/extensions/oracle/shared/process-helpers.d.mts +20 -0
  21. package/extensions/oracle/shared/process-helpers.mjs +128 -0
  22. package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
  23. package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
  24. package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
  25. package/extensions/oracle/worker/auth-bootstrap.mjs +125 -134
  26. package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
  27. package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
  28. package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
  29. package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
  30. package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
  31. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
  32. package/extensions/oracle/worker/run-job.mjs +166 -274
  33. package/extensions/oracle/worker/state-locks.mjs +31 -216
  34. package/package.json +4 -3
  35. 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 config = JSON.parse(rawConfig);
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
- ...options,
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", reject);
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"], { allowFailure: true });
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"], { allowFailure: true });
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"], { allowFailure: true });
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") || Object.hasOwn(maybeOptions, "input") || Object.hasOwn(maybeOptions, "cwd") || Object.hasOwn(maybeOptions, "logLabel"))
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, or set auth.chromeProfile / auth.chromeCookiePath in ~/.pi/agent/extensions/oracle.json.`,
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
- if (!result || typeof result !== "object") {
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
- const text = `${snapshot}\n${body}`;
584
- const allowedOrigins = buildAllowedChatGptOrigins(config.browser.chatUrl, config.browser.authUrl);
585
-
586
- const challengePatterns = [
587
- /just a moment/i,
588
- /verify you are human/i,
589
- /cloudflare/i,
590
- /captcha|turnstile|hcaptcha/i,
591
- /unusual activity detected/i,
592
- /we detect suspicious activity/i,
593
- ];
594
- if (challengePatterns.some((pattern) => pattern.test(text))) {
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;