@openclaw/plugin-inspector 0.3.6 → 0.3.8

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
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.8 - 2026-05-03
6
+
7
+ ### Fixed
8
+
9
+ - Synthesize manifest config for isolated runtime capture so configured hooks can be observed without credentials.
10
+
11
+ ## 0.3.7 - 2026-05-03
12
+
13
+ ### Changed
14
+
15
+ - Downgrade `registration-capture-gap` to advisory severity so missing capture evidence no longer reports as a P1 plugin contract risk.
16
+
5
17
  ## 0.3.6 - 2026-05-03
6
18
 
7
19
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
@@ -148,6 +148,7 @@ export function createCaptureContext(options = {}) {
148
148
  config: options.config ?? {},
149
149
  logger: options.logger ?? console,
150
150
  pluginConfig: options.pluginConfig ?? {},
151
+ resolvePath: options.resolvePath ?? ((value) => value),
151
152
  runtime: options.runtime ?? createRuntimeContext(options),
152
153
  secrets: options.secrets ?? createSecretContext(options),
153
154
  store: options.store ?? createStoreContext(options),
@@ -0,0 +1,126 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function captureApiOptionsForPlugin(apiOptions = {}, options = {}) {
5
+ if (apiOptions.pluginConfig !== undefined || !options.pluginRoot) {
6
+ return apiOptions;
7
+ }
8
+
9
+ const pluginConfig = await readSamplePluginConfig(options.pluginRoot);
10
+ if (pluginConfig === undefined) {
11
+ return apiOptions;
12
+ }
13
+ return {
14
+ ...apiOptions,
15
+ pluginConfig,
16
+ };
17
+ }
18
+
19
+ async function readSamplePluginConfig(pluginRoot) {
20
+ const manifestPath = path.join(pluginRoot, "openclaw.plugin.json");
21
+ let manifest;
22
+ try {
23
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
24
+ } catch {
25
+ return undefined;
26
+ }
27
+
28
+ const sample = sampleJsonSchema(manifest.configSchema, { key: "config" });
29
+ return isPlainObject(sample) && Object.keys(sample).length > 0 ? sample : undefined;
30
+ }
31
+
32
+ function sampleJsonSchema(schema, context = {}) {
33
+ if (!isPlainObject(schema)) {
34
+ return undefined;
35
+ }
36
+
37
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
38
+ return schema.enum[0];
39
+ }
40
+ if (Object.prototype.hasOwnProperty.call(schema, "const")) {
41
+ return schema.const;
42
+ }
43
+ if (Object.prototype.hasOwnProperty.call(schema, "default")) {
44
+ return schema.default;
45
+ }
46
+
47
+ const type = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
48
+ if (type === "object" || schema.properties) {
49
+ return sampleObjectSchema(schema);
50
+ }
51
+ if (type === "array") {
52
+ return [];
53
+ }
54
+ if (type === "boolean") {
55
+ return false;
56
+ }
57
+ if (type === "number" || type === "integer") {
58
+ return typeof schema.minimum === "number" ? schema.minimum : 1;
59
+ }
60
+ if (type === "string" || !type) {
61
+ return sampleString(context.key);
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ function sampleObjectSchema(schema) {
67
+ const properties = isPlainObject(schema.properties) ? schema.properties : {};
68
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
69
+ const output = {};
70
+
71
+ for (const key of Object.keys(properties)) {
72
+ if (required.has(key)) {
73
+ const value = sampleJsonSchema(properties[key], { key });
74
+ if (value !== undefined) {
75
+ output[key] = value;
76
+ }
77
+ }
78
+ }
79
+
80
+ for (const key of Object.keys(properties)) {
81
+ if (properties[key]?.type === "boolean") {
82
+ output[key] = false;
83
+ }
84
+ }
85
+
86
+ if (Object.keys(output).length === 0 && Number(schema.minProperties ?? 0) > 0) {
87
+ const key = preferredSamplePropertyKey(properties);
88
+ if (key) {
89
+ const value = sampleJsonSchema(properties[key], { key });
90
+ if (value !== undefined) {
91
+ output[key] = value;
92
+ }
93
+ }
94
+ }
95
+
96
+ return output;
97
+ }
98
+
99
+ function preferredSamplePropertyKey(properties) {
100
+ for (const key of ["provider", "model", "apiKey", "id", "name", ...Object.keys(properties)]) {
101
+ if (Object.prototype.hasOwnProperty.call(properties, key)) {
102
+ return key;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ function sampleString(key = "") {
109
+ if (key === "provider") {
110
+ return "openai";
111
+ }
112
+ if (key === "model") {
113
+ return "text-embedding-3-small";
114
+ }
115
+ if (key === "apiKey") {
116
+ return "fixture-api-key";
117
+ }
118
+ if (key === "dbPath") {
119
+ return ".plugin-inspector/state/lancedb";
120
+ }
121
+ return "fixture";
122
+ }
123
+
124
+ function isPlainObject(value) {
125
+ return value !== null && typeof value === "object" && !Array.isArray(value);
126
+ }
@@ -175,7 +175,6 @@ export function probePriority(code, fixturePriority) {
175
175
  "before-tool-call-probe",
176
176
  "conversation-access-hook",
177
177
  "missing-compat-record",
178
- "registration-capture-gap",
179
178
  "sdk-export-missing",
180
179
  ].includes(code)
181
180
  ) {
package/src/inspector.js CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { promisify } from "node:util";
7
7
  import { createCaptureApi } from "./capture-api.js";
8
+ import { captureApiOptionsForPlugin } from "./capture-config.js";
8
9
  import { fixtureCheckoutPath, fixtureSourceRoot } from "./config.js";
9
10
  import { buildCompatibilityFixtureReport } from "./fixture-summary.js";
10
11
  import { readOpenClawTargetSurface } from "./openclaw-target.js";
@@ -188,7 +189,12 @@ export async function captureEntrypoint(entrypoint, options = {}) {
188
189
  };
189
190
  }
190
191
 
191
- const api = createCaptureApi(options.apiOptions);
192
+ const apiOptions = await captureApiOptionsForPlugin(options.apiOptions, {
193
+ pluginRoot: options.pluginRoot
194
+ ? path.resolve(options.cwd ?? process.cwd(), options.pluginRoot)
195
+ : path.dirname(resolvedEntrypoint),
196
+ });
197
+ const api = createCaptureApi(apiOptions);
192
198
  try {
193
199
  await register(api);
194
200
  } catch (error) {
@@ -199,7 +205,7 @@ export async function captureEntrypoint(entrypoint, options = {}) {
199
205
  entrypoint: resolvedEntrypoint,
200
206
  captured: api.getCapturedContracts(),
201
207
  };
202
- if (options.apiOptions?.retainHandlers === true) {
208
+ if (apiOptions?.retainHandlers === true) {
203
209
  result.retained = api.getRetainedContracts();
204
210
  }
205
211
  return result;
package/src/issues.js CHANGED
@@ -220,10 +220,10 @@ export const issueMetadataByCode = {
220
220
  title: "providerAuthEnvVars legacy manifest metadata must stay covered",
221
221
  },
222
222
  "registration-capture-gap": {
223
- severity: "P1",
223
+ severity: "P2",
224
224
  owner: "inspector",
225
225
  decision: "inspector-follow-up",
226
- title: "runtime registrations need capture before contract judgment",
226
+ title: "runtime registrations need capture evidence before final contract judgment",
227
227
  },
228
228
  "runtime-tool-capture": {
229
229
  severity: "P2",
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { createCaptureApi } from "./capture-api.js";
9
+ import { captureApiOptionsForPlugin } from "./capture-config.js";
9
10
  import { createMockSdkPackage } from "./sdk-mock.js";
10
11
 
11
12
  const options = JSON.parse(process.argv[2] ?? "{}");
@@ -30,7 +31,7 @@ async function run(options) {
30
31
  cleanupTempDirOnExit(workspace);
31
32
  const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
32
33
  register(pathToFileURL(loaderPath));
33
- return await captureLinkedEntrypoint(entrypoint, options);
34
+ return await captureLinkedEntrypoint(entrypoint, { ...options, pluginRoot });
34
35
  }
35
36
 
36
37
  function cleanupTempDirOnExit(dir) {
@@ -65,7 +66,10 @@ async function captureLinkedEntrypoint(entrypoint, options) {
65
66
  );
66
67
  }
67
68
 
68
- const api = createCaptureApi(options.apiOptions);
69
+ const apiOptions = await captureApiOptionsForPlugin(options.apiOptions, {
70
+ pluginRoot: options.pluginRoot,
71
+ });
72
+ const api = createCaptureApi(apiOptions);
69
73
  try {
70
74
  await register(api);
71
75
  } catch (error) {
@@ -80,7 +84,7 @@ async function captureLinkedEntrypoint(entrypoint, options) {
80
84
  mockSdk: true,
81
85
  captured: api.getCapturedContracts(),
82
86
  };
83
- if (options.apiOptions?.retainHandlers === true) {
87
+ if (apiOptions?.retainHandlers === true) {
84
88
  result.retained = api.getRetainedContracts();
85
89
  }
86
90
  return withProcessOutput(result, outputCapture);