pi-oracle 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +51 -17
  3. package/docs/ORACLE_DESIGN.md +12 -5
  4. package/docs/platform-smoke.md +153 -0
  5. package/extensions/oracle/lib/config.ts +53 -27
  6. package/extensions/oracle/lib/jobs.ts +9 -5
  7. package/extensions/oracle/lib/runtime.ts +107 -32
  8. package/extensions/oracle/lib/tools.ts +138 -12
  9. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  10. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  11. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  12. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  13. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  14. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  15. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  16. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  17. package/extensions/oracle/worker/run-job.mjs +107 -25
  18. package/package.json +40 -11
  19. package/platform-smoke.config.mjs +59 -0
  20. package/scripts/oracle-real-smoke.mjs +497 -0
  21. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  22. package/scripts/platform-smoke/artifacts.mjs +87 -0
  23. package/scripts/platform-smoke/assertions.mjs +34 -0
  24. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  25. package/scripts/platform-smoke/doctor.mjs +239 -0
  26. package/scripts/platform-smoke/invariants.mjs +108 -0
  27. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  28. package/scripts/platform-smoke/targets.mjs +434 -0
  29. package/scripts/platform-smoke.mjs +149 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.5 - 2026-06-02
6
+
7
+ ### Added
8
+ - added a Crabbox platform release smoke gate for macOS, Ubuntu, and Windows native with doctor-first validation, packed-install real-extension proof, stop evidence, and platform artifacts
9
+ - added canonical workflow docs and scripts for everyday local validation, focused platform-sensitive runs, and full release/publish smoke coverage
10
+
11
+ ### Changed
12
+ - made the default packed real smoke deterministic by installing the packed package into a clean pi project and invoking the installed `oracle_submit` tool path without waiting on a model-agent turn
13
+ - made Windows native a supported package OS and moved reusable Windows smoke dependencies into the Parallels template/snapshot workflow
14
+
15
+ ### Fixed
16
+ - hardened Windows archive/process/profile handling, including taskkill cleanup, executable resolution, tar/zstd archive behavior, path safety, and auth-bootstrap absolute-path checks
17
+ - removed production `as unknown as` casts and switched Linux/Windows runtime profile copies to Node recursive copy instead of relying on POSIX `cp`
18
+
19
+ ### Validation
20
+ - verified `npm run verify:oracle`, `git diff --check`, and `npm run smoke:platform:all` across macOS, Ubuntu, and Windows native before release
21
+
22
+ ## 0.7.4 - 2026-05-28
23
+
24
+ ### Changed
25
+ - updated the local pi development baseline to `@earendil-works/pi-coding-agent` / `@earendil-works/pi-ai` `0.77.0` and regenerated the npm lockfile
26
+ - kept pi runtime packages as optional wildcard peers and removed the Node.js engine upper bound so future pi releases are not blocked at install time
27
+
28
+ ### Compatibility
29
+ - reviewed the pi `0.77.0` changelog and package guidance; the oracle extension remains compatible with current extension lifecycle and package install/update behavior
30
+
5
31
  ## 0.7.3 - 2026-05-27
6
32
 
7
33
  ### Changed
@@ -57,6 +83,7 @@
57
83
 
58
84
  ### Changed
59
85
  - made `/oracle-auth` success and failure output easier to scan, with compact source summaries and source-specific troubleshooting for configured Chromium cookie sources
86
+ - expanded package support to Linux, using ordinary profile copies off macOS and documenting `@steipete/sweet-cookie`'s Linux keyring/password options for Chromium cookie import
60
87
  - tightened README quickstart/command wording around preflight-first `/oracle` behavior, context-rich archive selection, cleanup retention, and preset defaults
61
88
  - added the resolved oracle model preset snapshot to `oracle_submit` queued/dispatched output so agents can see what preset will run
62
89
  - clarified `oracle_preflight` output so users can see that it validates the persisted pi session, local config, and ChatGPT auth seed created by `oracle_auth`
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `pi-oracle` lets a `pi` agent send hard, long-running work to ChatGPT.com or Grok through the web app, with repo archives, background execution, saved results, and a best-effort wake-up back into `pi` when the answer is ready.
4
4
 
5
- > Status: experimental public beta. Validated primarily on macOS with Google Chrome/Chromium and `pi` 0.65.0+. Normal oracle jobs run in an isolated browser profile, not your active browser window.
5
+ > Status: experimental public beta. Validated on macOS, Linux, and Windows native with Chromium-family browsers and the current pi package baseline. Pi-bundled runtime packages are optional wildcard peers so npm peer ranges do not block users from trying newer pi releases, though runtime behavior is only verified against the tested baseline until a follow-up package release confirms it. Normal oracle jobs run in an isolated browser profile, not your active browser window.
6
6
 
7
7
  ## What a successful run looks like
8
8
 
@@ -37,7 +37,7 @@ Do not use it for:
37
37
 
38
38
  - short local coding tasks that `pi` can handle directly
39
39
  - projects that must never be uploaded to ChatGPT.com, Grok, or another configured web provider
40
- - non-macOS environments or machines without the required local browser/tooling
40
+ - machines outside the currently supported local browser/tooling setup
41
41
 
42
42
  ## Problem it solves
43
43
 
@@ -75,13 +75,15 @@ pi install https://github.com/fitchmultz/pi-oracle
75
75
 
76
76
  You need:
77
77
 
78
- - macOS
78
+ - macOS, Linux, or Windows native
79
79
  - Node.js 22 or newer
80
80
  - `pi` 0.65.0 or newer
81
- - Google Chrome or another Chromium-family browser
81
+ - Google Chrome/Chromium or another Chromium-family browser
82
82
  - ChatGPT or Grok already signed in to the configured local browser profile for the provider you plan to use
83
83
  - `agent-browser`, `tar`, and `zstd` available on the machine
84
+ - on macOS APFS clone mode, `cp` available on PATH or via `PI_ORACLE_CP_PATH`; Linux/Windows runtime profile copies use Node's recursive copy
84
85
  - a normal persisted `pi` session, not `pi --no-session`
86
+ - on Linux, encrypted Chromium cookies may also require `secret-tool` (GNOME/libsecret) or `kwallet-query` + `dbus-send` (KDE), unless a Chrome/Brave safe-storage password override is set for the auth run
85
87
 
86
88
  ### 3. Sync provider auth once
87
89
 
@@ -202,11 +204,23 @@ Notes:
202
204
  - When an agent is unsure which oracle preset fits, it should omit `preset` and use the configured default model instead of asking by default. If the prompt says to use Grok, it should pass `provider: "grok"` to `oracle_submit`.
203
205
  - You usually do not need browser paths unless auto-detection fails.
204
206
 
207
+ ### Linux cookie import notes
208
+
209
+ `/oracle-auth` delegates the default cookie read to `@steipete/sweet-cookie`'s Linux Chrome/Chromium backend. The packaged default auto-detects existing Google Chrome, Chromium, Chromium Browser, or Brave profile roots under `${XDG_CONFIG_HOME:-~/.config}` and passes non-Google roots as absolute profile paths so the correct cookie DB is read. Set `auth.chromeProfile` to another profile name, a profile directory, or a `Cookies` DB path when needed, and leave `auth.chromiumKeychain` unset on Linux.
210
+
211
+ Sweet Cookie's Linux encrypted-cookie handling is controlled outside pi-oracle:
212
+
213
+ - `SWEET_COOKIE_LINUX_KEYRING=gnome|kwallet|basic` selects GNOME/libsecret, KDE KWallet, or no keyring probing.
214
+ - GNOME probing shells out to `secret-tool`; KDE probing shells out to `kwallet-query` and `dbus-send`.
215
+ - `SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD` and `SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD` bypass keyring probing when you already know the browser safe-storage password.
216
+
217
+ Do not put safe-storage passwords in project config or persistent shell startup files. Prefer keyring helpers when possible; if you use an environment override for one `/oracle-auth` run, pi-oracle scrubs it before launching browser/helper subprocesses after cookie import.
218
+
205
219
  ### Custom Chromium cookie sources
206
220
 
207
- Use this only for a Chromium-family browser that the default cookie importer cannot read.
221
+ Most Chrome/Chromium-compatible browsers should work through Sweet Cookie's default Chrome backend when `auth.chromeProfile` points at the right profile or cookie DB. pi-oracle does not currently select Sweet Cookie's Edge or Firefox backends. The `auth.chromiumKeychain` alternate path is macOS-only and is intended for a Chromium-family browser that is not one of Sweet Cookie's built-in Chrome/Brave/Arc/Chromium targets or otherwise cannot import cookies without dependency patching.
208
222
 
209
- Before running `/oracle-auth` with this path:
223
+ Before running `/oracle-auth` with this macOS path:
210
224
 
211
225
  1. Log into ChatGPT or Grok in the target browser profile, depending on `defaults.provider`.
212
226
  2. Fully quit the browser so its `Cookies` database is stable.
@@ -214,7 +228,7 @@ Before running `/oracle-auth` with this path:
214
228
  4. Find the browser's macOS Keychain safe-storage item account and service name.
215
229
  5. Configure all of `browser.executablePath`, `auth.chromeCookiePath`, and `auth.chromiumKeychain` in `~/.pi/agent/extensions/oracle.json`.
216
230
 
217
- Example Helium config:
231
+ Example macOS Helium config:
218
232
 
219
233
  ```json
220
234
  {
@@ -233,7 +247,9 @@ Example Helium config:
233
247
  }
234
248
  ```
235
249
 
236
- `auth.chromeCookiePath` must be paired with `auth.chromiumKeychain`; partial config is rejected so oracle does not silently fall back to another browser source.
250
+ `auth.chromeCookiePath` remains the cookie database path for backward compatibility. On macOS, `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 on macOS, `/oracle-auth` uses pi-oracle's repo-owned generic Chromium cookie reader instead of patching `@steipete/sweet-cookie` internals. On Linux, `auth.chromiumKeychain` is rejected; use Sweet Cookie's Linux keyring/password environment options instead.
251
+
252
+ 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`. On Linux, inspect `/oracle-auth` diagnostics for Sweet Cookie warnings about `secret-tool`, `kwallet-query`, or safe-storage password overrides.
237
253
 
238
254
  ## Available providers and presets
239
255
 
@@ -279,19 +295,19 @@ Review the code and design docs before using it with private or regulated materi
279
295
 
280
296
  ## Current limits
281
297
 
282
- - Experimental public beta, validated primarily on macOS.
298
+ - Experimental public beta, validated on macOS, Linux, and Windows native with Chromium-family browsers.
283
299
  - Provider UI, auth, model controls, and artifact download behavior can drift.
284
300
  - Archive uploads are capped at 250 MiB for ChatGPT and 200 MiB for Grok after default exclusions and automatic whole-repo pruning.
285
301
  - A real ChatGPT or Grok web session is required for the provider you use.
286
302
  - The README currently uses command-level proof and design docs; no public screenshot or demo GIF is checked into the repo.
287
- - Production hardening should keep focusing on UI drift detection, auth recovery, artifact capture, and environment diagnostics.
303
+ - Production hardening should keep focusing on UI drift detection, auth recovery, artifact capture, platform compatibility, and environment diagnostics.
288
304
 
289
305
  ## Troubleshooting
290
306
 
291
307
  ### `/oracle-auth` fails or says login is required
292
308
 
293
309
  - Make sure the selected provider works in the same local browser profile you configured.
294
- - 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.
310
+ - For custom Chromium cookie sources, confirm `auth.chromeCookiePath` points at that profile's `Cookies` DB. On macOS, also confirm `auth.chromiumKeychain.services` names the browser's safe-storage Keychain service. On Linux, leave `auth.chromiumKeychain` unset and use Sweet Cookie's `SWEET_COOKIE_LINUX_KEYRING`, `SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD`, or `SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD` options for encrypted Chrome/Chromium/Brave cookies.
295
311
  - Re-run `/oracle-auth`.
296
312
  - Agent callers can use `oracle_auth({})` once before retrying a stale-auth oracle submission.
297
313
  - If the provider is half-logged-in or challenge flow state looks weird, finish the login/challenge in the headed auth browser and retry.
@@ -304,7 +320,7 @@ This usually means the cookie import worked but the source cookies are not the a
304
320
  2. Confirm the selected provider works there without logging in again.
305
321
  3. Quit the browser fully so its `Cookies` DB is stable.
306
322
  4. Confirm `auth.chromeCookiePath` points at that exact profile's `Cookies` DB.
307
- 5. Confirm `auth.chromiumKeychain.services` names the browser's safe-storage Keychain service for that DB.
323
+ 5. On macOS, confirm `auth.chromiumKeychain.services` names the browser's safe-storage Keychain service for that DB. On Linux, confirm the relevant Sweet Cookie keyring helper or Chrome/Brave safe-storage password override is available.
308
324
  6. Re-run `/oracle-auth`.
309
325
 
310
326
  ### You hit a challenge or verification page
@@ -330,14 +346,14 @@ This usually means the cookie import worked but the source cookies are not the a
330
346
  - The command returns a `Retry after ...` timestamp when that guard is active.
331
347
  - Wait until that time, then rerun `/oracle-clean <job-id|all>`.
332
348
 
333
- ### `agent-browser`, `tar`, or `zstd` is missing
349
+ ### A local dependency like `agent-browser`, `tar`, or `zstd` is missing
334
350
 
335
- Install the missing local dependency and rerun the command.
351
+ Install the missing local dependency and rerun the command. On macOS APFS clone mode, `cp` must also be available on PATH or configured with `PI_ORACLE_CP_PATH`; Linux and Windows profile copies use Node's recursive copy.
336
352
 
337
353
  ### Auto-detection picked the wrong browser profile
338
354
 
339
355
  - Set `auth.chromeProfile` in `~/.pi/agent/extensions/oracle.json`.
340
- - For custom Chromium cookie sources, set `auth.chromeCookiePath` to the exact profile `Cookies` DB and pair it with `auth.chromiumKeychain`.
356
+ - For custom Chromium cookie sources, set `auth.chromeCookiePath` to the exact profile `Cookies` DB. Pair it with `auth.chromiumKeychain` only on macOS; on Linux, rely on Sweet Cookie's keyring/password environment options.
341
357
  - Re-run `/oracle-auth`.
342
358
 
343
359
  ### You want more details about a failed run
@@ -350,6 +366,7 @@ Useful local checks:
350
366
 
351
367
  ```bash
352
368
  npm run check:oracle-extension
369
+ npm run check:platform-smoke
353
370
  npm run typecheck
354
371
  npm run typecheck:worker-helpers
355
372
  npm run sanity:oracle
@@ -358,9 +375,25 @@ npm test
358
375
  npm run verify:oracle
359
376
  ```
360
377
 
361
- `npm publish` is guarded by `prepublishOnly`, which runs `npm run verify:oracle`.
378
+ `npm publish` is guarded by `prepublishOnly`, which runs `npm run release:check`. That release gate requires doctor-first macOS, Ubuntu, and Windows native Crabbox evidence. The required Crabbox runtime suite uses packed-install proof, not source-tree `pi -e` loading.
379
+
380
+ Use the narrowest validation workflow that proves the change:
381
+
382
+ | Situation | Command(s) |
383
+ | --- | --- |
384
+ | Everyday local iteration | `npm run verify:oracle` |
385
+ | Platform-sensitive changes | `npm run smoke:platform:doctor`, then a focused `node scripts/platform-smoke.mjs run --target <target> --suite <suite>` |
386
+ | Publish/release proof | `npm run smoke:platform:all` |
387
+
388
+ For macOS, Ubuntu, and Windows native package/build plus packed runtime validation, use [`docs/platform-smoke.md`](docs/platform-smoke.md). The full release proof is:
389
+
390
+ ```bash
391
+ npm run smoke:platform:all
392
+ ```
393
+
394
+ The real runtime suite defaults to deterministic installed-tool execution so platform proof stays bounded. Provider/model defaults remain `zai/glm-5.1` for doctor/config and for optional model-agent debugging; override with `PI_ORACLE_REAL_TEST_PROVIDER` and `PI_ORACLE_REAL_TEST_MODEL` when needed. For inner-loop source loading only, use `npm run smoke:real:source`; it is not release proof. Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` only when debugging the slower model-agent path. The optional second real-agent negative symlink check is opt-in via `PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK=1`; `npm run sanity:oracle` covers archive/symlink rejection by default without adding another model-agent turn to the platform release gate.
362
395
 
363
- For end-to-end local-extension smoke testing, use [`docs/ORACLE_ISOLATED_PI_VALIDATION.md`](docs/ORACLE_ISOLATED_PI_VALIDATION.md). That workflow launches isolated `pi` sessions against this checkout and uses `instant` or `thinking_light`, as required by the project validation policy.
396
+ For manual end-to-end local-extension smoke testing, use [`docs/ORACLE_ISOLATED_PI_VALIDATION.md`](docs/ORACLE_ISOLATED_PI_VALIDATION.md). That workflow launches isolated `pi` coding-agent sessions against this checkout and uses `instant` or `thinking_light`, as required by the project validation policy.
364
397
 
365
398
  ## Project map
366
399
 
@@ -373,6 +406,7 @@ For end-to-end local-extension smoke testing, use [`docs/ORACLE_ISOLATED_PI_VALI
373
406
  | [`prompts/oracle.md`](prompts/oracle.md) | `/oracle` prompt-template workflow |
374
407
  | [`prompts/oracle-followup.md`](prompts/oracle-followup.md) | `/oracle-followup` prompt-template workflow |
375
408
  | `scripts/oracle-sanity-*` | Local sanity and archive-safety checks |
409
+ | `scripts/platform-smoke*` | Crabbox macOS, Ubuntu, and Windows release smoke gate |
376
410
  | [`docs/ORACLE_DESIGN.md`](docs/ORACLE_DESIGN.md) | Architecture, lifecycle, queueing, persistence, recovery behavior |
377
411
  | [`docs/ORACLE_ISOLATED_PI_VALIDATION.md`](docs/ORACLE_ISOLATED_PI_VALIDATION.md) | Repeatable isolated `pi` validation workflow |
378
412
  | [`docs/ORACLE_RECOVERY_DRILL.md`](docs/ORACLE_RECOVERY_DRILL.md) | Safe expired-auth recovery drill |
@@ -246,7 +246,7 @@ Browser/auth settings are global-only because they control local privileged brow
246
246
  "authSeedProfileDir": "<absolute path to oracle auth seed profile>",
247
247
  "runtimeProfilesDir": "<absolute path to oracle runtime profiles dir>",
248
248
  "maxConcurrentJobs": 2,
249
- "cloneStrategy": "apfs-clone",
249
+ "cloneStrategy": "copy",
250
250
  "chatUrl": "https://chatgpt.com/",
251
251
  "authUrl": "https://chatgpt.com/auth/login",
252
252
  "runMode": "headless",
@@ -260,7 +260,7 @@ Browser/auth settings are global-only because they control local privileged brow
260
260
  "chromeProfile": "<optional Chrome/Chromium profile name>",
261
261
  "chromeCookiePath": "<optional absolute path to Chromium Cookies DB>",
262
262
  "chromiumKeychain": {
263
- "account": "<macOS Keychain account for non-built-in Chromium browsers>",
263
+ "account": "<macOS-only Keychain account for non-built-in Chromium browsers>",
264
264
  "services": ["<safe-storage service name>"],
265
265
  "label": "<optional human-readable label>"
266
266
  }
@@ -282,9 +282,13 @@ Browser/auth settings are global-only because they control local privileged brow
282
282
  }
283
283
  ```
284
284
 
285
- `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.
285
+ `browser.cloneStrategy` defaults to `apfs-clone` on macOS and `copy` on Linux/Windows. macOS APFS clone mode uses `cp -cR` and preflights `cp`; set `PI_ORACLE_CP_PATH` only when the default PATH lookup cannot find the intended copy executable. Linux and Windows runtime profile copies use Node's recursive copy instead of depending on POSIX `cp`.
286
286
 
287
- When both `auth.chromeCookiePath` and `auth.chromiumKeychain` are present, auth bootstrap:
287
+ The default `/oracle-auth` cookie importer delegates to `@steipete/sweet-cookie`'s Chrome/Chromium backend. On Linux, pi-oracle auto-detects existing Google Chrome, Chromium, Chromium Browser, or Brave profile roots under `${XDG_CONFIG_HOME:-~/.config}` and passes non-Google roots as absolute profile paths so Sweet Cookie reads the intended cookie DB. pi-oracle does not currently select Sweet Cookie's Edge or Firefox backends. Encrypted Linux Chromium cookies are handled by Sweet Cookie via `secret-tool`, `kwallet-query`/`dbus-send`, `SWEET_COOKIE_LINUX_KEYRING=gnome|kwallet|basic`, or the `SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD` / `SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD` overrides. Prefer keyring helpers over password environment variables; if a password override is used for `/oracle-auth`, pi-oracle scrubs it before launching browser/helper subprocesses after cookie import.
288
+
289
+ `auth.chromiumKeychain` is a macOS-only 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. On Linux, valid config should leave `auth.chromiumKeychain` unset and use Sweet Cookie's Linux keyring/password options instead.
290
+
291
+ When both `auth.chromeCookiePath` and `auth.chromiumKeychain` are present on macOS, auth bootstrap:
288
292
 
289
293
  1. reads the configured macOS Keychain safe-storage password using `account` and the ordered `services` list
290
294
  2. snapshots the Chromium `Cookies` DB plus `Cookies-wal` / `Cookies-shm` sidecars, tolerating sidecars that disappear while the browser is closing
@@ -292,7 +296,7 @@ When both `auth.chromeCookiePath` and `auth.chromiumKeychain` are present, auth
292
296
  4. dedupes duplicate cookie rows by keeping the first row after newest-expiry ordering
293
297
  5. filters importable provider auth cookies and seeds the isolated oracle auth profile
294
298
 
295
- Operational requirements for this path:
299
+ Operational requirements for this macOS-only path:
296
300
 
297
301
  - ChatGPT or Grok must already be logged in in the configured browser profile, depending on the provider being synced.
298
302
  - The target browser should be fully quit before `/oracle-auth` so the cookie DB snapshot is stable.
@@ -633,3 +637,6 @@ Recent proof points:
633
637
  - expired-auth drill post-repair success: `fa26a2a7-0057-4a21-b3e0-71c1d020facf`
634
638
  - successful multi-artifact completion: `b6b3599c-6b91-4315-adfa-8a83aa5eda9b`
635
639
  - repo-owned sanity harness: `npm run sanity:oracle`
640
+ - real installed-extension smoke source of truth: `scripts/oracle-real-smoke.mjs`; required release proof runs packed-install mode (`npm run smoke:real:packed`) and executes installed-package `oracle_submit` deterministically, with optional slower model-agent debugging via `PI_ORACLE_REAL_TEST_MODEL_AGENT=1`; source mode (`npm run smoke:real:source`) is inner-loop/debug only
641
+ - macOS, Ubuntu, and Windows native package/build/runtime smoke source of truth: `docs/platform-smoke.md`; use `npm run verify:oracle` for everyday local iteration, `npm run smoke:platform:doctor` plus a focused target/suite run for platform-sensitive changes, and `npm run smoke:platform:all` for doctor-first packed-install Crabbox release evidence
642
+ - release gate: `npm run release:check`, also used by `prepublishOnly`, combines static verification and all required Crabbox platform smokes
@@ -0,0 +1,153 @@
1
+ # pi-oracle Crabbox platform smoke
2
+
3
+ `pi-oracle` uses Crabbox for the local release-blocking platform gate. The gate runs on macOS, Ubuntu Linux, and native Windows and is meant to catch broken package installs, platform assumptions, and real `pi` tool-call failures before push or publish.
4
+
5
+ ## Source of truth
6
+
7
+ - Config: [`../platform-smoke.config.mjs`](../platform-smoke.config.mjs)
8
+ - CLI: [`../scripts/platform-smoke.mjs`](../scripts/platform-smoke.mjs)
9
+ - Target runner: [`../scripts/platform-smoke/targets.mjs`](../scripts/platform-smoke/targets.mjs)
10
+ - Windows build script: [`../scripts/platform-smoke/platform-build-windows.ps1`](../scripts/platform-smoke/platform-build-windows.ps1)
11
+ - Real runtime smoke: [`../scripts/oracle-real-smoke.mjs`](../scripts/oracle-real-smoke.mjs)
12
+ - Artifact root: `.artifacts/platform-smoke/` (gitignored)
13
+
14
+ Required targets: `macos`, `ubuntu`, `windows-native`.
15
+ Required suites: `platform-build`, `real-extension`.
16
+ Crabbox baseline: `0.24.0` or newer.
17
+
18
+ ## Required local setup
19
+
20
+ Install Crabbox with Homebrew and keep it on `PATH`:
21
+
22
+ ```bash
23
+ brew install openclaw/tap/crabbox
24
+ crabbox --version
25
+ crabbox providers
26
+ ```
27
+
28
+ `PI_ORACLE_SMOKE_CRABBOX` is optional and only needed to override the binary path.
29
+
30
+ Target setup:
31
+
32
+ - macOS: Remote Login enabled; noninteractive `ssh $USER@localhost` works; `node`, `npm`, `git`, `tar`, `rsync`, `zstd`, and `agent-browser` are on the SSH PATH.
33
+ - Ubuntu: Docker is running and the configured image (`PI_ORACLE_SMOKE_UBUNTU_IMAGE`, default `pi-oracle-platform-smoke:node24`) has `node`, `npm`, `git`, `tar`, `rsync`, `zstd`, and `agent-browser` on PATH. Build the local image when needed with `docker build -t pi-oracle-platform-smoke:node24 -f scripts/platform-smoke/Dockerfile.ubuntu .`.
34
+ - Windows native: Parallels has stopped source VM `pi-extension-windows-template` and the configured power-off snapshot (`crabbox-ready` by default for this repo). The template must have OpenSSH, PowerShell, Git, Node/npm, `tar`, `zstd`, and `agent-browser` on PATH. Do not bake API keys, browser sessions, project checkouts, `.pi` state, artifacts, or secrets into the template.
35
+
36
+ Real runtime suite auth:
37
+
38
+ - Default deterministic installed-tool smoke does not require provider API keys.
39
+ - Provider/model defaults remain `zai/glm-5.1` for optional model-agent debugging.
40
+ - Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` to run the slower model-agent path; then the provider auth env is required (`ZAI_API_KEY` by default, reported only as present/redacted).
41
+ - Override with `PI_ORACLE_REAL_TEST_PROVIDER` and `PI_ORACLE_REAL_TEST_MODEL`; auth variable names live in `platform-smoke.config.mjs`.
42
+
43
+ ## Canonical validation workflows
44
+
45
+ Use the narrowest workflow that proves the change. Do not run the full platform matrix for ordinary edits when the local gate and cheap invariants prove the change.
46
+
47
+ | Situation | Canonical command(s) | What it proves |
48
+ | --- | --- | --- |
49
+ | Everyday local iteration | `npm run verify:oracle` | Syntax, bundle, platform-smoke invariants, type checks, oracle sanity, and package dry-run pass locally. |
50
+ | Platform-sensitive change | `npm run smoke:platform:doctor`, then `node scripts/platform-smoke.mjs run --target <target> --suite <suite>` | Target setup is ready and the affected platform/suite works without paying for unrelated targets. |
51
+ | Publish/release proof | `npm run smoke:platform:all` | Doctor-first packed-install proof passes on every required target and suite. |
52
+
53
+ Platform-sensitive changes include archive behavior, process cleanup, runtime/browser profile handling, package metadata, Crabbox harness code, or anything that may differ across macOS/Linux/Windows.
54
+
55
+ ## Commands
56
+
57
+ Doctor is mandatory before the full release matrix. The canonical all-target release command enforces that:
58
+
59
+ ```bash
60
+ npm run smoke:platform:all
61
+ ```
62
+
63
+ Focused commands:
64
+
65
+ ```bash
66
+ npm run smoke:platform:doctor
67
+ npm run smoke:platform:macos
68
+ npm run smoke:platform:ubuntu
69
+ npm run smoke:platform:windows-native
70
+ node scripts/platform-smoke.mjs run --target windows-native --suite real-extension
71
+ ```
72
+
73
+ Release check:
74
+
75
+ ```bash
76
+ npm run release:check
77
+ ```
78
+
79
+ `prepublishOnly` runs `npm run release:check`.
80
+
81
+ ## What `platform-build` proves
82
+
83
+ On each required target, `platform-build`:
84
+
85
+ 1. checks Node major version against `nodeValidationMajor`;
86
+ 2. runs `npm ci`;
87
+ 3. requires target tools (`zstd`, `agent-browser`) to already be available from target setup;
88
+ 4. runs `npm run verify:oracle:platform`, the platform-focused gate for syntax, platform-smoke invariants, real-smoke script syntax, platform-sensitive oracle sanity coverage, and package dry-run;
89
+ 5. runs `npm pack`;
90
+ 6. creates a fresh target-local pi project;
91
+ 7. runs `npm install --no-save <packed tarball>`;
92
+ 8. runs `pi install -l ./node_modules/pi-oracle`;
93
+ 9. runs `pi list`;
94
+ 10. asserts the installed package came from `node_modules/pi-oracle` and did not use `pi -e` / source-extension shortcuts.
95
+
96
+ ## What `real-extension` proves
97
+
98
+ `real-extension` is required release proof. It runs `npm run smoke:real:packed` on each target, which:
99
+
100
+ 1. packs this checkout with `npm pack`;
101
+ 2. installs the tarball into a clean pi project;
102
+ 3. runs `pi install -l ./node_modules/pi-oracle`;
103
+ 4. asserts `pi list` shows the packed install path;
104
+ 5. executes `oracle_submit` from the installed package path, not source `pi -e`;
105
+ 6. asserts whole-project archive creation and default exclusions.
106
+
107
+ The default runtime suite executes the installed tool directly so platform proof is deterministic and bounded instead of waiting on a model turn. Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` only when you specifically need to debug the slower model-agent path. Symlink escape rejection and other negative archive cases are covered by `npm run sanity:oracle`; the optional second-agent negative check is available with `PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK=1` when debugging that path.
108
+
109
+ For inner-loop/debug only, use:
110
+
111
+ ```bash
112
+ npm run smoke:real:source
113
+ ```
114
+
115
+ That source-mode smoke loads `extensions/oracle/index.ts` with `pi --no-extensions -e`; it is useful while developing but is not release proof.
116
+
117
+ ## Artifacts
118
+
119
+ Each suite writes reviewable evidence under:
120
+
121
+ ```text
122
+ .artifacts/platform-smoke/<run-id>/<target>/<suite>/
123
+ summary.json
124
+ target.json
125
+ suite.json
126
+ command.txt
127
+ exit-code.txt
128
+ crabbox.stdout.txt
129
+ crabbox.stderr.txt
130
+ crabbox.timing.json
131
+ crabbox.stop.stdout.txt
132
+ crabbox.stop.stderr.txt
133
+ crabbox.stop.exit-code.txt
134
+ assertions.json
135
+ artifact-manifest.json
136
+ failures.md # only on failure
137
+ ```
138
+
139
+ `platform-build` also writes packed install extracts (`packed-tarball.txt`, `packed-node-install.*`, `pi-install.*`, `pi-list.*`). Passing suites require `summary.ok === true`, `assertions.ok === true`, and `artifact-manifest.missing.length === 0`.
140
+
141
+ Artifacts are local evidence only. Do not commit or share them without redaction. Secret scans fail on bearer/API-key/cookie-like values.
142
+
143
+ ## Windows template maintenance
144
+
145
+ When Windows lacks a reusable tool such as `zstd` or `agent-browser`, update `pi-extension-windows-template` rather than adding a per-run installer:
146
+
147
+ 1. boot `pi-extension-windows-template`;
148
+ 2. install/update the tool globally without secrets;
149
+ 3. verify from a fresh SSH session: `node --version`, `npm --version`, `git --version`, `tar --version`, `zstd --version`, `agent-browser --version`;
150
+ 4. remove downloads, caches, checkouts, `.pi`, `.artifacts`, `.debug`, and secrets;
151
+ 5. shut down cleanly;
152
+ 6. create/promote the configured power-off snapshot;
153
+ 7. clean stale clones/leases.
@@ -8,6 +8,15 @@ import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
10
10
  import { isAbsolute, join, normalize } from "node:path";
11
+ import {
12
+ assertNotKnownBrowserUserDataPath,
13
+ chromeUserAgentPlatformToken,
14
+ chromiumKeychainSupportedOnPlatform,
15
+ defaultCloneStrategyForPlatform,
16
+ detectDefaultBrowserProfileSource,
17
+ detectDefaultLinuxChromeExecutablePath,
18
+ sweetCookieSafeStoragePasswordScrubbedEnv,
19
+ } from "../shared/browser-profile-helpers.mjs";
11
20
  import { getProjectId } from "./runtime.js";
12
21
 
13
22
  export const ORACLE_PROVIDERS = ["chatgpt", "grok"] as const;
@@ -225,7 +234,6 @@ export type OracleCloneStrategy = (typeof CLONE_STRATEGIES)[number];
225
234
  const ALLOWED_CHATGPT_ORIGINS = new Set(["https://chatgpt.com", "https://chat.openai.com"]);
226
235
  const PROJECT_OVERRIDE_KEYS = new Set(["defaults", "worker", "poller", "artifacts", "cleanup"]);
227
236
  const DEFAULT_MAC_CHROME_EXECUTABLE = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
228
- const DEFAULT_MAC_CHROME_USER_DATA_DIR = join(homedir(), "Library", "Application Support", "Google", "Chrome");
229
237
 
230
238
  export interface OracleConfig {
231
239
  defaults: {
@@ -274,37 +282,36 @@ export interface OracleConfig {
274
282
  }
275
283
 
276
284
  function detectDefaultChromeExecutablePath(): string | undefined {
277
- return existsSync(DEFAULT_MAC_CHROME_EXECUTABLE) ? DEFAULT_MAC_CHROME_EXECUTABLE : undefined;
285
+ if (process.platform === "darwin") {
286
+ return existsSync(DEFAULT_MAC_CHROME_EXECUTABLE) ? DEFAULT_MAC_CHROME_EXECUTABLE : undefined;
287
+ }
288
+ if (process.platform === "linux") {
289
+ return detectDefaultLinuxChromeExecutablePath();
290
+ }
291
+ return undefined;
278
292
  }
279
293
 
280
294
  function detectDefaultChromeUserAgent(executablePath: string | undefined): string | undefined {
281
295
  if (!executablePath) return undefined;
296
+ // Linux executable discovery is PATH-based, so avoid executing that discovered
297
+ // binary during config module initialization just to derive a user agent.
298
+ if (process.platform === "linux") return undefined;
299
+ const platformToken = chromeUserAgentPlatformToken(process.platform);
300
+ if (!platformToken) return undefined;
282
301
  try {
283
- const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8" }).trim();
302
+ const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv() }).trim();
284
303
  const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+\.\d+)/);
285
304
  if (!versionMatch) return undefined;
286
- return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionMatch[1]} Safari/537.36`;
305
+ return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionMatch[1]} Safari/537.36`;
287
306
  } catch {
288
307
  return undefined;
289
308
  }
290
309
  }
291
310
 
292
- function detectDefaultChromeProfileName(): string {
293
- const localStatePath = join(DEFAULT_MAC_CHROME_USER_DATA_DIR, "Local State");
294
- if (!existsSync(localStatePath)) return "Default";
295
- try {
296
- const localState = JSON.parse(readFileSync(localStatePath, "utf8")) as { profile?: { last_used?: string } };
297
- const lastUsed = localState?.profile?.last_used;
298
- return typeof lastUsed === "string" && lastUsed.trim() ? lastUsed.trim() : "Default";
299
- } catch {
300
- return "Default";
301
- }
302
- }
303
-
304
311
  const detectedChromeExecutablePath = detectDefaultChromeExecutablePath();
305
312
  const detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecutablePath);
306
313
  const agentExtensionsDir = join(getAgentDir(), "extensions");
307
- const detectedChromeProfileName = detectDefaultChromeProfileName();
314
+ const detectedChromeProfileName = detectDefaultBrowserProfileSource(process.platform);
308
315
 
309
316
  export interface OracleConfigLoadDetails {
310
317
  agentDir: string;
@@ -367,7 +374,7 @@ export const DEFAULT_CONFIG: OracleConfig = {
367
374
  authSeedProfileDir: join(agentExtensionsDir, "oracle-auth-seed-profile"),
368
375
  runtimeProfilesDir: join(agentExtensionsDir, "oracle-runtime-profiles"),
369
376
  maxConcurrentJobs: 2,
370
- cloneStrategy: "apfs-clone",
377
+ cloneStrategy: defaultCloneStrategyForPlatform(process.platform),
371
378
  chatUrl: "https://chatgpt.com/",
372
379
  authUrl: "https://chatgpt.com/auth/login",
373
380
  runMode: "headless",
@@ -452,18 +459,28 @@ function expectAbsoluteNormalizedPath(value: unknown, path: string): string {
452
459
  return normalize(expanded);
453
460
  }
454
461
 
455
- function expectSafeProfilePath(pathValue: string, path: string): string {
462
+ function expectSafeProfilePath(
463
+ pathValue: string,
464
+ path: string,
465
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string },
466
+ ): string {
456
467
  if (pathValue === "/" || pathValue === homedir()) {
457
468
  throw new Error(`Invalid oracle config: ${path} points to an unsafe directory`);
458
469
  }
459
- if (pathValue === DEFAULT_MAC_CHROME_USER_DATA_DIR || pathValue.startsWith(`${DEFAULT_MAC_CHROME_USER_DATA_DIR}/`)) {
460
- throw new Error(`Invalid oracle config: ${path} must not point into the real Chrome user-data directory`);
470
+ try {
471
+ assertNotKnownBrowserUserDataPath(pathValue, `Invalid oracle config: ${path}`, { cookieSources });
472
+ } catch (error) {
473
+ throw new Error(error instanceof Error ? error.message : String(error));
461
474
  }
462
475
  return pathValue;
463
476
  }
464
477
 
465
- function expectSafeProfileDir(value: unknown, path: string): string {
466
- return expectSafeProfilePath(expectAbsoluteNormalizedPath(value, path), path);
478
+ function expectSafeProfileDir(
479
+ value: unknown,
480
+ path: string,
481
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string },
482
+ ): string {
483
+ return expectSafeProfilePath(expectAbsoluteNormalizedPath(value, path), path, cookieSources);
467
484
  }
468
485
 
469
486
  function expectBoolean(value: unknown, path: string): boolean {
@@ -584,17 +601,26 @@ function validateOracleConfig(value: unknown): OracleConfig {
584
601
  const artifacts = expectObject(root.artifacts, "artifacts");
585
602
  const cleanup = expectObject(root.cleanup, "cleanup");
586
603
 
587
- const authSeedProfileDir = expectSafeProfileDir(browser.authSeedProfileDir, "browser.authSeedProfileDir");
588
- const runtimeProfilesDir = expectSafeProfileDir(browser.runtimeProfilesDir, "browser.runtimeProfilesDir");
604
+ const chromeProfile = expectString(auth.chromeProfile, "auth.chromeProfile");
605
+ const chromeCookiePath = expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath");
606
+ const cookieSources = { chromeProfile, chromeCookiePath };
607
+ const authSeedProfileDir = expectSafeProfileDir(browser.authSeedProfileDir, "browser.authSeedProfileDir", cookieSources);
608
+ const runtimeProfilesDir = expectSafeProfileDir(browser.runtimeProfilesDir, "browser.runtimeProfilesDir", cookieSources);
589
609
  if (runtimeProfilesDir === authSeedProfileDir || runtimeProfilesDir.startsWith(`${authSeedProfileDir}/`)) {
590
610
  throw new Error("Invalid oracle config: browser.runtimeProfilesDir must be separate from browser.authSeedProfileDir");
591
611
  }
592
612
 
593
- const chromeCookiePath = expectOptionalAbsoluteNormalizedPath(auth.chromeCookiePath, "auth.chromeCookiePath");
594
613
  const chromiumKeychain = expectOptionalChromiumKeychain(auth.chromiumKeychain, "auth.chromiumKeychain");
595
614
  if (chromiumKeychain !== undefined && chromeCookiePath === undefined) {
596
615
  throw new Error("Invalid oracle config: auth.chromiumKeychain requires auth.chromeCookiePath");
597
616
  }
617
+ if (chromiumKeychain !== undefined && !chromiumKeychainSupportedOnPlatform(process.platform)) {
618
+ throw new Error(
619
+ "Invalid oracle config: auth.chromiumKeychain is macOS-only. " +
620
+ "On Linux, set auth.chromeCookiePath/auth.chromeProfile without auth.chromiumKeychain and use @steipete/sweet-cookie's " +
621
+ "SWEET_COOKIE_LINUX_KEYRING, SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD, or SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD options for encrypted Chromium cookies.",
622
+ );
623
+ }
598
624
 
599
625
  return {
600
626
  defaults: {
@@ -618,7 +644,7 @@ function validateOracleConfig(value: unknown): OracleConfig {
618
644
  auth: {
619
645
  pollMs: expectInteger(auth.pollMs, "auth.pollMs", 100),
620
646
  bootstrapTimeoutMs: expectInteger(auth.bootstrapTimeoutMs, "auth.bootstrapTimeoutMs", 1000),
621
- chromeProfile: expectString(auth.chromeProfile, "auth.chromeProfile"),
647
+ chromeProfile,
622
648
  chromeCookiePath,
623
649
  chromiumKeychain,
624
650
  },
@@ -197,9 +197,12 @@ export interface OracleRuntimeAllocation {
197
197
  seedGeneration?: string;
198
198
  }
199
199
 
200
+ function hasSessionFileAccessor(value: unknown): value is { getSessionFile: () => string | undefined } {
201
+ return typeof value === "object" && value !== null && "getSessionFile" in value && typeof value.getSessionFile === "function";
202
+ }
203
+
200
204
  export function getSessionFile(ctx: ExtensionContext): string | undefined {
201
- const manager = ctx.sessionManager as unknown as { getSessionFile?: () => string | undefined };
202
- return manager.getSessionFile?.();
205
+ return hasSessionFileAccessor(ctx.sessionManager) ? ctx.sessionManager.getSessionFile() : undefined;
203
206
  }
204
207
 
205
208
  export function getOracleJobsDir(): string {
@@ -989,13 +992,14 @@ export function resolveArchiveInputs(cwd: string, files: string[]): { absolute:
989
992
  throw new Error("Archive input must use '.' exactly for a whole-repo archive");
990
993
  }
991
994
  const absolute = resolve(cwd, file);
992
- if (absolute === cwd && file !== ".") {
995
+ const relativeFromCwd = relativePath(cwd, absolute);
996
+ if (relativeFromCwd === "" && file !== ".") {
993
997
  throw new Error("Archive input must use '.' exactly for a whole-repo archive");
994
998
  }
995
- const relative = absolute.startsWith(`${cwd}/`) ? absolute.slice(cwd.length + 1) : absolute === cwd ? "." : "";
996
- if (!relative) {
999
+ if (relativeFromCwd && (relativeFromCwd === ".." || relativeFromCwd.startsWith(`..${sep}`) || isAbsolute(relativeFromCwd))) {
997
1000
  throw new Error(`Archive input must be inside the project cwd: ${file}`);
998
1001
  }
1002
+ const relative = relativeFromCwd === "" ? "." : relativeFromCwd.split(sep).join("/");
999
1003
  if (!existsSync(absolute)) {
1000
1004
  throw new Error(`Archive input does not exist: ${file}`);
1001
1005
  }