@openclaw/plugin-inspector 0.3.24 → 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 +16 -0
- package/README.md +123 -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 +110 -45
- 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 +201 -34
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.25 - 2026-09-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Invoke each synthetic Gateway method once, including registrations with options, and validate its first emitted response's JSON wire representation instead of treating any nonthrowing callback as successful. Preserve explicit response authority, returned-payload fallback, deferred replies within the existing deadline, and accepted-only initial responses.
|
|
10
|
+
- Bound synthetic callback waits and cancellation, stop dependent probes after a timeout, and supervise CLI capture plus retained callbacks in one child. Validate report shape and counts before delivery while preserving complete failed-row reports, bounded plugin output, and in-process callback identity. Thanks @SebTardif.
|
|
11
|
+
- Run `registerService` start, stop, and dispose probes serially so teardown cannot overlap startup.
|
|
12
|
+
- Bound real-SDK CLI capture in an owned child, including stalled imports, busy registration, and retained timers. Give in-process capture a finite 30-second default deadline while preserving caller runtime and handler identity; arbitrary in-process JavaScript cannot be forcibly canceled. Thanks @SebTardif.
|
|
13
|
+
- Capture and synthetically probe compiled CommonJS plugins with generated SDK mocks, including lazy `require()` calls, and discover their SDK source references. CommonJS mocking requires Node.js 22.15+ synchronous module hooks; the package engine and existing ESM/TypeScript capture remain unchanged.
|
|
14
|
+
- Bound mock-SDK capture and profile child lifetimes, output, and process sampling; clean owned POSIX descendants through stdio close and keep timeout/cancellation outcomes unsuccessful. Flush complete capture JSON before exiting despite retained plugin timers. Thanks @SebTardif.
|
|
15
|
+
- Profile the default import-loop capture runner directly so its timeout also owns plugin execution. Validate fresh, bounded capture artifacts; RSS/CPU and wall-time measurements now exclude the intermediate CLI wrapper and are not directly comparable with historical profiles.
|
|
16
|
+
- Bound OpenClaw npm metadata and tarball downloads with a deadline through response-body reads, reject oversized responses, and release failed downloads. Resolve `latest` and `beta` through the small npm dist-tags endpoint before fetching exact-version metadata, keeping the 16 MiB metadata limit usable.
|
|
17
|
+
- Capture plugins that bind `api.runtime.modelAuth` during registration with credential-free defaults; auth acquisition remains an explicit synthetic failure.
|
|
18
|
+
- Report absent build output and missing entrypoints before SDK alias blockers in cold-import readiness, preserving build-required totals and all remediation evidence.
|
|
19
|
+
- Recognize board widget content kinds, memory prompt preparation, transcript source providers, worker providers, and MCP server connection resolvers as metadata-only synthetic probes without invoking runtime callbacks.
|
|
20
|
+
|
|
5
21
|
## 0.3.24 - 2026-08-31
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -232,6 +232,20 @@ That keeps compatibility CI offline and credential-free. It does not call live
|
|
|
232
232
|
services, launch OpenClaw, run provider SDKs, or emulate service lifecycle side
|
|
233
233
|
effects.
|
|
234
234
|
|
|
235
|
+
CommonJS SDK mocking, including compiled `.cjs` entrypoints and lazy `require()`
|
|
236
|
+
calls in synthetic handlers, requires Node.js 22.15 or newer with
|
|
237
|
+
`module.registerHooks()`. On older Node versions, upgrade Node.js or use an
|
|
238
|
+
ESM/TypeScript entrypoint. This capability requirement does not change the
|
|
239
|
+
package's Node.js `>=22` engine range or gate existing ESM/TypeScript capture.
|
|
240
|
+
Static inspection also discovers literal CommonJS SDK `require()` references.
|
|
241
|
+
|
|
242
|
+
The default capture `api.runtime.modelAuth` passes synthetic provider IDs through
|
|
243
|
+
unchanged, returns fresh empty auth stores and profile lists, and reports no
|
|
244
|
+
configured API keys. Auth acquisition rejects with a mock-auth-unavailable error;
|
|
245
|
+
it never looks up host credentials. This supports registration and no-auth
|
|
246
|
+
callbacks, not provider alias validation or authenticated execution. An explicitly
|
|
247
|
+
supplied runtime is preserved unchanged, including an empty runtime.
|
|
248
|
+
|
|
235
249
|
Synthetic probes classify widget presenters as metadata-only. They record the
|
|
236
250
|
registration without calling its match, availability, or presentation callbacks,
|
|
237
251
|
including when channel, provider, or lifecycle execution is enabled.
|
|
@@ -239,6 +253,20 @@ including when channel, provider, or lifecycle execution is enabled.
|
|
|
239
253
|
Use `--real-sdk` only when the plugin workspace already has real SDK
|
|
240
254
|
dependencies installed and you intentionally want that path.
|
|
241
255
|
|
|
256
|
+
Real-SDK CLI capture runs in an owned child, including runtime capture enabled
|
|
257
|
+
by flags or plugin config. The parent bounds imports and registration, then
|
|
258
|
+
cleans up retained plugin timers after the child flushes its complete result.
|
|
259
|
+
It uses installed SDK dependencies without loading the mock SDK.
|
|
260
|
+
|
|
261
|
+
The real-SDK programmatic API stays in-process to preserve supplied runtime
|
|
262
|
+
objects and retained handler identity. Its 30-second default deadline reports
|
|
263
|
+
`capture-timeout`, stops later inspector-owned phases, and aborts supported
|
|
264
|
+
setup reads. Caller `signal` cancellation also stops later phases. Neither
|
|
265
|
+
mechanism can preempt a synchronous JavaScript loop, unload an import, stop
|
|
266
|
+
arbitrary plugin side effects, or clear plugin-owned timers in the caller's
|
|
267
|
+
process. Only owned-child capture provides that process-lifetime boundary.
|
|
268
|
+
Override the API budget with `timeoutMs` or `PLUGIN_INSPECTOR_CAPTURE_TIMEOUT_MS`.
|
|
269
|
+
|
|
242
270
|
Runtime capture writes:
|
|
243
271
|
|
|
244
272
|
- `reports/plugin-inspector-runtime-capture.json`
|
|
@@ -250,6 +278,101 @@ Capture one entrypoint directly:
|
|
|
250
278
|
plugin-inspector capture ./dist/index.js --mock-sdk --allow-execute
|
|
251
279
|
```
|
|
252
280
|
|
|
281
|
+
CLI capture, mock-SDK API capture, and import-loop/runtime profiles give each
|
|
282
|
+
child a 30-second budget. Capture reports `capture-timeout`; timed-out profile samples
|
|
283
|
+
always have a nonzero `exitCode`, even if a SIGTERM handler exits zero.
|
|
284
|
+
Pass an `AbortSignal` as `signal` to cancel owned-child work. Cancellation is
|
|
285
|
+
never a successful capture or profile sample.
|
|
286
|
+
|
|
287
|
+
On POSIX, each child owns a separate process group. Completion waits for
|
|
288
|
+
stdout/stderr to close and cleans descendants, including after a successful
|
|
289
|
+
leader exit. Shutdown sends SIGTERM, then SIGKILL after a 1-second grace
|
|
290
|
+
period. A further 1-second close deadline fails the operation if pipes remain
|
|
291
|
+
open. Descendants that deliberately leave the group are not contained;
|
|
292
|
+
this is lifecycle supervision, not a sandbox. Windows retains direct-child
|
|
293
|
+
termination and the bounded close deadline, not POSIX group cleanup.
|
|
294
|
+
|
|
295
|
+
The API options `timeoutMs`, `killGraceMs`, and `maxOutputBytes` take precedence
|
|
296
|
+
over `PLUGIN_INSPECTOR_CAPTURE_TIMEOUT_MS`, `PLUGIN_INSPECTOR_CAPTURE_KILL_GRACE_MS`,
|
|
297
|
+
and `PLUGIN_INSPECTOR_CAPTURE_MAX_OUTPUT_BYTES` for owned-child capture. Profiles use
|
|
298
|
+
the corresponding `PLUGIN_INSPECTOR_PROFILE_*` variables. Values must be finite
|
|
299
|
+
positive numbers (zero does not disable limits); invalid values fall through
|
|
300
|
+
to the environment, then defaults. Durations/byte limits cannot exceed
|
|
301
|
+
2,147,483,647; grace cannot exceed 30,000 ms.
|
|
302
|
+
|
|
303
|
+
Each profiled stdout/stderr stream retains at most 1 MiB by default while
|
|
304
|
+
continuing to drain output. Owned-child capture retains at most 10 MiB per pipe and
|
|
305
|
+
fails if its JSON response is truncated; intercepted plugin stdout/stderr
|
|
306
|
+
inside that response retains at most 1 MiB each. The optional `ps` sampler
|
|
307
|
+
also has bounded output, execution, and cleanup.
|
|
308
|
+
|
|
309
|
+
Default import-loop profiles launch the mock capture runner directly under one
|
|
310
|
+
profile budget, for both baseline and plugin samples. Their JSON artifacts
|
|
311
|
+
retain capture's 10 MiB default limit (`PLUGIN_INSPECTOR_CAPTURE_MAX_OUTPUT_BYTES`,
|
|
312
|
+
or an explicit `maxOutputBytes` override), separately from the profile's
|
|
313
|
+
1 MiB stdout/stderr limits. Artifacts are accepted only after a successful
|
|
314
|
+
current capture. RSS and CPU now measure the actual runner, and wall time no
|
|
315
|
+
longer includes intermediate CLI startup. Historical measurements from the
|
|
316
|
+
CLI-wrapper route are not directly comparable. Custom `captureCommand` and
|
|
317
|
+
`captureScript` launch contracts are unchanged; custom detached groups are
|
|
318
|
+
outside the owned process group.
|
|
319
|
+
|
|
320
|
+
These limits apply to owned child processes only. The public in-process
|
|
321
|
+
`captureEntrypoint` path preserves retained handler identity and does not
|
|
322
|
+
claim to cancel synchronous plugin code or retained callbacks.
|
|
323
|
+
|
|
324
|
+
Synthetic probe APIs give each invoked callback a 30-second default budget.
|
|
325
|
+
Set `timeoutMs` or `PLUGIN_INSPECTOR_PROBE_TIMEOUT_MS`; the same finite positive
|
|
326
|
+
API-then-environment validation applies, with no zero or infinite opt-out.
|
|
327
|
+
A timed-out callback becomes a failed row and remaining dependent probes are
|
|
328
|
+
blocked. Ordinary handler failures remain failed rows without stopping
|
|
329
|
+
independent probes. Caller `signal` cancellation rejects the API call.
|
|
330
|
+
Timeout and cancellation abort supported handler signal arguments and observe
|
|
331
|
+
late promise settlement, but cannot preempt synchronous JavaScript or arbitrary
|
|
332
|
+
plugin side effects. Programmatic probes stay in-process and preserve caller
|
|
333
|
+
runtime objects and retained callback identity.
|
|
334
|
+
|
|
335
|
+
Gateway method probes invoke each registered handler once, including positional
|
|
336
|
+
`(method, handler, options)` registrations; captured `handler`/`run`/`execute`
|
|
337
|
+
aliases do not create extra calls. The handler receives synthetic Gateway
|
|
338
|
+
options and a void `respond(ok, payload, error, meta)` callback. Existing
|
|
339
|
+
`registrationProbeInputs` overrides remain available.
|
|
340
|
+
|
|
341
|
+
The first emitted response is authoritative, even when malformed. Probes check
|
|
342
|
+
its JSON-serialized response/error shape: `ok: true` passes, `ok: false` fails,
|
|
343
|
+
and later responses cannot overwrite the outcome. Logging `meta` is not a wire
|
|
344
|
+
field. Without an explicit response, a non-`undefined` return is adapted into a
|
|
345
|
+
successful payload, including `false`, `null`, and `{ ok: false }`, matching the
|
|
346
|
+
OpenClaw plugin registrar. A void handler can respond later within the existing
|
|
347
|
+
probe deadline; no response or return before that deadline fails. Handler throws,
|
|
348
|
+
rejections, and timeouts still fail under the ordinary probe rules.
|
|
349
|
+
|
|
350
|
+
This observes the initial RPC response plus ordinary handler settlement within
|
|
351
|
+
the existing deadline. It does not prove asynchronous operation completion,
|
|
352
|
+
transport delivery, or authorization. An accepted-only success is valid once the
|
|
353
|
+
handler settles; probes do not implicitly request `expectFinal` or reject legal
|
|
354
|
+
multi-frame methods. No Gateway connection or live host call is made.
|
|
355
|
+
|
|
356
|
+
`synthetic-probes-cli.js` runs capture and retained callbacks together in one
|
|
357
|
+
owned child. Its whole-child budget also defaults to 30 seconds, including
|
|
358
|
+
imports and registration, with `PLUGIN_INSPECTOR_PROBE_TIMEOUT_MS`,
|
|
359
|
+
`PLUGIN_INSPECTOR_PROBE_KILL_GRACE_MS`, and
|
|
360
|
+
`PLUGIN_INSPECTOR_PROBE_MAX_OUTPUT_BYTES` overrides. Shutdown uses the same
|
|
361
|
+
bounded grace and process-group cleanup as capture. The default report and
|
|
362
|
+
per-pipe limit is 10 MiB, matching capture; intercepted plugin stdout and stderr are each capped
|
|
363
|
+
at 1 MiB and kept separate from the report protocol.
|
|
364
|
+
|
|
365
|
+
Completed synthetic reports are still written even when they contain failed
|
|
366
|
+
probe rows; the CLI exits successfully after delivering them, and CI policy
|
|
367
|
+
evaluates those rows. Child timeout, cancellation, truncated output, or an
|
|
368
|
+
oversized or malformed report instead exits unsuccessfully without writing a
|
|
369
|
+
new output artifact. The CLI validates the report shape, row identities and
|
|
370
|
+
statuses, and summary counts before publication; valid empty and blocked
|
|
371
|
+
reports are preserved. This is protocol validation, not authenticated
|
|
372
|
+
completion or a security sandbox: same-process plugin code can still fabricate
|
|
373
|
+
a valid report. Healthy retained intervals cannot keep the child alive after
|
|
374
|
+
its complete report is flushed.
|
|
375
|
+
|
|
253
376
|
## CI
|
|
254
377
|
|
|
255
378
|
`plugin-inspector ci` writes the normal compatibility report plus CI-native
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -226,6 +226,13 @@ export async function runPluginCheck(options = {}) {
|
|
|
226
226
|
mockSdk,
|
|
227
227
|
report,
|
|
228
228
|
rootDir: config.rootDir,
|
|
229
|
+
isolateCapture: options.isolateCapture,
|
|
230
|
+
timeoutMs: options.timeoutMs,
|
|
231
|
+
killGraceMs: options.killGraceMs,
|
|
232
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
233
|
+
signal: options.signal,
|
|
234
|
+
env: options.env,
|
|
235
|
+
apiOptions: options.apiOptions,
|
|
229
236
|
});
|
|
230
237
|
const runtimeCapturePaths = await writeRuntimeCaptureReport(runtimeCapture, {
|
|
231
238
|
jsonPath: path.resolve(config.rootDir, outDir, "plugin-inspector-runtime-capture.json"),
|
package/src/artifacts.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { mkdir, open, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
5
|
+
export async function readBoundedJsonArtifact(filePath, maxBytes) {
|
|
6
|
+
const file = await open(filePath, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
7
|
+
try {
|
|
8
|
+
const stat = await file.stat();
|
|
9
|
+
if (!stat.isFile()) throw new Error("expected a regular result file");
|
|
10
|
+
if (stat.size > maxBytes) throw new Error("result exceeded its byte limit");
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let bytes = 0;
|
|
13
|
+
// end is inclusive: read at most limit + 1 even if the file grew after stat.
|
|
14
|
+
for await (const chunk of file.createReadStream({ end: maxBytes, autoClose: false })) {
|
|
15
|
+
chunks.push(chunk);
|
|
16
|
+
bytes += chunk.length;
|
|
17
|
+
}
|
|
18
|
+
if (bytes > maxBytes) throw new Error("result exceeded its byte limit");
|
|
19
|
+
return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
|
|
20
|
+
} finally {
|
|
21
|
+
await file.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
export async function writeArtifacts(artifacts, options = {}) {
|
|
5
26
|
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
6
27
|
throw new TypeError("writeArtifacts requires at least one artifact");
|
package/src/capture-api.js
CHANGED
|
@@ -203,6 +203,17 @@ function createRuntimeContext(options) {
|
|
|
203
203
|
logger: options.logger ?? console,
|
|
204
204
|
now: () => new Date(0),
|
|
205
205
|
tts: runtime.tts ?? {},
|
|
206
|
+
// Synthetic provider IDs pass through; capture never resolves host aliases or credentials.
|
|
207
|
+
modelAuth: {
|
|
208
|
+
resolveProviderIdForAuth: (provider) => provider,
|
|
209
|
+
ensureAuthProfileStore: () => ({ version: 1, profiles: {} }),
|
|
210
|
+
resolveAuthProfileOrder: () => [],
|
|
211
|
+
listProfilesForProvider: () => [],
|
|
212
|
+
isProviderApiKeyConfigured: () => false,
|
|
213
|
+
getApiKeyForModel: rejectCaptureModelAuth,
|
|
214
|
+
getRuntimeAuthForModel: rejectCaptureModelAuth,
|
|
215
|
+
resolveApiKeyForProvider: rejectCaptureModelAuth,
|
|
216
|
+
},
|
|
206
217
|
state: {
|
|
207
218
|
resolveStateDir: () => options.stateDir ?? process.cwd(),
|
|
208
219
|
openBlobStore(storeOptions) {
|
|
@@ -228,6 +239,10 @@ function createRuntimeContext(options) {
|
|
|
228
239
|
};
|
|
229
240
|
}
|
|
230
241
|
|
|
242
|
+
async function rejectCaptureModelAuth() {
|
|
243
|
+
throw new Error("Model auth is unavailable in capture mocks");
|
|
244
|
+
}
|
|
245
|
+
|
|
231
246
|
function createBlobStoreContext(options) {
|
|
232
247
|
const values = new Map();
|
|
233
248
|
const entryInfo = (key, entry) => ({
|
package/src/capture-cli.js
CHANGED
|
@@ -23,7 +23,7 @@ async function run(commandArgs) {
|
|
|
23
23
|
throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
|
|
26
|
+
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot, isolateCapture: true });
|
|
27
27
|
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
28
28
|
if (outputPath) {
|
|
29
29
|
await writeArtifacts([{ path: outputPath, content: json }]);
|
package/src/capture-config.js
CHANGED
|
@@ -6,7 +6,7 @@ export async function captureApiOptionsForPlugin(apiOptions = {}, options = {})
|
|
|
6
6
|
return apiOptions;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const pluginConfig = await readSamplePluginConfig(options.pluginRoot);
|
|
9
|
+
const pluginConfig = await readSamplePluginConfig(options.pluginRoot, options.signal);
|
|
10
10
|
if (pluginConfig === undefined) {
|
|
11
11
|
return apiOptions;
|
|
12
12
|
}
|
|
@@ -16,15 +16,16 @@ export async function captureApiOptionsForPlugin(apiOptions = {}, options = {})
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
async function readSamplePluginConfig(pluginRoot) {
|
|
20
|
-
const manifestPath = await findNearestManifestPath(pluginRoot);
|
|
19
|
+
async function readSamplePluginConfig(pluginRoot, signal) {
|
|
20
|
+
const manifestPath = await findNearestManifestPath(pluginRoot, signal);
|
|
21
21
|
if (!manifestPath) {
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
24
|
let manifest;
|
|
25
25
|
try {
|
|
26
|
-
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
26
|
+
manifest = JSON.parse(await readFile(manifestPath, { encoding: "utf8", signal }));
|
|
27
27
|
} catch {
|
|
28
|
+
signal?.throwIfAborted();
|
|
28
29
|
return undefined;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -32,14 +33,16 @@ async function readSamplePluginConfig(pluginRoot) {
|
|
|
32
33
|
return isPlainObject(sample) && Object.keys(sample).length > 0 ? sample : undefined;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
async function findNearestManifestPath(pluginRoot) {
|
|
36
|
+
async function findNearestManifestPath(pluginRoot, signal) {
|
|
36
37
|
let current = path.resolve(pluginRoot);
|
|
37
38
|
while (true) {
|
|
38
39
|
const manifestPath = path.join(current, "openclaw.plugin.json");
|
|
39
40
|
try {
|
|
40
|
-
await readFile(manifestPath, "utf8");
|
|
41
|
+
await readFile(manifestPath, { encoding: "utf8", signal });
|
|
41
42
|
return manifestPath;
|
|
42
|
-
} catch {
|
|
43
|
+
} catch {
|
|
44
|
+
signal?.throwIfAborted();
|
|
45
|
+
}
|
|
43
46
|
|
|
44
47
|
const parent = path.dirname(current);
|
|
45
48
|
if (parent === current) {
|
package/src/cli.js
CHANGED
|
@@ -115,6 +115,7 @@ async function runCheck(commandArgs, options = {}) {
|
|
|
115
115
|
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
116
116
|
const authorFacing = readAuthorFacingFlag(commandArgs);
|
|
117
117
|
const { report, paths } = await runPluginCheck({
|
|
118
|
+
isolateCapture: true,
|
|
118
119
|
allowExecution,
|
|
119
120
|
authorFacing,
|
|
120
121
|
capture,
|
|
@@ -279,6 +280,7 @@ async function runCiCompatibilityReport({
|
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
const { report } = await runPluginCheck({
|
|
283
|
+
isolateCapture: true,
|
|
282
284
|
allowExecution,
|
|
283
285
|
authorFacing,
|
|
284
286
|
capture,
|
|
@@ -307,7 +309,7 @@ async function runCapture(commandArgs) {
|
|
|
307
309
|
throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 or --allow-execute in an isolated workspace");
|
|
308
310
|
}
|
|
309
311
|
|
|
310
|
-
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
|
|
312
|
+
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot, isolateCapture: true });
|
|
311
313
|
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
312
314
|
if (outputPath) {
|
|
313
315
|
await writeArtifacts([{ path: outputPath, content: json }]);
|
|
@@ -226,15 +226,15 @@ function readinessStatus(blockers) {
|
|
|
226
226
|
if (blockers.length === 0) {
|
|
227
227
|
return "ready";
|
|
228
228
|
}
|
|
229
|
-
if (blockers.some((blocker) => blocker.code === "sdk-alias-required")) {
|
|
230
|
-
return "sdk-alias-required";
|
|
231
|
-
}
|
|
232
229
|
if (blockers.some((blocker) => blocker.code === "build-required")) {
|
|
233
230
|
return "build-required";
|
|
234
231
|
}
|
|
235
232
|
if (blockers.some((blocker) => blocker.code === "missing-entrypoint")) {
|
|
236
233
|
return "missing";
|
|
237
234
|
}
|
|
235
|
+
if (blockers.some((blocker) => blocker.code === "sdk-alias-required")) {
|
|
236
|
+
return "sdk-alias-required";
|
|
237
|
+
}
|
|
238
238
|
if (blockers.some((blocker) => blocker.code === "ts-loader-required")) {
|
|
239
239
|
return "ts-loader-required";
|
|
240
240
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
4
|
+
import { readBoundedJsonArtifact, renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
5
5
|
import { resolveFromRoot } from "./path-utils.js";
|
|
6
|
-
import { runProfiledProcess } from "./process-profile.js";
|
|
6
|
+
import { resolveProcessLimits, runProfiledProcess } from "./process-profile.js";
|
|
7
7
|
import { assertRunCount, percentile } from "./stats.js";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const defaultRunnerPath = fileURLToPath(new URL("./mock-sdk-capture-runner.js", import.meta.url));
|
|
10
10
|
|
|
11
11
|
export const defaultImportLoopProfileOptions = {
|
|
12
12
|
entrypoint: "test/fixtures/lazy-import-plugin.mjs",
|
|
@@ -255,18 +255,49 @@ async function runCaptureSample(options) {
|
|
|
255
255
|
const outputPath = path.join(outputDir, `${options.sampleName ?? "capture"}-${options.index}.json`);
|
|
256
256
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
257
257
|
|
|
258
|
-
const
|
|
258
|
+
const defaultCapture = typeof options.captureCommand !== "function" && !options.captureScript;
|
|
259
|
+
const maxOutputBytes = resolveProcessLimits({
|
|
260
|
+
...options,
|
|
261
|
+
env: { ...process.env, ...options.env, ...options.captureEnv },
|
|
262
|
+
}, "CAPTURE").maxOutputBytes;
|
|
263
|
+
// Only the built-in route owns these sample files. An early process.exit(0)
|
|
264
|
+
// must not turn a previous capture into this run's successful result.
|
|
265
|
+
if (defaultCapture) await rm(outputPath, { force: true });
|
|
266
|
+
const command = buildCaptureCommand({ ...options, outputPath, maxOutputBytes });
|
|
259
267
|
const profile = await runProfiledProcess({
|
|
260
268
|
command: command.command,
|
|
261
269
|
args: command.args,
|
|
262
270
|
cwd: command.cwd ?? options.rootDir,
|
|
263
|
-
env: { ...process.env, ...command.env },
|
|
271
|
+
env: { ...process.env, ...options.env, ...command.env },
|
|
272
|
+
timeoutMs: options.timeoutMs,
|
|
273
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
274
|
+
killGraceMs: options.killGraceMs,
|
|
275
|
+
signal: options.signal,
|
|
264
276
|
});
|
|
265
|
-
|
|
277
|
+
let output = null;
|
|
278
|
+
if (profile.exitCode === 0 && !profile.timedOut && !profile.cancelled) {
|
|
279
|
+
if (defaultCapture) {
|
|
280
|
+
try {
|
|
281
|
+
output = await readCaptureOutput(outputPath, maxOutputBytes);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
profile.exitCode = 1;
|
|
284
|
+
profile.stderrPreview = `Invalid capture artifact: ${error.message}`;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
output = await readCaptureOutput(outputPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (options.signal?.aborted) {
|
|
291
|
+
profile.exitCode = 1;
|
|
292
|
+
profile.cancelled = true;
|
|
293
|
+
output = null;
|
|
294
|
+
}
|
|
266
295
|
|
|
267
296
|
return {
|
|
268
297
|
index: options.index,
|
|
269
298
|
exitCode: profile.exitCode,
|
|
299
|
+
timedOut: profile.timedOut === true,
|
|
300
|
+
cancelled: profile.cancelled === true,
|
|
270
301
|
status: output?.status ?? "failed",
|
|
271
302
|
capturedCount: output?.captured?.length ?? 0,
|
|
272
303
|
openClawLifecycle: output?.openClawLifecycle ?? null,
|
|
@@ -407,15 +438,29 @@ function buildCaptureCommand(options) {
|
|
|
407
438
|
}
|
|
408
439
|
return {
|
|
409
440
|
command: process.execPath,
|
|
410
|
-
args: [
|
|
441
|
+
args: [
|
|
442
|
+
"--no-warnings",
|
|
443
|
+
"--preserve-symlinks",
|
|
444
|
+
defaultRunnerPath,
|
|
445
|
+
JSON.stringify({
|
|
446
|
+
entrypoint: options.entrypoint,
|
|
447
|
+
cwd: options.rootDir,
|
|
448
|
+
outputPath: options.outputPath,
|
|
449
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
450
|
+
}),
|
|
451
|
+
],
|
|
411
452
|
cwd: options.rootDir,
|
|
412
453
|
env: { PLUGIN_INSPECTOR_EXECUTE_ISOLATED: "1", ...options.captureEnv },
|
|
413
454
|
};
|
|
414
455
|
}
|
|
415
456
|
|
|
416
|
-
async function readCaptureOutput(outputPath) {
|
|
417
|
-
|
|
418
|
-
|
|
457
|
+
async function readCaptureOutput(outputPath, maxOutputBytes) {
|
|
458
|
+
if (maxOutputBytes === undefined) return JSON.parse(await readFile(outputPath, "utf8"));
|
|
459
|
+
const result = await readBoundedJsonArtifact(outputPath, maxOutputBytes);
|
|
460
|
+
if (!result || typeof result.status !== "string" || !Array.isArray(result.captured)) {
|
|
461
|
+
throw new Error("expected a capture status and captured contracts");
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
419
464
|
}
|
|
420
465
|
|
|
421
466
|
function markdownTable(rows, headers) {
|