@riddledc/riddle-proof 0.8.30 → 0.8.32
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/dist/advanced/engine-harness.cjs +132 -10
- package/dist/advanced/engine-harness.js +2 -2
- package/dist/advanced/index.cjs +132 -10
- package/dist/advanced/index.d.cts +2 -2
- package/dist/advanced/index.d.ts +2 -2
- package/dist/advanced/index.js +4 -4
- package/dist/advanced/proof-run-core.cjs +3 -1
- package/dist/advanced/proof-run-core.d.cts +1 -1
- package/dist/advanced/proof-run-core.d.ts +1 -1
- package/dist/advanced/proof-run-core.js +1 -1
- package/dist/advanced/proof-run-engine.cjs +80 -1
- package/dist/advanced/proof-run-engine.d.cts +2 -2
- package/dist/advanced/proof-run-engine.d.ts +2 -2
- package/dist/advanced/proof-run-engine.js +2 -2
- package/dist/advanced/runner.js +2 -2
- package/dist/{chunk-3OTO7IDH.js → chunk-C2NHHBFV.js} +1 -1
- package/dist/{chunk-32RE64IO.js → chunk-IOI6QR3B.js} +78 -1
- package/dist/{chunk-XJA2GDVN.js → chunk-U73JPBZW.js} +1 -1
- package/dist/{chunk-K6HZUSHH.js → chunk-X7SQTCIQ.js} +3 -1
- package/dist/{chunk-UWO4YR7I.js → chunk-ZREWMTFA.js} +53 -10
- package/dist/cli/index.js +3 -3
- package/dist/cli.cjs +132 -10
- package/dist/cli.js +3 -3
- package/dist/engine-harness.cjs +132 -10
- package/dist/engine-harness.js +2 -2
- package/dist/index.cjs +132 -10
- package/dist/index.js +3 -3
- package/dist/{proof-run-core-C8FDUhle.d.cts → proof-run-core-B1GeqkR8.d.cts} +2 -0
- package/dist/{proof-run-core-C8FDUhle.d.ts → proof-run-core-B1GeqkR8.d.ts} +2 -0
- package/dist/proof-run-core.cjs +3 -1
- package/dist/proof-run-core.d.cts +1 -1
- package/dist/proof-run-core.d.ts +1 -1
- package/dist/proof-run-core.js +1 -1
- package/dist/{proof-run-engine-By7oLsF-.d.ts → proof-run-engine-DYfmd8d7.d.ts} +4 -4
- package/dist/{proof-run-engine-D80hVFMf.d.cts → proof-run-engine-DeHxtGnW.d.cts} +4 -4
- package/dist/proof-run-engine.cjs +80 -1
- package/dist/proof-run-engine.d.cts +2 -2
- package/dist/proof-run-engine.d.ts +2 -2
- package/dist/proof-run-engine.js +2 -2
- package/dist/runner.js +2 -2
- package/lib/workspace-core.mjs +62 -7
- package/package.json +2 -2
- package/runtime/lib/riddle_core_call.mjs +662 -40
- package/runtime/lib/ship.py +363 -16
- package/runtime/lib/util.py +117 -40
- package/runtime/lib/verify.py +4 -3
- package/runtime/pipelines/riddle-proof-ship.lobster +11 -1
- package/runtime/tests/recon_verify_smoke.py +132 -0
- package/runtime/tests/ship_artifact_publication.py +185 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { createServer } from "node:net";
|
|
3
6
|
import { homedir } from "node:os";
|
|
4
7
|
import { join } from "node:path";
|
|
5
8
|
import { pathToFileURL } from "node:url";
|
|
6
9
|
|
|
7
10
|
const TOOL_DEFAULT_INCLUDE = ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"];
|
|
11
|
+
const DEFAULT_BASE_URL = "https://api.riddledc.com";
|
|
12
|
+
const DEFAULT_API_KEY_FILE = "/tmp/riddle-api-key";
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
8
14
|
|
|
9
15
|
function readJson(path) {
|
|
10
16
|
try {
|
|
@@ -33,39 +39,648 @@ function readOpenClawPluginConfig() {
|
|
|
33
39
|
function buildConfig() {
|
|
34
40
|
const pluginCfg = readOpenClawPluginConfig();
|
|
35
41
|
return {
|
|
36
|
-
apiKey: process.env.RIDDLE_API_KEY || pluginCfg.apiKey,
|
|
37
|
-
baseUrl: pluginCfg.baseUrl ||
|
|
42
|
+
apiKey: process.env.RIDDLE_API_KEY || readApiKeyFile() || pluginCfg.apiKey,
|
|
43
|
+
baseUrl: process.env.RIDDLE_API_BASE_URL || pluginCfg.baseUrl || DEFAULT_BASE_URL,
|
|
38
44
|
workspace: process.env.OPENCLAW_WORKSPACE || process.cwd(),
|
|
39
45
|
};
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
function readApiKeyFile() {
|
|
49
|
+
const keyFile = process.env.RIDDLE_API_KEY_FILE || DEFAULT_API_KEY_FILE;
|
|
50
|
+
try {
|
|
51
|
+
const key = readFileSync(keyFile, "utf8").trim();
|
|
52
|
+
return key || "";
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireConfig(config) {
|
|
59
|
+
const apiKey = config.apiKey || process.env.RIDDLE_API_KEY || readApiKeyFile();
|
|
60
|
+
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
61
|
+
if (!apiKey) {
|
|
62
|
+
throw new Error("Missing Riddle API key. Set RIDDLE_API_KEY or RIDDLE_API_KEY_FILE before running Riddle Proof direct browser evidence.");
|
|
63
|
+
}
|
|
64
|
+
assertAllowedBaseUrl(baseUrl);
|
|
65
|
+
return { apiKey, baseUrl, workspace: config.workspace || process.cwd() };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertAllowedBaseUrl(baseUrl) {
|
|
69
|
+
const url = new URL(baseUrl);
|
|
70
|
+
if (url.protocol !== "https:") throw new Error("Riddle baseUrl must be https: " + baseUrl);
|
|
71
|
+
if (url.hostname !== "api.riddledc.com") {
|
|
72
|
+
throw new Error("Refusing to use non-official Riddle host: " + url.hostname);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function detectMode(payload) {
|
|
77
|
+
if (payload.url) return "url";
|
|
78
|
+
if (payload.urls) return "urls";
|
|
79
|
+
if (payload.steps) return "steps";
|
|
80
|
+
if (payload.script) return "script";
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function abToBase64(ab) {
|
|
85
|
+
return Buffer.from(ab).toString("base64");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function truthy(value) {
|
|
93
|
+
return String(value || "").toLowerCase().match(/^(1|true|yes|y|on)$/);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function localRiddleScriptAllowed() {
|
|
97
|
+
const runner = String(process.env.RIDDLE_PROOF_BROWSER_RUNNER || "").toLowerCase();
|
|
98
|
+
if (runner === "hosted" || runner === "riddle") return false;
|
|
99
|
+
if (String(process.env.RIDDLE_PROOF_DISABLE_LOCAL_RIDDLE_SCRIPT || "").toLowerCase().match(/^(1|true|yes)$/)) return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function localRiddleScriptPreferred() {
|
|
104
|
+
const runner = String(process.env.RIDDLE_PROOF_BROWSER_RUNNER || "").toLowerCase();
|
|
105
|
+
return runner === "local" || runner === "playwright" || runner === "local-playwright";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sanitizeArtifactName(input, fallback = "artifact") {
|
|
109
|
+
return String(input || fallback)
|
|
110
|
+
.trim()
|
|
111
|
+
.replace(/^\.\.?(\/|\\)/g, "")
|
|
112
|
+
.replace(/[/\\]/g, "-")
|
|
113
|
+
.replace(/\s+/g, "-")
|
|
114
|
+
.replace(/[^a-zA-Z0-9._-]/g, "-")
|
|
115
|
+
.replace(/-+/g, "-")
|
|
116
|
+
.replace(/^-|-$/g, "") || fallback;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function localArtifactRoot() {
|
|
120
|
+
const root = process.env.RIDDLE_PROOF_LOCAL_ARTIFACT_DIR || join(process.cwd(), "artifacts", "riddle-proof", "local-browser");
|
|
121
|
+
const id = `${new Date().toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`;
|
|
122
|
+
const dir = join(root, id);
|
|
123
|
+
mkdirSync(dir, { recursive: true });
|
|
124
|
+
mkdirSync(join(dir, "screenshots"), { recursive: true });
|
|
125
|
+
return dir;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function loadLocalPlaywright() {
|
|
129
|
+
const packagePaths = [
|
|
130
|
+
process.cwd(),
|
|
131
|
+
join(process.cwd(), "packages", "riddle-proof-runner-playwright"),
|
|
132
|
+
join(process.cwd(), "..", "riddle-proof-runner-playwright"),
|
|
133
|
+
process.env.RIDDLE_PROOF_PLAYWRIGHT_PACKAGE_DIR || "",
|
|
134
|
+
].filter(Boolean);
|
|
135
|
+
|
|
136
|
+
let resolved = "";
|
|
137
|
+
try {
|
|
138
|
+
resolved = require.resolve("playwright");
|
|
139
|
+
} catch {
|
|
140
|
+
for (const packagePath of packagePaths) {
|
|
141
|
+
try {
|
|
142
|
+
resolved = require.resolve("playwright", { paths: [packagePath] });
|
|
143
|
+
break;
|
|
144
|
+
} catch {
|
|
145
|
+
// Try next path.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!resolved) {
|
|
150
|
+
throw new Error("Playwright is required for local Riddle Proof browser evidence. Install playwright or set RIDDLE_PROOF_BROWSER_RUNNER=hosted with a Riddle API key.");
|
|
151
|
+
}
|
|
152
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
153
|
+
return mod.default || mod;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function runLocalRiddleScript(payload, defaults = {}, blockedHostedReason = "") {
|
|
157
|
+
if (!payload.script || typeof payload.script !== "string") {
|
|
158
|
+
return { ok: false, error: blockedHostedReason || "Local Riddle Proof browser evidence requires a script payload." };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const outputs = [];
|
|
162
|
+
const screenshots = [];
|
|
163
|
+
const consoleMessages = [];
|
|
164
|
+
const resultArtifacts = {};
|
|
165
|
+
const outputDir = localArtifactRoot();
|
|
166
|
+
const playwright = await loadLocalPlaywright();
|
|
167
|
+
const browserName = payload.browser || "chromium";
|
|
168
|
+
const browserType = playwright[browserName] || playwright.chromium;
|
|
169
|
+
const browser = await browserType.launch({ headless: payload.headful !== true });
|
|
170
|
+
const context = await browser.newContext({
|
|
171
|
+
viewport: payload.viewport || undefined,
|
|
172
|
+
});
|
|
173
|
+
const page = await context.newPage();
|
|
174
|
+
const timeoutMs = Number(payload.timeout_sec ?? payload.timeoutSec ?? 60) * 1000;
|
|
175
|
+
page.setDefaultTimeout(Math.min(timeoutMs, 120_000));
|
|
176
|
+
page.setDefaultNavigationTimeout(Math.min(timeoutMs, 120_000));
|
|
177
|
+
page.on("console", (message) => {
|
|
178
|
+
consoleMessages.push({ type: message.type(), text: message.text() });
|
|
179
|
+
});
|
|
180
|
+
page.on("pageerror", (error) => {
|
|
181
|
+
consoleMessages.push({ type: "pageerror", text: error?.message || String(error) });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const saveScreenshot = async (label = "screenshot", options = {}) => {
|
|
185
|
+
const name = sanitizeArtifactName(label, "screenshot").replace(/\.(png|jpg|jpeg)$/i, "") + ".png";
|
|
186
|
+
const path = join(outputDir, "screenshots", name);
|
|
187
|
+
const buffer = await page.screenshot({ fullPage: options?.fullPage === true });
|
|
188
|
+
writeFileSync(path, buffer);
|
|
189
|
+
const item = {
|
|
190
|
+
name,
|
|
191
|
+
url: pathToFileURL(path).href,
|
|
192
|
+
path,
|
|
193
|
+
size: buffer.byteLength,
|
|
194
|
+
};
|
|
195
|
+
outputs.push({ name, url: item.url, path, size_bytes: buffer.byteLength });
|
|
196
|
+
screenshots.push(item);
|
|
197
|
+
return { name, url: item.url, path };
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const saveJson = async (name = "artifact", value = {}) => {
|
|
201
|
+
const fileName = sanitizeArtifactName(name, "artifact").replace(/\.json$/i, "") + ".json";
|
|
202
|
+
const path = join(outputDir, fileName);
|
|
203
|
+
writeFileSync(path, JSON.stringify(value, null, 2));
|
|
204
|
+
const item = { name: fileName, url: pathToFileURL(path).href, path };
|
|
205
|
+
outputs.push(item);
|
|
206
|
+
resultArtifacts[fileName] = value;
|
|
207
|
+
return item;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const previousEvidence = globalThis.__riddleProofEvidence;
|
|
211
|
+
globalThis.__riddleProofEvidence = undefined;
|
|
212
|
+
let scriptResult;
|
|
213
|
+
let proofEvidence;
|
|
214
|
+
try {
|
|
215
|
+
const runnerFactory = Object.getPrototypeOf(async function () {}).constructor;
|
|
216
|
+
const runScript = new runnerFactory("page", "saveScreenshot", "saveJson", "url", payload.script);
|
|
217
|
+
scriptResult = await Promise.race([
|
|
218
|
+
runScript(page, saveScreenshot, saveJson, payload.url || ""),
|
|
219
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`local riddle_script timed out after ${timeoutMs}ms`)), timeoutMs + 1000)),
|
|
220
|
+
]);
|
|
221
|
+
proofEvidence = globalThis.__riddleProofEvidence;
|
|
222
|
+
} catch (err) {
|
|
223
|
+
const failedProofEvidence = globalThis.__riddleProofEvidence;
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
mode: "script",
|
|
227
|
+
runner: "local-playwright",
|
|
228
|
+
error: err instanceof Error ? err.message : String(err),
|
|
229
|
+
script_error: err instanceof Error ? err.stack || err.message : String(err),
|
|
230
|
+
outputs,
|
|
231
|
+
screenshots,
|
|
232
|
+
screenshot: screenshots[0],
|
|
233
|
+
console: consoleMessages,
|
|
234
|
+
result: failedProofEvidence ? { proofEvidence: failedProofEvidence } : undefined,
|
|
235
|
+
};
|
|
236
|
+
} finally {
|
|
237
|
+
try {
|
|
238
|
+
await context.close();
|
|
239
|
+
await browser.close();
|
|
240
|
+
} finally {
|
|
241
|
+
globalThis.__riddleProofEvidence = previousEvidence;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let result = typeof scriptResult === "object" && scriptResult !== null && !Array.isArray(scriptResult)
|
|
246
|
+
? scriptResult
|
|
247
|
+
: scriptResult !== undefined
|
|
248
|
+
? { value: scriptResult }
|
|
249
|
+
: {};
|
|
250
|
+
if (proofEvidence !== undefined && result.proofEvidence === undefined && result.proof_evidence === undefined) {
|
|
251
|
+
result = { ...result, proofEvidence };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!outputs.find((item) => item.name === "result.json")) {
|
|
255
|
+
await saveJson("result", result);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
mode: "script",
|
|
261
|
+
runner: "local-playwright",
|
|
262
|
+
local: true,
|
|
263
|
+
outputs,
|
|
264
|
+
screenshots,
|
|
265
|
+
screenshot: screenshots[0],
|
|
266
|
+
console: consoleMessages,
|
|
267
|
+
result,
|
|
268
|
+
resultArtifacts,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function localServerPreviewAllowed() {
|
|
273
|
+
const runner = String(process.env.RIDDLE_PROOF_BROWSER_RUNNER || "").toLowerCase();
|
|
274
|
+
if (runner === "hosted" || runner === "riddle") return false;
|
|
275
|
+
if (truthy(process.env.RIDDLE_PROOF_DISABLE_LOCAL_SERVER_PREVIEW)) return false;
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function isPortAvailable(port) {
|
|
280
|
+
return new Promise((resolve) => {
|
|
281
|
+
const server = createServer();
|
|
282
|
+
server.once("error", () => resolve(false));
|
|
283
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
284
|
+
server.listen(port, "127.0.0.1");
|
|
285
|
+
});
|
|
45
286
|
}
|
|
46
287
|
|
|
47
|
-
async function
|
|
288
|
+
async function availablePort(preferred) {
|
|
289
|
+
const start = numberValue(preferred, 3000);
|
|
290
|
+
for (let offset = 0; offset < 50; offset += 1) {
|
|
291
|
+
const port = start + offset;
|
|
292
|
+
if (await isPortAvailable(port)) return port;
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`No available local preview port near ${start}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function appendLimited(lines, value, maxLength = 20_000) {
|
|
298
|
+
if (!value) return;
|
|
299
|
+
lines.push(String(value));
|
|
300
|
+
while (lines.join("").length > maxLength && lines.length > 1) lines.shift();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function localPreviewUrl(port, targetPath = "/") {
|
|
304
|
+
return new URL(targetPath || "/", `http://127.0.0.1:${port}/`).href;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function waitForReadiness(url, timeoutMs, child, stdoutLines, stderrLines) {
|
|
308
|
+
const start = Date.now();
|
|
309
|
+
let lastError = "";
|
|
310
|
+
while (Date.now() - start < timeoutMs) {
|
|
311
|
+
if (child.exitCode !== null) {
|
|
312
|
+
const tail = [stdoutLines.join(""), stderrLines.join("")].filter(Boolean).join("\n").slice(-2000);
|
|
313
|
+
throw new Error(`local server preview command exited before readiness (${child.exitCode}). ${tail}`.trim());
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
317
|
+
if (response.status < 500) return;
|
|
318
|
+
lastError = `HTTP ${response.status}`;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
321
|
+
}
|
|
322
|
+
await sleep(500);
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`local server preview was not ready at ${url} within ${timeoutMs}ms${lastError ? `: ${lastError}` : ""}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function terminateChild(child) {
|
|
328
|
+
if (!child || child.exitCode !== null) return;
|
|
48
329
|
try {
|
|
49
|
-
|
|
50
|
-
} catch
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
330
|
+
if (child.pid) process.kill(-child.pid, "SIGTERM");
|
|
331
|
+
} catch {
|
|
332
|
+
try {
|
|
333
|
+
child.kill("SIGTERM");
|
|
334
|
+
} catch {
|
|
335
|
+
// Best effort cleanup.
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
setTimeout(() => {
|
|
339
|
+
try {
|
|
340
|
+
if (child.exitCode === null && child.pid) process.kill(-child.pid, "SIGKILL");
|
|
341
|
+
} catch {
|
|
342
|
+
// Best effort cleanup.
|
|
343
|
+
}
|
|
344
|
+
}, 2000).unref?.();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function runLocalServerPreview(rawArgs) {
|
|
348
|
+
if (!localServerPreviewAllowed()) {
|
|
349
|
+
return { ok: false, error: "Local Riddle Proof server preview is disabled." };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const args = normalizeServerArgs(rawArgs);
|
|
353
|
+
const directory = String(args.directory || "").trim();
|
|
354
|
+
const command = String(args.command || "").trim();
|
|
355
|
+
if (!directory || !existsSync(directory)) {
|
|
356
|
+
return { ok: false, error: `Local Riddle Proof server preview directory does not exist: ${directory || "(missing)"}` };
|
|
357
|
+
}
|
|
358
|
+
if (!command) {
|
|
359
|
+
return { ok: false, error: "Local Riddle Proof server preview requires a command." };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const port = await availablePort(args.port);
|
|
363
|
+
const previewUrl = `http://127.0.0.1:${port}/`;
|
|
364
|
+
const targetUrl = localPreviewUrl(port, args.path || "/");
|
|
365
|
+
const readinessUrl = localPreviewUrl(port, args.readiness_path || args.path || "/");
|
|
366
|
+
const readinessTimeoutMs = numberValue(args.readiness_timeout ?? args.readinessTimeout, 120) * 1000;
|
|
367
|
+
const stdoutLines = [];
|
|
368
|
+
const stderrLines = [];
|
|
369
|
+
const env = {
|
|
370
|
+
...process.env,
|
|
371
|
+
...(args.env || {}),
|
|
372
|
+
PORT: String(port),
|
|
373
|
+
HOSTNAME: String((args.env || {}).HOSTNAME || "127.0.0.1"),
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const child = spawn(command, {
|
|
377
|
+
cwd: directory,
|
|
378
|
+
env,
|
|
379
|
+
shell: true,
|
|
380
|
+
detached: true,
|
|
381
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
382
|
+
});
|
|
383
|
+
child.stdout?.on("data", (chunk) => appendLimited(stdoutLines, chunk.toString()));
|
|
384
|
+
child.stderr?.on("data", (chunk) => appendLimited(stderrLines, chunk.toString()));
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await waitForReadiness(readinessUrl, readinessTimeoutMs, child, stdoutLines, stderrLines);
|
|
388
|
+
const waitUntil = String(args.wait_until || args.waitUntil || "domcontentloaded");
|
|
389
|
+
const captureTimeoutSec = numberValue(args.timeout_sec ?? args.timeout, 300);
|
|
390
|
+
const scriptParts = [
|
|
391
|
+
`await page.goto(${JSON.stringify(targetUrl)}, { waitUntil: ${JSON.stringify(waitUntil)}, timeout: ${JSON.stringify(captureTimeoutSec * 1000)} });`,
|
|
57
392
|
];
|
|
393
|
+
if (args.wait_for_selector || args.waitForSelector) {
|
|
394
|
+
scriptParts.push(`await page.waitForSelector(${JSON.stringify(args.wait_for_selector || args.waitForSelector)}, { timeout: 30000 });`);
|
|
395
|
+
}
|
|
396
|
+
if (args.script && String(args.script).trim()) {
|
|
397
|
+
scriptParts.push(String(args.script).trim());
|
|
398
|
+
} else {
|
|
399
|
+
scriptParts.push("await saveScreenshot('after-proof');");
|
|
400
|
+
}
|
|
401
|
+
const capture = await runLocalRiddleScript({
|
|
402
|
+
...args,
|
|
403
|
+
url: targetUrl,
|
|
404
|
+
script: scriptParts.join("\n"),
|
|
405
|
+
timeout_sec: captureTimeoutSec,
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
...capture,
|
|
409
|
+
local: true,
|
|
410
|
+
runner: "local-server-preview",
|
|
411
|
+
preview_url: previewUrl,
|
|
412
|
+
target_url: targetUrl,
|
|
413
|
+
readiness_url: readinessUrl,
|
|
414
|
+
port,
|
|
415
|
+
stdout: stdoutLines.join("").slice(-8000),
|
|
416
|
+
stderr: stderrLines.join("").slice(-8000),
|
|
417
|
+
};
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
mode: "server_preview",
|
|
422
|
+
runner: "local-server-preview",
|
|
423
|
+
error: err instanceof Error ? err.message : String(err),
|
|
424
|
+
preview_url: previewUrl,
|
|
425
|
+
target_url: targetUrl,
|
|
426
|
+
readiness_url: readinessUrl,
|
|
427
|
+
port,
|
|
428
|
+
stdout: stdoutLines.join("").slice(-8000),
|
|
429
|
+
stderr: stderrLines.join("").slice(-8000),
|
|
430
|
+
};
|
|
431
|
+
} finally {
|
|
432
|
+
terminateChild(child);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function postRun(baseUrl, apiKey, payload) {
|
|
437
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/run`, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: {
|
|
440
|
+
Authorization: `Bearer ${apiKey}`,
|
|
441
|
+
"Content-Type": "application/json",
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify(payload),
|
|
444
|
+
});
|
|
445
|
+
return {
|
|
446
|
+
contentType: response.headers.get("content-type"),
|
|
447
|
+
body: await response.arrayBuffer(),
|
|
448
|
+
headers: response.headers,
|
|
449
|
+
status: response.status,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
58
452
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
453
|
+
async function pollJobStatus(config, jobId, maxWaitMs) {
|
|
454
|
+
const start = Date.now();
|
|
455
|
+
const pollIntervalMs = 2000;
|
|
456
|
+
|
|
457
|
+
while (Date.now() - start < maxWaitMs) {
|
|
458
|
+
const response = await fetch(`${config.baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}`, {
|
|
459
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
460
|
+
});
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
return { status: "poll_error", error: "HTTP " + response.status };
|
|
463
|
+
}
|
|
464
|
+
const data = await response.json();
|
|
465
|
+
if (["completed", "completed_timeout", "completed_error", "failed"].includes(data.status)) {
|
|
466
|
+
return data;
|
|
62
467
|
}
|
|
468
|
+
await sleep(pollIntervalMs);
|
|
469
|
+
}
|
|
63
470
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
471
|
+
return { status: "poll_timeout", error: `Job ${jobId} did not complete within ${maxWaitMs}ms` };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function fetchArtifactsAndBuild(config, jobId, include) {
|
|
475
|
+
const response = await fetch(
|
|
476
|
+
`${config.baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}/artifacts?include=${include.join(",")}`,
|
|
477
|
+
{ headers: { Authorization: `Bearer ${config.apiKey}` } }
|
|
478
|
+
);
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
return { error: "Artifacts fetch failed: HTTP " + response.status };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const data = await response.json();
|
|
484
|
+
const artifacts = data.artifacts || [];
|
|
485
|
+
const result = { outputs: artifacts };
|
|
486
|
+
if (data.status) result._artifactsStatus = data.status;
|
|
487
|
+
if (data.timeout) result._timeout = data.timeout;
|
|
488
|
+
if (data.error) result._error = data.error;
|
|
489
|
+
|
|
490
|
+
const screenshots = artifacts.filter((artifact) => artifact.name && /\.(png|jpg|jpeg)$/i.test(artifact.name));
|
|
491
|
+
if (screenshots.length > 0) {
|
|
492
|
+
result.screenshots = [];
|
|
493
|
+
for (const screenshot of screenshots) {
|
|
494
|
+
if (!screenshot.url) continue;
|
|
495
|
+
try {
|
|
496
|
+
const imageResponse = await fetch(screenshot.url);
|
|
497
|
+
if (imageResponse.ok) {
|
|
498
|
+
const buffer = await imageResponse.arrayBuffer();
|
|
499
|
+
result.screenshots.push({
|
|
500
|
+
name: screenshot.name,
|
|
501
|
+
data: `data:image/png;base64,${Buffer.from(buffer).toString("base64")}`,
|
|
502
|
+
size: buffer.byteLength,
|
|
503
|
+
url: screenshot.url,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
// Screenshot artifacts are optional diagnostics.
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (result.screenshots.length > 0) {
|
|
511
|
+
result.screenshot = result.screenshots[0];
|
|
512
|
+
}
|
|
68
513
|
}
|
|
514
|
+
|
|
515
|
+
const consoleArtifact = artifacts.find((artifact) => artifact.name === "console.json");
|
|
516
|
+
if (consoleArtifact?.url) {
|
|
517
|
+
try {
|
|
518
|
+
const consoleResponse = await fetch(consoleArtifact.url);
|
|
519
|
+
if (consoleResponse.ok) result.console = await consoleResponse.json();
|
|
520
|
+
} catch {
|
|
521
|
+
// Optional diagnostic.
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const resultArtifact = artifacts.find((artifact) => artifact.name === "result.json");
|
|
526
|
+
if (resultArtifact?.url) {
|
|
527
|
+
try {
|
|
528
|
+
const resultResponse = await fetch(resultArtifact.url);
|
|
529
|
+
if (resultResponse.ok) result.result = await resultResponse.json();
|
|
530
|
+
} catch {
|
|
531
|
+
// Optional diagnostic.
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const visualDiffArtifact = artifacts.find((artifact) => artifact.name === "visual-diff.json");
|
|
536
|
+
if (include.includes("visual_diff") && visualDiffArtifact?.url) {
|
|
537
|
+
try {
|
|
538
|
+
const visualDiffResponse = await fetch(visualDiffArtifact.url);
|
|
539
|
+
if (visualDiffResponse.ok) result.visual_diff = await visualDiffResponse.json();
|
|
540
|
+
} catch {
|
|
541
|
+
// Optional diagnostic.
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const visualDiffImages = artifacts.filter((artifact) => /^visual-diff.*\.(png|jpg|jpeg)$/i.test(artifact.name || ""));
|
|
546
|
+
if (visualDiffImages.length > 0) {
|
|
547
|
+
result.visual_diff_images = visualDiffImages;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (include.includes("har")) {
|
|
551
|
+
const harArtifact = artifacts.find((artifact) => artifact.name === "network.har");
|
|
552
|
+
if (harArtifact?.url) {
|
|
553
|
+
try {
|
|
554
|
+
const harResponse = await fetch(harArtifact.url);
|
|
555
|
+
if (harResponse.ok) result.har = await harResponse.json();
|
|
556
|
+
} catch {
|
|
557
|
+
// Optional diagnostic.
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function runWithDefaults(payload, defaults = {}) {
|
|
566
|
+
if (localRiddleScriptPreferred() && payload.script && localRiddleScriptAllowed()) {
|
|
567
|
+
try {
|
|
568
|
+
return await runLocalRiddleScript(payload, defaults);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let config;
|
|
575
|
+
try {
|
|
576
|
+
config = requireConfig(buildConfig());
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (payload.script && localRiddleScriptAllowed()) {
|
|
579
|
+
try {
|
|
580
|
+
return await runLocalRiddleScript(payload, defaults, err instanceof Error ? err.message : String(err));
|
|
581
|
+
} catch (localErr) {
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
error: [
|
|
585
|
+
err instanceof Error ? err.message : String(err),
|
|
586
|
+
localErr instanceof Error ? localErr.message : String(localErr),
|
|
587
|
+
].filter(Boolean).join(" Local fallback also failed: "),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const mode = detectMode(payload);
|
|
595
|
+
const userInclude = Array.isArray(payload.include) ? payload.include : [];
|
|
596
|
+
const returnAsync = !!defaults.returnAsync;
|
|
597
|
+
const merged = { ...payload };
|
|
598
|
+
if (returnAsync) merged.sync = false;
|
|
599
|
+
merged.include = Array.from(new Set([...userInclude, ...(defaults.include || ["screenshot", "console"])]));
|
|
600
|
+
merged.inlineConsole = merged.inlineConsole ?? true;
|
|
601
|
+
merged.inlineResult = merged.inlineResult ?? true;
|
|
602
|
+
if (userInclude.includes("har")) merged.inlineHar = true;
|
|
603
|
+
|
|
604
|
+
const out = { ok: true, mode };
|
|
605
|
+
const { contentType, body, headers, status } = await postRun(config.baseUrl, config.apiKey, merged);
|
|
606
|
+
out.rawContentType = contentType || undefined;
|
|
607
|
+
|
|
608
|
+
if (status === 408) {
|
|
609
|
+
let jobId = "";
|
|
610
|
+
try {
|
|
611
|
+
jobId = JSON.parse(Buffer.from(body).toString("utf8")).job_id || "";
|
|
612
|
+
} catch {
|
|
613
|
+
// Fall through.
|
|
614
|
+
}
|
|
615
|
+
if (!jobId) {
|
|
616
|
+
return { ...out, ok: false, error: "Sync poll timed out but no job_id in 408 response" };
|
|
617
|
+
}
|
|
618
|
+
out.job_id = jobId;
|
|
619
|
+
if (returnAsync) {
|
|
620
|
+
out.status = "submitted";
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
return finishPolledRun(config, out, jobId, merged);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (status >= 400) {
|
|
627
|
+
const text = Buffer.from(body).toString("utf8");
|
|
628
|
+
try {
|
|
629
|
+
return { ...out, ok: false, error: JSON.parse(text) };
|
|
630
|
+
} catch {
|
|
631
|
+
return { ...out, ok: false, error: `HTTP ${status}: ${text.slice(0, 500)}` };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (contentType && contentType.includes("image/png")) {
|
|
636
|
+
out.rawPngBase64 = abToBase64(body);
|
|
637
|
+
out.job_id = headers.get("x-job-id") || undefined;
|
|
638
|
+
const duration = headers.get("x-duration-ms");
|
|
639
|
+
out.duration_ms = duration ? Number(duration) : undefined;
|
|
640
|
+
out.sync = true;
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const text = Buffer.from(body).toString("utf8");
|
|
645
|
+
const json = JSON.parse(text);
|
|
646
|
+
Object.assign(out, json);
|
|
647
|
+
out.job_id = json.job_id || json.jobId || out.job_id;
|
|
648
|
+
|
|
649
|
+
if (status === 202 && out.job_id && json.status_url) {
|
|
650
|
+
if (returnAsync) {
|
|
651
|
+
out.status = "submitted";
|
|
652
|
+
return out;
|
|
653
|
+
}
|
|
654
|
+
return finishPolledRun(config, out, out.job_id, merged);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (json.status === "completed_timeout" || json.status === "completed_error") {
|
|
658
|
+
out.ok = false;
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function finishPolledRun(config, out, jobId, payload) {
|
|
664
|
+
const scriptTimeoutMs = ((payload.timeout_sec ?? payload.timeoutSec ?? 60) * 1000);
|
|
665
|
+
const jobStatus = await pollJobStatus(config, jobId, scriptTimeoutMs + 30_000);
|
|
666
|
+
if (["poll_timeout", "poll_error", "failed"].includes(jobStatus.status)) {
|
|
667
|
+
out.ok = false;
|
|
668
|
+
out.status = jobStatus.status;
|
|
669
|
+
out.error = jobStatus.error || `Job ${jobStatus.status}`;
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const artifacts = await fetchArtifactsAndBuild(config, jobId, payload.include || []);
|
|
674
|
+
Object.assign(out, artifacts);
|
|
675
|
+
out.job_id = jobId;
|
|
676
|
+
out.status = jobStatus.status;
|
|
677
|
+
out.duration_ms = jobStatus.duration_ms;
|
|
678
|
+
if (jobStatus.status !== "completed") {
|
|
679
|
+
out.ok = false;
|
|
680
|
+
if (jobStatus.timeout) out.timeout = jobStatus.timeout;
|
|
681
|
+
if (jobStatus.error) out.error = jobStatus.error;
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
69
684
|
}
|
|
70
685
|
|
|
71
686
|
function numberValue(value, fallback) {
|
|
@@ -99,38 +714,35 @@ function normalizeJobId(args) {
|
|
|
99
714
|
}
|
|
100
715
|
|
|
101
716
|
async function run(tool, args) {
|
|
102
|
-
const core = await loadCore();
|
|
103
|
-
const config = buildConfig();
|
|
104
|
-
|
|
105
717
|
if (tool === "riddle_preview") {
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
718
|
+
return {
|
|
719
|
+
ok: false,
|
|
720
|
+
error: "Direct Riddle Proof preview creation is not wired in this runtime bridge yet. Use remote/no-diff evidence or add generic preview support before enabling implementation previews.",
|
|
721
|
+
};
|
|
110
722
|
}
|
|
111
723
|
|
|
112
724
|
if (tool === "riddle_preview_delete") {
|
|
113
|
-
return
|
|
725
|
+
return { ok: false, error: "Direct Riddle Proof preview deletion is not wired in this runtime bridge yet." };
|
|
114
726
|
}
|
|
115
727
|
|
|
116
728
|
if (tool === "riddle_server_preview") {
|
|
117
|
-
return
|
|
729
|
+
return runLocalServerPreview(args);
|
|
118
730
|
}
|
|
119
731
|
|
|
120
732
|
if (tool === "riddle_server_preview_status") {
|
|
121
|
-
return
|
|
733
|
+
return { ok: false, error: "Direct Riddle Proof server preview status is not wired in this runtime bridge yet." };
|
|
122
734
|
}
|
|
123
735
|
|
|
124
736
|
if (tool === "riddle_build_preview") {
|
|
125
|
-
return
|
|
737
|
+
return { ok: false, error: "Direct Riddle Proof build preview is not wired in this runtime bridge yet." };
|
|
126
738
|
}
|
|
127
739
|
|
|
128
740
|
if (tool === "riddle_build_preview_status") {
|
|
129
|
-
return
|
|
741
|
+
return { ok: false, error: "Direct Riddle Proof build preview status is not wired in this runtime bridge yet." };
|
|
130
742
|
}
|
|
131
743
|
|
|
132
744
|
if (tool === "riddle_script") {
|
|
133
|
-
return
|
|
745
|
+
return runWithDefaults(args, {
|
|
134
746
|
include: TOOL_DEFAULT_INCLUDE,
|
|
135
747
|
returnAsync: !!args.async,
|
|
136
748
|
});
|
|
@@ -145,7 +757,7 @@ async function run(tool, args) {
|
|
|
145
757
|
...visualDiffOptions
|
|
146
758
|
} = args;
|
|
147
759
|
const script = `return await visualDiff(${JSON.stringify(visualDiffOptions)});`;
|
|
148
|
-
return
|
|
760
|
+
return runWithDefaults({
|
|
149
761
|
url: args.url_before,
|
|
150
762
|
script,
|
|
151
763
|
options: { ...(options ?? {}), returnResult: true },
|
|
@@ -159,7 +771,7 @@ async function run(tool, args) {
|
|
|
159
771
|
}
|
|
160
772
|
|
|
161
773
|
if (tool === "riddle_run") {
|
|
162
|
-
return
|
|
774
|
+
return runWithDefaults(args.payload ?? args, {
|
|
163
775
|
include: ["screenshot", "console", "result"],
|
|
164
776
|
returnAsync: !!args.async,
|
|
165
777
|
});
|
|
@@ -174,12 +786,22 @@ async function main() {
|
|
|
174
786
|
if (!tool) throw new Error("Usage: riddle_core_call.mjs <tool> <json-args>");
|
|
175
787
|
const args = JSON.parse(rawArgs);
|
|
176
788
|
const result = await run(tool, args);
|
|
177
|
-
|
|
789
|
+
writeResult(result);
|
|
178
790
|
}
|
|
179
791
|
|
|
180
792
|
main().catch((err) => {
|
|
181
|
-
|
|
793
|
+
writeResult({
|
|
182
794
|
ok: false,
|
|
183
795
|
error: err instanceof Error ? err.message : String(err),
|
|
184
|
-
})
|
|
796
|
+
});
|
|
185
797
|
});
|
|
798
|
+
|
|
799
|
+
function writeResult(result) {
|
|
800
|
+
const encoded = JSON.stringify(result);
|
|
801
|
+
if (process.env.RIDDLE_PROOF_DIRECT_RESULT_FILE) {
|
|
802
|
+
writeFileSync(process.env.RIDDLE_PROOF_DIRECT_RESULT_FILE, encoded);
|
|
803
|
+
process.exit(0);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
console.log(encoded);
|
|
807
|
+
}
|