@openclaw/plugin-inspector 0.3.23 → 0.3.25
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 +23 -0
- package/README.md +129 -0
- package/package.json +1 -1
- package/src/api.js +7 -0
- package/src/artifacts.js +22 -1
- package/src/capture-api.js +15 -0
- package/src/capture-cli.js +1 -1
- package/src/capture-config.js +10 -7
- package/src/cli.js +3 -1
- package/src/cold-import-readiness.js +3 -3
- package/src/import-loop-profile.js +56 -11
- package/src/inspector.js +116 -50
- package/src/mock-sdk-capture-runner.js +61 -22
- package/src/openclaw-version.js +121 -9
- package/src/process-profile.js +255 -107
- package/src/runtime-capture-report.js +6 -0
- package/src/runtime-profile.js +4 -0
- package/src/sdk-mock.js +133 -13
- package/src/synthetic-entrypoint.js +19 -19
- package/src/synthetic-probes-cli.js +79 -2
- package/src/synthetic-probes.js +206 -34
package/src/inspector.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
3
|
import * as nodeModule from "node:module";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
6
|
import { createCaptureApi } from "./capture-api.js";
|
|
9
7
|
import { captureApiOptionsForPlugin } from "./capture-config.js";
|
|
10
8
|
import { fixtureCheckoutPath, fixtureSourceRoot } from "./config.js";
|
|
11
9
|
import { buildCompatibilityFixtureReport } from "./fixture-summary.js";
|
|
12
10
|
import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
13
11
|
import { prepareOpenClawTarget, resolveOpenClawTargetVersion } from "./openclaw-version.js";
|
|
12
|
+
import { resolveProcessLimits, startOwnedProcess } from "./process-profile.js";
|
|
14
13
|
import { buildCompatibilityReport, buildReport } from "./report.js";
|
|
15
14
|
import { inspectSdkDeprecations } from "./sdk-deprecation-rules.js";
|
|
15
|
+
import { collectCommonJsRequires } from "./sdk-mock.js";
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const pluginFactoryNames = "defineBundledChannelEntry|defineChannelPluginEntry|createChatChannelPlugin|definePluginEntry";
|
|
18
|
+
// Bundlers emit unbound calls as (0, sdk.factory)(...), including inline require receivers.
|
|
19
|
+
const compiledFactoryCall = new RegExp(String.raw`\(\s*0\s*,\s*(?:require\s*\(\s*(?:"[^"\r\n]*"|'[^'\r\n]*')\s*\)|[$A-Z_a-z][$\w]*)(?:\s*\.\s*[$A-Z_a-z][$\w]*)*\s*\.\s*(${pluginFactoryNames})\s*\)\s*\(`, "dg");
|
|
18
20
|
const registrationEquivalents = new Map([
|
|
19
21
|
["registerChannel", new Set(["createChatChannelPlugin", "defineBundledChannelEntry", "defineChannelPluginEntry", "registerChannel"])],
|
|
20
22
|
]);
|
|
@@ -164,10 +166,8 @@ export function inspectSourceText(text, filePath = "source.js") {
|
|
|
164
166
|
const hooks = collectDetailedMatches(searchableText, /\bapi\.on\(\s*["'`]([^"'`]+)["'`]/g, filePath, "name");
|
|
165
167
|
const registrations = [
|
|
166
168
|
...collectDetailedMatches(searchableText, /\bapi\.(register[A-Za-z0-9]+)\s*\(/g, filePath, "name"),
|
|
167
|
-
...collectDetailedMatches(searchableText,
|
|
168
|
-
...collectDetailedMatches(searchableText,
|
|
169
|
-
...collectDetailedMatches(searchableText, /\b(createChatChannelPlugin)\s*\(/g, filePath, "name"),
|
|
170
|
-
...collectDetailedMatches(searchableText, /\b(definePluginEntry)\s*\(/g, filePath, "name"),
|
|
169
|
+
...collectDetailedMatches(searchableText, new RegExp(String.raw`\b(${pluginFactoryNames})\s*\(`, "g"), filePath, "name"),
|
|
170
|
+
...collectDetailedMatches(searchableText, compiledFactoryCall, filePath, "name"),
|
|
171
171
|
];
|
|
172
172
|
const sdkImports = collectSdkImports(searchableText, filePath);
|
|
173
173
|
const sdkDeprecations = inspectSdkDeprecations(searchableText, filePath);
|
|
@@ -184,14 +184,23 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
184
184
|
if (options.mockSdk === true) {
|
|
185
185
|
return captureEntrypointWithMockSdk(entrypoint, options);
|
|
186
186
|
}
|
|
187
|
+
if (options.isolateCapture === true) {
|
|
188
|
+
return captureEntrypointInChild(entrypoint, { ...options, mockSdk: false });
|
|
189
|
+
}
|
|
190
|
+
return invokeWithTimeout((signal) => captureInProcess(entrypoint, options, signal), options);
|
|
191
|
+
}
|
|
187
192
|
|
|
193
|
+
async function captureInProcess(entrypoint, options, signal) {
|
|
194
|
+
signal.throwIfAborted();
|
|
188
195
|
const resolvedEntrypoint = path.resolve(options.cwd ?? process.cwd(), entrypoint);
|
|
189
196
|
let module;
|
|
190
197
|
try {
|
|
191
198
|
module = await import(pathToFileURL(resolvedEntrypoint).href);
|
|
192
199
|
} catch (error) {
|
|
200
|
+
signal.throwIfAborted();
|
|
193
201
|
throw classifyCapturePhaseError(error, "entrypoint-import-error");
|
|
194
202
|
}
|
|
203
|
+
signal.throwIfAborted();
|
|
195
204
|
const register = findRegisterExport(module);
|
|
196
205
|
|
|
197
206
|
if (!register) {
|
|
@@ -206,13 +215,17 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
206
215
|
pluginRoot: options.pluginRoot
|
|
207
216
|
? path.resolve(options.cwd ?? process.cwd(), options.pluginRoot)
|
|
208
217
|
: path.dirname(resolvedEntrypoint),
|
|
218
|
+
signal,
|
|
209
219
|
});
|
|
220
|
+
signal.throwIfAborted();
|
|
210
221
|
const api = createCaptureApi(apiOptions);
|
|
211
222
|
try {
|
|
212
223
|
await register(api);
|
|
213
224
|
} catch (error) {
|
|
225
|
+
signal.throwIfAborted();
|
|
214
226
|
throw classifyCapturePhaseError(error, "registration-execution-error");
|
|
215
227
|
}
|
|
228
|
+
signal.throwIfAborted();
|
|
216
229
|
const result = {
|
|
217
230
|
status: "captured",
|
|
218
231
|
entrypoint: resolvedEntrypoint,
|
|
@@ -225,60 +238,68 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
export async function captureEntrypointWithMockSdk(entrypoint, options = {}) {
|
|
241
|
+
return captureEntrypointInChild(entrypoint, { ...options, mockSdk: true });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function captureEntrypointInChild(entrypoint, options) {
|
|
245
|
+
const label = options.mockSdk ? "Mock SDK" : "Real SDK";
|
|
228
246
|
const runnerPath = fileURLToPath(new URL("./mock-sdk-capture-runner.js", import.meta.url));
|
|
229
247
|
const payload = {
|
|
230
248
|
entrypoint,
|
|
249
|
+
mockSdk: options.mockSdk,
|
|
231
250
|
cwd: options.cwd ?? process.cwd(),
|
|
232
251
|
pluginRoot: options.pluginRoot,
|
|
233
252
|
apiOptions: options.apiOptions,
|
|
253
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
234
254
|
};
|
|
255
|
+
const { result } = startOwnedProcess({
|
|
256
|
+
command: process.execPath,
|
|
257
|
+
args: ["--no-warnings", ...(options.mockSdk ? ["--preserve-symlinks"] : []), runnerPath, JSON.stringify(payload)],
|
|
258
|
+
cwd: options.cwd ?? process.cwd(),
|
|
259
|
+
env: { ...process.env, ...options.env },
|
|
260
|
+
timeoutMs: options.timeoutMs,
|
|
261
|
+
killGraceMs: options.killGraceMs,
|
|
262
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
263
|
+
signal: options.signal,
|
|
264
|
+
}, "CAPTURE");
|
|
265
|
+
const outcome = await result;
|
|
266
|
+
if (outcome.exitCode !== 0 || outcome.outputTruncated) {
|
|
267
|
+
const message = outcome.cancelled ? `${label} capture cancelled`
|
|
268
|
+
: outcome.outputTruncated ? `${label} capture output exceeded its byte limit`
|
|
269
|
+
: `${label} capture child failed`;
|
|
270
|
+
const { error: childError, ...details } = outcome;
|
|
271
|
+
throw classifyChildCaptureError(Object.assign(childError ?? new Error(message), details), options.mockSdk);
|
|
272
|
+
}
|
|
235
273
|
try {
|
|
236
|
-
|
|
237
|
-
process.execPath,
|
|
238
|
-
["--no-warnings", "--preserve-symlinks", runnerPath, JSON.stringify(payload)],
|
|
239
|
-
{
|
|
240
|
-
cwd: options.cwd ?? process.cwd(),
|
|
241
|
-
env: {
|
|
242
|
-
...process.env,
|
|
243
|
-
...(options.env ?? {}),
|
|
244
|
-
},
|
|
245
|
-
maxBuffer: 1024 * 1024 * 10,
|
|
246
|
-
},
|
|
247
|
-
);
|
|
248
|
-
return JSON.parse(stdout);
|
|
274
|
+
return JSON.parse(outcome.stdout);
|
|
249
275
|
} catch (error) {
|
|
250
|
-
|
|
251
|
-
if (captured) {
|
|
252
|
-
return captured;
|
|
253
|
-
}
|
|
254
|
-
throw classifyMockSdkCaptureError(error);
|
|
276
|
+
throw classifyChildCaptureError(error, options.mockSdk);
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
279
|
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
280
|
+
export function classifyMockSdkCaptureError(error) {
|
|
281
|
+
return classifyChildCaptureError(error, true);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function classifyChildCaptureError(error, mockSdk) {
|
|
285
|
+
const label = mockSdk ? "Mock SDK" : "Real SDK";
|
|
286
|
+
const fallbackClass = mockSdk ? "mock-sdk-capture-error" : "capture-error";
|
|
287
|
+
if (error?.timedOut === true) {
|
|
288
|
+
return enrichCaptureError(error, {
|
|
289
|
+
message: `${label} capture timed out after ${error.timeoutMs}ms`,
|
|
290
|
+
failureClass: "capture-timeout",
|
|
291
|
+
});
|
|
261
292
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
typeof parsed.status === "string" &&
|
|
268
|
-
Array.isArray(parsed.captured)
|
|
269
|
-
) {
|
|
270
|
-
return parsed;
|
|
271
|
-
}
|
|
272
|
-
} catch {
|
|
273
|
-
return null;
|
|
293
|
+
if (error?.cancelled || error?.outputTruncated) {
|
|
294
|
+
return enrichCaptureError(error, {
|
|
295
|
+
message: error.message,
|
|
296
|
+
failureClass: fallbackClass,
|
|
297
|
+
});
|
|
274
298
|
}
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
299
|
|
|
278
|
-
export function classifyMockSdkCaptureError(error) {
|
|
279
300
|
const rawMessage = [error?.stderr, error?.stdout, error?.message].filter(Boolean).join("\n");
|
|
280
301
|
const missingExport = rawMessage.match(/does not provide an export named ['"]([^'"]+)['"]/)?.[1];
|
|
281
|
-
if (missingExport) {
|
|
302
|
+
if (mockSdk && missingExport) {
|
|
282
303
|
return enrichCaptureError(error, {
|
|
283
304
|
message: `Mock SDK import failed: openclaw/plugin-sdk is missing export ${missingExport}`,
|
|
284
305
|
failureClass: "missing-sdk-export",
|
|
@@ -289,9 +310,9 @@ export function classifyMockSdkCaptureError(error) {
|
|
|
289
310
|
const missingModule =
|
|
290
311
|
rawMessage.match(/Cannot find (?:package|module) ['"]([^'"]*openclaw\/plugin-sdk[^'"]*)['"]/)?.[1] ??
|
|
291
312
|
rawMessage.match(/Package subpath ['"](\.\/plugin-sdk\/[^'"]+)['"]/)?.[1];
|
|
292
|
-
if (
|
|
313
|
+
if (mockSdk && missingModule) {
|
|
293
314
|
return enrichCaptureError(error, {
|
|
294
|
-
message: `Mock SDK import failed: ${missingModule
|
|
315
|
+
message: `Mock SDK import failed: ${missingModule}`,
|
|
295
316
|
failureClass: "missing-sdk-module",
|
|
296
317
|
missingModule,
|
|
297
318
|
});
|
|
@@ -300,17 +321,52 @@ export function classifyMockSdkCaptureError(error) {
|
|
|
300
321
|
const failureClass = rawMessage.match(/\[plugin-inspector:([^\]]+)\]/)?.[1];
|
|
301
322
|
if (failureClass) {
|
|
302
323
|
return enrichCaptureError(error, {
|
|
303
|
-
message: firstMeaningfulErrorLine(rawMessage.replace(/\[plugin-inspector:[^\]]+\]/, "")) ??
|
|
324
|
+
message: firstMeaningfulErrorLine(rawMessage.replace(/\[plugin-inspector:[^\]]+\]/, "")) ?? `${label} capture failed`,
|
|
304
325
|
failureClass,
|
|
305
326
|
});
|
|
306
327
|
}
|
|
328
|
+
if (mockSdk && rawMessage.includes("openclaw/plugin-sdk")) {
|
|
329
|
+
return enrichCaptureError(error, {
|
|
330
|
+
message: "Mock SDK import failed: openclaw/plugin-sdk module could not be resolved",
|
|
331
|
+
failureClass: "missing-sdk-module",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
307
334
|
|
|
308
335
|
return enrichCaptureError(error, {
|
|
309
|
-
message: firstMeaningfulErrorLine(rawMessage) ??
|
|
310
|
-
failureClass:
|
|
336
|
+
message: firstMeaningfulErrorLine(rawMessage) ?? `${label} capture failed`,
|
|
337
|
+
failureClass: fallbackClass,
|
|
311
338
|
});
|
|
312
339
|
}
|
|
313
340
|
|
|
341
|
+
function invokeWithTimeout(invoke, options) {
|
|
342
|
+
const { timeoutMs } = resolveProcessLimits(options, "CAPTURE");
|
|
343
|
+
const controller = new AbortController();
|
|
344
|
+
let rejectAborted;
|
|
345
|
+
const aborted = new Promise((_, reject) => { rejectAborted = reject; });
|
|
346
|
+
const onAbort = () => rejectAborted(controller.signal.reason);
|
|
347
|
+
const onCancel = () => controller.abort(classifyCapturePhaseError(
|
|
348
|
+
new Error("In-process capture cancelled"), "capture-error",
|
|
349
|
+
));
|
|
350
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
351
|
+
const timeoutId = setTimeout(() => controller.abort(classifyCapturePhaseError(
|
|
352
|
+
new Error(`In-process capture timed out after ${timeoutMs}ms`), "capture-timeout",
|
|
353
|
+
)), timeoutMs);
|
|
354
|
+
// Observe late settlement, but stop inspector-owned phases after the deadline.
|
|
355
|
+
// Arbitrary plugin JavaScript is only preemptable in the supervised CLI child.
|
|
356
|
+
const run = Promise.resolve().then(() => {
|
|
357
|
+
controller.signal.throwIfAborted();
|
|
358
|
+
return invoke(controller.signal);
|
|
359
|
+
});
|
|
360
|
+
const result = Promise.race([run, aborted]).finally(() => {
|
|
361
|
+
clearTimeout(timeoutId);
|
|
362
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
363
|
+
options.signal?.removeEventListener("abort", onCancel);
|
|
364
|
+
});
|
|
365
|
+
options.signal?.addEventListener("abort", onCancel, { once: true });
|
|
366
|
+
if (options.signal?.aborted) onCancel();
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
314
370
|
export function classifyCapturePhaseError(error, failureClass) {
|
|
315
371
|
return enrichCaptureError(error, {
|
|
316
372
|
message: error instanceof Error ? error.message : String(error),
|
|
@@ -373,7 +429,7 @@ function emptyInspection(fixture, status) {
|
|
|
373
429
|
function collectDetailedMatches(text, regex, filePath, key) {
|
|
374
430
|
const details = [];
|
|
375
431
|
for (const match of text.matchAll(regex)) {
|
|
376
|
-
const line = lineForOffset(text, match.index ?? 0);
|
|
432
|
+
const line = lineForOffset(text, match.indices?.[1]?.[0] ?? match.index ?? 0);
|
|
377
433
|
details.push({
|
|
378
434
|
[key]: match[1],
|
|
379
435
|
file: filePath,
|
|
@@ -413,6 +469,16 @@ function collectSdkImports(text, filePath) {
|
|
|
413
469
|
ref: `${filePath}:${line}`,
|
|
414
470
|
});
|
|
415
471
|
}
|
|
472
|
+
for (const { specifier, index } of collectCommonJsRequires(text)) {
|
|
473
|
+
if (specifier !== "openclaw/plugin-sdk" && !specifier.startsWith("openclaw/plugin-sdk/")) continue;
|
|
474
|
+
const line = lineForOffset(text, index);
|
|
475
|
+
details.push({
|
|
476
|
+
specifier,
|
|
477
|
+
file: filePath,
|
|
478
|
+
line,
|
|
479
|
+
ref: `${filePath}:${line}`,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
416
482
|
return details.sort((left, right) => left.line - right.line || left.specifier.localeCompare(right.specifier));
|
|
417
483
|
}
|
|
418
484
|
|
|
@@ -1,37 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { rmSync } from "node:fs";
|
|
3
3
|
import { mkdtemp } from "node:fs/promises";
|
|
4
|
-
import { register } from "node:module";
|
|
5
4
|
import os from "node:os";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { writeArtifacts } from "./artifacts.js";
|
|
8
8
|
import { createCaptureApi } from "./capture-api.js";
|
|
9
9
|
import { captureApiOptionsForPlugin } from "./capture-config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createCappedCollector, resolveProcessLimits } from "./process-profile.js";
|
|
11
|
+
import { createMockSdkPackage, installMockSdkLoader } from "./sdk-mock.js";
|
|
11
12
|
|
|
12
13
|
const options = JSON.parse(process.argv[2] ?? "{}");
|
|
13
14
|
let activeOutputCapture = null;
|
|
14
15
|
|
|
15
16
|
try {
|
|
16
17
|
const result = await run(options);
|
|
17
|
-
|
|
18
|
+
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
19
|
+
const { maxOutputBytes } = resolveProcessLimits(options, options.syntheticProbes ? "PROBE" : "CAPTURE");
|
|
20
|
+
if (Buffer.byteLength(json) > maxOutputBytes) {
|
|
21
|
+
const label = options.syntheticProbes ? "Synthetic probe" : `${options.mockSdk === false ? "Real SDK" : "Mock SDK"} capture`;
|
|
22
|
+
throw new Error(`${label} result exceeded its ${maxOutputBytes}-byte limit`);
|
|
23
|
+
}
|
|
24
|
+
if (options.outputPath) {
|
|
25
|
+
await writeArtifacts([{ path: options.outputPath, content: json }]);
|
|
26
|
+
} else {
|
|
27
|
+
await writeRunnerStdout(json);
|
|
28
|
+
}
|
|
29
|
+
process.exit(0);
|
|
18
30
|
} catch (error) {
|
|
19
31
|
if (error.failureClass) {
|
|
20
|
-
writeRunnerStderr(`[plugin-inspector:${error.failureClass}]\n`);
|
|
32
|
+
await writeRunnerStderr(`[plugin-inspector:${error.failureClass}]\n`);
|
|
21
33
|
}
|
|
22
|
-
writeRunnerStderr(`${error.stack ?? error.message}\n`);
|
|
23
|
-
process.
|
|
34
|
+
await writeRunnerStderr(`${options.outputPath ? error.message : (error.stack ?? error.message)}\n`);
|
|
35
|
+
process.exit(1);
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
async function run(options) {
|
|
27
39
|
const entrypoint = path.resolve(options.cwd ?? process.cwd(), options.entrypoint);
|
|
28
40
|
const pluginRoot = path.resolve(options.cwd ?? process.cwd(), options.pluginRoot ?? path.dirname(entrypoint));
|
|
41
|
+
if (options.mockSdk === false) {
|
|
42
|
+
return await captureLinkedEntrypoint(entrypoint, { ...options, pluginRoot });
|
|
43
|
+
}
|
|
29
44
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-"));
|
|
30
45
|
|
|
31
46
|
cleanupTempDirOnExit(workspace);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
const mockPackage = await createMockSdkPackage(workspace, { pluginRoot });
|
|
48
|
+
const stopLoader = await installMockSdkLoader(mockPackage);
|
|
49
|
+
try {
|
|
50
|
+
return await captureLinkedEntrypoint(entrypoint, { ...options, pluginRoot });
|
|
51
|
+
} finally {
|
|
52
|
+
stopLoader();
|
|
53
|
+
}
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
function cleanupTempDirOnExit(dir) {
|
|
@@ -55,14 +74,15 @@ async function captureLinkedEntrypoint(entrypoint, options) {
|
|
|
55
74
|
|
|
56
75
|
if (!register) {
|
|
57
76
|
await drainAsyncOutput();
|
|
58
|
-
return
|
|
77
|
+
return finishCapture(
|
|
59
78
|
{
|
|
60
79
|
status: "no-register-export",
|
|
61
|
-
entrypoint: options.entrypoint,
|
|
62
|
-
mockSdk:
|
|
80
|
+
entrypoint: options.mockSdk === false ? entrypoint : options.entrypoint,
|
|
81
|
+
mockSdk: options.mockSdk !== false,
|
|
63
82
|
captured: [],
|
|
64
83
|
},
|
|
65
84
|
outputCapture,
|
|
85
|
+
options,
|
|
66
86
|
);
|
|
67
87
|
}
|
|
68
88
|
|
|
@@ -80,13 +100,22 @@ async function captureLinkedEntrypoint(entrypoint, options) {
|
|
|
80
100
|
|
|
81
101
|
const result = {
|
|
82
102
|
status: "captured",
|
|
83
|
-
entrypoint: options.entrypoint,
|
|
84
|
-
mockSdk:
|
|
103
|
+
entrypoint: options.mockSdk === false ? entrypoint : options.entrypoint,
|
|
104
|
+
mockSdk: options.mockSdk !== false,
|
|
85
105
|
captured: api.getCapturedContracts(),
|
|
86
106
|
};
|
|
87
107
|
if (apiOptions?.retainHandlers === true) {
|
|
88
108
|
result.retained = api.getRetainedContracts();
|
|
89
109
|
}
|
|
110
|
+
return finishCapture(result, outputCapture, options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function finishCapture(result, outputCapture, options) {
|
|
114
|
+
if (options.syntheticProbes) {
|
|
115
|
+
const { runCapturedSyntheticProbes } = await import("./synthetic-probes.js");
|
|
116
|
+
result = await runCapturedSyntheticProbes(result, options);
|
|
117
|
+
await drainAsyncOutput();
|
|
118
|
+
}
|
|
90
119
|
return withProcessOutput(result, outputCapture);
|
|
91
120
|
}
|
|
92
121
|
|
|
@@ -124,18 +153,18 @@ function findRegisterExport(module) {
|
|
|
124
153
|
}
|
|
125
154
|
|
|
126
155
|
function installProcessOutputCapture() {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
156
|
+
const stdout = createCappedCollector(1024 * 1024);
|
|
157
|
+
const stderr = createCappedCollector(1024 * 1024);
|
|
129
158
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
130
159
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
131
160
|
|
|
132
161
|
process.stdout.write = (chunk, encoding, callback) => {
|
|
133
|
-
|
|
162
|
+
stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof encoding === "string" ? encoding : "utf8"));
|
|
134
163
|
invokeWriteCallback(encoding, callback);
|
|
135
164
|
return true;
|
|
136
165
|
};
|
|
137
166
|
process.stderr.write = (chunk, encoding, callback) => {
|
|
138
|
-
|
|
167
|
+
stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof encoding === "string" ? encoding : "utf8"));
|
|
139
168
|
invokeWriteCallback(encoding, callback);
|
|
140
169
|
return true;
|
|
141
170
|
};
|
|
@@ -143,8 +172,8 @@ function installProcessOutputCapture() {
|
|
|
143
172
|
return {
|
|
144
173
|
originalStdoutWrite,
|
|
145
174
|
originalStderrWrite,
|
|
146
|
-
stdout: () =>
|
|
147
|
-
stderr: () =>
|
|
175
|
+
stdout: () => stdout.text(),
|
|
176
|
+
stderr: () => stderr.text(),
|
|
148
177
|
};
|
|
149
178
|
}
|
|
150
179
|
|
|
@@ -162,9 +191,19 @@ async function drainAsyncOutput() {
|
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
function writeRunnerStdout(text) {
|
|
165
|
-
|
|
194
|
+
const write = activeOutputCapture?.originalStdoutWrite ?? process.stdout.write.bind(process.stdout);
|
|
195
|
+
return flushWrite(write, text);
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
function writeRunnerStderr(text) {
|
|
169
|
-
|
|
199
|
+
const write = activeOutputCapture?.originalStderrWrite ?? process.stderr.write.bind(process.stderr);
|
|
200
|
+
return flushWrite(write, text);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The runner exits deliberately to shed plugin timers, but only after the
|
|
204
|
+
// complete protocol response has reached its pipe.
|
|
205
|
+
function flushWrite(write, text) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
write(text, (error) => error ? reject(error) : resolve());
|
|
208
|
+
});
|
|
170
209
|
}
|
package/src/openclaw-version.js
CHANGED
|
@@ -8,6 +8,9 @@ import { x as extractTar } from "tar";
|
|
|
8
8
|
import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
9
9
|
|
|
10
10
|
const defaultRegistryUrl = "https://registry.npmjs.org";
|
|
11
|
+
const defaultFetchTimeoutMs = 30_000;
|
|
12
|
+
const defaultMaxArchiveBytes = 256 * 1024 * 1024;
|
|
13
|
+
const defaultMaxMetadataBytes = 16 * 1024 * 1024;
|
|
11
14
|
const supportedTags = new Set(["latest", "beta"]);
|
|
12
15
|
const downloadUrls = new WeakMap();
|
|
13
16
|
|
|
@@ -25,8 +28,8 @@ export async function resolveOpenClawTargetVersion(requestedVersion, options = {
|
|
|
25
28
|
let distTag = null;
|
|
26
29
|
|
|
27
30
|
if (supportedTags.has(requested)) {
|
|
28
|
-
const
|
|
29
|
-
version =
|
|
31
|
+
const distTags = await fetchJson(`${registryUrl}/-/package/openclaw/dist-tags`, fetchImpl, options);
|
|
32
|
+
version = distTags?.[requested];
|
|
30
33
|
if (typeof version !== "string" || version.length === 0) {
|
|
31
34
|
throw new Error(`OpenClaw npm dist-tag ${requested} did not resolve to an exact version`);
|
|
32
35
|
}
|
|
@@ -38,7 +41,7 @@ export async function resolveOpenClawTargetVersion(requestedVersion, options = {
|
|
|
38
41
|
throw new Error("--openclaw-version must be latest, beta, or an exact OpenClaw version");
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
const versionMetadata = await fetchJson(`${registryUrl}/openclaw/${encodeURIComponent(version)}`, fetchImpl);
|
|
44
|
+
const versionMetadata = await fetchJson(`${registryUrl}/openclaw/${encodeURIComponent(version)}`, fetchImpl, options);
|
|
42
45
|
if (versionMetadata.version !== version || typeof versionMetadata.dist?.tarball !== "string") {
|
|
43
46
|
throw new Error(`OpenClaw npm metadata for ${version} is incomplete`);
|
|
44
47
|
}
|
|
@@ -137,11 +140,12 @@ export function satisfiesOpenClawCompatibilityRange({ targetVersion, eligibility
|
|
|
137
140
|
|
|
138
141
|
async function preparePackageArchive(resolvedTarget, options) {
|
|
139
142
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
140
|
-
const response = await fetchImpl
|
|
143
|
+
const response = await fetchWithTimeout(fetchImpl, downloadUrlFor(resolvedTarget), {}, options, "npm archive");
|
|
141
144
|
if (!response.ok) {
|
|
145
|
+
await cancelBody(response.body);
|
|
142
146
|
throw new Error(`failed to download OpenClaw ${resolvedTarget.version}: HTTP ${response.status}`);
|
|
143
147
|
}
|
|
144
|
-
const archive =
|
|
148
|
+
const archive = await readLimitedBody(response, maxArchiveBytes(options), "npm archive");
|
|
145
149
|
verifyArchive(archive, resolvedTarget.source);
|
|
146
150
|
|
|
147
151
|
await mkdir(path.dirname(options.targetDir), { recursive: true });
|
|
@@ -191,10 +195,118 @@ function verifyArchive(archive, source) {
|
|
|
191
195
|
throw new Error("OpenClaw npm archive has no supported integrity metadata");
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
async function fetchJson(url, fetchImpl) {
|
|
195
|
-
const response = await
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
async function fetchJson(url, fetchImpl, options = {}) {
|
|
199
|
+
const response = await fetchWithTimeout(
|
|
200
|
+
fetchImpl,
|
|
201
|
+
url,
|
|
202
|
+
{ headers: { accept: "application/json" } },
|
|
203
|
+
options,
|
|
204
|
+
"npm metadata",
|
|
205
|
+
);
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
await cancelBody(response.body);
|
|
208
|
+
throw new Error(`failed to resolve OpenClaw npm metadata: HTTP ${response.status}`);
|
|
209
|
+
}
|
|
210
|
+
const body = await readLimitedBody(response, maxMetadataBytes(options), "npm metadata");
|
|
211
|
+
return JSON.parse(body.toString("utf8"));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function fetchWithTimeout(fetchImpl, url, init, options, what) {
|
|
215
|
+
try {
|
|
216
|
+
return await fetchImpl(url, { ...init, signal: AbortSignal.timeout(fetchTimeoutMs(options)) });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw mapTargetFetchError(error, what);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function readLimitedBody(response, maxBytes, what) {
|
|
223
|
+
const declared = Number(response.headers.get("content-length"));
|
|
224
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
225
|
+
await cancelBody(response.body);
|
|
226
|
+
throw targetDownloadLimitError(what, maxBytes);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let reader;
|
|
230
|
+
try {
|
|
231
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
232
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
233
|
+
if (buffer.length > maxBytes) throw targetDownloadLimitError(what, maxBytes);
|
|
234
|
+
return buffer;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
reader = response.body.getReader();
|
|
238
|
+
const chunks = [];
|
|
239
|
+
let received = 0;
|
|
240
|
+
while (true) {
|
|
241
|
+
const { done, value } = await reader.read();
|
|
242
|
+
if (done) break;
|
|
243
|
+
received += value.byteLength;
|
|
244
|
+
if (received > maxBytes) {
|
|
245
|
+
try {
|
|
246
|
+
await reader.cancel();
|
|
247
|
+
} catch {}
|
|
248
|
+
throw targetDownloadLimitError(what, maxBytes);
|
|
249
|
+
}
|
|
250
|
+
chunks.push(value);
|
|
251
|
+
}
|
|
252
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw mapTargetFetchError(error, what);
|
|
255
|
+
} finally {
|
|
256
|
+
reader?.releaseLock();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function cancelBody(body) {
|
|
261
|
+
try {
|
|
262
|
+
await body?.cancel?.();
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function fetchTimeoutMs(options) {
|
|
267
|
+
const timeout = positiveInteger(
|
|
268
|
+
options.fetchTimeoutMs ?? process.env.PLUGIN_INSPECTOR_TARGET_FETCH_TIMEOUT_MS,
|
|
269
|
+
defaultFetchTimeoutMs,
|
|
270
|
+
);
|
|
271
|
+
// Node clamps overflowing timer delays to 1ms instead of honoring the budget.
|
|
272
|
+
return timeout <= 2_147_483_647 ? timeout : defaultFetchTimeoutMs;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function maxArchiveBytes(options) {
|
|
276
|
+
return positiveInteger(options.maxArchiveBytes ?? process.env.PLUGIN_INSPECTOR_TARGET_ARCHIVE_MAX_BYTES, defaultMaxArchiveBytes);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function maxMetadataBytes(options) {
|
|
280
|
+
return positiveInteger(options.maxMetadataBytes ?? process.env.PLUGIN_INSPECTOR_TARGET_METADATA_MAX_BYTES, defaultMaxMetadataBytes);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function positiveInteger(value, fallback) {
|
|
284
|
+
const parsed = Number(value);
|
|
285
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function mapTargetFetchError(error, what) {
|
|
289
|
+
if (error?.failureClass) return error;
|
|
290
|
+
if (isTimeoutError(error)) {
|
|
291
|
+
const wrapped = new Error(`OpenClaw ${what} download timed out`);
|
|
292
|
+
wrapped.failureClass = "target-download-timeout";
|
|
293
|
+
wrapped.cause = error;
|
|
294
|
+
return wrapped;
|
|
295
|
+
}
|
|
296
|
+
return error;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isTimeoutError(error) {
|
|
300
|
+
for (let current = error; current; current = current.cause) {
|
|
301
|
+
if (current.name === "TimeoutError" || current.name === "AbortError") return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function targetDownloadLimitError(what, maxBytes) {
|
|
307
|
+
const error = new Error(`OpenClaw ${what} exceeds the ${maxBytes} byte download limit`);
|
|
308
|
+
error.failureClass = "target-download-too-large";
|
|
309
|
+
return error;
|
|
198
310
|
}
|
|
199
311
|
|
|
200
312
|
function cacheKeyFor(target) {
|