@openclaw/plugin-inspector 0.3.17 → 0.3.19

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 CHANGED
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.19 - 2026-07-27
4
+
5
+ ### Highlights
6
+
7
+ - Restore silently-lost manifest contract findings after OpenClaw moved its public manifest types into `manifest-types.ts`.
8
+
9
+ ### Changed
10
+
11
+ - Refresh generated and repository GitHub Actions workflows to the current checkout, setup-node, upload-artifact, and pnpm/action-setup releases.
12
+
13
+ ## 0.3.18 - 2026-07-21
14
+
15
+ ### Highlights
16
+
17
+ - Keep compatibility capture current with OpenClaw's evolving hook, gateway, workspace-linking, blob-store, and Zod runtime surfaces.
18
+
19
+ ### Fixed
20
+
21
+ - Mock the current plugin blob-store runtime so Diffs and other trusted plugins can be captured without persistent state.
22
+ - Parse OpenClaw hook names after their source declaration became private and added a TypeScript `satisfies` constraint.
23
+ - Run synthetic gateway lifecycle hooks around ordinary probes while preserving capture indexes and report order, so `gateway_stop` teardown cannot invalidate later compatibility checks.
24
+ - Link isolated plugin workspaces to the OpenClaw checkout without npm normalizing duplicated dependency and peer declarations back to registry ranges.
25
+ - Keep mocked Zod schemas chainable through unsupported methods such as `pipe()` and `catch()`.
4
26
 
5
27
  ## 0.3.17 - 2026-06-29
6
28
 
package/README.md CHANGED
@@ -256,14 +256,14 @@ jobs:
256
256
  check:
257
257
  runs-on: ubuntu-latest
258
258
  steps:
259
- - uses: actions/checkout@v5
260
- - uses: actions/setup-node@v5
259
+ - uses: actions/checkout@v7
260
+ - uses: actions/setup-node@v7
261
261
  with:
262
262
  node-version: 24
263
263
  cache: npm
264
264
  - run: npm ci
265
265
  - run: npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
266
- - uses: actions/upload-artifact@v5
266
+ - uses: actions/upload-artifact@v7
267
267
  if: always()
268
268
  with:
269
269
  name: plugin-inspector-reports
@@ -13,8 +13,8 @@ jobs:
13
13
  check:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v5
17
- - uses: actions/setup-node@v5
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-node@v7
18
18
  with:
19
19
  node-version: 24
20
20
  cache: npm
@@ -24,7 +24,7 @@ jobs:
24
24
  if: always()
25
25
  with:
26
26
  sarif_file: reports/plugin-inspector.sarif
27
- - uses: actions/upload-artifact@v5
27
+ - uses: actions/upload-artifact@v7
28
28
  if: always()
29
29
  with:
30
30
  name: plugin-inspector-reports
@@ -9,14 +9,14 @@ jobs:
9
9
  check:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@v5
13
- - uses: actions/setup-node@v5
12
+ - uses: actions/checkout@v7
13
+ - uses: actions/setup-node@v7
14
14
  with:
15
15
  node-version: 24
16
16
  cache: npm
17
17
  - run: npm ci
18
18
  - run: npx @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
19
- - uses: actions/upload-artifact@v5
19
+ - uses: actions/upload-artifact@v7
20
20
  if: always()
21
21
  with:
22
22
  name: plugin-inspector-reports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
@@ -194,6 +194,8 @@ function registrationReturnValue(name, args, context) {
194
194
 
195
195
  function createRuntimeContext(options) {
196
196
  const runtime = options.runtime ?? {};
197
+ const blobStores = new Map();
198
+ const syncKeyedStores = new Map();
197
199
  return {
198
200
  ...runtime,
199
201
  agent: runtime.agent ?? {},
@@ -203,11 +205,149 @@ function createRuntimeContext(options) {
203
205
  tts: runtime.tts ?? {},
204
206
  state: {
205
207
  resolveStateDir: () => options.stateDir ?? process.cwd(),
208
+ openBlobStore(storeOptions) {
209
+ const existing = blobStores.get(storeOptions.namespace);
210
+ if (existing) {
211
+ return existing;
212
+ }
213
+ const store = createBlobStoreContext(storeOptions);
214
+ blobStores.set(storeOptions.namespace, store);
215
+ return store;
216
+ },
217
+ openSyncKeyedStore({ namespace }) {
218
+ const existing = syncKeyedStores.get(namespace);
219
+ if (existing) {
220
+ return existing;
221
+ }
222
+ const store = createSyncKeyedStoreContext();
223
+ syncKeyedStores.set(namespace, store);
224
+ return store;
225
+ },
206
226
  ...(runtime.state ?? {}),
207
227
  },
208
228
  };
209
229
  }
210
230
 
231
+ function createBlobStoreContext(options) {
232
+ const values = new Map();
233
+ const entryInfo = (key, entry) => ({
234
+ key,
235
+ metadata: entry.metadata,
236
+ sizeBytes: entry.bytes.byteLength,
237
+ createdAt: entry.createdAt,
238
+ ...(entry.expiresAt === undefined ? {} : { expiresAt: entry.expiresAt }),
239
+ });
240
+ const read = (key) => {
241
+ const entry = values.get(key);
242
+ if (!entry) {
243
+ return undefined;
244
+ }
245
+ if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {
246
+ return undefined;
247
+ }
248
+ return { ...entryInfo(key, entry), bytes: Uint8Array.from(entry.bytes) };
249
+ };
250
+ const register = async (key, bytes, metadata, registerOptions) => {
251
+ const createdAt = Date.now();
252
+ const ttlMs = registerOptions?.ttlMs ?? options.defaultTtlMs;
253
+ values.set(key, {
254
+ bytes: Uint8Array.from(bytes),
255
+ metadata,
256
+ createdAt,
257
+ ...(ttlMs === undefined ? {} : { expiresAt: createdAt + ttlMs }),
258
+ });
259
+ };
260
+ return {
261
+ register,
262
+ async registerIfAbsent(key, bytes, metadata, registerOptions) {
263
+ if (values.has(key)) {
264
+ return false;
265
+ }
266
+ await register(key, bytes, metadata, registerOptions);
267
+ return true;
268
+ },
269
+ async lookup(key) {
270
+ return read(key);
271
+ },
272
+ async entries() {
273
+ return [...values.keys()].flatMap((key) => {
274
+ const entry = read(key);
275
+ if (!entry) {
276
+ return [];
277
+ }
278
+ const { bytes: _bytes, ...info } = entry;
279
+ return [info];
280
+ });
281
+ },
282
+ async delete(key) {
283
+ return values.delete(key);
284
+ },
285
+ async deleteExpiredKey(key) {
286
+ const entry = values.get(key);
287
+ if (!entry || entry.expiresAt === undefined || entry.expiresAt > Date.now()) {
288
+ return undefined;
289
+ }
290
+ values.delete(key);
291
+ return entryInfo(key, entry);
292
+ },
293
+ async deleteExpired() {
294
+ const expired = [];
295
+ for (const [key, entry] of values) {
296
+ if (entry.expiresAt === undefined || entry.expiresAt > Date.now()) {
297
+ continue;
298
+ }
299
+ values.delete(key);
300
+ expired.push(entryInfo(key, entry));
301
+ }
302
+ return expired;
303
+ },
304
+ async clear() {
305
+ values.clear();
306
+ },
307
+ };
308
+ }
309
+
310
+ function createSyncKeyedStoreContext() {
311
+ const values = new Map();
312
+ return {
313
+ register(key, value) {
314
+ values.set(key, value);
315
+ },
316
+ registerIfAbsent(key, value) {
317
+ if (values.has(key)) {
318
+ return false;
319
+ }
320
+ values.set(key, value);
321
+ return true;
322
+ },
323
+ update(key, updateValue) {
324
+ const next = updateValue(values.get(key));
325
+ if (next === undefined) {
326
+ return false;
327
+ }
328
+ values.set(key, next);
329
+ return true;
330
+ },
331
+ lookup(key) {
332
+ return values.get(key);
333
+ },
334
+ consume(key) {
335
+ const value = values.get(key);
336
+ values.delete(key);
337
+ return value;
338
+ },
339
+ delete(key) {
340
+ return values.delete(key);
341
+ },
342
+ entries() {
343
+ return [...values].map(([key, value]) => ({ key, value, createdAt: 0 }));
344
+ },
345
+ clear() {
346
+ values.clear();
347
+ },
348
+ };
349
+ }
350
+
211
351
  function createSecretContext(options) {
212
352
  const secrets = new Map(Object.entries(options.secretValues ?? {}));
213
353
  return {
package/src/init.js CHANGED
@@ -108,14 +108,14 @@ jobs:
108
108
  check:
109
109
  runs-on: ubuntu-latest
110
110
  steps:
111
- - uses: actions/checkout@v5
112
- - uses: actions/setup-node@v5
111
+ - uses: actions/checkout@v7
112
+ - uses: actions/setup-node@v7
113
113
  with:
114
114
  node-version: 24
115
115
  cache: ${setup.cache}
116
116
  ${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
117
117
  - run: ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
118
- - uses: actions/upload-artifact@v5
118
+ - uses: actions/upload-artifact@v7
119
119
  if: always()
120
120
  with:
121
121
  name: plugin-inspector-reports
package/src/issues.js CHANGED
@@ -583,10 +583,6 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
583
583
  };
584
584
  }
585
585
 
586
- export function isInspectorGapFinding(finding, targetOpenClaw) {
587
- return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
588
- }
589
-
590
586
  export function isAuthorFacingFinding(finding, targetOpenClaw) {
591
587
  return Boolean(issueMetadata(finding, targetOpenClaw).authorRemediation);
592
588
  }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { writeJsonFileAtomic } from "./json-file.js";
6
+
7
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
8
+ const specifier = process.argv[2];
9
+ await linkOpenClawWorkspace(path.resolve(process.cwd(), "package.json"), specifier);
10
+ }
11
+
12
+ export async function linkOpenClawWorkspace(packageJsonPath, specifier) {
13
+ if (typeof specifier !== "string" || !specifier.startsWith("file:")) {
14
+ throw new TypeError("link-openclaw-workspace requires a file: dependency specifier");
15
+ }
16
+
17
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
18
+ let linked = false;
19
+ for (const section of ["dependencies", "optionalDependencies"]) {
20
+ if (Object.hasOwn(packageJson[section] ?? {}, "openclaw")) {
21
+ packageJson[section].openclaw = specifier;
22
+ linked = true;
23
+ }
24
+ }
25
+
26
+ if (!linked) {
27
+ packageJson.dependencies ??= {};
28
+ packageJson.dependencies.openclaw = specifier;
29
+ }
30
+
31
+ await writeJsonFileAtomic(packageJsonPath, packageJson);
32
+ }
@@ -30,19 +30,32 @@ export async function readOpenClawTargetSurface(options = {}) {
30
30
  const hookTypesPath = path.join(resolvedPath, "src/plugins/hook-types.ts");
31
31
  const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
32
32
  const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
33
- const manifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
33
+ const currentManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest-types.ts");
34
+ const legacyManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
34
35
  const pluginSdkEntrypointsPath = path.join(resolvedPath, "src/plugin-sdk/entrypoints.ts");
35
36
  const packagePath = path.join(resolvedPath, "package.json");
36
37
 
37
38
  const registrySource = await readFile(registryPath, "utf8");
38
39
  const compatRecordEntries = parseCompatRecordEntries(registrySource);
39
40
  const hookTypesSource = existsSync(hookTypesPath) ? await readFile(hookTypesPath, "utf8") : "";
40
- const hookNames = hookTypesSource ? parseExportedStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
41
+ const hookNames = hookTypesSource ? parseConstStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
41
42
  const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
42
43
  const apiRegistrars = apiBuilderSource ? parseApiRegistrars(apiBuilderSource) : [];
43
- const manifestTypesSource = existsSync(manifestTypesPath) ? await readFile(manifestTypesPath, "utf8") : "";
44
- const manifestFields = manifestTypesSource ? parseTypeFields(manifestTypesSource, "PluginManifest") : [];
45
- const manifestContractFields = manifestTypesSource ? parseTypeFields(manifestTypesSource, "PluginManifestContracts") : [];
44
+ const currentManifestTypesSource = existsSync(currentManifestTypesPath)
45
+ ? await readFile(currentManifestTypesPath, "utf8")
46
+ : "";
47
+ const legacyManifestTypesSource = existsSync(legacyManifestTypesPath)
48
+ ? await readFile(legacyManifestTypesPath, "utf8")
49
+ : "";
50
+ const currentManifestFields = parseTypeFields(currentManifestTypesSource, "PluginManifest");
51
+ const legacyManifestFields = parseTypeFields(legacyManifestTypesSource, "PluginManifest");
52
+ const currentManifestContractFields = parseTypeFields(currentManifestTypesSource, "PluginManifestContracts");
53
+ const legacyManifestContractFields = parseTypeFields(legacyManifestTypesSource, "PluginManifestContracts");
54
+ const useCurrentManifestTypes = currentManifestFields.length > 0;
55
+ const manifestTypesPath = useCurrentManifestTypes ? currentManifestTypesPath : legacyManifestTypesPath;
56
+ const manifestFields = useCurrentManifestTypes ? currentManifestFields : legacyManifestFields;
57
+ const manifestContractFields =
58
+ currentManifestContractFields.length > 0 ? currentManifestContractFields : legacyManifestContractFields;
46
59
  const capturedRegistrars = existsSync(capturedRegistrationPath)
47
60
  ? parseCapturedRegistrars(await readFile(capturedRegistrationPath, "utf8"))
48
61
  : [];
@@ -198,6 +211,15 @@ export function parsePluginSdkExports(packageJson) {
198
211
 
199
212
  export function parseExportedStringArray(source, exportName) {
200
213
  const match = source.match(new RegExp(`export\\s+const\\s+${exportName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
214
+ return parseStringArrayMatch(match);
215
+ }
216
+
217
+ function parseConstStringArray(source, constName) {
218
+ const match = source.match(new RegExp(`(?:export\\s+)?const\\s+${constName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s+as\\s+const`));
219
+ return parseStringArrayMatch(match);
220
+ }
221
+
222
+ function parseStringArrayMatch(match) {
201
223
  if (!match) {
202
224
  return [];
203
225
  }
package/src/sdk-mock.js CHANGED
@@ -968,11 +968,11 @@ function createSchema(defaultValue, shape) {
968
968
  },
969
969
  };
970
970
  return new Proxy(schema, {
971
- get(target, property) {
971
+ get(target, property, receiver) {
972
972
  if (property in target) {
973
973
  return target[property];
974
974
  }
975
- return () => target;
975
+ return () => receiver;
976
976
  },
977
977
  });
978
978
  }
@@ -236,6 +236,11 @@ export const syntheticRegistrationExecutionProfiles = {
236
236
  callableProperties: [],
237
237
  reason: "session actions are captured as registration metadata before session runtime execution",
238
238
  },
239
+ registerSessionCatalog: {
240
+ mode: "metadata-only",
241
+ callableProperties: [],
242
+ reason: "session catalogs are captured as registration metadata before catalog runtime execution",
243
+ },
239
244
  registerService: {
240
245
  mode: "lifecycle-opt-in",
241
246
  callableProperties: ["start", "stop", "dispose"],
@@ -591,23 +596,37 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
591
596
  const hookContexts = options.hookContexts ?? defaultSyntheticHookContexts;
592
597
  const captured = capture.captured ?? [];
593
598
  const retained = new Map((capture.retained ?? []).map((item) => [item.captureIndex, item]));
594
- const results = [];
599
+ const resultsByCaptureIndex = new Map();
600
+ const executionEntries = captured
601
+ .map((entry, captureIndex) => ({ entry, captureIndex }))
602
+ .sort(
603
+ (left, right) =>
604
+ syntheticLifecyclePhase(left.entry) - syntheticLifecyclePhase(right.entry) ||
605
+ left.captureIndex - right.captureIndex,
606
+ );
595
607
 
596
- for (let index = 0; index < captured.length; index += 1) {
597
- const entry = captured[index];
598
- const retainedEntry = retained.get(index);
608
+ for (const { entry, captureIndex } of executionEntries) {
609
+ const retainedEntry = retained.get(captureIndex);
599
610
  if (!retainedEntry) {
600
- results.push(blockedResult(entry, index, "handler retention was not enabled"));
611
+ resultsByCaptureIndex.set(captureIndex, [blockedResult(entry, captureIndex, "handler retention was not enabled")]);
601
612
  continue;
602
613
  }
603
614
  if (entry.kind === "hook") {
604
- results.push(await runHookProbe(entry, retainedEntry, index, { hookEvents, hookContexts }));
615
+ resultsByCaptureIndex.set(captureIndex, [
616
+ await runHookProbe(entry, retainedEntry, captureIndex, { hookEvents, hookContexts }),
617
+ ]);
605
618
  continue;
606
619
  }
607
620
  if (entry.kind === "registration") {
608
- results.push(...(await runRegistrationProbes(entry, retainedEntry, index, options)));
621
+ resultsByCaptureIndex.set(
622
+ captureIndex,
623
+ await runRegistrationProbes(entry, retainedEntry, captureIndex, options),
624
+ );
609
625
  }
610
626
  }
627
+ // Execute in lifecycle order, but keep report rows and captureIndex values
628
+ // stable for downstream consumers that compare artifacts across runs.
629
+ const results = captured.flatMap((_, captureIndex) => resultsByCaptureIndex.get(captureIndex) ?? []);
611
630
 
612
631
  return {
613
632
  entrypoint: capture.entrypoint,
@@ -622,6 +641,19 @@ export async function runCapturedSyntheticProbes(capture, options = {}) {
622
641
  };
623
642
  }
624
643
 
644
+ function syntheticLifecyclePhase(entry) {
645
+ if (entry.kind !== "hook") {
646
+ return 1;
647
+ }
648
+ if (entry.name === "gateway_start") {
649
+ return 0;
650
+ }
651
+ if (entry.name === "gateway_stop") {
652
+ return 2;
653
+ }
654
+ return 1;
655
+ }
656
+
625
657
  export function renderSyntheticProbeMarkdown(plan, options = {}) {
626
658
  return [
627
659
  `# ${options.title ?? "Plugin Inspector Synthetic Probes"}`,
@@ -241,9 +241,10 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
241
241
  });
242
242
 
243
243
  if (requiredCapabilities.includes("target-openclaw-link")) {
244
+ const targetSpecifier = `file:${targetOpenClawWorkspacePath(settings, fixtureId, targetOpenClawPath)}`;
244
245
  steps.push({
245
246
  kind: "link-openclaw",
246
- command: `${shellQuote(packageManager)} pkg set ${shellQuote(`dependencies.openclaw=file:${targetOpenClawWorkspacePath(settings, fixtureId, targetOpenClawPath)}`)}`,
247
+ command: `node ${shellQuote(helperScript(settings, workspacePath, settings.linkOpenClawWorkspaceScript, "link-openclaw-workspace-cli.js"))} ${shellQuote(targetSpecifier)}`,
247
248
  cwd: workspacePath,
248
249
  reason: "link the plugin's openclaw peer dependency to the target checkout under test",
249
250
  });
@@ -325,6 +326,7 @@ function workspaceSettings(options) {
325
326
  return {
326
327
  captureScript: options.captureScript ?? defaultWorkspacePlanOptions.captureScript,
327
328
  defaultTargetOpenClawWorkspacePath: options.defaultTargetOpenClawWorkspacePath ?? "../../../openclaw",
329
+ linkOpenClawWorkspaceScript: options.linkOpenClawWorkspaceScript,
328
330
  optInEnv: options.optInEnv ?? defaultWorkspacePlanOptions.optInEnv,
329
331
  resultsRoot: repoRelative(options.resultsRoot ?? defaultWorkspacePlanOptions.resultsRoot),
330
332
  rootDir: path.resolve(options.rootDir ?? process.cwd()),