@openclaw/plugin-inspector 0.3.24 → 0.3.26
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 +31 -0
- package/README.md +141 -0
- package/package.json +4 -2
- package/src/api.js +7 -0
- package/src/artifacts.js +22 -1
- package/src/batch.js +7 -12
- 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 +6 -2
- package/src/cold-import-readiness.js +3 -3
- package/src/fixture-summary.js +43 -6
- package/src/import-loop-profile.js +56 -11
- package/src/inspector.js +120 -99
- package/src/mock-sdk-capture-runner.js +61 -22
- package/src/openclaw-target.js +110 -5
- 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-imports.js +191 -0
- package/src/runtime-profile.js +4 -0
- package/src/runtime-reconciliation.js +0 -6
- package/src/sdk-mock.js +128 -13
- package/src/synthetic-entrypoint.js +19 -19
- package/src/synthetic-probes-cli.js +79 -2
- package/src/synthetic-probes.js +212 -34
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.26 - 2026-09-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Recognize OpenClaw's declared private-local Plugin SDK subpaths and reserved bundled-plugin imports when inspecting bundled `extensions/*` fixtures, while continuing to report those imports for external plugins and genuinely missing SDK aliases.
|
|
10
|
+
- Treat active OpenClaw conversation-access compat records with present contract tests as target-owned proof instead of repeatedly emitting a P1 Inspector probe backlog advisory.
|
|
11
|
+
- Record explicit method-scoped Gateway probe prerequisites before calling handlers that require unavailable host state or live credentials; preserve actual response failures for admitted probes.
|
|
12
|
+
- Preserve absent optional strings in SDK mocks and supply a stable runtime config snapshot to Gateway probes instead of inventing configured values or missing host accessors.
|
|
13
|
+
- Generate SDK mocks for literal dynamic imports in retained handlers, including their named exports, while excluding TypeScript import types and keeping source inspection aligned with runtime capture.
|
|
14
|
+
- Honor the OpenClaw lazy-runtime SDK contract in generated mocks, preserving deferred module loading, shared promise caches, explicit cache clearing, and rejected imports instead of returning callable placeholders.
|
|
15
|
+
- Reject invalid batch concurrency instead of reporting success without inspecting any plugins, and preserve relative plugin paths in retained reports so similar directory names cannot overwrite each other's results.
|
|
16
|
+
- Preserve error messages from the OpenClaw `error-runtime` SDK subpath in synthetic probes, including lazy CommonJS imports, so Gateway rejections report their actual cause and remain failures.
|
|
17
|
+
- Keep tool-hook and conversation-privacy contract gaps open when runtime artifacts only capture hook registration. Preserve registration coverage and semantic contract probes.
|
|
18
|
+
- Read OpenClaw compatibility records from the explicitly imported `registry-records.ts` data module, avoiding false missing-record findings after the registry split. Preserve inline registries and report missing delegated data as an error.
|
|
19
|
+
|
|
20
|
+
## 0.3.25 - 2026-09-09
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- Run `registerService` start, stop, and dispose probes serially so teardown cannot overlap startup.
|
|
27
|
+
- 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.
|
|
28
|
+
- 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.
|
|
29
|
+
- 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.
|
|
30
|
+
- 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.
|
|
31
|
+
- 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.
|
|
32
|
+
- Capture plugins that bind `api.runtime.modelAuth` during registration with credential-free defaults; auth acquisition remains an explicit synthetic failure.
|
|
33
|
+
- Report absent build output and missing entrypoints before SDK alias blockers in cold-import readiness, preserving build-required totals and all remediation evidence.
|
|
34
|
+
- 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.
|
|
35
|
+
|
|
5
36
|
## 0.3.24 - 2026-08-31
|
|
6
37
|
|
|
7
38
|
### Fixed
|
package/README.md
CHANGED
|
@@ -200,6 +200,13 @@ Common options:
|
|
|
200
200
|
| `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
|
|
201
201
|
| `--no-sarif` / `--no-junit` | Disable default `ci` outputs. |
|
|
202
202
|
|
|
203
|
+
For `batch`, `--concurrency <n>` must be a finite number (default `4`, rounded
|
|
204
|
+
and clamped to `1`–`32` workers). Invalid or missing values fail before inspection
|
|
205
|
+
or report writes. `--keep-plugin-reports` retains individual reports under
|
|
206
|
+
`<out>/plugins/<relative-plugin-path>/`, preserving the corpus directory layout
|
|
207
|
+
and names so plugins such as `a/b`, `a-b`, and `a b` cannot overwrite each other's
|
|
208
|
+
reports. If the corpus root is itself a plugin, its reports go in `<out>/plugins/`.
|
|
209
|
+
|
|
203
210
|
Run the built-in help for the exact CLI surface:
|
|
204
211
|
|
|
205
212
|
```bash
|
|
@@ -232,6 +239,20 @@ That keeps compatibility CI offline and credential-free. It does not call live
|
|
|
232
239
|
services, launch OpenClaw, run provider SDKs, or emulate service lifecycle side
|
|
233
240
|
effects.
|
|
234
241
|
|
|
242
|
+
CommonJS SDK mocking, including compiled `.cjs` entrypoints and lazy `require()`
|
|
243
|
+
calls in synthetic handlers, requires Node.js 22.15 or newer with
|
|
244
|
+
`module.registerHooks()`. On older Node versions, upgrade Node.js or use an
|
|
245
|
+
ESM/TypeScript entrypoint. This capability requirement does not change the
|
|
246
|
+
package's Node.js `>=22` engine range or gate existing ESM/TypeScript capture.
|
|
247
|
+
Static inspection also discovers literal CommonJS SDK `require()` references.
|
|
248
|
+
|
|
249
|
+
The default capture `api.runtime.modelAuth` passes synthetic provider IDs through
|
|
250
|
+
unchanged, returns fresh empty auth stores and profile lists, and reports no
|
|
251
|
+
configured API keys. Auth acquisition rejects with a mock-auth-unavailable error;
|
|
252
|
+
it never looks up host credentials. This supports registration and no-auth
|
|
253
|
+
callbacks, not provider alias validation or authenticated execution. An explicitly
|
|
254
|
+
supplied runtime is preserved unchanged, including an empty runtime.
|
|
255
|
+
|
|
235
256
|
Synthetic probes classify widget presenters as metadata-only. They record the
|
|
236
257
|
registration without calling its match, availability, or presentation callbacks,
|
|
237
258
|
including when channel, provider, or lifecycle execution is enabled.
|
|
@@ -239,6 +260,20 @@ including when channel, provider, or lifecycle execution is enabled.
|
|
|
239
260
|
Use `--real-sdk` only when the plugin workspace already has real SDK
|
|
240
261
|
dependencies installed and you intentionally want that path.
|
|
241
262
|
|
|
263
|
+
Real-SDK CLI capture runs in an owned child, including runtime capture enabled
|
|
264
|
+
by flags or plugin config. The parent bounds imports and registration, then
|
|
265
|
+
cleans up retained plugin timers after the child flushes its complete result.
|
|
266
|
+
It uses installed SDK dependencies without loading the mock SDK.
|
|
267
|
+
|
|
268
|
+
The real-SDK programmatic API stays in-process to preserve supplied runtime
|
|
269
|
+
objects and retained handler identity. Its 30-second default deadline reports
|
|
270
|
+
`capture-timeout`, stops later inspector-owned phases, and aborts supported
|
|
271
|
+
setup reads. Caller `signal` cancellation also stops later phases. Neither
|
|
272
|
+
mechanism can preempt a synchronous JavaScript loop, unload an import, stop
|
|
273
|
+
arbitrary plugin side effects, or clear plugin-owned timers in the caller's
|
|
274
|
+
process. Only owned-child capture provides that process-lifetime boundary.
|
|
275
|
+
Override the API budget with `timeoutMs` or `PLUGIN_INSPECTOR_CAPTURE_TIMEOUT_MS`.
|
|
276
|
+
|
|
242
277
|
Runtime capture writes:
|
|
243
278
|
|
|
244
279
|
- `reports/plugin-inspector-runtime-capture.json`
|
|
@@ -250,6 +285,110 @@ Capture one entrypoint directly:
|
|
|
250
285
|
plugin-inspector capture ./dist/index.js --mock-sdk --allow-execute
|
|
251
286
|
```
|
|
252
287
|
|
|
288
|
+
CLI capture, mock-SDK API capture, and import-loop/runtime profiles give each
|
|
289
|
+
child a 30-second budget. Capture reports `capture-timeout`; timed-out profile samples
|
|
290
|
+
always have a nonzero `exitCode`, even if a SIGTERM handler exits zero.
|
|
291
|
+
Pass an `AbortSignal` as `signal` to cancel owned-child work. Cancellation is
|
|
292
|
+
never a successful capture or profile sample.
|
|
293
|
+
|
|
294
|
+
On POSIX, each child owns a separate process group. Completion waits for
|
|
295
|
+
stdout/stderr to close and cleans descendants, including after a successful
|
|
296
|
+
leader exit. Shutdown sends SIGTERM, then SIGKILL after a 1-second grace
|
|
297
|
+
period. A further 1-second close deadline fails the operation if pipes remain
|
|
298
|
+
open. Descendants that deliberately leave the group are not contained;
|
|
299
|
+
this is lifecycle supervision, not a sandbox. Windows retains direct-child
|
|
300
|
+
termination and the bounded close deadline, not POSIX group cleanup.
|
|
301
|
+
|
|
302
|
+
The API options `timeoutMs`, `killGraceMs`, and `maxOutputBytes` take precedence
|
|
303
|
+
over `PLUGIN_INSPECTOR_CAPTURE_TIMEOUT_MS`, `PLUGIN_INSPECTOR_CAPTURE_KILL_GRACE_MS`,
|
|
304
|
+
and `PLUGIN_INSPECTOR_CAPTURE_MAX_OUTPUT_BYTES` for owned-child capture. Profiles use
|
|
305
|
+
the corresponding `PLUGIN_INSPECTOR_PROFILE_*` variables. Values must be finite
|
|
306
|
+
positive numbers (zero does not disable limits); invalid values fall through
|
|
307
|
+
to the environment, then defaults. Durations/byte limits cannot exceed
|
|
308
|
+
2,147,483,647; grace cannot exceed 30,000 ms.
|
|
309
|
+
|
|
310
|
+
Each profiled stdout/stderr stream retains at most 1 MiB by default while
|
|
311
|
+
continuing to drain output. Owned-child capture retains at most 10 MiB per pipe and
|
|
312
|
+
fails if its JSON response is truncated; intercepted plugin stdout/stderr
|
|
313
|
+
inside that response retains at most 1 MiB each. The optional `ps` sampler
|
|
314
|
+
also has bounded output, execution, and cleanup.
|
|
315
|
+
|
|
316
|
+
Default import-loop profiles launch the mock capture runner directly under one
|
|
317
|
+
profile budget, for both baseline and plugin samples. Their JSON artifacts
|
|
318
|
+
retain capture's 10 MiB default limit (`PLUGIN_INSPECTOR_CAPTURE_MAX_OUTPUT_BYTES`,
|
|
319
|
+
or an explicit `maxOutputBytes` override), separately from the profile's
|
|
320
|
+
1 MiB stdout/stderr limits. Artifacts are accepted only after a successful
|
|
321
|
+
current capture. RSS and CPU now measure the actual runner, and wall time no
|
|
322
|
+
longer includes intermediate CLI startup. Historical measurements from the
|
|
323
|
+
CLI-wrapper route are not directly comparable. Custom `captureCommand` and
|
|
324
|
+
`captureScript` launch contracts are unchanged; custom detached groups are
|
|
325
|
+
outside the owned process group.
|
|
326
|
+
|
|
327
|
+
These limits apply to owned child processes only. The public in-process
|
|
328
|
+
`captureEntrypoint` path preserves retained handler identity and does not
|
|
329
|
+
claim to cancel synchronous plugin code or retained callbacks.
|
|
330
|
+
|
|
331
|
+
Synthetic probe APIs give each invoked callback a 30-second default budget.
|
|
332
|
+
Set `timeoutMs` or `PLUGIN_INSPECTOR_PROBE_TIMEOUT_MS`; the same finite positive
|
|
333
|
+
API-then-environment validation applies, with no zero or infinite opt-out.
|
|
334
|
+
A timed-out callback becomes a failed row and remaining dependent probes are
|
|
335
|
+
blocked. Ordinary handler failures remain failed rows without stopping
|
|
336
|
+
independent probes. Caller `signal` cancellation rejects the API call.
|
|
337
|
+
Timeout and cancellation abort supported handler signal arguments and observe
|
|
338
|
+
late promise settlement, but cannot preempt synchronous JavaScript or arbitrary
|
|
339
|
+
plugin side effects. Programmatic probes stay in-process and preserve caller
|
|
340
|
+
runtime objects and retained callback identity.
|
|
341
|
+
|
|
342
|
+
Gateway method probes invoke each registered handler once, including positional
|
|
343
|
+
`(method, handler, options)` registrations; captured `handler`/`run`/`execute`
|
|
344
|
+
aliases do not create extra calls. The handler receives synthetic Gateway
|
|
345
|
+
options and a void `respond(ok, payload, error, meta)` callback. Existing
|
|
346
|
+
`registrationProbeInputs` overrides remain available.
|
|
347
|
+
|
|
348
|
+
For a method that needs unavailable host state or live credentials, pass
|
|
349
|
+
`gatewayMethodPrerequisites: { "fixture.account": "saved account required" }`
|
|
350
|
+
to `runCapturedSyntheticProbes` or `runEntrypointSyntheticProbes`. The named
|
|
351
|
+
method produces a `blocked` row with its method and reason before its handler
|
|
352
|
+
runs. Other methods retain normal response validation. Once a caller supplies
|
|
353
|
+
the required inputs and runtime, omit that method from the prerequisite map;
|
|
354
|
+
rejected or malformed responses still fail. This option never reports a
|
|
355
|
+
missing prerequisite as a passing runtime check.
|
|
356
|
+
|
|
357
|
+
The first emitted response is authoritative, even when malformed. Probes check
|
|
358
|
+
its JSON-serialized response/error shape: `ok: true` passes, `ok: false` fails,
|
|
359
|
+
and later responses cannot overwrite the outcome. Logging `meta` is not a wire
|
|
360
|
+
field. Without an explicit response, a non-`undefined` return is adapted into a
|
|
361
|
+
successful payload, including `false`, `null`, and `{ ok: false }`, matching the
|
|
362
|
+
OpenClaw plugin registrar. A void handler can respond later within the existing
|
|
363
|
+
probe deadline; no response or return before that deadline fails. Handler throws,
|
|
364
|
+
rejections, and timeouts still fail under the ordinary probe rules.
|
|
365
|
+
|
|
366
|
+
This observes the initial RPC response plus ordinary handler settlement within
|
|
367
|
+
the existing deadline. It does not prove asynchronous operation completion,
|
|
368
|
+
transport delivery, or authorization. An accepted-only success is valid once the
|
|
369
|
+
handler settles; probes do not implicitly request `expectFinal` or reject legal
|
|
370
|
+
multi-frame methods. No Gateway connection or live host call is made.
|
|
371
|
+
|
|
372
|
+
`synthetic-probes-cli.js` runs capture and retained callbacks together in one
|
|
373
|
+
owned child. Its whole-child budget also defaults to 30 seconds, including
|
|
374
|
+
imports and registration, with `PLUGIN_INSPECTOR_PROBE_TIMEOUT_MS`,
|
|
375
|
+
`PLUGIN_INSPECTOR_PROBE_KILL_GRACE_MS`, and
|
|
376
|
+
`PLUGIN_INSPECTOR_PROBE_MAX_OUTPUT_BYTES` overrides. Shutdown uses the same
|
|
377
|
+
bounded grace and process-group cleanup as capture. The default report and
|
|
378
|
+
per-pipe limit is 10 MiB, matching capture; intercepted plugin stdout and stderr are each capped
|
|
379
|
+
at 1 MiB and kept separate from the report protocol.
|
|
380
|
+
|
|
381
|
+
Completed synthetic reports are still written even when they contain failed
|
|
382
|
+
probe rows; the CLI exits successfully after delivering them, and CI policy
|
|
383
|
+
evaluates those rows. Child timeout, cancellation, truncated output, or an
|
|
384
|
+
oversized or malformed report instead exits unsuccessfully without writing a
|
|
385
|
+
new output artifact. The CLI validates the report shape, row identities and
|
|
386
|
+
statuses, and summary counts before publication; valid empty and blocked
|
|
387
|
+
reports are preserved. This is protocol validation, not authenticated
|
|
388
|
+
completion or a security sandbox: same-process plugin code can still fabricate
|
|
389
|
+
a valid report. Healthy retained intervals cannot keep the child alive after
|
|
390
|
+
its complete report is flushed.
|
|
391
|
+
|
|
253
392
|
## CI
|
|
254
393
|
|
|
255
394
|
`plugin-inspector ci` writes the normal compatibility report plus CI-native
|
|
@@ -444,6 +583,8 @@ npm run check
|
|
|
444
583
|
```
|
|
445
584
|
|
|
446
585
|
`npm run check` runs the Node test suite and the package-contents guard. The
|
|
586
|
+
test runner limits parallel test files to four so process-supervision tests do
|
|
587
|
+
not contend with a machine-wide burst of child processes for their deadlines. The
|
|
447
588
|
contents guard shells through `npm pack --dry-run --json` and verifies the npm
|
|
448
589
|
tarball includes package entrypoints, examples, README assets, and no private
|
|
449
590
|
`test/`, `scripts/`, or `.github/` paths.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/plugin-inspector",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Offline compatibility inspector for OpenClaw plugins.",
|
|
6
6
|
"type": "module",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"release:readiness": "npm run release:local && npm run release:crabpot",
|
|
58
58
|
"release:local": "npm run check",
|
|
59
59
|
"release:notes": "node scripts/release-notes.mjs --unreleased",
|
|
60
|
-
"test": "node --test test/*.test.js",
|
|
60
|
+
"test": "node --test --test-concurrency=4 test/*.test.js",
|
|
61
61
|
"check:changed": "npm run check",
|
|
62
62
|
"test:changed": "npm test",
|
|
63
63
|
"crabbox:hydrate": "crabbox actions hydrate",
|
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
"ci"
|
|
73
73
|
],
|
|
74
74
|
"dependencies": {
|
|
75
|
+
"acorn": "^8.18.0",
|
|
76
|
+
"eslint-scope": "^8.4.0",
|
|
75
77
|
"semver": "^7.8.5",
|
|
76
78
|
"tar": "^7.5.22"
|
|
77
79
|
}
|
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/batch.js
CHANGED
|
@@ -21,7 +21,11 @@ export async function runBatchAnalysis(options = {}) {
|
|
|
21
21
|
const rootDir = path.resolve(options.rootDir ?? options.inputDir ?? process.cwd());
|
|
22
22
|
const outDir = options.outDir ?? "reports";
|
|
23
23
|
const outRoot = path.resolve(rootDir, outDir);
|
|
24
|
-
const
|
|
24
|
+
const requestedConcurrency = options.concurrency ?? 4;
|
|
25
|
+
if (!Number.isFinite(requestedConcurrency)) {
|
|
26
|
+
throw new TypeError("batch concurrency must be a finite number");
|
|
27
|
+
}
|
|
28
|
+
const concurrency = Math.max(1, Math.min(Math.round(requestedConcurrency), 32));
|
|
25
29
|
const keepPluginReports = options.keepPluginReports === true;
|
|
26
30
|
const targetOpenClaw =
|
|
27
31
|
options.targetOpenClaw ??
|
|
@@ -35,8 +39,8 @@ export async function runBatchAnalysis(options = {}) {
|
|
|
35
39
|
try {
|
|
36
40
|
await runWithConcurrency(pluginRoots, concurrency, async (pluginRoot) => {
|
|
37
41
|
const reportsRoot = keepPluginReports
|
|
38
|
-
? path.join(outRoot, "plugins",
|
|
39
|
-
: path.join(tempRoot,
|
|
42
|
+
? path.join(outRoot, "plugins", path.relative(rootDir, pluginRoot))
|
|
43
|
+
: path.join(tempRoot, path.relative(rootDir, pluginRoot));
|
|
40
44
|
entries.push(
|
|
41
45
|
await inspectBatchPlugin(pluginRoot, {
|
|
42
46
|
...options,
|
|
@@ -306,12 +310,3 @@ function packageNameFromReport(report) {
|
|
|
306
310
|
"plugin"
|
|
307
311
|
);
|
|
308
312
|
}
|
|
309
|
-
|
|
310
|
-
function slugForPath(value) {
|
|
311
|
-
return (
|
|
312
|
-
String(value)
|
|
313
|
-
.replaceAll(path.sep, "-")
|
|
314
|
-
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
315
|
-
.replace(/^-+|-+$/g, "") || "plugin"
|
|
316
|
-
);
|
|
317
|
-
}
|
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
|
@@ -61,7 +61,9 @@ async function runBatch(commandArgs) {
|
|
|
61
61
|
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
62
62
|
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
63
63
|
const openclawVersion = readOpenClawVersion(commandArgs);
|
|
64
|
-
const concurrency =
|
|
64
|
+
const concurrency = commandArgs.includes("--concurrency")
|
|
65
|
+
? Number(readFlag(commandArgs, "--concurrency") ?? NaN)
|
|
66
|
+
: 4;
|
|
65
67
|
const json = commandArgs.includes("--json");
|
|
66
68
|
const check = commandArgs.includes("--check");
|
|
67
69
|
const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
|
|
@@ -115,6 +117,7 @@ async function runCheck(commandArgs, options = {}) {
|
|
|
115
117
|
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
116
118
|
const authorFacing = readAuthorFacingFlag(commandArgs);
|
|
117
119
|
const { report, paths } = await runPluginCheck({
|
|
120
|
+
isolateCapture: true,
|
|
118
121
|
allowExecution,
|
|
119
122
|
authorFacing,
|
|
120
123
|
capture,
|
|
@@ -279,6 +282,7 @@ async function runCiCompatibilityReport({
|
|
|
279
282
|
}
|
|
280
283
|
|
|
281
284
|
const { report } = await runPluginCheck({
|
|
285
|
+
isolateCapture: true,
|
|
282
286
|
allowExecution,
|
|
283
287
|
authorFacing,
|
|
284
288
|
capture,
|
|
@@ -307,7 +311,7 @@ async function runCapture(commandArgs) {
|
|
|
307
311
|
throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 or --allow-execute in an isolated workspace");
|
|
308
312
|
}
|
|
309
313
|
|
|
310
|
-
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
|
|
314
|
+
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot, isolateCapture: true });
|
|
311
315
|
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
312
316
|
if (outputPath) {
|
|
313
317
|
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
|
}
|
package/src/fixture-summary.js
CHANGED
|
@@ -550,14 +550,16 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
550
550
|
|
|
551
551
|
const conversationHooks = inspection.hooks.filter((hook) => conversationAccessHooks.has(hook));
|
|
552
552
|
const conversationHookDetails = inspection.hookDetails.filter((hook) => conversationAccessHooks.has(hook.name));
|
|
553
|
-
|
|
553
|
+
const conversationCompatRecord = compatRecordForIssueCode("conversation-access-hook");
|
|
554
|
+
const conversationContractCovered = hasActiveTargetContractTests(targetOpenClaw, conversationCompatRecord);
|
|
555
|
+
if (conversationHooks.length > 0 && !conversationContractCovered) {
|
|
554
556
|
warnings.push({
|
|
555
557
|
fixture: fixture.id,
|
|
556
558
|
code: "conversation-access-hook",
|
|
557
559
|
level: "warning",
|
|
558
560
|
message: "fixture observes raw model or conversation content and needs privacy-boundary contract probes",
|
|
559
561
|
evidence: detailEvidence(conversationHookDetails),
|
|
560
|
-
compatRecord:
|
|
562
|
+
compatRecord: conversationCompatRecord,
|
|
561
563
|
});
|
|
562
564
|
decisions.push({
|
|
563
565
|
fixture: fixture.id,
|
|
@@ -713,6 +715,15 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
713
715
|
return { breakages, warnings, suggestions, logs, decisions };
|
|
714
716
|
}
|
|
715
717
|
|
|
718
|
+
function hasActiveTargetContractTests(targetOpenClaw, compatRecord) {
|
|
719
|
+
if (!compatRecord || !["active", "supported"].includes(targetOpenClaw.compatRecordStatuses?.[compatRecord])) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
const tests = targetOpenClaw.compatRecordTests?.[compatRecord] ?? [];
|
|
723
|
+
const missingTests = targetOpenClaw.compatRecordMissingTests?.[compatRecord] ?? [];
|
|
724
|
+
return tests.length > 0 && missingTests.length === 0;
|
|
725
|
+
}
|
|
726
|
+
|
|
716
727
|
function classifySdkDeprecations({ fixture, inspection, fixtureReport, warnings, decisions }) {
|
|
717
728
|
const grouped = new Map();
|
|
718
729
|
for (const finding of fixtureReport.sdkDeprecations ?? inspection.sdkDeprecations ?? []) {
|
|
@@ -883,10 +894,22 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
883
894
|
}
|
|
884
895
|
|
|
885
896
|
const sdkExports = new Set(targetOpenClaw.sdkExports);
|
|
886
|
-
const
|
|
897
|
+
const bundledPluginId = bundledOpenClawPluginId(fixture, targetOpenClaw);
|
|
898
|
+
const isBundledFixture = bundledPluginId !== null;
|
|
899
|
+
const privateLocalSdkExports = new Set(targetOpenClaw.privateLocalSdkExports ?? []);
|
|
900
|
+
const unknownImports = fixtureReport.sdkImportDetails.filter(
|
|
901
|
+
(sdkImport) =>
|
|
902
|
+
!sdkExports.has(sdkImport.specifier) &&
|
|
903
|
+
!(isBundledFixture && privateLocalSdkExports.has(sdkImport.specifier)),
|
|
904
|
+
);
|
|
887
905
|
const reservedSdkExports = new Set(targetOpenClaw.reservedSdkExports ?? []);
|
|
888
|
-
const reservedImports = fixtureReport.sdkImportDetails.filter(
|
|
889
|
-
|
|
906
|
+
const reservedImports = fixtureReport.sdkImportDetails.filter(
|
|
907
|
+
(sdkImport) =>
|
|
908
|
+
reservedSdkExports.has(sdkImport.specifier) &&
|
|
909
|
+
!(
|
|
910
|
+
bundledPluginId !== null &&
|
|
911
|
+
targetOpenClaw.reservedSdkExportOwners?.[sdkImport.specifier] === bundledPluginId
|
|
912
|
+
),
|
|
890
913
|
);
|
|
891
914
|
|
|
892
915
|
if (reservedImports.length === 0 && unknownImports.length === 0) {
|
|
@@ -894,7 +917,9 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
894
917
|
fixture: fixture.id,
|
|
895
918
|
code: "sdk-exports-present",
|
|
896
919
|
level: "log",
|
|
897
|
-
message:
|
|
920
|
+
message: isBundledFixture
|
|
921
|
+
? "all observed plugin SDK imports are exported or declared private-local for bundled plugins"
|
|
922
|
+
: "all observed plugin SDK imports exist in target OpenClaw package exports",
|
|
898
923
|
evidence: fixtureReport.sdkImports,
|
|
899
924
|
});
|
|
900
925
|
return;
|
|
@@ -938,6 +963,18 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
938
963
|
}
|
|
939
964
|
}
|
|
940
965
|
|
|
966
|
+
function bundledOpenClawPluginId(fixture, targetOpenClaw) {
|
|
967
|
+
if (fixture.repo !== "local" || !fixture.checkoutPath || !targetOpenClaw.checkoutPath) return null;
|
|
968
|
+
const fixturePath = normalizeRepoPath(fixture.checkoutPath);
|
|
969
|
+
const targetPath = normalizeRepoPath(targetOpenClaw.checkoutPath);
|
|
970
|
+
const relativePath = targetPath === "."
|
|
971
|
+
? fixturePath
|
|
972
|
+
: fixturePath.startsWith(`${targetPath}/`)
|
|
973
|
+
? fixturePath.slice(targetPath.length + 1)
|
|
974
|
+
: "";
|
|
975
|
+
return relativePath.match(/^extensions\/([^/]+)(?:\/|$)/)?.[1] ?? null;
|
|
976
|
+
}
|
|
977
|
+
|
|
941
978
|
function addVersionDerivedFinding({ finding, fixtureReport, targetOpenClaw, breakages, warnings, suggestions }) {
|
|
942
979
|
const compatibility = targetCompatibility(fixtureReport, targetOpenClaw);
|
|
943
980
|
if (!compatibility) {
|