pi-oracle 0.7.4 → 0.7.6

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 (31) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +53 -18
  3. package/docs/ORACLE_DESIGN.md +16 -8
  4. package/docs/platform-smoke.md +156 -0
  5. package/extensions/oracle/index.ts +10 -4
  6. package/extensions/oracle/lib/config.ts +53 -27
  7. package/extensions/oracle/lib/jobs.ts +9 -5
  8. package/extensions/oracle/lib/poller.ts +1 -0
  9. package/extensions/oracle/lib/runtime.ts +107 -32
  10. package/extensions/oracle/lib/tools.ts +138 -12
  11. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  12. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  13. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  14. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  15. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  16. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  17. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  18. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  19. package/extensions/oracle/worker/run-job.mjs +107 -25
  20. package/package.json +30 -9
  21. package/platform-smoke.config.mjs +66 -0
  22. package/scripts/oracle-real-smoke.mjs +500 -0
  23. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  24. package/scripts/platform-smoke/artifacts.mjs +87 -0
  25. package/scripts/platform-smoke/assertions.mjs +34 -0
  26. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  27. package/scripts/platform-smoke/doctor.mjs +239 -0
  28. package/scripts/platform-smoke/invariants.mjs +124 -0
  29. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  30. package/scripts/platform-smoke/targets.mjs +434 -0
  31. package/scripts/platform-smoke.mjs +152 -0
@@ -0,0 +1,500 @@
1
+ #!/usr/bin/env node
2
+ // Purpose: Run real isolated pi-agent smoke tests against pi-oracle.
3
+ // Default mode is packed-install release proof. Source mode is inner-loop/debug only.
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
7
+ import { mkdtempSync } from "node:fs";
8
+ import { createRequire } from "node:module";
9
+ import { tmpdir } from "node:os";
10
+ import { basename, dirname, join, resolve } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const tsxCli = require.resolve("tsx/cli");
15
+
16
+ const DEFAULT_PROVIDER = "zai";
17
+ const DEFAULT_MODEL = "glm-5.1";
18
+ const DEFAULT_TIMEOUT_MS = 180_000;
19
+ const PACKAGE_NAME = "pi-oracle";
20
+
21
+ function usage() {
22
+ console.log(`Usage: node scripts/oracle-real-smoke.mjs <doctor|run> [--mode packed|source]
23
+
24
+ Modes:
25
+ packed Release proof. npm pack -> clean pi project -> npm install tarball -> pi install -l -> run through installed package. Default.
26
+ source Inner-loop/debug only. Loads this checkout with pi --no-extensions -e extensions/oracle/index.ts.
27
+
28
+ Environment:
29
+ PI_ORACLE_REAL_TEST_PROVIDER pi provider for the test agent (default: ${DEFAULT_PROVIDER})
30
+ PI_ORACLE_REAL_TEST_MODEL pi model for the test agent (default: ${DEFAULT_MODEL})
31
+ PI_ORACLE_REAL_TEST_TIMEOUT_MS per-agent timeout in ms (default: ${DEFAULT_TIMEOUT_MS})
32
+ PI_ORACLE_REAL_TEST_ARTIFACT_ROOT artifact root (default: .artifacts/real-smoke)
33
+ PI_ORACLE_REAL_TEST_KEEP_TMP keep temporary fixture directory when set to 1/true/yes
34
+ PI_ORACLE_REAL_TEST_MODEL_AGENT run oracle_submit through a model-agent turn instead of direct installed-tool execution (off by default)
35
+ PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK run optional second-agent symlink rejection check (off by default; covered by sanity:oracle)
36
+ `);
37
+ }
38
+
39
+ function parseArgs(argv) {
40
+ const args = { command: argv[2] ?? "run", mode: "packed" };
41
+ for (let i = 3; i < argv.length; i += 1) {
42
+ const arg = argv[i];
43
+ if (arg === "--mode" && argv[i + 1]) {
44
+ args.mode = argv[i + 1];
45
+ i += 1;
46
+ continue;
47
+ }
48
+ if (arg === "--help" || arg === "-h") args.command = "help";
49
+ else throw new Error(`unknown argument: ${arg}`);
50
+ }
51
+ if (!["packed", "source"].includes(args.mode)) throw new Error(`unknown mode: ${args.mode}`);
52
+ return args;
53
+ }
54
+
55
+ function env(name) {
56
+ const value = process.env[name];
57
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
58
+ }
59
+
60
+ function truthy(value) {
61
+ return ["1", "true", "yes", "on"].includes(String(value ?? "").toLowerCase());
62
+ }
63
+
64
+ function commandExists(command, args = ["--version"]) {
65
+ return new Promise((resolvePromise) => {
66
+ const child = spawn(command, args, { stdio: "ignore", shell: process.platform === "win32" });
67
+ child.on("error", () => resolvePromise(false));
68
+ child.on("exit", (code) => resolvePromise(code === 0 || code === 1));
69
+ });
70
+ }
71
+
72
+ function apiKeyNameForProvider(provider) {
73
+ return {
74
+ zai: "ZAI_API_KEY",
75
+ openai: "OPENAI_API_KEY",
76
+ anthropic: "ANTHROPIC_API_KEY",
77
+ google: "GEMINI_API_KEY",
78
+ xai: "XAI_API_KEY",
79
+ groq: "GROQ_API_KEY",
80
+ deepseek: "DEEPSEEK_API_KEY",
81
+ cerebras: "CEREBRAS_API_KEY",
82
+ fireworks: "FIREWORKS_API_KEY",
83
+ together: "TOGETHER_API_KEY",
84
+ openrouter: "OPENROUTER_API_KEY",
85
+ ai_gateway: "AI_GATEWAY_API_KEY",
86
+ mistral: "MISTRAL_API_KEY",
87
+ minimax: "MINIMAX_API_KEY",
88
+ "minimax-cn": "MINIMAX_CN_API_KEY",
89
+ "ant-ling": "ANT_LING_API_KEY",
90
+ nvidia: "NVIDIA_API_KEY",
91
+ moonshot: "MOONSHOT_API_KEY",
92
+ opencode: "OPENCODE_API_KEY",
93
+ kimi: "KIMI_API_KEY",
94
+ cloudflare: "CLOUDFLARE_API_KEY",
95
+ xiaomi: "XIAOMI_API_KEY",
96
+ }[provider];
97
+ }
98
+
99
+ async function doctor() {
100
+ const provider = env("PI_ORACLE_REAL_TEST_PROVIDER") ?? DEFAULT_PROVIDER;
101
+ const model = env("PI_ORACLE_REAL_TEST_MODEL") ?? DEFAULT_MODEL;
102
+ const failures = [];
103
+ const warnings = [];
104
+ const requiredCommands = [
105
+ [process.platform === "win32" ? "pi.cmd" : "pi", ["--version"], "pi CLI"],
106
+ [process.platform === "win32" ? "tar.exe" : "tar", ["--version"], "tar"],
107
+ ["zstd", ["--version"], "zstd"],
108
+ [process.platform === "win32" ? "agent-browser.cmd" : "agent-browser", ["--version"], "agent-browser"],
109
+ ];
110
+ for (const [command, args, label] of requiredCommands) {
111
+ if (!(await commandExists(command, args))) failures.push(`${label} is not available on PATH`);
112
+ }
113
+ const keyName = apiKeyNameForProvider(provider);
114
+ if (truthy(env("PI_ORACLE_REAL_TEST_MODEL_AGENT"))) {
115
+ if (keyName && !env(keyName)) failures.push(`${keyName} is not set for provider ${provider}`);
116
+ if (!keyName) warnings.push(`No known API-key env mapping for provider ${provider}; pi may still work if that provider is configured another way.`);
117
+ }
118
+
119
+ console.log("Oracle real smoke doctor");
120
+ console.log(` provider: ${provider}`);
121
+ console.log(` model: ${model}`);
122
+ console.log(` artifact root: ${resolve(env("PI_ORACLE_REAL_TEST_ARTIFACT_ROOT") ?? ".artifacts/real-smoke")}`);
123
+ for (const warning of warnings) console.log(` ⚠ ${warning}`);
124
+ if (failures.length) {
125
+ for (const failure of failures) console.error(` ✗ ${failure}`);
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+ console.log(" ✓ ready");
130
+ }
131
+
132
+ function runCommand(command, args, options) {
133
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
134
+ return new Promise((resolvePromise) => {
135
+ const child = spawn(command, args, {
136
+ cwd: options.cwd,
137
+ env: options.env,
138
+ shell: process.platform === "win32",
139
+ stdio: ["ignore", "pipe", "pipe"],
140
+ });
141
+ let stdout = "";
142
+ let stderr = "";
143
+ let timedOut = false;
144
+ let stoppedAfterCondition = false;
145
+ const stopChild = () => {
146
+ child.kill("SIGTERM");
147
+ setTimeout(() => child.kill("SIGKILL"), 2_000).unref();
148
+ };
149
+ const timer = setTimeout(() => {
150
+ timedOut = true;
151
+ stopChild();
152
+ }, timeoutMs);
153
+ const conditionTimer = options.until
154
+ ? setInterval(async () => {
155
+ try {
156
+ if (await options.until()) {
157
+ stoppedAfterCondition = true;
158
+ stopChild();
159
+ }
160
+ } catch {
161
+ // Keep the command running; the caller validates artifacts after exit.
162
+ }
163
+ }, 500)
164
+ : undefined;
165
+ conditionTimer?.unref?.();
166
+ child.stdout.setEncoding("utf8");
167
+ child.stderr.setEncoding("utf8");
168
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
169
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
170
+ child.on("error", (error) => {
171
+ clearTimeout(timer);
172
+ if (conditionTimer) clearInterval(conditionTimer);
173
+ resolvePromise({ code: 1, signal: undefined, timedOut, stoppedAfterCondition, stdout, stderr: `${stderr}${error.stack ?? error.message}\n` });
174
+ });
175
+ child.on("close", (code, signal) => {
176
+ clearTimeout(timer);
177
+ if (conditionTimer) clearInterval(conditionTimer);
178
+ resolvePromise({ code, signal, timedOut, stoppedAfterCondition, stdout, stderr });
179
+ });
180
+ });
181
+ }
182
+
183
+ function writeRunResult(dir, name, result) {
184
+ writeFileSync(join(dir, `${name}.stdout.txt`), result.stdout);
185
+ writeFileSync(join(dir, `${name}.stderr.txt`), result.stderr);
186
+ writeFileSync(join(dir, `${name}.exit.json`), `${JSON.stringify({ code: result.code, signal: result.signal, timedOut: result.timedOut, stoppedAfterCondition: result.stoppedAfterCondition }, null, 2)}\n`);
187
+ }
188
+
189
+ async function mustRun(dir, name, command, args, options) {
190
+ const result = await runCommand(command, args, options);
191
+ writeRunResult(dir, name, result);
192
+ if (result.timedOut) throw new Error(`${name} timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`);
193
+ if (result.code !== 0) throw new Error(`${name} exited ${result.code}; see ${join(dir, `${name}.stderr.txt`)}`);
194
+ return result;
195
+ }
196
+
197
+ function latestJobDir(jobsDir) {
198
+ if (!existsSync(jobsDir)) return undefined;
199
+ let names = [];
200
+ try { names = readdirSync(jobsDir); } catch { return undefined; }
201
+ const candidates = names.filter((name) => name.startsWith("oracle-")).map((name) => join(jobsDir, name));
202
+ candidates.sort();
203
+ return candidates.at(-1);
204
+ }
205
+
206
+ function parseJobArchivePath(jobDir) {
207
+ if (!jobDir) return undefined;
208
+ const jobJsonPath = join(jobDir, "job.json");
209
+ if (!existsSync(jobJsonPath)) return undefined;
210
+ try {
211
+ const job = JSON.parse(readFileSync(jobJsonPath, "utf8"));
212
+ return typeof job.archivePath === "string" ? job.archivePath : undefined;
213
+ } catch {
214
+ return undefined;
215
+ }
216
+ }
217
+
218
+ async function tarList(archivePath) {
219
+ const result = await runCommand(process.platform === "win32" ? "tar.exe" : "tar", ["--zstd", "-tf", process.platform === "win32" ? basename(archivePath) : archivePath], {
220
+ cwd: process.platform === "win32" ? dirname(archivePath) : process.cwd(),
221
+ env: process.env,
222
+ timeoutMs: 60_000,
223
+ });
224
+ if (result.code !== 0) throw new Error(`tar failed for ${archivePath}: ${result.stderr || result.stdout}`);
225
+ return result.stdout.split(/\r?\n/).filter(Boolean);
226
+ }
227
+
228
+ function entryExists(entries, path) {
229
+ return entries.some((entry) => entry === path || entry.startsWith(`${path}/`));
230
+ }
231
+
232
+ function piCommand() {
233
+ return process.platform === "win32" ? "pi.cmd" : "pi";
234
+ }
235
+
236
+ function npmCommand() {
237
+ return process.platform === "win32" ? "npm.cmd" : "npm";
238
+ }
239
+
240
+ async function preparePackedProject({ runDir, provider, model, timeoutMs }) {
241
+ const installDir = join(runDir, "packed-install");
242
+ const packDir = join(installDir, "pack");
243
+ const piProject = join(installDir, "pi-project");
244
+ mkdirSync(packDir, { recursive: true });
245
+ mkdirSync(piProject, { recursive: true });
246
+ const npm = npmCommand();
247
+
248
+ const pack = await mustRun(installDir, "npm-pack", npm, ["pack", "--silent", "--pack-destination", packDir], {
249
+ cwd: process.cwd(),
250
+ env: process.env,
251
+ timeoutMs: 120_000,
252
+ });
253
+ const tarballName = pack.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1);
254
+ const tarballPath = tarballName ? join(packDir, tarballName) : undefined;
255
+ if (!tarballPath || !existsSync(tarballPath)) throw new Error(`npm pack did not produce a tarball in ${packDir}`);
256
+ writeFileSync(join(installDir, "packed-tarball.txt"), `${tarballPath}\n`);
257
+
258
+ await mustRun(installDir, "npm-init", npm, ["init", "-y"], { cwd: piProject, env: process.env, timeoutMs: 60_000 });
259
+ await mustRun(installDir, "packed-node-install", npm, ["install", "--no-save", tarballPath], { cwd: piProject, env: process.env, timeoutMs: 120_000 });
260
+ await mustRun(installDir, "pi-install", piCommand(), ["install", "-l", `./node_modules/${PACKAGE_NAME}`], {
261
+ cwd: piProject,
262
+ env: { ...process.env, PI_OFFLINE: "1" },
263
+ timeoutMs: 120_000,
264
+ });
265
+ const piList = await mustRun(installDir, "pi-list", piCommand(), ["list"], {
266
+ cwd: piProject,
267
+ env: { ...process.env, PI_OFFLINE: "1" },
268
+ timeoutMs: 60_000,
269
+ });
270
+ if (!piList.stdout.includes(PACKAGE_NAME) || !new RegExp(`node_modules[\\\\/]${PACKAGE_NAME}`).test(piList.stdout)) {
271
+ throw new Error(`pi list did not show packed ${PACKAGE_NAME} install`);
272
+ }
273
+
274
+ writeFileSync(join(piProject, "README.md"), `# ${PACKAGE_NAME} packed real smoke fixture\n`);
275
+ mkdirSync(join(piProject, ".artifacts", "ignored"), { recursive: true });
276
+ writeFileSync(join(piProject, ".artifacts", "ignored", "artifact.txt"), "ignore me\n");
277
+ mkdirSync(join(piProject, ".crabbox", "ignored"), { recursive: true });
278
+ writeFileSync(join(piProject, ".crabbox", "ignored", "capture.txt"), "ignore me\n");
279
+
280
+ return { mode: "packed", cwd: piProject, installDir, provider, model, extensionPath: `./node_modules/${PACKAGE_NAME}` };
281
+ }
282
+
283
+ function prepareSourceProject({ provider, model }) {
284
+ const extensionPath = resolve("extensions/oracle/index.ts");
285
+ return { mode: "source", cwd: process.cwd(), installDir: undefined, provider, model, extensionPath };
286
+ }
287
+
288
+ function hasCreatedJobArchive(jobsDir) {
289
+ const jobDir = latestJobDir(jobsDir);
290
+ const archivePath = parseJobArchivePath(jobDir);
291
+ return Boolean(jobDir && archivePath && existsSync(archivePath));
292
+ }
293
+
294
+ async function runDirectOracleSubmit({ prepared, agentDir, sessionDir, jobsDir, outDir, timeoutMs }) {
295
+ mkdirSync(outDir, { recursive: true });
296
+ const sessionFile = join(sessionDir, "real-smoke-session.jsonl");
297
+ const fakeWorkerPath = join(outDir, "fake-worker.mjs");
298
+ const scriptPath = join(outDir, "direct-submit.mjs");
299
+ const packageRoot = prepared.mode === "packed" ? join(prepared.cwd, "node_modules", PACKAGE_NAME) : process.cwd();
300
+ const toolsUrl = pathToFileURL(join(packageRoot, "extensions", "oracle", "lib", "tools.ts")).href;
301
+ mkdirSync(sessionDir, { recursive: true });
302
+ writeFileSync(sessionFile, "");
303
+ writeFileSync(fakeWorkerPath, "process.exit(0);\n");
304
+ writeFileSync(scriptPath, `
305
+ import { registerOracleTools } from ${JSON.stringify(toolsUrl)};
306
+ const tools = new Map();
307
+ const pi = {
308
+ on() { return undefined; },
309
+ registerTool(tool) { tools.set(tool.name, tool); },
310
+ };
311
+ registerOracleTools(pi, ${JSON.stringify(fakeWorkerPath)});
312
+ const submit = tools.get("oracle_submit");
313
+ if (!submit) throw new Error("oracle_submit was not registered by the installed package");
314
+ const ctx = {
315
+ cwd: process.cwd(),
316
+ sessionManager: { getSessionFile() { return ${JSON.stringify(sessionFile)}; } },
317
+ };
318
+ const result = await submit.execute("real-smoke", {
319
+ prompt: "Real smoke archive test. Reply OK if this reaches the provider.",
320
+ files: ["."],
321
+ preset: "instant",
322
+ }, new AbortController().signal, () => undefined, ctx);
323
+ console.log(JSON.stringify(result, null, 2));
324
+ `);
325
+ writeFileSync(join(outDir, "command.json"), `${JSON.stringify({ command: process.execPath, args: [tsxCli, scriptPath], cwd: prepared.cwd, mode: prepared.mode, extensionPath: prepared.extensionPath }, null, 2)}\n`);
326
+ const result = await runCommand(process.execPath, [tsxCli, scriptPath], {
327
+ cwd: prepared.cwd,
328
+ env: {
329
+ ...process.env,
330
+ PI_CODING_AGENT_DIR: agentDir,
331
+ PI_ORACLE_JOBS_DIR: jobsDir,
332
+ PI_TELEMETRY: process.env.PI_TELEMETRY ?? "0",
333
+ },
334
+ timeoutMs,
335
+ });
336
+ writeRunResult(outDir, "direct-submit", result);
337
+ if (result.timedOut) throw new Error(`direct oracle_submit timed out after ${timeoutMs}ms`);
338
+ if (result.code !== 0) throw new Error(`direct oracle_submit exited ${result.code}; see ${join(outDir, "direct-submit.stderr.txt")}`);
339
+ return result;
340
+ }
341
+
342
+ async function runPiAgent({ prepared, agentDir, sessionDir, jobsDir, prompt, outDir, label, timeoutMs, stopAfterJobArchive = false }) {
343
+ mkdirSync(outDir, { recursive: true });
344
+ const args = [
345
+ "--print",
346
+ "--provider", prepared.provider,
347
+ "--model", prepared.model,
348
+ "--session-dir", sessionDir,
349
+ "--tools", "oracle_submit",
350
+ ];
351
+ if (prepared.mode === "source") args.push("--no-extensions", "-e", prepared.extensionPath);
352
+ args.push(prompt);
353
+
354
+ writeFileSync(join(outDir, "command.json"), `${JSON.stringify({ command: piCommand(), args, cwd: prepared.cwd, mode: prepared.mode, extensionPath: prepared.extensionPath, provider: prepared.provider, model: prepared.model }, null, 2)}\n`);
355
+ const result = await runCommand(piCommand(), args, {
356
+ cwd: prepared.cwd,
357
+ env: {
358
+ ...process.env,
359
+ PI_CODING_AGENT_DIR: agentDir,
360
+ PI_ORACLE_JOBS_DIR: jobsDir,
361
+ PI_TELEMETRY: process.env.PI_TELEMETRY ?? "0",
362
+ },
363
+ timeoutMs,
364
+ until: stopAfterJobArchive ? () => hasCreatedJobArchive(jobsDir) : undefined,
365
+ });
366
+ writeRunResult(outDir, "pi-agent", result);
367
+ if (result.timedOut) throw new Error(`${label} pi agent timed out after ${timeoutMs}ms`);
368
+ if (!result.stoppedAfterCondition && result.code !== 0) throw new Error(`${label} pi agent exited ${result.code}; see ${join(outDir, "pi-agent.stderr.txt")}`);
369
+ return result;
370
+ }
371
+
372
+ async function run(mode = "packed") {
373
+ const provider = env("PI_ORACLE_REAL_TEST_PROVIDER") ?? DEFAULT_PROVIDER;
374
+ const model = env("PI_ORACLE_REAL_TEST_MODEL") ?? DEFAULT_MODEL;
375
+ const timeoutMs = Number(env("PI_ORACLE_REAL_TEST_TIMEOUT_MS") ?? DEFAULT_TIMEOUT_MS);
376
+ const artifactRoot = resolve(env("PI_ORACLE_REAL_TEST_ARTIFACT_ROOT") ?? ".artifacts/real-smoke");
377
+ const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
378
+ const runDir = join(artifactRoot, runId);
379
+ const tmpRoot = mkdtempSync(join(tmpdir(), "pi-oracle-real-smoke-"));
380
+ const assertions = [];
381
+
382
+ mkdirSync(runDir, { recursive: true });
383
+ const prepared = mode === "packed"
384
+ ? await preparePackedProject({ runDir, provider, model, timeoutMs })
385
+ : prepareSourceProject({ provider, model });
386
+ console.log(`Oracle real smoke mode=${prepared.mode} extension=${prepared.extensionPath}`);
387
+ writeFileSync(join(runDir, "run.json"), `${JSON.stringify({ runId, mode: prepared.mode, provider, model, extensionPath: prepared.extensionPath, timeoutMs, startedAt: new Date().toISOString() }, null, 2)}\n`);
388
+
389
+ function assert(id, condition, message) {
390
+ assertions.push({ id, ok: Boolean(condition), ...(condition ? {} : { error: message }) });
391
+ if (!condition) throw new Error(message);
392
+ }
393
+
394
+ try {
395
+ const test1 = join(runDir, "whole-repo-submit");
396
+ const agent1 = join(runDir, "agent1");
397
+ const sessions1 = join(runDir, "sessions1");
398
+ const jobs1 = join(runDir, "jobs1");
399
+ mkdirSync(join(agent1, "extensions", "oracle-auth-seed-profile"), { recursive: true });
400
+ mkdirSync(sessions1, { recursive: true });
401
+ mkdirSync(jobs1, { recursive: true });
402
+
403
+ const submitResult = truthy(env("PI_ORACLE_REAL_TEST_MODEL_AGENT"))
404
+ ? await runPiAgent({
405
+ prepared,
406
+ agentDir: agent1,
407
+ sessionDir: sessions1,
408
+ jobsDir: jobs1,
409
+ outDir: test1,
410
+ label: "whole-repo-submit",
411
+ timeoutMs,
412
+ stopAfterJobArchive: true,
413
+ prompt: 'Call oracle_submit directly with prompt "Real smoke archive test. Reply OK if this reaches the provider." files ["."] and preset "instant". Do not use bash, read, edit, write, or any tool except oracle_submit.',
414
+ })
415
+ : await runDirectOracleSubmit({
416
+ prepared,
417
+ agentDir: agent1,
418
+ sessionDir: sessions1,
419
+ jobsDir: jobs1,
420
+ outDir: test1,
421
+ timeoutMs,
422
+ });
423
+
424
+ const jobDir1 = latestJobDir(jobs1);
425
+ const archivePath = parseJobArchivePath(jobDir1);
426
+ writeFileSync(join(test1, "job-dir.txt"), `${jobDir1 ?? ""}\n`);
427
+ writeFileSync(join(test1, "archive-path.txt"), `${archivePath ?? ""}\n`);
428
+ assert("whole-repo-job-created", Boolean(jobDir1), `whole-repo submit did not create an oracle job; stdout=${submitResult.stdout.slice(-1000)} stderr=${submitResult.stderr.slice(-1000)}`);
429
+ assert("whole-repo-archive-created", Boolean(archivePath && existsSync(archivePath)), `whole-repo submit did not create a readable archive; jobDir=${jobDir1 ?? "<none>"} stdout=${submitResult.stdout.slice(-1000)} stderr=${submitResult.stderr.slice(-1000)}`);
430
+ const entries = await tarList(archivePath);
431
+ writeFileSync(join(test1, "archive-list.txt"), `${entries.join("\n")}\n`);
432
+ assert("archive-includes-readme", entryExists(entries, "README.md"), "archive should include README.md");
433
+ for (const excluded of [".pi", ".oracle-context", ".scratchpad.md", ".artifacts", ".crabbox", ".debug"]) {
434
+ assert(`archive-excludes-${excluded.replace(/[^a-z0-9]+/gi, "-")}`, !entryExists(entries, excluded), `archive should exclude ${excluded}`);
435
+ }
436
+ if (prepared.mode === "packed") {
437
+ assert("packed-mode-no-source-extension", !readFileSync(join(test1, "command.json"), "utf8").includes("extensions/oracle/index.ts"), "packed real smoke must not use source extension path");
438
+ }
439
+
440
+ if (truthy(env("PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK"))) {
441
+ const test2 = join(runDir, "symlink-escape");
442
+ const agent2 = join(runDir, "agent2");
443
+ const sessions2 = join(runDir, "sessions2");
444
+ const jobs2 = join(runDir, "jobs2");
445
+ const outside = join(tmpRoot, "outside");
446
+ mkdirSync(join(agent2, "extensions", "oracle-auth-seed-profile"), { recursive: true });
447
+ mkdirSync(sessions2, { recursive: true });
448
+ mkdirSync(jobs2, { recursive: true });
449
+ mkdirSync(outside, { recursive: true });
450
+ writeFileSync(join(outside, "secret.txt"), "secret\n");
451
+ const linkPath = join(prepared.mode === "packed" ? prepared.cwd : tmpRoot, "linked-outside");
452
+ try { symlinkSync(outside, linkPath, process.platform === "win32" ? "junction" : "dir"); }
453
+ catch (error) { throw new Error(`could not create symlink fixture: ${error.message}`); }
454
+ const symlinkPrepared = prepared.mode === "source" ? { ...prepared, cwd: tmpRoot } : prepared;
455
+
456
+ const symlinkResult = await runPiAgent({
457
+ prepared: symlinkPrepared,
458
+ agentDir: agent2,
459
+ sessionDir: sessions2,
460
+ jobsDir: jobs2,
461
+ outDir: test2,
462
+ label: "symlink-escape",
463
+ timeoutMs,
464
+ prompt: 'Call oracle_submit directly with prompt "Real smoke symlink escape rejection test." files ["linked-outside/secret.txt"] and preset "instant". Do not use bash, read, edit, write, or any tool except oracle_submit. The expected result is rejection because the file resolves outside the project. After the tool returns or errors, answer with one concise sentence starting with REAL_SMOKE_SYMLINK_DONE.',
465
+ });
466
+
467
+ const jobDir2 = latestJobDir(jobs2);
468
+ writeFileSync(join(test2, "job-dir.txt"), `${jobDir2 ?? ""}\n`);
469
+ const symlinkOutput = `${symlinkResult.stdout}\n${symlinkResult.stderr}`;
470
+ assert("symlink-rejected", /resolve inside|symlink|outside|escape|must be inside/i.test(symlinkOutput), "symlink escape test output did not show the expected rejection");
471
+ assert("symlink-no-job-created", !jobDir2, "symlink escape rejection should not create an oracle job");
472
+ }
473
+
474
+ writeFileSync(join(runDir, "assertions.json"), `${JSON.stringify({ ok: true, assertions, completedAt: new Date().toISOString() }, null, 2)}\n`);
475
+ console.log(`Oracle real smoke passed: ${runDir}`);
476
+ } catch (error) {
477
+ assertions.push({ id: "run-error", ok: false, error: error.message });
478
+ writeFileSync(join(runDir, "assertions.json"), `${JSON.stringify({ ok: false, assertions, completedAt: new Date().toISOString() }, null, 2)}\n`);
479
+ writeFileSync(join(runDir, "failures.md"), `# Oracle real smoke failed\n\n${error.stack ?? error.message}\n`);
480
+ console.error(`Oracle real smoke failed: ${error.message}`);
481
+ console.error(`Artifacts: ${runDir}`);
482
+ process.exitCode = 1;
483
+ } finally {
484
+ if (!truthy(env("PI_ORACLE_REAL_TEST_KEEP_TMP"))) rmSync(tmpRoot, { recursive: true, force: true });
485
+ }
486
+ }
487
+
488
+ try {
489
+ const args = parseArgs(process.argv);
490
+ if (["-h", "--help", "help"].includes(args.command)) usage();
491
+ else if (args.command === "doctor") await doctor();
492
+ else if (args.command === "run") await run(args.mode);
493
+ else {
494
+ usage();
495
+ process.exitCode = 1;
496
+ }
497
+ } catch (error) {
498
+ console.error(error instanceof Error ? error.message : String(error));
499
+ process.exitCode = 1;
500
+ }
@@ -0,0 +1,8 @@
1
+ FROM cimg/node:24.16
2
+
3
+ USER root
4
+ RUN apt-get update \
5
+ && apt-get install -y --no-install-recommends zstd rsync \
6
+ && rm -rf /var/lib/apt/lists/*
7
+ RUN npm install -g agent-browser@0.27.0
8
+ USER circleci
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Artifact helpers for the pi-oracle Crabbox platform smoke gate.
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { relative, resolve } from "node:path";
7
+
8
+ export function createSuiteDir(artifactRoot, runId, targetName, suiteName) {
9
+ const dir = resolve(process.cwd(), artifactRoot, runId, targetName, suiteName);
10
+ mkdirSync(dir, { recursive: true });
11
+ return dir;
12
+ }
13
+
14
+ export function writeCommand(dir, command) {
15
+ writeFileSync(resolve(dir, "command.txt"), `${command}\n`);
16
+ }
17
+
18
+ export function writeExitCode(dir, code, signal) {
19
+ writeFileSync(resolve(dir, "exit-code.txt"), `code=${code}\nsignal=${signal ?? "none"}\n`);
20
+ }
21
+
22
+ export function writeSummary(dir, data) {
23
+ writeFileSync(resolve(dir, "summary.json"), JSON.stringify({ ...data, writtenAt: new Date().toISOString() }, null, 2));
24
+ }
25
+
26
+ export function writeManifest(dir, expectedFiles) {
27
+ const present = [];
28
+ function walk(current) {
29
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
30
+ const fullPath = resolve(current, entry.name);
31
+ if (entry.isDirectory()) walk(fullPath);
32
+ else if (entry.isFile()) present.push(relative(dir, fullPath));
33
+ }
34
+ }
35
+ if (existsSync(dir)) walk(dir);
36
+ if (!present.includes("artifact-manifest.json")) present.push("artifact-manifest.json");
37
+ const expected = [...new Set([...(expectedFiles ?? []), "artifact-manifest.json"])];
38
+ const manifest = {
39
+ expected,
40
+ present,
41
+ missing: expected.filter((file) => !present.includes(file)),
42
+ writtenAt: new Date().toISOString(),
43
+ };
44
+ writeFileSync(resolve(dir, "artifact-manifest.json"), JSON.stringify(manifest, null, 2));
45
+ return manifest;
46
+ }
47
+
48
+ export function scanForSecrets(text) {
49
+ const violations = [];
50
+ for (const [pattern, label] of [
51
+ [/bearer\s+[A-Za-z0-9\-._~+/]{20,}=*/gi, "bearer token"],
52
+ [/Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}=*/gi, "authorization header"],
53
+ [/connect\.sid=[A-Za-z0-9%]+/gi, "session cookie"],
54
+ [/"(?:apiKey|accessToken|refreshToken|session|cookie|password)"\s*:\s*"[^"\s]{12,}"/gi, "auth/token JSON field"],
55
+ [/SWEET_COOKIE_(?:CHROME|BRAVE|EDGE)_SAFE_STORAGE_PASSWORD=[^\s]+/gi, "Sweet Cookie safe-storage password"],
56
+ [/[A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)=[A-Za-z0-9_./+~=-]{16,}/g, "secret environment assignment"],
57
+ [/\bsk-[A-Za-z0-9_-]{20,}\b/g, "OpenAI-style API key"],
58
+ [/\bAIza[0-9A-Za-z_-]{20,}\b/g, "Google-style API key"],
59
+ ]) {
60
+ if (pattern.test(text)) violations.push(`potential ${label}`);
61
+ }
62
+ return violations;
63
+ }
64
+
65
+ export function scanArtifacts(dir) {
66
+ const findings = [];
67
+ function walk(current) {
68
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
69
+ const fullPath = resolve(current, entry.name);
70
+ if (entry.isDirectory()) {
71
+ walk(fullPath);
72
+ continue;
73
+ }
74
+ if (!entry.isFile()) continue;
75
+ const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
76
+ if (!["txt", "json", "jsonl", "md", "log", "ansi", "html", "yml", "yaml", "js", "mjs", "ts"].includes(ext)) continue;
77
+ try {
78
+ const content = readFileSync(fullPath, "utf8");
79
+ for (const violation of scanForSecrets(content)) findings.push({ file: relative(dir, fullPath), violation });
80
+ } catch {
81
+ // Ignore unreadable/binary artifacts.
82
+ }
83
+ }
84
+ }
85
+ if (existsSync(dir)) walk(dir);
86
+ return findings;
87
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Minimal assertion writer for platform smoke artifacts.
3
+ */
4
+
5
+ import { writeFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+
8
+ export function runAssertions(dir, checks) {
9
+ let ok = true;
10
+ const results = [];
11
+ for (const check of checks) {
12
+ try {
13
+ const passed = Boolean(check.fn());
14
+ if (!passed) ok = false;
15
+ results.push({ id: check.id, ok: passed, ...(passed ? {} : { error: check.error ?? "failed" }) });
16
+ } catch (error) {
17
+ ok = false;
18
+ results.push({ id: check.id, ok: false, error: error instanceof Error ? error.message : String(error) });
19
+ }
20
+ }
21
+ const assertions = { ok, checks: results, writtenAt: new Date().toISOString() };
22
+ writeFileSync(resolve(dir, "assertions.json"), JSON.stringify(assertions, null, 2));
23
+ if (!ok) {
24
+ const lines = [
25
+ "# Assertion Failures",
26
+ "",
27
+ ...results.filter((result) => !result.ok).map((result) => `- **${result.id}**: ${result.error ?? "failed"}`),
28
+ "",
29
+ `Total: ${results.filter((result) => !result.ok).length} failure(s)`,
30
+ ];
31
+ writeFileSync(resolve(dir, "failures.md"), `${lines.join("\n")}\n`);
32
+ }
33
+ return assertions;
34
+ }