pi-oracle 0.6.14 → 0.6.16

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 CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Changed
6
+ - made `/oracle-auth` success and failure output easier to scan, with compact source summaries and source-specific troubleshooting for configured Chromium cookie sources
7
+
8
+ ## 0.6.16 - 2026-05-07
9
+
10
+ ### Changed
11
+ - migrated the local pi development baseline and peer metadata from deprecated `@mariozechner/*` packages to maintained `@earendil-works/*` `0.74.0`
12
+ - regenerated the npm lockfile against the current stable dependency graph and refreshed the `basic-ftp` override to the current patched major
13
+
14
+ ### Compatibility
15
+ - reviewed the pi `0.74.0` changelog and confirmed the oracle extension remains compatible with current extension lifecycle and package install/update guidance
16
+
17
+ ## 0.6.15 - 2026-05-03
18
+
19
+ ### Fixed
20
+ - anchored the `oracle_submit.files[]` schema pattern so OpenAI-compatible parsers that require anchored JSON Schema regexes, including llama.cpp, can load the oracle tools
21
+ - made background poller queued-job promotion best-effort under normal global admission-lock contention, avoiding noisy scan failures when multiple pi sessions are live
22
+ - added configured Chromium-family cookie source support for `/oracle-auth`, including macOS Keychain decryption for browser cookie stores not handled by `@steipete/sweet-cookie`
23
+
5
24
  ## 0.6.14 - 2026-05-02
6
25
 
7
26
  ### Fixed
package/README.md CHANGED
@@ -9,7 +9,7 @@ Use it when you want:
9
9
  - async background execution
10
10
  - durable saved responses/artifacts plus best-effort wake-ups back into `pi`
11
11
 
12
- Normal oracle jobs run in an isolated browser profile, not your active Chrome window.
12
+ Normal oracle jobs run in an isolated browser profile, not your active browser window.
13
13
 
14
14
  > Status: experimental public beta. Validated primarily on macOS with Google Chrome and `pi` 0.65.0+.
15
15
 
@@ -43,7 +43,7 @@ pi install https://github.com/fitchmultz/pi-oracle
43
43
  ## Quickstart
44
44
 
45
45
  1. Start a normal persisted `pi` session. Do not use `pi --no-session` for oracle.
46
- 2. Make sure ChatGPT already works in your local Chrome profile.
46
+ 2. Make sure ChatGPT already works in your configured local browser profile.
47
47
  3. Make sure these are installed: Google Chrome, `agent-browser`, `tar`, and `zstd`.
48
48
  4. Optional: create `~/.pi/agent/extensions/oracle.json` if you want non-default settings.
49
49
  5. Run `/oracle-auth`.
@@ -97,7 +97,7 @@ If concurrency is full, the job is queued and starts automatically later.
97
97
  User-facing commands:
98
98
  - `/oracle <request>` — prompt template that tells the agent to gather context and dispatch an oracle job
99
99
  - `/oracle-followup <job-id> <request>` — prompt template that continues an earlier oracle job in the same ChatGPT thread
100
- - `/oracle-auth` — sync ChatGPT cookies from your real Chrome profile into the isolated oracle auth profile
100
+ - `/oracle-auth` — sync ChatGPT cookies from your configured local browser profile into the isolated oracle auth profile
101
101
  - `/oracle-read [job-id]` — inspect job status plus the saved response preview
102
102
  - `/oracle-status [job-id]` — inspect job status and list recent job ids when no explicit id is given
103
103
  - `/oracle-cancel <job-id>` — cancel a queued or active job by id
@@ -112,7 +112,7 @@ Agent-facing tools:
112
112
 
113
113
  ## Minimal config
114
114
 
115
- Most users can start with the packaged defaults and only set the Chrome profile if needed.
115
+ Most users can start with the packaged defaults and only set the browser profile if needed.
116
116
 
117
117
  `~/.pi/agent/extensions/oracle.json`
118
118
 
@@ -133,6 +133,41 @@ Notes:
133
133
  - If the packaged default is fine, you can omit `defaults.preset` entirely.
134
134
  - You usually do not need to set browser paths unless auto-detection fails.
135
135
 
136
+ ### Custom Chromium cookie sources
137
+
138
+ Most Chrome-compatible browsers should work through the default cookie importer. Use this alternate path only for a Chromium-family browser that is not one of `@steipete/sweet-cookie`'s built-in Chrome/Brave/Arc/Chromium targets or otherwise cannot import cookies without dependency patching.
139
+
140
+ Before running `/oracle-auth` with this path:
141
+
142
+ 1. Log into ChatGPT in the target browser profile.
143
+ 2. Fully quit the browser so its `Cookies` database is stable.
144
+ 3. Find the profile `Cookies` SQLite DB path.
145
+ 4. Find the browser's macOS Keychain safe-storage item account and service name.
146
+ 5. Configure all of `browser.executablePath`, `auth.chromeCookiePath`, and `auth.chromiumKeychain` in the agent-level config at `~/.pi/agent/extensions/oracle.json`.
147
+
148
+ Example Helium config:
149
+
150
+ ```json
151
+ {
152
+ "browser": {
153
+ "executablePath": "/Applications/Helium.app/Contents/MacOS/Helium"
154
+ },
155
+ "auth": {
156
+ "chromeProfile": "Default",
157
+ "chromeCookiePath": "/Users/you/Library/Application Support/net.imput.helium/Default/Cookies",
158
+ "chromiumKeychain": {
159
+ "account": "Helium",
160
+ "services": ["Helium Storage Key"],
161
+ "label": "Helium Storage Key"
162
+ }
163
+ }
164
+ }
165
+ ```
166
+
167
+ `auth.chromeCookiePath` remains the cookie database path for backward compatibility. `auth.chromiumKeychain` must be paired with `auth.chromeCookiePath`; partial config is rejected so oracle does not silently fall back to a different browser source. When both are present, `/oracle-auth` uses pi-oracle's repo-owned generic Chromium cookie reader instead of patching `@steipete/sweet-cookie` internals.
168
+
169
+ If macOS prompts for Keychain access during `/oracle-auth`, allow access for the configured browser safe-storage item. If auth still fails after cookies are synced, the cookie DB may be stale, from the wrong profile, or for an account that is logged out; reopen the configured browser profile, confirm ChatGPT works there, quit the browser, and rerun `/oracle-auth`.
170
+
136
171
  ## Available presets
137
172
 
138
173
  | Preset id | Description |
@@ -173,8 +208,8 @@ Project config should only override safe, non-privileged settings.
173
208
 
174
209
  - macOS
175
210
  - Node.js 22 or newer
176
- - Google Chrome installed
177
- - ChatGPT already signed into a local Chrome profile
211
+ - Google Chrome or another Chromium-family browser installed
212
+ - ChatGPT already signed into the configured local browser profile
178
213
  - `pi` 0.65.0 or newer
179
214
  - `agent-browser` available on the machine
180
215
  - `tar` and `zstd` available
@@ -183,10 +218,22 @@ Project config should only override safe, non-privileged settings.
183
218
 
184
219
  ### `/oracle-auth` fails or says login is required
185
220
 
186
- - Make sure ChatGPT works in the same local Chrome profile you configured.
221
+ - Make sure ChatGPT works in the same local browser profile you configured.
222
+ - For custom Chromium cookie sources, confirm `auth.chromeCookiePath` points at that profile's `Cookies` DB and `auth.chromiumKeychain.services` names the browser's safe-storage Keychain service.
187
223
  - Re-run `/oracle-auth`.
188
224
  - If ChatGPT is half-logged-in or challenge flow state looks weird, finish the login/challenge in the headed auth browser and retry.
189
225
 
226
+ ### Custom Chromium auth says cookies synced but the session is rejected
227
+
228
+ This usually means the cookie import worked but the source cookies are not the active ChatGPT session you expected.
229
+
230
+ 1. Open the configured browser profile.
231
+ 2. Confirm ChatGPT works there without logging in again.
232
+ 3. Quit the browser fully so its `Cookies` DB is stable.
233
+ 4. Confirm `auth.chromeCookiePath` points at that exact profile's `Cookies` DB.
234
+ 5. Confirm `auth.chromiumKeychain.services` names the browser's safe-storage Keychain service for that DB.
235
+ 6. Re-run `/oracle-auth`.
236
+
190
237
  ### You hit a challenge / verification page
191
238
 
192
239
  - Solve it in the auth/bootstrap browser if prompted.
@@ -214,9 +261,10 @@ Project config should only override safe, non-privileged settings.
214
261
 
215
262
  - Install the missing local dependency and rerun the command.
216
263
 
217
- ### Auto-detection picked the wrong Chrome profile
264
+ ### Auto-detection picked the wrong browser profile
218
265
 
219
266
  - Set `auth.chromeProfile` in `~/.pi/agent/extensions/oracle.json`.
267
+ - For custom Chromium cookie sources, set `auth.chromeCookiePath` to the exact profile `Cookies` DB and pair it with `auth.chromiumKeychain`.
220
268
  - Re-run `/oracle-auth`.
221
269
 
222
270
  ### You want more details about a failed run
@@ -233,7 +281,7 @@ Project config should only override safe, non-privileged settings.
233
281
  ## Privacy / local data
234
282
 
235
283
  This extension is local-first, but it does read and persist local data:
236
- - `/oracle-auth` reads ChatGPT cookies from a local Chrome profile
284
+ - `/oracle-auth` reads ChatGPT cookies from the configured local browser profile
237
285
  - job archives are uploaded to ChatGPT.com
238
286
  - responses and artifacts are written under the configured oracle jobs dir
239
287
 
@@ -73,7 +73,7 @@ The extension now follows the current `pi` session lifecycle model:
73
73
  ### Commands
74
74
 
75
75
  - `/oracle-auth`
76
- - syncs ChatGPT cookies from the user’s real Chrome into the isolated oracle profile and verifies them there
76
+ - syncs ChatGPT cookies from the configured local browser profile into the isolated oracle profile and verifies them there
77
77
  - `/oracle-read [job-id]`
78
78
  - shows job status plus the saved response preview
79
79
  - `/oracle-status [job-id]`
@@ -127,9 +127,10 @@ Auth bootstrap flow:
127
127
 
128
128
  1. load oracle config
129
129
  2. acquire the global auth-maintenance lock
130
- 3. read ChatGPT cookies directly from the user’s real Chrome cookie store in read-only mode
130
+ 3. read ChatGPT cookies directly from the configured local browser cookie store in read-only mode
131
131
  - configurable source profile / cookie DB path
132
- - no launch or mutation of the real Chrome profile
132
+ - optional configured Chromium Keychain source for browsers outside the default importer
133
+ - no launch or mutation of the real browser profile
133
134
  4. validate that `browser.authSeedProfileDir` is an absolute safe path and not inside the real Chrome user-data tree
134
135
  5. create a staged seed-profile path next to the target seed profile
135
136
  6. launch the isolated auth browser headed with:
@@ -246,15 +247,20 @@ Browser/auth settings are global-only because they control local privileged brow
246
247
  "chatUrl": "https://chatgpt.com/",
247
248
  "authUrl": "https://chatgpt.com/auth/login",
248
249
  "runMode": "headless",
249
- "executablePath": "<optional absolute path to Chrome executable>",
250
+ "executablePath": "<optional absolute path to Chrome/Chromium executable>",
250
251
  "userAgent": "<optional real-Chrome UA override>",
251
252
  "args": ["--disable-blink-features=AutomationControlled"]
252
253
  },
253
254
  "auth": {
254
255
  "pollMs": 1000,
255
256
  "bootstrapTimeoutMs": 600000,
256
- "chromeProfile": "<optional Chrome profile name>",
257
- "chromeCookiePath": "<optional absolute path to Chrome Cookies DB>"
257
+ "chromeProfile": "<optional Chrome/Chromium profile name>",
258
+ "chromeCookiePath": "<optional absolute path to Chromium Cookies DB>",
259
+ "chromiumKeychain": {
260
+ "account": "<macOS Keychain account for non-built-in Chromium browsers>",
261
+ "services": ["<safe-storage service name>"],
262
+ "label": "<optional human-readable label>"
263
+ }
258
264
  },
259
265
  "worker": {
260
266
  "pollMs": 5000,
@@ -273,6 +279,23 @@ Browser/auth settings are global-only because they control local privileged brow
273
279
  }
274
280
  ```
275
281
 
282
+ `auth.chromiumKeychain` is an opt-in alternate cookie source for Chromium-family browsers that are not handled by the default `@steipete/sweet-cookie` Chrome-compatible importer. It must be configured with `auth.chromeCookiePath`; partial config is rejected so `/oracle-auth` cannot silently fall back to a different browser profile.
283
+
284
+ When both `auth.chromeCookiePath` and `auth.chromiumKeychain` are present, auth bootstrap:
285
+
286
+ 1. reads the configured macOS Keychain safe-storage password using `account` and the ordered `services` list
287
+ 2. snapshots the Chromium `Cookies` DB plus `Cookies-wal` / `Cookies-shm` sidecars, tolerating sidecars that disappear while the browser is closing
288
+ 3. decrypts Chromium AES-CBC cookie values, including Chromium v24+ host-hash-prefixed values
289
+ 4. dedupes duplicate cookie rows by keeping the first row after newest-expiry ordering
290
+ 5. filters importable ChatGPT auth cookies and seeds the isolated oracle auth profile
291
+
292
+ Operational requirements for this path:
293
+
294
+ - ChatGPT must already be logged in in the configured browser profile.
295
+ - The target browser should be fully quit before `/oracle-auth` so the cookie DB snapshot is stable.
296
+ - The configured Keychain item must be accessible to the current macOS user; allow Keychain access if prompted.
297
+ - `browser.executablePath` should point at the same Chromium-family browser so the headed auth/bootstrap browser uses the intended app.
298
+
276
299
  ## Cleanup maintenance model
277
300
 
278
301
  Long-run hygiene is intentionally conservative:
@@ -565,7 +588,7 @@ Live-validated after the concurrency redesign:
565
588
  - the poller no longer needs the worker to stay alive just to observe completion for artifact-producing runs
566
589
  - expired/missing auth now fails as a clean auth-related error instead of generic UI/config drift
567
590
  - `/oracle-auth` repairs the seed profile and a post-repair probe succeeds again
568
- - live auth recovery also exposed and corrected a real source-profile misconfiguration during validation; the configured Chrome profile must actually contain the active ChatGPT session cookies
591
+ - live auth recovery also exposed and corrected a real source-profile misconfiguration during validation; the configured browser profile must actually contain the active ChatGPT session cookies
569
592
 
570
593
  ## Known remaining work
571
594
 
@@ -5,7 +5,7 @@
5
5
  // Invariants/Assumptions: Oracle only runs against persisted sessions, and startup maintenance should be best-effort without breaking session initialization.
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { dirname, join } from "node:path";
8
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { loadOracleConfig } from "./lib/config.js";
10
10
  import { registerOracleCommands } from "./lib/commands.js";
11
11
  import { getSessionFile, pruneTerminalOracleJobs, reconcileStaleOracleJobs } from "./lib/jobs.js";
@@ -5,7 +5,7 @@
5
5
  // Invariants/Assumptions: Commands operate on persisted project-scoped jobs and rely on shared observability formatting for detached-state clarity.
6
6
  import { existsSync } from "node:fs";
7
7
  import { readFile } from "node:fs/promises";
8
- import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
9
9
  import { formatOracleCancelOutcome, formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
10
10
  import { runOracleAuthBootstrap } from "./auth.js";
11
11
  import {
@@ -67,9 +67,9 @@ function readScopedJob(jobId: string, cwd: string) {
67
67
 
68
68
  export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string): void {
69
69
  pi.registerCommand("oracle-auth", {
70
- description: "Sync ChatGPT cookies from real Chrome into the oracle auth seed profile",
70
+ description: "Sync ChatGPT cookies from the configured local browser profile into the oracle auth seed profile",
71
71
  handler: async (_args, ctx) => {
72
- ctx.ui.notify("Syncing ChatGPT cookies from real Chrome into the oracle auth seed profile…", "info");
72
+ ctx.ui.notify("Syncing ChatGPT cookies from the configured local browser profile into the oracle auth seed profile…", "info");
73
73
  try {
74
74
  const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd);
75
75
  ctx.ui.notify(result, "info");
@@ -6,7 +6,7 @@
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
9
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
10
10
  import { isAbsolute, join, normalize } from "node:path";
11
11
  import { getProjectId } from "./runtime.js";
12
12
 
@@ -209,6 +209,11 @@ export interface OracleConfig {
209
209
  bootstrapTimeoutMs: number;
210
210
  chromeProfile: string;
211
211
  chromeCookiePath?: string;
212
+ chromiumKeychain?: {
213
+ account: string;
214
+ services: string[];
215
+ label?: string;
216
+ };
212
217
  };
213
218
  worker: {
214
219
  pollMs: number;
@@ -286,11 +291,12 @@ export function getOracleConfigLoadDetails(cwd: string): OracleConfigLoadDetails
286
291
  }
287
292
 
288
293
  export function formatOracleAuthConfigRemediation(details: OracleConfigLoadDetails): string {
294
+ const authFields = "auth.chromeProfile / auth.chromeCookiePath / auth.chromiumKeychain";
289
295
  if (!details.projectConfigExists) {
290
- return `Set auth.chromeProfile / auth.chromeCookiePath in ${details.effectiveAuthConfigPath}.`;
296
+ return `Set ${authFields} in ${details.effectiveAuthConfigPath}.`;
291
297
  }
292
298
  return (
293
- `Set auth.chromeProfile / auth.chromeCookiePath in ${details.effectiveAuthConfigPath}. ` +
299
+ `Set ${authFields} in ${details.effectiveAuthConfigPath}. ` +
294
300
  `Project overrides are also read from ${details.projectConfigPath}, but auth.* is loaded from ${details.effectiveAuthConfigPath}.`
295
301
  );
296
302
  }
@@ -330,6 +336,7 @@ export const DEFAULT_CONFIG: OracleConfig = {
330
336
  bootstrapTimeoutMs: 10 * 60 * 1000,
331
337
  chromeProfile: detectedChromeProfileName,
332
338
  chromeCookiePath: undefined,
339
+ chromiumKeychain: undefined,
333
340
  },
334
341
  worker: {
335
342
  pollMs: 5000,
@@ -439,6 +446,20 @@ function expectStringArray(value: unknown, path: string): string[] {
439
446
  return value;
440
447
  }
441
448
 
449
+ function expectOptionalChromiumKeychain(value: unknown, path: string): OracleConfig["auth"]["chromiumKeychain"] {
450
+ if (value === undefined) return undefined;
451
+ const keychain = expectObject(value, path);
452
+ const services = expectStringArray(keychain.services, `${path}.services`);
453
+ if (services.length === 0) {
454
+ throw new Error(`Invalid oracle config: ${path}.services must include at least one service name`);
455
+ }
456
+ return {
457
+ account: expectString(keychain.account, `${path}.account`),
458
+ services,
459
+ label: expectOptionalString(keychain.label, `${path}.label`),
460
+ };
461
+ }
462
+
442
463
  function expectInteger(value: unknown, path: string, minimum: number, maximum?: number): number {
443
464
  if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || (maximum !== undefined && value > maximum)) {
444
465
  const range = maximum === undefined ? `>= ${minimum}` : `between ${minimum} and ${maximum}`;
@@ -523,6 +544,12 @@ function validateOracleConfig(value: unknown): OracleConfig {
523
544
  throw new Error("Invalid oracle config: browser.runtimeProfilesDir must be separate from browser.authSeedProfileDir");
524
545
  }
525
546
 
547
+ const chromeCookiePath = expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath");
548
+ const chromiumKeychain = expectOptionalChromiumKeychain(auth.chromiumKeychain, "auth.chromiumKeychain");
549
+ if (chromiumKeychain !== undefined && chromeCookiePath === undefined) {
550
+ throw new Error("Invalid oracle config: auth.chromiumKeychain requires auth.chromeCookiePath");
551
+ }
552
+
526
553
  return {
527
554
  defaults: {
528
555
  preset,
@@ -544,7 +571,8 @@ function validateOracleConfig(value: unknown): OracleConfig {
544
571
  pollMs: expectInteger(auth.pollMs, "auth.pollMs", 100),
545
572
  bootstrapTimeoutMs: expectInteger(auth.bootstrapTimeoutMs, "auth.bootstrapTimeoutMs", 1000),
546
573
  chromeProfile: expectString(auth.chromeProfile, "auth.chromeProfile"),
547
- chromeCookiePath: expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath"),
574
+ chromeCookiePath,
575
+ chromiumKeychain,
548
576
  },
549
577
  worker: {
550
578
  pollMs: expectInteger(worker.pollMs, "worker.pollMs", 100),
@@ -7,7 +7,7 @@ import { createHash, randomUUID } from "node:crypto";
7
7
  import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
8
8
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
9
9
  import { isAbsolute, join, relative as relativePath, resolve, sep } from "node:path";
10
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
10
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
  import {
12
12
  ACTIVE_ORACLE_JOB_STATUSES,
13
13
  applyOracleJobCleanupWarnings,
@@ -4,7 +4,7 @@
4
4
  // Usage: Imported by the oracle extension entrypoint to start or stop per-session oracle polling.
5
5
  // Invariants/Assumptions: Poller scans are serialized per session key, wake-up delivery is best-effort, and terminal-job notifications always re-read durable job state before send.
6
6
  import { existsSync } from "node:fs";
7
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
8
  import { buildOracleStatusText, buildOracleWakeupNotificationContent } from "../shared/job-observability-helpers.mjs";
9
9
  import { isProcessAlive, readProcessStartedAt } from "../shared/process-helpers.mjs";
10
10
  import { isLockTimeoutError, listLeaseMetadata, releaseLease, withGlobalReconcileLock, writeLeaseMetadata } from "./locks.js";
@@ -246,7 +246,11 @@ async function scan(
246
246
  }
247
247
  if (await releaseWakeupLeaseIfInactive(wakeupTargetLeaseKey, lifecycle)) return;
248
248
 
249
- await promoteQueuedJobs({ workerPath, source: "poller" });
249
+ try {
250
+ await promoteQueuedJobs({ workerPath, source: "poller", lockTimeoutMs: POLLER_LOCK_TIMEOUT_MS });
251
+ } catch (error) {
252
+ if (!isLockTimeoutError(error, "admission", "global")) throw error;
253
+ }
250
254
  if (await releaseWakeupLeaseIfInactive(wakeupTargetLeaseKey, lifecycle)) return;
251
255
 
252
256
  const terminalJobs = listOracleJobDirs()
@@ -25,6 +25,7 @@ export interface OracleQueuePosition {
25
25
  export interface PromoteQueuedJobsOptions {
26
26
  workerPath: string;
27
27
  source: string;
28
+ lockTimeoutMs?: number;
28
29
  spawnWorkerFn?: typeof spawnWorker;
29
30
  loadConfigFn?: typeof loadOracleConfig;
30
31
  }
@@ -141,7 +142,7 @@ export async function promoteQueuedJobsWithinAdmissionLock(options: PromoteQueue
141
142
  export async function promoteQueuedJobs(options: PromoteQueuedJobsOptions): Promise<{ promotedJobIds: string[] }> {
142
143
  return withLock("admission", "global", { processPid: process.pid, source: options.source }, async () => {
143
144
  return promoteQueuedJobsWithinAdmissionLock(options);
144
- });
145
+ }, { timeoutMs: options.lockTimeoutMs });
145
146
  }
146
147
 
147
148
  export async function createQueuedJob(
@@ -8,7 +8,7 @@ import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/pr
8
8
  import { tmpdir } from "node:os";
9
9
  import { basename, join, posix } from "node:path";
10
10
  import { runOracleAuthBootstrap } from "./auth.js";
11
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
11
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { Type } from "typebox";
13
13
  import { formatOracleCancelOutcome, formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
14
14
  import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent, transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
@@ -63,7 +63,7 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
63
63
  files: Type.Array(Type.String({
64
64
  description: "Project-relative file or directory path to include in the archive.",
65
65
  minLength: 1,
66
- pattern: ".*\\S.*",
66
+ pattern: "^.*\\S.*$",
67
67
  }), {
68
68
  description: "Exact project-relative files/directories to include in the oracle archive.",
69
69
  minItems: 1,
@@ -1055,7 +1055,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1055
1055
  pi.registerTool({
1056
1056
  name: "oracle_auth",
1057
1057
  label: "Oracle Auth",
1058
- description: "Refresh the shared oracle auth seed profile by importing ChatGPT cookies from your configured real Chrome profile.",
1058
+ description: "Refresh the shared oracle auth seed profile by importing ChatGPT cookies from your configured local browser profile.",
1059
1059
  promptSnippet: "Refresh oracle auth before retrying a login-required oracle run.",
1060
1060
  promptGuidelines: [
1061
1061
  "Call oracle_auth when an oracle run failed because ChatGPT login is required, the worker said to rerun /oracle-auth, or stale auth appears to be blocking submission execution.",
@@ -1,8 +1,8 @@
1
- // Purpose: Bootstrap isolated oracle browser auth by importing real Chrome cookies and validating ChatGPT session readiness.
1
+ // Purpose: Bootstrap isolated oracle browser auth by importing real Chromium-family cookies and validating ChatGPT session readiness.
2
2
  // Responsibilities: Copy/import cookies, classify auth pages, drive lightweight account-selection flows, and persist diagnostics for auth failures.
3
3
  // Scope: Auth bootstrap worker only; long-running oracle job execution stays in run-job.mjs and shared lifecycle/state helpers stay elsewhere.
4
4
  // Usage: Spawned by /oracle-auth to prepare the shared auth seed profile used by future oracle jobs.
5
- // Invariants/Assumptions: Runs against a local macOS Chrome profile, preserves private diagnostics, and must fail clearly when auth state cannot be verified.
5
+ // Invariants/Assumptions: Runs against a local macOS Chromium-family profile, preserves private diagnostics, and must fail clearly when auth state cannot be verified.
6
6
  import { withLock } from "./state-locks.mjs";
7
7
  import { spawn } from "node:child_process";
8
8
  import { existsSync } from "node:fs";
@@ -11,6 +11,7 @@ import { homedir, tmpdir } from "node:os";
11
11
  import { basename, dirname, join, resolve } from "node:path";
12
12
  import { getCookies } from "@steipete/sweet-cookie";
13
13
  import { ensureAccountCookie, filterImportableAuthCookies } from "./auth-cookie-policy.mjs";
14
+ import { getCookiesFromConfiguredChromiumSource } from "./chromium-cookie-source.mjs";
14
15
  import { buildAllowedChatGptOrigins } from "./chatgpt-ui-helpers.mjs";
15
16
  import { buildAccountChooserCandidateLabels, classifyChatAuthPage, normalizeLoginProbeResult } from "./auth-flow-helpers.mjs";
16
17
 
@@ -91,11 +92,11 @@ function authConfigRemediation() {
91
92
  if (typeof configLoad?.remediation === "string" && configLoad.remediation) return configLoad.remediation;
92
93
  if (typeof configLoad?.projectConfigPath === "string" && configLoad.projectConfigPath && configLoad.projectConfigExists) {
93
94
  return (
94
- `Set auth.chromeProfile / auth.chromeCookiePath in ${effectiveAuthConfigPath()}. ` +
95
+ `Set auth.chromeProfile / auth.chromeCookiePath / auth.chromiumKeychain in ${effectiveAuthConfigPath()}. ` +
95
96
  `Project overrides are also read from ${configLoad.projectConfigPath}, but auth.* is loaded from ${effectiveAuthConfigPath()}.`
96
97
  );
97
98
  }
98
- return `Set auth.chromeProfile / auth.chromeCookiePath in ${effectiveAuthConfigPath()}.`;
99
+ return `Set auth.chromeProfile / auth.chromeCookiePath / auth.chromiumKeychain in ${effectiveAuthConfigPath()}.`;
99
100
  }
100
101
 
101
102
  function authConfigSummary() {
@@ -462,15 +463,109 @@ function cookieSource() {
462
463
  return config.auth.chromeCookiePath || config.auth.chromeProfile;
463
464
  }
464
465
 
466
+ function usesConfiguredChromiumCookieSource() {
467
+ return Boolean(config.auth.chromeCookiePath && config.auth.chromiumKeychain);
468
+ }
469
+
470
+ function browserSourceName() {
471
+ if (config.browser.executablePath) return basename(config.browser.executablePath);
472
+ if (usesConfiguredChromiumCookieSource()) return "configured Chromium browser";
473
+ return "Google Chrome";
474
+ }
475
+
476
+ function keychainSummary() {
477
+ const keychain = config.auth.chromiumKeychain;
478
+ if (!keychain) return undefined;
479
+ const services = Array.isArray(keychain.services) && keychain.services.length > 0 ? keychain.services.join(", ") : "(none configured)";
480
+ const label = keychain.label || services;
481
+ return `${label} (account: ${keychain.account}, services: ${services})`;
482
+ }
483
+
465
484
  function cookieSourceLabel() {
485
+ if (usesConfiguredChromiumCookieSource()) return `configured Chromium cookie DB ${config.auth.chromeCookiePath}`;
466
486
  return config.auth.chromeCookiePath
467
487
  ? `Chrome cookie DB ${config.auth.chromeCookiePath}`
468
488
  : `Chrome profile ${config.auth.chromeProfile}`;
469
489
  }
470
490
 
471
- async function readSourceCookies() {
472
- await log(`Reading ChatGPT cookies from ${cookieSourceLabel()}`);
473
- const { cookies, warnings } = await getCookies({
491
+ function formatAuthSuccessMessage({ classificationMessage, appliedCount, targetDir }) {
492
+ const lines = [
493
+ "Oracle auth synced.",
494
+ "",
495
+ "Source:",
496
+ `- Browser: ${browserSourceName()}`,
497
+ ];
498
+
499
+ if (config.auth.chromeCookiePath) lines.push(`- Cookie DB: ${config.auth.chromeCookiePath}`);
500
+ else lines.push(`- Profile: ${config.auth.chromeProfile}`);
501
+
502
+ const keychain = keychainSummary();
503
+ if (keychain) lines.push(`- Keychain: ${keychain}`);
504
+ lines.push(`- Cookies synced: ${appliedCount}`);
505
+ lines.push("", "Auth seed profile:", targetDir, "", "Diagnostics:", DIAGNOSTICS_DIR, "", "Status:", classificationMessage);
506
+ return lines.join("\n");
507
+ }
508
+
509
+ function formatAuthFailureGuidance(error) {
510
+ const reason = error instanceof Error ? error.message : String(error);
511
+ const lines = ["Oracle auth failed.", "", `Reason: ${reason}`, "", "Likely causes:"];
512
+
513
+ if (usesConfiguredChromiumCookieSource()) {
514
+ lines.push(
515
+ "- the configured cookie DB is stale or from the wrong browser profile",
516
+ "- ChatGPT is logged out in that browser profile",
517
+ "- auth.chromiumKeychain does not match the browser safe-storage item for that cookie DB",
518
+ "- the target browser was still running while /oracle-auth read its Cookies DB",
519
+ "",
520
+ "Next:",
521
+ "1. Open the configured browser profile.",
522
+ "2. Confirm ChatGPT works there.",
523
+ "3. Quit the browser fully.",
524
+ "4. Confirm auth.chromeCookiePath points at that profile's Cookies DB.",
525
+ "5. Confirm auth.chromiumKeychain.services names the browser's safe-storage Keychain service.",
526
+ "6. Re-run /oracle-auth.",
527
+ );
528
+ } else {
529
+ lines.push(
530
+ "- ChatGPT is logged out in the configured local browser profile",
531
+ "- auth.chromeProfile or auth.chromeCookiePath points at the wrong profile",
532
+ "- the profile cookie store is stale or unreadable",
533
+ "",
534
+ "Next:",
535
+ "1. Open the configured local browser profile.",
536
+ "2. Confirm ChatGPT works there.",
537
+ "3. Quit the browser fully.",
538
+ "4. Re-run /oracle-auth.",
539
+ );
540
+ }
541
+
542
+ lines.push(
543
+ "",
544
+ "Details:",
545
+ `- Source: ${cookieSourceLabel()}`,
546
+ `- Browser: ${browserSourceName()}`,
547
+ `- Auth seed profile: ${config.browser.authSeedProfileDir}`,
548
+ `- Diagnostics: ${DIAGNOSTICS_DIR || "(oracle-auth diagnostics dir unavailable)"}`,
549
+ `- Log: ${LOG_PATH}`,
550
+ "",
551
+ authConfigSummary(),
552
+ );
553
+
554
+ return lines.join("\n");
555
+ }
556
+
557
+ async function readRawSourceCookies() {
558
+ if (config.auth.chromeCookiePath && config.auth.chromiumKeychain) {
559
+ return await getCookiesFromConfiguredChromiumSource({
560
+ dbPath: config.auth.chromeCookiePath,
561
+ keychain: config.auth.chromiumKeychain,
562
+ origins: cookieOrigins(),
563
+ profile: config.auth.chromeProfile,
564
+ timeoutMs: 5_000,
565
+ });
566
+ }
567
+
568
+ return await getCookies({
474
569
  url: config.browser.chatUrl,
475
570
  origins: cookieOrigins(),
476
571
  browsers: ["chrome"],
@@ -478,9 +573,14 @@ async function readSourceCookies() {
478
573
  chromeProfile: cookieSource(),
479
574
  timeoutMs: 5_000,
480
575
  });
576
+ }
577
+
578
+ async function readSourceCookies() {
579
+ await log(`Reading ChatGPT cookies from ${cookieSourceLabel()}`);
580
+ const { cookies, warnings } = await readRawSourceCookies();
481
581
 
482
582
  if (warnings.length) {
483
- await log(`sweet-cookie warnings: ${warnings.join(" | ")}`);
583
+ await log(`Cookie source warnings: ${warnings.join(" | ")}`);
484
584
  }
485
585
 
486
586
  const filtered = filterImportableAuthCookies(cookies, config.browser.chatUrl);
@@ -501,7 +601,7 @@ async function readSourceCookies() {
501
601
 
502
602
  if (!hasSessionToken) {
503
603
  throw new Error(
504
- `No ChatGPT session-token cookies were found in ${cookieSourceLabel()}. Make sure ChatGPT is logged into that Chrome profile. ${authConfigRemediation()}`,
604
+ `No ChatGPT session-token cookies were found in ${cookieSourceLabel()}. Make sure ChatGPT is logged into that browser profile. ${authConfigRemediation()}`,
505
605
  );
506
606
  }
507
607
 
@@ -809,9 +909,7 @@ async function run() {
809
909
  const generation = new Date().toISOString();
810
910
  await writeFile(join(profilePlan.targetDir, ".oracle-seed-generation"), `${generation}\n`, { encoding: "utf8", mode: 0o600 });
811
911
  committedProfile = true;
812
- process.stdout.write(
813
- `${classification.message} Synced ${appliedCount} cookies into ${profilePlan.targetDir}. Diagnostics: ${DIAGNOSTICS_DIR}`,
814
- );
912
+ process.stdout.write(formatAuthSuccessMessage({ classificationMessage: classification.message, appliedCount, targetDir: profilePlan.targetDir }));
815
913
  } catch (error) {
816
914
  shouldPreserveBrowser = Boolean(error && typeof error === "object" && error.preserveBrowser === true);
817
915
  await log(`Auth bootstrap failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -827,8 +925,6 @@ async function run() {
827
925
  }
828
926
 
829
927
  run().catch((error) => {
830
- process.stderr.write(
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.`,
832
- );
928
+ process.stderr.write(formatAuthFailureGuidance(error));
833
929
  process.exit(1);
834
930
  });
@@ -114,7 +114,8 @@ export function classifyChatAuthPage(args) {
114
114
  state: "login_required",
115
115
  message:
116
116
  `Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
117
- `(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
117
+ `(status=${args.probe?.status ?? 0}); the cookie DB may be stale, from the wrong browser profile, or for an account that is logged out. ` +
118
+ `Check auth.chromeProfile/auth.chromeCookiePath/auth.chromiumKeychain and inspect ${args.logPath}.`,
118
119
  };
119
120
  }
120
121
 
@@ -131,7 +132,8 @@ export function classifyChatAuthPage(args) {
131
132
  state: "login_required",
132
133
  message:
133
134
  `Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
134
- `(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
135
+ `(status=${args.probe?.status ?? 0}); the cookie DB may be stale, from the wrong browser profile, or for an account that is logged out. ` +
136
+ `Check auth.chromeProfile/auth.chromeCookiePath/auth.chromiumKeychain and inspect ${args.logPath}.`,
135
137
  };
136
138
  }
137
139
 
@@ -0,0 +1,21 @@
1
+ import type { ImportedAuthCookie } from "./auth-cookie-policy.mjs";
2
+
3
+ export interface ChromiumKeychainConfig {
4
+ account: string;
5
+ services: string[];
6
+ label?: string;
7
+ service?: string;
8
+ }
9
+
10
+ export interface ConfiguredChromiumSourceOptions {
11
+ dbPath: string;
12
+ keychain: ChromiumKeychainConfig;
13
+ origins: string[];
14
+ profile: string;
15
+ timeoutMs?: number;
16
+ includeExpired?: boolean;
17
+ }
18
+
19
+ export function getCookiesFromConfiguredChromiumSource(
20
+ options: ConfiguredChromiumSourceOptions,
21
+ ): Promise<{ cookies: ImportedAuthCookie[]; warnings: string[] }>;
@@ -0,0 +1,291 @@
1
+ // Purpose: Read ChatGPT cookies from arbitrary macOS Chromium-family cookie stores when sweet-cookie's built-in browser list is too narrow.
2
+ // Responsibilities: Snapshot a Chromium Cookies SQLite DB, decrypt AES-CBC cookie values with a configured Keychain item, and return sweet-cookie-shaped cookie objects.
3
+ // Scope: macOS Chromium cookie extraction only; auth policy filtering and browser seeding stay in auth-bootstrap.mjs.
4
+ // Usage: auth-bootstrap.mjs uses this when auth.chromiumKeychain is configured alongside auth.chromeCookiePath.
5
+ // Invariants/Assumptions: The configured cookie path points at a Chromium Cookies DB and the configured Keychain item is the browser's safe-storage secret.
6
+ import { spawn } from "node:child_process";
7
+ import { createDecipheriv, pbkdf2Sync } from "node:crypto";
8
+ import { copyFileSync, existsSync, mkdtempSync, rmSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { DatabaseSync } from "node:sqlite";
12
+
13
+ const CHROMIUM_EPOCH_OFFSET_SECONDS = 11_644_473_600n;
14
+ const COOKIE_VALUE_DECODER = new TextDecoder("utf-8", { fatal: true });
15
+ const MACOS_CHROMIUM_KEY_ITERATIONS = 1003;
16
+
17
+ function spawnCapture(command, args, options = {}) {
18
+ return new Promise((resolve) => {
19
+ const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
20
+ let stdout = "";
21
+ let stderr = "";
22
+ const timeoutMs = options.timeoutMs ?? 5_000;
23
+ let timedOut = false;
24
+ const timer = setTimeout(() => {
25
+ timedOut = true;
26
+ child.kill("SIGTERM");
27
+ }, timeoutMs);
28
+ timer.unref?.();
29
+
30
+ child.stdout.on("data", (data) => { stdout += String(data); });
31
+ child.stderr.on("data", (data) => { stderr += String(data); });
32
+ child.on("error", (error) => {
33
+ clearTimeout(timer);
34
+ resolve({ ok: false, stdout, stderr, error: error.message });
35
+ });
36
+ child.on("close", (code) => {
37
+ clearTimeout(timer);
38
+ resolve({
39
+ ok: code === 0 && !timedOut,
40
+ stdout,
41
+ stderr,
42
+ error: timedOut ? `Timed out after ${timeoutMs}ms` : stderr.trim(),
43
+ });
44
+ });
45
+ });
46
+ }
47
+
48
+ async function readKeychainPassword(keychain, timeoutMs) {
49
+ const services = Array.isArray(keychain.services) && keychain.services.length > 0 ? keychain.services : [keychain.service];
50
+ for (const service of services.filter(Boolean)) {
51
+ const result = await spawnCapture("security", ["find-generic-password", "-w", "-a", keychain.account, "-s", service], { timeoutMs });
52
+ if (result.ok) {
53
+ const password = result.stdout.trim();
54
+ if (password) return { ok: true, password, service };
55
+ return { ok: false, error: `macOS Keychain returned an empty ${keychain.label || service} password.` };
56
+ }
57
+ }
58
+ return { ok: false, error: `Failed to read macOS Keychain (${keychain.label || keychain.account}): no configured service returned a password.` };
59
+ }
60
+
61
+ function snapshotCookieDb(dbPath) {
62
+ const tempDir = mkdtempSync(join(tmpdir(), "pi-oracle-chromium-cookies-"));
63
+ const tempDbPath = join(tempDir, "Cookies");
64
+ try {
65
+ copyFileSync(dbPath, tempDbPath);
66
+ copySidecar(dbPath, `${tempDbPath}-wal`, "-wal");
67
+ copySidecar(dbPath, `${tempDbPath}-shm`, "-shm");
68
+ return { tempDir, tempDbPath };
69
+ } catch (error) {
70
+ rmSync(tempDir, { recursive: true, force: true });
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ function copySidecar(sourceDbPath, targetPath, suffix) {
76
+ const sidecarPath = `${sourceDbPath}${suffix}`;
77
+ try {
78
+ copyFileSync(sidecarPath, targetPath);
79
+ } catch (error) {
80
+ if (isMissingFileError(error)) return;
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ function isMissingFileError(error) {
86
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
87
+ }
88
+
89
+ function readMetaVersion(db) {
90
+ try {
91
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'version'").get();
92
+ const parsed = Number.parseInt(String(row?.value ?? "0"), 10);
93
+ return Number.isFinite(parsed) ? parsed : 0;
94
+ } catch {
95
+ return 0;
96
+ }
97
+ }
98
+
99
+ function parentCookieDomains(host) {
100
+ const labels = host.split(".").filter(Boolean);
101
+ const domains = new Set([host, `.${host}`]);
102
+ for (let index = 1; index < labels.length - 1; index += 1) {
103
+ const parent = labels.slice(index).join(".");
104
+ domains.add(parent);
105
+ domains.add(`.${parent}`);
106
+ }
107
+ return [...domains];
108
+ }
109
+
110
+ function sqlStringLiteral(value) {
111
+ return `'${value.replaceAll("'", "''")}'`;
112
+ }
113
+
114
+ function buildHostWhereClause(origins) {
115
+ const domains = new Set();
116
+ for (const origin of origins) {
117
+ try {
118
+ for (const domain of parentCookieDomains(new URL(origin).hostname)) domains.add(domain);
119
+ } catch {
120
+ // Ignore malformed origins; validated ChatGPT config supplies the real set.
121
+ }
122
+ }
123
+ if (domains.size === 0) return "0";
124
+ return `host_key IN (${[...domains].map(sqlStringLiteral).join(", ")})`;
125
+ }
126
+
127
+ function hostMatchesAny(originHosts, hostKey) {
128
+ const cookieDomain = hostKey.startsWith(".") ? hostKey.slice(1) : hostKey;
129
+ return originHosts.some((host) => host === cookieDomain || host.endsWith(`.${cookieDomain}`));
130
+ }
131
+
132
+ function chromiumExpirationToUnixSeconds(value) {
133
+ if (value === undefined || value === null || String(value) === "0") return undefined;
134
+ try {
135
+ const raw = BigInt(String(value));
136
+ const seconds = raw / 1_000_000n - CHROMIUM_EPOCH_OFFSET_SECONDS;
137
+ if (seconds <= 0n) return undefined;
138
+ return Number(seconds);
139
+ } catch {
140
+ return undefined;
141
+ }
142
+ }
143
+
144
+ function normalizeSameSite(value) {
145
+ const normalized = String(value ?? "").toLowerCase();
146
+ if (normalized === "2" || normalized === "strict") return "Strict";
147
+ if (normalized === "1" || normalized === "lax") return "Lax";
148
+ if (normalized === "0" || normalized === "none" || normalized === "no_restriction") return "None";
149
+ return undefined;
150
+ }
151
+
152
+ function deriveMacosChromiumKey(password) {
153
+ return pbkdf2Sync(password, "saltysalt", MACOS_CHROMIUM_KEY_ITERATIONS, 16, "sha1");
154
+ }
155
+
156
+ function decryptCookieValue(encryptedValue, key, options) {
157
+ const buffer = Buffer.from(encryptedValue);
158
+ if (buffer.length < 3) return null;
159
+ const prefix = buffer.subarray(0, 3).toString("utf8");
160
+ if (!/^v\d\d$/.test(prefix)) return decodeCookieBytes(buffer, false);
161
+
162
+ try {
163
+ const iv = Buffer.alloc(16, 0x20);
164
+ const decipher = createDecipheriv("aes-128-cbc", key, iv);
165
+ decipher.setAutoPadding(false);
166
+ const padded = Buffer.concat([decipher.update(buffer.subarray(3)), decipher.final()]);
167
+ const unpadded = removePkcs7Padding(padded);
168
+ return decodeCookieBytes(unpadded, options.stripHashPrefix);
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ function removePkcs7Padding(value) {
175
+ if (!value.length) return value;
176
+ const padding = value[value.length - 1];
177
+ if (!padding || padding > 16) return value;
178
+ return value.subarray(0, value.length - padding);
179
+ }
180
+
181
+ function decodeCookieBytes(value, stripHashPrefix) {
182
+ const bytes = stripHashPrefix && value.length >= 32 ? value.subarray(32) : value;
183
+ try {
184
+ return stripLeadingControlChars(COOKIE_VALUE_DECODER.decode(bytes));
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ function stripLeadingControlChars(value) {
191
+ let index = 0;
192
+ while (index < value.length && value.charCodeAt(index) < 0x20) index += 1;
193
+ return value.slice(index);
194
+ }
195
+
196
+ function collectCookies(rows, options, key, warnings) {
197
+ const cookies = [];
198
+ const now = Math.floor(Date.now() / 1000);
199
+ const hosts = options.origins.map((origin) => new URL(origin).hostname);
200
+ let warnedEncryptedType = false;
201
+
202
+ for (const row of rows) {
203
+ const name = typeof row.name === "string" ? row.name : "";
204
+ const hostKey = typeof row.host_key === "string" ? row.host_key : "";
205
+ if (!name || !hostKey || !hostMatchesAny(hosts, hostKey)) continue;
206
+
207
+ let value = typeof row.value === "string" && row.value.length > 0 ? row.value : null;
208
+ if (value === null) {
209
+ if (!(row.encrypted_value instanceof Uint8Array)) {
210
+ if (!warnedEncryptedType && row.encrypted_value !== undefined) {
211
+ warnings.push("Chromium cookie encrypted_value is in an unsupported type.");
212
+ warnedEncryptedType = true;
213
+ }
214
+ continue;
215
+ }
216
+ value = decryptCookieValue(row.encrypted_value, key, { stripHashPrefix: options.stripHashPrefix });
217
+ }
218
+ if (value === null) continue;
219
+
220
+ const expires = chromiumExpirationToUnixSeconds(row.expires_utc);
221
+ if (!options.includeExpired && expires !== undefined && expires < now) continue;
222
+
223
+ const cookie = {
224
+ name,
225
+ value,
226
+ domain: hostKey.startsWith(".") ? hostKey.slice(1) : hostKey,
227
+ path: typeof row.path === "string" && row.path ? row.path : "/",
228
+ secure: row.is_secure === 1 || row.is_secure === "1" || row.is_secure === true,
229
+ httpOnly: row.is_httponly === 1 || row.is_httponly === "1" || row.is_httponly === true,
230
+ source: { browser: "chromium", profile: options.profile },
231
+ };
232
+ if (expires !== undefined) cookie.expires = expires;
233
+ const sameSite = normalizeSameSite(row.samesite);
234
+ if (sameSite !== undefined) cookie.sameSite = sameSite;
235
+ cookies.push(cookie);
236
+ }
237
+
238
+ return dedupeCookies(cookies);
239
+ }
240
+
241
+ function dedupeCookies(cookies) {
242
+ const seen = new Map();
243
+ for (const cookie of cookies) {
244
+ const key = `${cookie.domain}\t${cookie.path}\t${cookie.name}`;
245
+ if (!seen.has(key)) seen.set(key, cookie);
246
+ }
247
+ return [...seen.values()];
248
+ }
249
+
250
+ export async function getCookiesFromConfiguredChromiumSource(options) {
251
+ const warnings = [];
252
+ if (!options.dbPath || !existsSync(options.dbPath)) {
253
+ return { cookies: [], warnings: [`Chromium cookies database not found: ${options.dbPath || "(missing path)"}`] };
254
+ }
255
+
256
+ const passwordResult = await readKeychainPassword(options.keychain, options.timeoutMs ?? 5_000);
257
+ if (!passwordResult.ok) return { cookies: [], warnings: [passwordResult.error] };
258
+
259
+ let snapshot;
260
+ try {
261
+ snapshot = snapshotCookieDb(options.dbPath);
262
+ } catch (error) {
263
+ return { cookies: [], warnings: [`Failed to copy Chromium cookie DB: ${error instanceof Error ? error.message : String(error)}`] };
264
+ }
265
+
266
+ try {
267
+ const db = new DatabaseSync(snapshot.tempDbPath, { readOnly: true });
268
+ try {
269
+ const metaVersion = readMetaVersion(db);
270
+ const where = buildHostWhereClause(options.origins);
271
+ const sql =
272
+ `SELECT name, value, host_key, path, CAST(expires_utc AS TEXT) AS expires_utc, samesite, encrypted_value, ` +
273
+ `is_secure AS is_secure, is_httponly AS is_httponly FROM cookies WHERE (${where}) ORDER BY cookies.expires_utc DESC;`;
274
+ const rows = db.prepare(sql).all();
275
+ const key = deriveMacosChromiumKey(passwordResult.password);
276
+ const cookies = collectCookies(rows, {
277
+ origins: options.origins,
278
+ profile: options.profile,
279
+ includeExpired: options.includeExpired === true,
280
+ stripHashPrefix: metaVersion >= 24,
281
+ }, key, warnings);
282
+ return { cookies, warnings };
283
+ } finally {
284
+ db.close();
285
+ }
286
+ } catch (error) {
287
+ return { cookies: [], warnings: [`Failed to read Chromium cookies (requires a modern Chromium cookie DB): ${error instanceof Error ? error.message : String(error)}`] };
288
+ } finally {
289
+ rmSync(snapshot.tempDir, { recursive: true, force: true });
290
+ }
291
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
4
4
  "description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  ]
43
43
  },
44
44
  "scripts": {
45
- "check:oracle-extension": "node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/chatgpt-ui-helpers.mjs && node --check extensions/oracle/worker/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
45
+ "check:oracle-extension": "node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/chatgpt-ui-helpers.mjs && node --check extensions/oracle/worker/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/chromium-cookie-source.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@earendil-works/pi-coding-agent --external:@earendil-works/pi-ai --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
46
46
  "typecheck": "tsc --noEmit -p tsconfig.json",
47
47
  "typecheck:worker-helpers": "tsc --noEmit -p tsconfig.worker-helpers.json",
48
48
  "sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
@@ -55,19 +55,19 @@
55
55
  "@steipete/sweet-cookie": "^0.2.0"
56
56
  },
57
57
  "peerDependencies": {
58
- "typebox": "*",
59
- "@mariozechner/pi-coding-agent": "*"
58
+ "@earendil-works/pi-coding-agent": "*",
59
+ "typebox": "*"
60
60
  },
61
61
  "overrides": {
62
- "basic-ftp": "5.3.0",
62
+ "basic-ftp": "6.0.1",
63
63
  "protobufjs": "7.5.5"
64
64
  },
65
65
  "devDependencies": {
66
- "@mariozechner/pi-coding-agent": "^0.72.0",
67
- "@types/node": "^25.6.0",
66
+ "@earendil-works/pi-coding-agent": "^0.74.0",
67
+ "@types/node": "^25.6.1",
68
68
  "esbuild": "^0.28.0",
69
69
  "tsx": "^4.21.0",
70
- "typebox": "^1.1.37",
70
+ "typebox": "^1.1.38",
71
71
  "typescript": "^6.0.3"
72
72
  },
73
73
  "engines": {
@@ -76,5 +76,5 @@
76
76
  "os": [
77
77
  "darwin"
78
78
  ],
79
- "packageManager": "npm@10.9.8"
79
+ "packageManager": "npm@11.14.0"
80
80
  }