pi-oracle 0.7.20 → 0.7.22
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 +25 -0
- package/README.md +49 -377
- package/docs/ORACLE_DESIGN.md +5 -2
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +9 -7
- package/docs/development.md +99 -0
- package/docs/platform-smoke.md +3 -3
- package/docs/reference.md +331 -0
- package/extensions/oracle/index.ts +1 -0
- package/extensions/oracle/lib/poller.ts +3 -8
- package/extensions/oracle/lib/queue.ts +2 -4
- package/extensions/oracle/shared/browser-profile-helpers.d.mts +1 -0
- package/extensions/oracle/shared/browser-profile-helpers.mjs +32 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +65 -12
- package/extensions/oracle/worker/auth-flow-helpers.d.mts +2 -0
- package/extensions/oracle/worker/auth-flow-helpers.mjs +24 -3
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +4 -1
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +1 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +7 -3
- package/extensions/oracle/worker/chromium-cookie-source.mjs +50 -6
- package/extensions/oracle/worker/linux-chromium-cookie-source.d.mts +6 -0
- package/extensions/oracle/worker/linux-chromium-cookie-source.mjs +98 -0
- package/extensions/oracle/worker/run-job.mjs +72 -7
- package/package.json +15 -11
- package/platform-smoke.config.mjs +1 -22
- package/scripts/oracle-auth.test.mjs +164 -0
- package/scripts/oracle-chatgpt-preset-proof.mjs +19 -12
- package/scripts/oracle-cookie-import.test.mjs +204 -0
- package/scripts/oracle-model.test.mjs +87 -0
- package/scripts/oracle-real-smoke.mjs +30 -13
- package/scripts/platform-smoke/Dockerfile.ubuntu +3 -2
- package/scripts/platform-smoke/crabbox-runner.mjs +3 -5
- package/scripts/platform-smoke/doctor.mjs +9 -24
- package/scripts/platform-smoke/invariants.mjs +234 -22
- package/scripts/platform-smoke/platform-build-windows.ps1 +4 -16
- package/scripts/platform-smoke/targets.mjs +35 -27
- package/scripts/platform-smoke.mjs +2 -2
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { createCipheriv, createHash, pbkdf2Sync } from "node:crypto";
|
|
4
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
// Independent standard Chromium v24 SQLite/v11 fixture; the real auth worker
|
|
10
|
+
// reads/decrypts/filters it and replays into a fake browser, never a real one.
|
|
11
|
+
test("Linux auth imports native Chromium with its own key and preserves Chrome/Brave", {
|
|
12
|
+
skip: process.platform !== "linux", timeout: 90_000,
|
|
13
|
+
}, async (t) => {
|
|
14
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
15
|
+
const { detectDefaultLinuxCookieProfileSource } = await import("../extensions/oracle/shared/browser-profile-helpers.mjs");
|
|
16
|
+
const root = await mkdtemp(join(tmpdir(), "oracle-cookie-import-"));
|
|
17
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
18
|
+
const home = join(root, "home");
|
|
19
|
+
const configHome = join(home, ".config");
|
|
20
|
+
const bin = join(root, "bin");
|
|
21
|
+
const helperLog = join(root, "helpers.jsonl");
|
|
22
|
+
const browserLog = join(root, "browser.jsonl");
|
|
23
|
+
await mkdir(bin, { recursive: true });
|
|
24
|
+
const guard = `
|
|
25
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
26
|
+
import { basename } from 'node:path';
|
|
27
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
28
|
+
if (name.startsWith('SWEET_COOKIE_') && name.endsWith('_PASSWORD') && value) {
|
|
29
|
+
throw Error('safe-storage password leaked to subprocess');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
`;
|
|
34
|
+
const helper = `#!${process.execPath}
|
|
35
|
+
${guard}
|
|
36
|
+
const command = basename(process.argv[1]);
|
|
37
|
+
appendFileSync(process.env.HELPER_LOG, JSON.stringify({ command, args }) + '\\n');
|
|
38
|
+
const mode = process.env.FIXTURE_MODE;
|
|
39
|
+
if (mode === 'timeout') { process.on('SIGTERM', () => {}); setInterval(() => {}, 1000); }
|
|
40
|
+
else if (mode === 'failure') { console.error('fixture-key-do-not-log'); process.exit(1); }
|
|
41
|
+
else if (command === 'dbus-send') {
|
|
42
|
+
if (mode === 'wallet-fallback') process.exit(1);
|
|
43
|
+
console.log('"fixture-wallet"');
|
|
44
|
+
} else if (command === 'kwallet-query') {
|
|
45
|
+
if (mode === 'wallet-empty') console.log('failed to read password');
|
|
46
|
+
else if (args.join('|') === '--read-password|Chromium Safe Storage|--folder|Chromium Keys|' + (mode === 'wallet-fallback' ? 'kdewallet' : 'fixture-wallet')) console.log('fixture-chromium-key');
|
|
47
|
+
else process.exit(1);
|
|
48
|
+
} else {
|
|
49
|
+
if (mode === 'application' && args.includes('service')) process.exit(1);
|
|
50
|
+
const browser = args.includes('Chromium') || args.includes('chromium') ? 'chromium'
|
|
51
|
+
: args.includes('Brave') || args.includes('brave') ? 'brave' : 'chrome';
|
|
52
|
+
console.log('fixture-' + browser + '-key');
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
for (const command of ["secret-tool", "kwallet-query", "dbus-send"]) {
|
|
56
|
+
await writeFile(join(bin, command), helper, { mode: 0o700 });
|
|
57
|
+
}
|
|
58
|
+
await writeFile(join(bin, "agent-browser"), `#!${process.execPath}
|
|
59
|
+
${guard}
|
|
60
|
+
appendFileSync(process.env.BROWSER_LOG, JSON.stringify(args) + '\\n');
|
|
61
|
+
if (args.includes('--profile')) mkdirSync(args[args.indexOf('--profile') + 1], { recursive: true });
|
|
62
|
+
if (args.includes('status')) console.log(JSON.stringify({ data: { connected: true } }));
|
|
63
|
+
else if (args.includes('snapshot')) console.log('- textbox "Chat with ChatGPT" [ref=e1]\\n- button "Add files and more" [ref=e2]');
|
|
64
|
+
else if (args.includes('eval')) { process.stdin.resume(); process.stdin.on('end', () => console.log(JSON.stringify({ ok: true, status: 200, bodyHasId: true, bodyHasEmail: true }))); }
|
|
65
|
+
else if (args.includes('url')) console.log('https://chatgpt.com/');
|
|
66
|
+
`, { mode: 0o700 });
|
|
67
|
+
|
|
68
|
+
const cases = [
|
|
69
|
+
{ browser: "google-chrome", password: "fixture-chrome-key" },
|
|
70
|
+
{ browser: "BraveSoftware/Brave-Browser", password: "fixture-brave-key" },
|
|
71
|
+
{ browser: "chromium", password: "fixture-chromium-key" },
|
|
72
|
+
{ browser: "chromium", password: "fixture-chromium-key", auto: true },
|
|
73
|
+
{ browser: "chromium", password: "fixture-chromium-key", relative: true },
|
|
74
|
+
{ browser: "chromium", password: "fixture-chromium-key", braveAncestor: true },
|
|
75
|
+
{ browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true },
|
|
76
|
+
{ browser: "chromium", password: "fixture-chromium-key", db: true, network: true },
|
|
77
|
+
{ browser: "chromium-browser", password: "fixture-chromium-key", mode: "application" },
|
|
78
|
+
{ browser: "chromium-browser", password: "fixture-chromium-key", db: true },
|
|
79
|
+
...["6", "5", ""].map((kde) => ({ browser: "chromium", password: "fixture-chromium-key", backend: "kwallet", kde })),
|
|
80
|
+
{ browser: "chromium", password: "fixture-chromium-key", backend: "kwallet", mode: "wallet-fallback" },
|
|
81
|
+
{ browser: "chromium", password: "", backend: "kwallet", mode: "failure" },
|
|
82
|
+
{ browser: "chromium", password: "", backend: "kwallet", mode: "wallet-empty" },
|
|
83
|
+
{ browser: "chromium", password: "peanuts", backend: "basic", prefix: "v10" },
|
|
84
|
+
{ browser: "chromium", password: "", backend: "basic" },
|
|
85
|
+
{ browser: "chromium", password: "fixture-override-key", override: true },
|
|
86
|
+
{ browser: "chromium", password: "fixture-chromium-key", backend: "invalid", desktop: "GNOME:KDE", kde: "6" },
|
|
87
|
+
{ browser: "chromium", password: "", mode: "failure" },
|
|
88
|
+
{ browser: "chromium", password: "", mode: "timeout" },
|
|
89
|
+
{ browser: "google-chrome", password: "fixture-chrome-key", profileName: true },
|
|
90
|
+
{ browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true, relative: true },
|
|
91
|
+
{ browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true, tilde: true },
|
|
92
|
+
{ browser: "chromium", password: "fixture-override-key", override: true, db: true, dotSegment: true },
|
|
93
|
+
];
|
|
94
|
+
for (const [index, c] of cases.entries()) {
|
|
95
|
+
// Keep the release artifact scanner strict: fixture password values stay in
|
|
96
|
+
// this file, out of captured platform-smoke stdout (case flags stay unique).
|
|
97
|
+
const { password: _fixturePassword, ...label } = c;
|
|
98
|
+
await t.test(JSON.stringify(label), async () => {
|
|
99
|
+
const sourceHome = c.tilde ? join(root, "BraveSoftware", "home") : home;
|
|
100
|
+
const sourceConfigHome = c.tilde ? join(sourceHome, ".config")
|
|
101
|
+
: c.braveAncestor ? join(root, "BraveSoftware", "config")
|
|
102
|
+
: c.auto ? join(root, "xdg-native") : configHome;
|
|
103
|
+
const profile = join(sourceConfigHome, c.browser, "Default");
|
|
104
|
+
const dbPath = join(profile, ...(c.network ? ["Network"] : []), "Cookies");
|
|
105
|
+
await mkdir(join(profile, ...(c.network ? ["Network"] : [])), { recursive: true });
|
|
106
|
+
if (c.dotSegment) await mkdir(join(sourceConfigHome, "BraveSoftware"), { recursive: true });
|
|
107
|
+
const cookiePath = c.dotSegment ? `${sourceConfigHome}/BraveSoftware/../${c.browser}/Default/Cookies` : dbPath;
|
|
108
|
+
await rm(join(profile, "Cookies"), { force: true });
|
|
109
|
+
const db = new DatabaseSync(dbPath);
|
|
110
|
+
try {
|
|
111
|
+
db.exec(`CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);
|
|
112
|
+
INSERT INTO meta VALUES ('version', '24');
|
|
113
|
+
CREATE TABLE cookies (name TEXT, value TEXT, host_key TEXT, path TEXT,
|
|
114
|
+
expires_utc INTEGER, samesite INTEGER, encrypted_value BLOB,
|
|
115
|
+
is_secure INTEGER, is_httponly INTEGER);`);
|
|
116
|
+
const cipher = createCipheriv("aes-128-cbc", pbkdf2Sync(c.password, "saltysalt", 1, 16, "sha1"), Buffer.alloc(16, 32));
|
|
117
|
+
const plaintext = Buffer.concat([createHash("sha256").update(".chatgpt.com").digest(), Buffer.from("fixture-session-token")]);
|
|
118
|
+
const encrypted = Buffer.concat([Buffer.from(c.prefix ?? "v11"), cipher.update(plaintext), cipher.final()]);
|
|
119
|
+
db.prepare("INSERT INTO cookies VALUES (?, '', '.chatgpt.com', '/', 0, 1, ?, 1, 1)")
|
|
120
|
+
.run("__Secure-next-auth.session-token", encrypted);
|
|
121
|
+
} finally { db.close(); }
|
|
122
|
+
await writeFile(helperLog, "");
|
|
123
|
+
await writeFile(browserLog, "");
|
|
124
|
+
const config = {
|
|
125
|
+
defaults: { provider: "chatgpt", preset: "instant" },
|
|
126
|
+
browser: { authSeedProfileDir: join(root, `seed-${index}`), sessionPrefix: "fixture", chatUrl: "https://chatgpt.com/" },
|
|
127
|
+
auth: { chromeProfile: c.auto ? detectDefaultLinuxCookieProfileSource({ homeDir: sourceHome, env: { XDG_CONFIG_HOME: sourceConfigHome } })
|
|
128
|
+
: c.profileName ? "Default" : c.relative ? "./Default" : c.tilde ? `~/${relative(sourceHome, profile)}` : profile,
|
|
129
|
+
...(c.db ? { chromeCookiePath: cookiePath } : {}), bootstrapTimeoutMs: 1000, pollMs: 1 },
|
|
130
|
+
};
|
|
131
|
+
const env = { HOME: sourceHome, XDG_CONFIG_HOME: sourceConfigHome, PATH: bin, TMPDIR: root,
|
|
132
|
+
AGENT_BROWSER_PATH: join(bin, "agent-browser"), PI_ORACLE_STATE_DIR: join(root, "state"),
|
|
133
|
+
SWEET_COOKIE_LINUX_KEYRING: c.backend ?? "gnome", KDE_SESSION_VERSION: c.kde ?? "",
|
|
134
|
+
XDG_CURRENT_DESKTOP: c.desktop ?? "",
|
|
135
|
+
...(c.override ? { SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD: c.password } : {}),
|
|
136
|
+
FIXTURE_MODE: c.mode ?? "", HELPER_LOG: helperLog, BROWSER_LOG: browserLog };
|
|
137
|
+
const output = execFileSync(process.execPath, [new URL("../extensions/oracle/worker/auth-bootstrap.mjs", import.meta.url).pathname, JSON.stringify(config)], {
|
|
138
|
+
encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"], env,
|
|
139
|
+
...(c.relative || c.profileName ? { cwd: join(sourceConfigHome, "chromium") } : {}),
|
|
140
|
+
});
|
|
141
|
+
assert.match(output, /Oracle auth synced/);
|
|
142
|
+
const replay = (await readFile(browserLog, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
|
|
143
|
+
assert(replay.some((args) => args.includes("__Secure-next-auth.session-token") && args.includes("fixture-session-token")), "production import must decrypt and replay the known cookie");
|
|
144
|
+
const calls = (await readFile(helperLog, "utf8")).trim();
|
|
145
|
+
if (c.override || c.backend === "basic") assert.equal(calls, "");
|
|
146
|
+
else if (c.browser.startsWith("chromium")) {
|
|
147
|
+
assert.doesNotMatch(calls, /Chrome Safe Storage|application","chrome"/);
|
|
148
|
+
if (c.mode === "application") assert.match(calls, /application","chromium/);
|
|
149
|
+
if (c.backend === "kwallet") {
|
|
150
|
+
assert.match(calls, new RegExp(`org.kde.kwalletd${c.kde ?? ""}`));
|
|
151
|
+
assert.match(calls, /Chromium Keys/);
|
|
152
|
+
assert.match(calls, ["wallet-fallback", "failure"].includes(c.mode) ? /kdewallet/ : /fixture-wallet/);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const diagnostics = output.match(/Diagnostics:\n([^\n]+)/)[1];
|
|
156
|
+
const log = await readFile(join(diagnostics, "oracle-auth.log"), "utf8");
|
|
157
|
+
assert.doesNotMatch(log + output, /fixture-(?:chromium|override)-key|fixture-key-do-not-log/);
|
|
158
|
+
if (["failure", "timeout"].includes(c.mode)) assert.match(log, /Failed to read Linux keyring via (secret-tool|kwallet-query)/);
|
|
159
|
+
if (index === 2 || (c.braveAncestor && !c.override)) {
|
|
160
|
+
// A prior successful Chrome read must not cache the wrong key. The
|
|
161
|
+
// adapter's legitimate API must restore env on success AND rejection.
|
|
162
|
+
execFileSync(process.execPath, ["--input-type=module", "-e", `
|
|
163
|
+
import assert from 'node:assert/strict';
|
|
164
|
+
import { getCookies } from '@steipete/sweet-cookie';
|
|
165
|
+
import { getCookiesFromNativeLinuxChromium } from './extensions/oracle/worker/linux-chromium-cookie-source.mjs';
|
|
166
|
+
const options = {url:'https://chatgpt.com/',browsers:['chrome'],mode:'merge',chromeProfile:process.argv[1]};
|
|
167
|
+
assert.equal((await getCookies({...options,chromeProfile:process.argv[2]})).cookies[0].value,'fixture-session-token');
|
|
168
|
+
process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD = ' ';
|
|
169
|
+
process.env.SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD = 'fixture-unused-brave-password';
|
|
170
|
+
const previous = {...process.env};
|
|
171
|
+
assert.equal((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies[0].value,'fixture-session-token');
|
|
172
|
+
assert.deepEqual({...process.env},previous);
|
|
173
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
174
|
+
assert.deepEqual({...process.env},previous);
|
|
175
|
+
process.env.FIXTURE_MODE = 'failure';
|
|
176
|
+
const failureEnv = {...process.env};
|
|
177
|
+
const failedKeyring = await getCookiesFromNativeLinuxChromium(options, getCookies);
|
|
178
|
+
assert.deepEqual(failedKeyring.cookies,[]);
|
|
179
|
+
assert.match(failedKeyring.warnings.join('|'),/Failed to read Linux keyring via secret-tool/);
|
|
180
|
+
assert.deepEqual({...process.env},failureEnv);
|
|
181
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
182
|
+
assert.deepEqual({...process.env},failureEnv);
|
|
183
|
+
process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD = ' fixture-chromium-key ';
|
|
184
|
+
const calibratedEnv = {...process.env};
|
|
185
|
+
assert.equal((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies[0].value,'fixture-session-token');
|
|
186
|
+
assert.deepEqual({...process.env},calibratedEnv);
|
|
187
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
188
|
+
assert.deepEqual({...process.env},calibratedEnv);
|
|
189
|
+
delete process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD;
|
|
190
|
+
process.env.SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD = 'fixture-chromium-key';
|
|
191
|
+
process.env.SWEET_COOKIE_LINUX_KEYRING = 'basic';
|
|
192
|
+
const basicEnv = {...process.env};
|
|
193
|
+
assert.deepEqual((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies,[]);
|
|
194
|
+
assert.deepEqual({...process.env},basicEnv);
|
|
195
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
196
|
+
assert.deepEqual({...process.env},basicEnv);
|
|
197
|
+
`, profile, join(configHome, "google-chrome", "Default")], {
|
|
198
|
+
cwd: new URL("../", import.meta.url), encoding: "utf8", timeout: 10_000, env,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
await rm(join(profile, "Network"), { recursive: true, force: true });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import vm from "node:vm";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import * as ui from "../extensions/oracle/worker/chatgpt-ui-helpers.mjs";
|
|
6
|
+
import { parseSnapshotEntries } from "../extensions/oracle/worker/artifact-heuristics.mjs";
|
|
7
|
+
|
|
8
|
+
const worker = await readFile(new URL("../extensions/oracle/worker/run-job.mjs", import.meta.url), "utf8");
|
|
9
|
+
const names = ["findEntry", "matchesModelConfigurationOpener", "canUseOpenModelMenuForSelection",
|
|
10
|
+
"openModelConfiguration", "configurePowerSlider", "configureModel", "waitForModelConfigurationToSettle", "toJsonScript"];
|
|
11
|
+
const functions = names.flatMap((name) => {
|
|
12
|
+
const start = worker.indexOf(`function ${name}(`);
|
|
13
|
+
if (start < 0) return []; // Allows the same regression to run against the pre-slider worker.
|
|
14
|
+
const end = worker.indexOf("\n}", start) + 2;
|
|
15
|
+
return `${worker.slice(start - 6, start) === "async " ? "async " : ""}${worker.slice(start, end)}`;
|
|
16
|
+
}).join("\n");
|
|
17
|
+
|
|
18
|
+
function harness({ initial = 3, max = "4", wrongLabel = false, stuck = false, mountingReads = 0 } = {}) {
|
|
19
|
+
const labels = ["Instant", "Medium", "High", "Extra High", "Pro"];
|
|
20
|
+
let value = initial;
|
|
21
|
+
let open = false;
|
|
22
|
+
let verified = false;
|
|
23
|
+
const snapshot = () => open
|
|
24
|
+
? '- button "Thinking effort" [expanded=true, ref=e1]\n- menu "Thinking effort" [ref=e2]\n- menuitem "Select model" [ref=e3]\n- menuitem "Power" [ref=e4]'
|
|
25
|
+
: `- button "${value === 4 ? "6 Pro" : labels[value]}" [expanded=false, ref=e1]\n- textbox "Ask ChatGPT" [ref=e5]\n- button "Add files and more" [ref=e6]`;
|
|
26
|
+
const control = {
|
|
27
|
+
closest: () => null,
|
|
28
|
+
getAttribute: () => "description",
|
|
29
|
+
querySelector: () => mountingReads-- > 0 ? null : ({ getAttribute: (name) => ({ "aria-valuemin": "0", "aria-valuemax": max, "aria-valuenow": String(value) })[name] }),
|
|
30
|
+
};
|
|
31
|
+
const context = vm.createContext({ ...ui, parseSnapshotEntries, Date, Error, JSON, Number,
|
|
32
|
+
CHATGPT_LABELS: { configure: "Configure..." },
|
|
33
|
+
MODEL_CONFIGURATION_OPEN_TIMEOUT_MS: 30, MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS: 30,
|
|
34
|
+
MODEL_CONFIGURATION_CLOSE_RETRY_MS: 1, MODEL_CONFIGURATION_SETTLE_POLL_MS: 0,
|
|
35
|
+
isGrokJob: () => false, snapshotText: async () => snapshot(),
|
|
36
|
+
throwIfProviderTransientError: () => {}, dismissProFeedbackModal: async () => false,
|
|
37
|
+
clickRef: async () => { open = !open; }, sleep: async () => {},
|
|
38
|
+
composerControlsVisible: ui.snapshotHasUsableComposerControls,
|
|
39
|
+
log: async (line) => { if (line.startsWith("Verified ChatGPT Power tier")) verified = true; },
|
|
40
|
+
agentBrowser: async (_job, command, arg) => {
|
|
41
|
+
if (command === "focus") {
|
|
42
|
+
assert(open);
|
|
43
|
+
assert(mountingReads < 0, "wait for the Power slider to mount before focusing it");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (command !== "press") return;
|
|
47
|
+
if (arg === "Escape") open = false;
|
|
48
|
+
else if (!stuck) value = Math.max(0, Math.min(4, value + (arg === "ArrowRight" ? 1 : -1)));
|
|
49
|
+
},
|
|
50
|
+
evalPage: async (_job, script) => JSON.parse(vm.runInNewContext(script, { document: {
|
|
51
|
+
querySelector: () => open ? control : null,
|
|
52
|
+
getElementById: () => ({ textContent: `${wrongLabel ? "Unknown" : labels[value]}, ${value + 1} of 5.` }),
|
|
53
|
+
} })),
|
|
54
|
+
});
|
|
55
|
+
vm.runInContext(functions, context);
|
|
56
|
+
return { configure: (selection) => context.configureModel({ selection }),
|
|
57
|
+
get state() { return { value, open, verified }; } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("worker configures every preset through the current Power picker and closes it", async () => {
|
|
61
|
+
for (const [selection, expected] of [
|
|
62
|
+
[{ modelFamily: "instant", autoSwitchToThinking: false }, 0],
|
|
63
|
+
[{ modelFamily: "instant", autoSwitchToThinking: true }, 0],
|
|
64
|
+
[{ modelFamily: "thinking", effort: "light" }, 1],
|
|
65
|
+
[{ modelFamily: "thinking", effort: "standard" }, 1],
|
|
66
|
+
[{ modelFamily: "thinking", effort: "extended" }, 2],
|
|
67
|
+
[{ modelFamily: "thinking", effort: "heavy" }, 3],
|
|
68
|
+
[{ modelFamily: "pro", effort: "standard" }, 4],
|
|
69
|
+
[{ modelFamily: "pro", effort: "extended" }, 4],
|
|
70
|
+
]) {
|
|
71
|
+
const h = harness({ initial: expected === 4 ? 0 : 4, mountingReads: 2 });
|
|
72
|
+
await h.configure(selection);
|
|
73
|
+
assert.deepEqual(h.state, { value: expected, open: false, verified: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("worker refuses unknown, mislabelled, or unresponsive Power controls", async () => {
|
|
78
|
+
for (const [options, error] of [
|
|
79
|
+
[{ max: "5" }, /unrecognized range/],
|
|
80
|
+
[{ wrongLabel: true }, /Could not verify ChatGPT Power tier/],
|
|
81
|
+
[{ stuck: true }, /did not reach requested tier/],
|
|
82
|
+
]) {
|
|
83
|
+
const h = harness(options);
|
|
84
|
+
await assert.rejects(h.configure({ modelFamily: "instant", autoSwitchToThinking: false }), error);
|
|
85
|
+
assert.equal(h.state.verified, false);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -11,7 +11,8 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
11
11
|
const DEFAULT_PROVIDER = "zai";
|
|
12
12
|
const DEFAULT_MODEL = "glm-5.2";
|
|
13
13
|
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
14
|
-
const EXPECTED_PI_VERSION =
|
|
14
|
+
const EXPECTED_PI_VERSION = process.env.PI_COMPAT_EXPECTED_VERSION
|
|
15
|
+
?? JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).devDependencies["@earendil-works/pi-coding-agent"];
|
|
15
16
|
const PACKAGE_NAME = "pi-oracle";
|
|
16
17
|
|
|
17
18
|
function usage() {
|
|
@@ -22,6 +23,8 @@ Modes:
|
|
|
22
23
|
source Inner-loop/debug only. Loads this checkout with pi --no-extensions -e extensions/oracle/index.ts.
|
|
23
24
|
|
|
24
25
|
Environment:
|
|
26
|
+
PI_HOST_CLI selected Pi manifest bin entry, invoked with this Node executable (default: pi on PATH)
|
|
27
|
+
PI_COMPAT_EXPECTED_VERSION expected Pi version (default: package development baseline)
|
|
25
28
|
PI_ORACLE_REAL_TEST_PROVIDER pi provider for the test agent (default: ${DEFAULT_PROVIDER})
|
|
26
29
|
PI_ORACLE_REAL_TEST_MODEL pi model for the test agent (default: ${DEFAULT_MODEL})
|
|
27
30
|
PI_ORACLE_REAL_TEST_TIMEOUT_MS per-agent timeout in ms (default: ${DEFAULT_TIMEOUT_MS})
|
|
@@ -65,7 +68,10 @@ function positiveTimeoutMs(value, label) {
|
|
|
65
68
|
|
|
66
69
|
function commandExists(command, args = ["--version"]) {
|
|
67
70
|
return new Promise((resolvePromise) => {
|
|
68
|
-
const
|
|
71
|
+
const selectedCli = command === piCommand() && env("PI_HOST_CLI");
|
|
72
|
+
const child = spawn(selectedCli ? process.execPath : command, selectedCli ? [selectedCli, ...args] : args, {
|
|
73
|
+
stdio: "ignore", shell: !selectedCli && process.platform === "win32",
|
|
74
|
+
});
|
|
69
75
|
child.on("error", () => resolvePromise(false));
|
|
70
76
|
child.on("exit", (code) => resolvePromise(code === 0 || code === 1));
|
|
71
77
|
});
|
|
@@ -104,7 +110,7 @@ async function doctor() {
|
|
|
104
110
|
const failures = [];
|
|
105
111
|
const warnings = [];
|
|
106
112
|
const requiredCommands = [
|
|
107
|
-
[
|
|
113
|
+
[piCommand(), ["--version"], "pi CLI"],
|
|
108
114
|
[process.platform === "win32" ? "tar.exe" : "tar", ["--version"], "tar"],
|
|
109
115
|
["zstd", ["--version"], "zstd"],
|
|
110
116
|
[process.platform === "win32" ? "agent-browser.cmd" : "agent-browser", ["--version"], "agent-browser"],
|
|
@@ -134,10 +140,11 @@ async function doctor() {
|
|
|
134
140
|
function runCommand(command, args, options) {
|
|
135
141
|
const timeoutMs = positiveTimeoutMs(options.timeoutMs, "command timeout");
|
|
136
142
|
return new Promise((resolvePromise) => {
|
|
137
|
-
const
|
|
143
|
+
const selectedCli = command === piCommand() && env("PI_HOST_CLI");
|
|
144
|
+
const child = spawn(selectedCli ? process.execPath : command, selectedCli ? [selectedCli, ...args] : args, {
|
|
138
145
|
cwd: options.cwd,
|
|
139
146
|
env: options.env,
|
|
140
|
-
shell: process.platform === "win32",
|
|
147
|
+
shell: !selectedCli && process.platform === "win32",
|
|
141
148
|
stdio: ["ignore", "pipe", "pipe"],
|
|
142
149
|
});
|
|
143
150
|
let stdout = "";
|
|
@@ -234,7 +241,7 @@ function entryExists(entries, path) {
|
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
function piCommand() {
|
|
237
|
-
return process.platform === "win32" ? "pi.cmd" : "pi";
|
|
244
|
+
return env("PI_HOST_CLI") ?? (process.platform === "win32" ? "pi.cmd" : "pi");
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
function npmCommand() {
|
|
@@ -280,6 +287,11 @@ async function preparePackedProject({ runDir, provider, model, timeoutMs }) {
|
|
|
280
287
|
writeFileSync(join(piProject, ".artifacts", "ignored", "artifact.txt"), "ignore me\n");
|
|
281
288
|
mkdirSync(join(piProject, ".crabbox", "ignored"), { recursive: true });
|
|
282
289
|
writeFileSync(join(piProject, ".crabbox", "ignored", "capture.txt"), "ignore me\n");
|
|
290
|
+
mkdirSync(join(piProject, ".pi"), { recursive: true });
|
|
291
|
+
writeFileSync(join(piProject, ".pi", "workspace-notes.txt"), "ignore me\n");
|
|
292
|
+
mkdirSync(join(piProject, ".oracle-context"), { recursive: true });
|
|
293
|
+
writeFileSync(join(piProject, ".oracle-context", "context.md"), "ignore me\n");
|
|
294
|
+
writeFileSync(join(piProject, ".scratchpad.md"), "ignore me\n");
|
|
283
295
|
|
|
284
296
|
return { mode: "packed", cwd: piProject, installDir, provider, model, extensionPath: `./node_modules/${PACKAGE_NAME}` };
|
|
285
297
|
}
|
|
@@ -362,11 +374,6 @@ async function run(mode = "packed") {
|
|
|
362
374
|
const piVersion = await mustRun(runDir, "pi-version", piCommand(), ["--version"], { cwd: process.cwd(), env: process.env, timeoutMs: 30_000 });
|
|
363
375
|
if (piVersion.stdout.trim() !== EXPECTED_PI_VERSION) throw new Error(`real smoke requires Pi ${EXPECTED_PI_VERSION}; found ${piVersion.stdout.trim() || "unknown"}`);
|
|
364
376
|
const tmpRoot = mkdtempSync(join(tmpdir(), "pi-oracle-real-smoke-"));
|
|
365
|
-
const prepared = mode === "packed"
|
|
366
|
-
? await preparePackedProject({ runDir, provider, model, timeoutMs })
|
|
367
|
-
: prepareSourceProject({ provider, model });
|
|
368
|
-
console.log(`Oracle real smoke mode=${prepared.mode} extension=${prepared.extensionPath}`);
|
|
369
|
-
writeFileSync(join(runDir, "run.json"), `${JSON.stringify({ runId, mode: prepared.mode, provider, model, extensionPath: prepared.extensionPath, timeoutMs, startedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
370
377
|
|
|
371
378
|
function assert(id, condition, message) {
|
|
372
379
|
assertions.push({ id, ok: Boolean(condition), ...(condition ? {} : { error: message }) });
|
|
@@ -374,6 +381,12 @@ async function run(mode = "packed") {
|
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
try {
|
|
384
|
+
const prepared = mode === "packed"
|
|
385
|
+
? await preparePackedProject({ runDir, provider, model, timeoutMs })
|
|
386
|
+
: prepareSourceProject({ provider, model });
|
|
387
|
+
console.log(`Oracle real smoke mode=${prepared.mode} extension=${prepared.extensionPath}`);
|
|
388
|
+
writeFileSync(join(runDir, "run.json"), `${JSON.stringify({ runId, mode: prepared.mode, provider, model, extensionPath: prepared.extensionPath, timeoutMs, startedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
389
|
+
|
|
377
390
|
const test1 = join(runDir, "whole-repo-submit");
|
|
378
391
|
const agent1 = join(runDir, "agent1");
|
|
379
392
|
const sessions1 = join(runDir, "sessions1");
|
|
@@ -416,7 +429,7 @@ async function run(mode = "packed") {
|
|
|
416
429
|
const entries = await tarList(archivePath);
|
|
417
430
|
writeFileSync(join(test1, "archive-list.txt"), `${entries.join("\n")}\n`);
|
|
418
431
|
assert("archive-includes-readme", entryExists(entries, "README.md"), "archive should include README.md");
|
|
419
|
-
for (const excluded of [".pi", ".oracle-context", ".scratchpad.md", ".artifacts", ".crabbox"
|
|
432
|
+
for (const excluded of [".pi", ".oracle-context", ".scratchpad.md", ".artifacts", ".crabbox"]) {
|
|
420
433
|
assert(`archive-excludes-${excluded.replace(/[^a-z0-9]+/gi, "-")}`, !entryExists(entries, excluded), `archive should exclude ${excluded}`);
|
|
421
434
|
}
|
|
422
435
|
} else {
|
|
@@ -459,7 +472,11 @@ async function run(mode = "packed") {
|
|
|
459
472
|
const jobDir2 = latestJobDir(jobs2);
|
|
460
473
|
writeFileSync(join(test2, "job-dir.txt"), `${jobDir2 ?? ""}\n`);
|
|
461
474
|
const symlinkOutput = `${symlinkResult.stdout}\n${symlinkResult.stderr}`;
|
|
462
|
-
assert(
|
|
475
|
+
assert(
|
|
476
|
+
"symlink-rejected",
|
|
477
|
+
symlinkOutput.includes("archive_input_symlink_escape") || symlinkOutput.includes("Archive input must resolve inside the project cwd without symlink escapes"),
|
|
478
|
+
"symlink escape test output did not show the actual oracle_submit rejection evidence",
|
|
479
|
+
);
|
|
463
480
|
assert("symlink-no-job-created", !jobDir2, "symlink escape rejection should not create an oracle job");
|
|
464
481
|
}
|
|
465
482
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
FROM cimg/node:24.
|
|
1
|
+
FROM cimg/node:24.21
|
|
2
2
|
|
|
3
3
|
USER root
|
|
4
4
|
RUN apt-get update \
|
|
5
5
|
&& apt-get install -y --no-install-recommends zstd rsync \
|
|
6
6
|
&& rm -rf /var/lib/apt/lists/*
|
|
7
|
-
RUN npm install -g
|
|
7
|
+
RUN npm install -g npm@11.21.0 \
|
|
8
|
+
&& npm install -g agent-browser@0.29.1
|
|
8
9
|
USER circleci
|
|
@@ -14,8 +14,7 @@ export function execCrabbox(args, opts = {}) {
|
|
|
14
14
|
return new Promise((resolve) => {
|
|
15
15
|
const child = spawn(CRABBOX_BIN, args, {
|
|
16
16
|
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
-
env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false"
|
|
18
|
-
...opts.spawnOpts,
|
|
17
|
+
env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" },
|
|
19
18
|
});
|
|
20
19
|
const stdoutChunks = [];
|
|
21
20
|
const stderrChunks = [];
|
|
@@ -122,10 +121,9 @@ export async function runOnLease(config, targetName, leaseId, command, opts = {}
|
|
|
122
121
|
for (const name of opts.allowEnvNames ?? []) args.push("--allow-env", name);
|
|
123
122
|
if (opts.sync === false) args.push("--no-sync");
|
|
124
123
|
else args.push("--fresh-sync");
|
|
125
|
-
|
|
126
|
-
else args.push("--", ...(Array.isArray(command) ? command : command.split(" ")));
|
|
124
|
+
args.push("--shell", command);
|
|
127
125
|
console.log(` [crabbox] run ${targetName} ${opts.sync === false ? "--no-sync" : "--fresh-sync"} ...`);
|
|
128
|
-
return execCrabbox(args, { timeout: opts.timeout ?? 900_000
|
|
126
|
+
return execCrabbox(args, { timeout: opts.timeout ?? 900_000 });
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
export async function stopLease(config, targetName, leaseId) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { execFileSync, execSync } from "node:child_process";
|
|
4
4
|
import { accessSync, constants, existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
+
import { buildTargetBaseArgs } from "./crabbox-runner.mjs";
|
|
6
7
|
|
|
7
8
|
let failures = 0;
|
|
8
9
|
|
|
@@ -63,24 +64,6 @@ function parseJson(text) {
|
|
|
63
64
|
try { return JSON.parse(text); } catch { return undefined; }
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function targetBaseArgs(targetName, config) {
|
|
67
|
-
if (targetName === "macos") {
|
|
68
|
-
const host = env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
|
|
69
|
-
const user = env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
70
|
-
const workRoot = env("PI_ORACLE_SMOKE_MAC_WORK_ROOT") || env("PLATFORM_SMOKE_MAC_WORK_ROOT") || `/Users/${env("USER")}/crabbox/${config.packageName}`;
|
|
71
|
-
return ["--provider", "ssh", "--target", "macos", "--static-host", host, "--static-user", user, "--static-port", "22", "--static-work-root", workRoot];
|
|
72
|
-
}
|
|
73
|
-
if (targetName === "ubuntu") {
|
|
74
|
-
const image = env("PI_ORACLE_SMOKE_UBUNTU_IMAGE") || env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || "pi-oracle-platform-smoke:node24";
|
|
75
|
-
return ["--provider", "local-container", "--target", "linux", "--local-container-image", image];
|
|
76
|
-
}
|
|
77
|
-
const vm = windowsVmName(config);
|
|
78
|
-
const snapshot = windowsSnapshotName(config);
|
|
79
|
-
const user = env("PI_ORACLE_SMOKE_WINDOWS_USER") || env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
|
|
80
|
-
const workRoot = env("PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${config.packageName}`;
|
|
81
|
-
return ["--provider", "parallels", "--target", "windows", "--windows-mode", "normal", "--parallels-source", vm, "--parallels-source-snapshot", snapshot, "--parallels-user", user, "--parallels-work-root", workRoot];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
67
|
function runCrabboxDoctor(cbox, label, args, timeout = 120_000) {
|
|
85
68
|
const output = silent(cbox, ["doctor", ...args, "--json"], { env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" }, timeout });
|
|
86
69
|
if (!output) {
|
|
@@ -100,7 +83,7 @@ function runTargetToolProbe(cbox, targetName, config) {
|
|
|
100
83
|
const command = targetName === "windows-native"
|
|
101
84
|
? `cmd.exe /c "where node && where npm && where git && where tar && where zstd && where agent-browser && zstd --version && agent-browser --version && echo tools-ok"`
|
|
102
85
|
: `missing=""; for tool in ${required.join(" ")}; do command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"; done; if [ -n "$missing" ]; then echo "missing=$missing"; exit 1; fi; zstd --version >/dev/null && agent-browser --version >/dev/null && echo tools-ok`;
|
|
103
|
-
const baseArgs =
|
|
86
|
+
const baseArgs = buildTargetBaseArgs(targetName, config);
|
|
104
87
|
if (targetName === "macos") baseArgs.push("--reclaim");
|
|
105
88
|
const result = silent(cbox, ["run", ...baseArgs, "--no-sync", "--shell", command], {
|
|
106
89
|
env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" },
|
|
@@ -137,11 +120,13 @@ function verifyPackageExclusions() {
|
|
|
137
120
|
const forbiddenPatterns = [/^\.env(?:\.|$)/, /^\.artifacts(?:\/|$)/, /^\.crabbox(?:\/|$)/, /^\.debug(?:\/|$)/, /^\.platform-smoke-runs(?:\/|$)/, /\.tgz$/];
|
|
138
121
|
const output = shell("npm pack --dry-run --json", { timeout: 120_000 });
|
|
139
122
|
const parsed = parseJson(output);
|
|
140
|
-
|
|
123
|
+
// npm 11 returns an array; npm 12 keys the same records by package name.
|
|
124
|
+
const [pack] = parsed && typeof parsed === "object" ? Object.values(parsed) : [];
|
|
125
|
+
if (!pack?.files) {
|
|
141
126
|
warn("could not inspect npm pack file list");
|
|
142
127
|
return;
|
|
143
128
|
}
|
|
144
|
-
const files =
|
|
129
|
+
const files = pack.files.map((file) => file.path);
|
|
145
130
|
const forbidden = files.filter((file) => forbiddenPatterns.some((pattern) => pattern.test(file)));
|
|
146
131
|
forbidden.length ? fail(`forbidden files in package: ${forbidden.join(", ")}`) : ok("package excludes local env/artifact/debug state");
|
|
147
132
|
}
|
|
@@ -184,9 +169,9 @@ export async function runDoctor(config) {
|
|
|
184
169
|
for (const provider of ["ssh", "local-container", "parallels"]) {
|
|
185
170
|
new RegExp(`^${provider}$`, "m").test(providers) ? ok(`provider available: ${provider}`) : fail(`crabbox providers missing ${provider}`);
|
|
186
171
|
}
|
|
187
|
-
runCrabboxDoctor(cbox, "macOS static SSH",
|
|
188
|
-
runCrabboxDoctor(cbox, "Windows native Parallels",
|
|
189
|
-
runCrabboxDoctor(cbox, "local-container",
|
|
172
|
+
runCrabboxDoctor(cbox, "macOS static SSH", buildTargetBaseArgs("macos", config), 120_000);
|
|
173
|
+
runCrabboxDoctor(cbox, "Windows native Parallels", buildTargetBaseArgs("windows-native", config), 180_000);
|
|
174
|
+
runCrabboxDoctor(cbox, "local-container", buildTargetBaseArgs("ubuntu", config), 120_000);
|
|
190
175
|
|
|
191
176
|
console.log("\n── Host tools ──");
|
|
192
177
|
for (const [name, command] of [["Docker", "docker info --format '{{.ServerVersion}}'"], ["Node", "node --version"], ["npm", "npm --version"], ["git", "git --version"], ["tar", "tar --version"], ["rsync", "rsync --version"]]) {
|