@openclaw/plugin-inspector 0.3.11 → 0.3.13
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 +14 -0
- package/README.md +7 -2
- package/package.json +1 -1
- package/src/advanced.js +1 -0
- package/src/api.js +4 -0
- package/src/batch.js +309 -0
- package/src/cli.js +110 -9
- package/src/compatibility-report.js +13 -0
- package/src/index.js +13 -1
- package/src/inspector.js +1 -0
- package/src/issues.js +187 -0
- package/src/report.js +30 -15
- package/src/sdk-mock.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.13 - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Add `authorRemediation.summary` and `authorRemediation.docsUrl` guidance to author-facing compatibility issues and Markdown reports.
|
|
10
|
+
- Add `--author-facing` for `check`, `ci`, and `batch` reports while keeping default output complete for internal coverage findings.
|
|
11
|
+
- Replace the recent `--include-inspector-gaps` option with a clear error pointing to `--author-facing`.
|
|
12
|
+
|
|
13
|
+
## 0.3.12 - 2026-06-09
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Hide maintainer-facing `inspector-gap` findings from author-facing `check`, `ci`, and `batch` output by default, with `--include-inspector-gaps` for internal coverage reports.
|
|
18
|
+
|
|
5
19
|
## 0.3.11 - 2026-05-26
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -170,6 +170,7 @@ Copy-ready examples live in:
|
|
|
170
170
|
| `plugin-inspector config` | Print resolved plugin-root config as text or JSON. |
|
|
171
171
|
| `plugin-inspector init` | Write starter config, scripts, and optional GitHub Actions workflow. |
|
|
172
172
|
| `plugin-inspector report` | Run a fixture-suite config with many plugins. |
|
|
173
|
+
| `plugin-inspector batch` | Discover plugin roots under a folder and write one aggregate impact report. |
|
|
173
174
|
| `plugin-inspector capture` | Runtime-capture one entrypoint directly. |
|
|
174
175
|
|
|
175
176
|
Common options:
|
|
@@ -186,6 +187,7 @@ Common options:
|
|
|
186
187
|
| `--mock-sdk` / `--sdk mock` | Use generated SDK and external-package mocks for runtime capture. |
|
|
187
188
|
| `--real-sdk` / `--sdk real` | Use installed real SDK dependencies instead of mocks. |
|
|
188
189
|
| `--allow-execute` | Permit commands that import plugin code. |
|
|
190
|
+
| `--author-facing` | Limit `check`, `ci`, and `batch` reports to findings with `authorRemediation` guidance. |
|
|
189
191
|
| `--json` | Print machine-readable JSON to stdout. |
|
|
190
192
|
| `--sarif [path]` | Write SARIF from `check` or `inspect`; `ci` enables this by default. |
|
|
191
193
|
| `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
|
|
@@ -305,8 +307,10 @@ Important report sections:
|
|
|
305
307
|
| `logs` | Informational inventory and coverage rows. |
|
|
306
308
|
| `decisions` | Maintainer-facing follow-up or compatibility-policy decisions. |
|
|
307
309
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
+
Default `check`, `ci`, and `batch` reports include both author-facing and
|
|
311
|
+
internal findings. Pass `--author-facing` when producing plugin-author output;
|
|
312
|
+
that filtered view includes only findings with `authorRemediation.summary` and
|
|
313
|
+
`authorRemediation.docsUrl`.
|
|
310
314
|
|
|
311
315
|
## CI Policy And Shared Reporting Primitives
|
|
312
316
|
|
|
@@ -404,6 +408,7 @@ Stable grouped facades:
|
|
|
404
408
|
| `reports` | Render/write reports and classify issue findings. |
|
|
405
409
|
| `contracts` | Build, render, validate, and write contract captures and coverage. |
|
|
406
410
|
| `ci` | Build summaries, policy reports, execution results, SARIF, and JUnit outputs. |
|
|
411
|
+
| `batch` | Discover plugin roots and aggregate compatibility findings across a corpus. |
|
|
407
412
|
| `runtime` | Build runtime profiles, profile diffs, ref diffs, and import-loop profiles. |
|
|
408
413
|
| `synthetic` | Build and run synthetic probe plans. |
|
|
409
414
|
|
package/package.json
CHANGED
package/src/advanced.js
CHANGED
package/src/api.js
CHANGED
|
@@ -43,6 +43,7 @@ export async function loadPluginConfig(options = {}) {
|
|
|
43
43
|
export async function inspectPluginRoot(options = {}) {
|
|
44
44
|
const config = await loadPluginConfig(options);
|
|
45
45
|
return inspectCompatibilityFixtureSet(config, {
|
|
46
|
+
authorFacing: options.authorFacing,
|
|
46
47
|
generatedAt: options.generatedAt,
|
|
47
48
|
openclawPath: options.openclawPath,
|
|
48
49
|
executionResults: options.executionResults,
|
|
@@ -58,6 +59,7 @@ export async function inspectFixtureSetConfig(options = {}) {
|
|
|
58
59
|
export async function inspectCompatibilityFixtureSetConfig(options = {}) {
|
|
59
60
|
const config = await loadFixtureSetConfig(options);
|
|
60
61
|
return inspectCompatibilityFixtureSet(config, {
|
|
62
|
+
authorFacing: options.authorFacing,
|
|
61
63
|
generatedAt: options.generatedAt,
|
|
62
64
|
openclawPath: options.openclawPath,
|
|
63
65
|
executionResults: options.executionResults,
|
|
@@ -112,6 +114,7 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
|
|
|
112
114
|
const report =
|
|
113
115
|
options.report ??
|
|
114
116
|
(await inspectCompatibilityFixtureSet(config, {
|
|
117
|
+
authorFacing: options.authorFacing,
|
|
115
118
|
generatedAt: options.generatedAt,
|
|
116
119
|
openclawPath: options.openclawPath,
|
|
117
120
|
executionResults: options.executionResults,
|
|
@@ -144,6 +147,7 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
|
|
|
144
147
|
const report =
|
|
145
148
|
options.report ??
|
|
146
149
|
(await inspectCompatibilityFixtureSet(config, {
|
|
150
|
+
authorFacing: options.authorFacing,
|
|
147
151
|
generatedAt: options.generatedAt,
|
|
148
152
|
openclawPath: options.openclawPath,
|
|
149
153
|
executionResults: options.executionResults,
|
package/src/batch.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { renderMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
6
|
+
import { runPluginCheck } from "./api.js";
|
|
7
|
+
|
|
8
|
+
const ignoredDirs = new Set([
|
|
9
|
+
".git",
|
|
10
|
+
".hg",
|
|
11
|
+
".svn",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"reports",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
".plugin-inspector",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export async function runBatchAnalysis(options = {}) {
|
|
20
|
+
const rootDir = path.resolve(options.rootDir ?? options.inputDir ?? process.cwd());
|
|
21
|
+
const outDir = options.outDir ?? "reports";
|
|
22
|
+
const outRoot = path.resolve(rootDir, outDir);
|
|
23
|
+
const concurrency = Math.max(1, Math.min(Math.round(options.concurrency ?? 4), 32));
|
|
24
|
+
const keepPluginReports = options.keepPluginReports === true;
|
|
25
|
+
const pluginRoots = await discoverPluginRoots(rootDir);
|
|
26
|
+
const tempRoot = keepPluginReports ? null : await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-batch-"));
|
|
27
|
+
const entries = [];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await runWithConcurrency(pluginRoots, concurrency, async (pluginRoot) => {
|
|
31
|
+
const reportsRoot = keepPluginReports
|
|
32
|
+
? path.join(outRoot, "plugins", slugForPath(path.relative(rootDir, pluginRoot)))
|
|
33
|
+
: path.join(tempRoot, slugForPath(path.relative(rootDir, pluginRoot)));
|
|
34
|
+
entries.push(
|
|
35
|
+
await inspectBatchPlugin(pluginRoot, {
|
|
36
|
+
...options,
|
|
37
|
+
rootDir,
|
|
38
|
+
outDir: reportsRoot,
|
|
39
|
+
openclawPath: options.openclawPath,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
} finally {
|
|
44
|
+
if (tempRoot) await rm(tempRoot, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
48
|
+
const report = buildBatchReport({
|
|
49
|
+
rootDir,
|
|
50
|
+
entries,
|
|
51
|
+
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
const paths = await writeBatchReport(report, { outDir: outRoot, check: options.checkArtifacts });
|
|
54
|
+
return { report, paths };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function discoverPluginRoots(rootDir) {
|
|
58
|
+
const roots = [];
|
|
59
|
+
await walk(path.resolve(rootDir));
|
|
60
|
+
roots.sort();
|
|
61
|
+
return roots;
|
|
62
|
+
|
|
63
|
+
async function walk(dir) {
|
|
64
|
+
if (await isPluginRoot(dir)) {
|
|
65
|
+
roots.push(dir);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!entry.isDirectory() || ignoredDirs.has(entry.name)) continue;
|
|
71
|
+
await walk(path.join(dir, entry.name));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function writeBatchReport(report, options = {}) {
|
|
77
|
+
return writeJsonMarkdownArtifacts({
|
|
78
|
+
jsonPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.json"),
|
|
79
|
+
markdownPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.md"),
|
|
80
|
+
json: report,
|
|
81
|
+
markdown: renderBatchMarkdown(report),
|
|
82
|
+
check: options.check,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildBatchReport({ rootDir, entries, generatedAt }) {
|
|
87
|
+
const findingFrequency = findingFrequencyRows(entries);
|
|
88
|
+
const summary = {
|
|
89
|
+
pluginCount: entries.length,
|
|
90
|
+
passed: entries.filter((entry) => entry.status === "pass").length,
|
|
91
|
+
failed: entries.filter((entry) => entry.status !== "pass").length,
|
|
92
|
+
pluginsWithErrors: entries.filter((entry) => entry.errorCount > 0).length,
|
|
93
|
+
pluginsWithWarnings: entries.filter((entry) => entry.warningCount > 0).length,
|
|
94
|
+
errorCount: entries.reduce((sum, entry) => sum + entry.errorCount, 0),
|
|
95
|
+
warningCount: entries.reduce((sum, entry) => sum + entry.warningCount, 0),
|
|
96
|
+
findingCodeCount: findingFrequency.length,
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
generatedAt,
|
|
100
|
+
rootDir,
|
|
101
|
+
summary,
|
|
102
|
+
findingFrequency,
|
|
103
|
+
plugins: entries,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function inspectBatchPlugin(pluginRoot, options) {
|
|
108
|
+
try {
|
|
109
|
+
const { report } = await runPluginCheck({
|
|
110
|
+
allowExecution: options.allowExecution,
|
|
111
|
+
authorFacing: options.authorFacing,
|
|
112
|
+
capture: options.capture,
|
|
113
|
+
configPath: options.configPath,
|
|
114
|
+
mockSdk: options.mockSdk,
|
|
115
|
+
openclawPath: options.openclawPath,
|
|
116
|
+
outDir: options.outDir,
|
|
117
|
+
pluginRoot,
|
|
118
|
+
});
|
|
119
|
+
const findings = normalizeReportFindings(report);
|
|
120
|
+
return {
|
|
121
|
+
pluginRoot,
|
|
122
|
+
relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
|
|
123
|
+
status: report.status,
|
|
124
|
+
packageName: packageNameFromReport(report),
|
|
125
|
+
targetOpenClaw: report.targetOpenClaw,
|
|
126
|
+
errorCount: findings.filter((finding) => finding.kind === "error").length,
|
|
127
|
+
warningCount: findings.filter((finding) => finding.kind === "warning").length,
|
|
128
|
+
findings,
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
pluginRoot,
|
|
133
|
+
relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
|
|
134
|
+
status: "error",
|
|
135
|
+
packageName: path.basename(pluginRoot),
|
|
136
|
+
targetOpenClaw: null,
|
|
137
|
+
errorCount: 1,
|
|
138
|
+
warningCount: 0,
|
|
139
|
+
findings: [
|
|
140
|
+
{
|
|
141
|
+
kind: "error",
|
|
142
|
+
code: "plugin-inspector-batch-failure",
|
|
143
|
+
message: error instanceof Error ? error.message : String(error),
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeReportFindings(report) {
|
|
151
|
+
const issueFindings = (report.issues ?? []).map((finding) =>
|
|
152
|
+
normalizeFinding(
|
|
153
|
+
finding,
|
|
154
|
+
finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
const issueKeys = new Set(issueFindings.map(findingKey));
|
|
158
|
+
const rawFindings = [
|
|
159
|
+
...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
|
|
160
|
+
...(report.warnings ?? []).map((finding) => normalizeFinding(finding, "warning")),
|
|
161
|
+
...(report.suggestions ?? []).map((finding) => normalizeFinding(finding, "warning")),
|
|
162
|
+
].filter((finding) => !issueKeys.has(findingKey(finding)));
|
|
163
|
+
|
|
164
|
+
return [...issueFindings, ...rawFindings];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeFinding(finding, kind) {
|
|
168
|
+
return {
|
|
169
|
+
kind,
|
|
170
|
+
code: finding.code ?? "plugin-inspector-finding",
|
|
171
|
+
severity: finding.severity,
|
|
172
|
+
issueClass: finding.issueClass,
|
|
173
|
+
message: finding.message ?? finding.title ?? "See plugin report.",
|
|
174
|
+
evidence: finding.evidence,
|
|
175
|
+
...(finding.authorRemediation ? { authorRemediation: finding.authorRemediation } : {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function findingKey(finding) {
|
|
180
|
+
return [
|
|
181
|
+
finding.fixture ?? "",
|
|
182
|
+
finding.code ?? "",
|
|
183
|
+
...(Array.isArray(finding.evidence) ? finding.evidence : []),
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function findingFrequencyRows(entries) {
|
|
188
|
+
const byCode = new Map();
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const seenForPlugin = new Set();
|
|
191
|
+
for (const finding of entry.findings) {
|
|
192
|
+
const current = byCode.get(finding.code) ?? {
|
|
193
|
+
code: finding.code,
|
|
194
|
+
count: 0,
|
|
195
|
+
plugins: 0,
|
|
196
|
+
errors: 0,
|
|
197
|
+
warnings: 0,
|
|
198
|
+
};
|
|
199
|
+
current.count += 1;
|
|
200
|
+
if (!seenForPlugin.has(finding.code)) {
|
|
201
|
+
current.plugins += 1;
|
|
202
|
+
seenForPlugin.add(finding.code);
|
|
203
|
+
}
|
|
204
|
+
if (finding.kind === "error") current.errors += 1;
|
|
205
|
+
else current.warnings += 1;
|
|
206
|
+
byCode.set(finding.code, current);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return [...byCode.values()].sort((a, b) => b.plugins - a.plugins || b.count - a.count);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderBatchMarkdown(report) {
|
|
213
|
+
return [
|
|
214
|
+
"# Plugin Inspector Batch Report",
|
|
215
|
+
"",
|
|
216
|
+
`Generated: ${report.generatedAt}`,
|
|
217
|
+
`Root: ${report.rootDir}`,
|
|
218
|
+
"",
|
|
219
|
+
"## Summary",
|
|
220
|
+
"",
|
|
221
|
+
renderMarkdownTable(
|
|
222
|
+
[
|
|
223
|
+
["Plugins", report.summary.pluginCount],
|
|
224
|
+
["Passed", report.summary.passed],
|
|
225
|
+
["Failed", report.summary.failed],
|
|
226
|
+
["Plugins with errors", report.summary.pluginsWithErrors],
|
|
227
|
+
["Plugins with warnings", report.summary.pluginsWithWarnings],
|
|
228
|
+
["Errors", report.summary.errorCount],
|
|
229
|
+
["Warnings", report.summary.warningCount],
|
|
230
|
+
],
|
|
231
|
+
["Metric", "Value"],
|
|
232
|
+
),
|
|
233
|
+
"",
|
|
234
|
+
"## Finding Frequency",
|
|
235
|
+
"",
|
|
236
|
+
report.findingFrequency.length
|
|
237
|
+
? renderMarkdownTable(
|
|
238
|
+
report.findingFrequency.map((row) => [
|
|
239
|
+
row.code,
|
|
240
|
+
row.plugins,
|
|
241
|
+
row.count,
|
|
242
|
+
row.errors,
|
|
243
|
+
row.warnings,
|
|
244
|
+
]),
|
|
245
|
+
["Code", "Plugins", "Findings", "Errors", "Warnings"],
|
|
246
|
+
)
|
|
247
|
+
: "_No findings._",
|
|
248
|
+
"",
|
|
249
|
+
"## Plugins",
|
|
250
|
+
"",
|
|
251
|
+
report.plugins.length
|
|
252
|
+
? renderMarkdownTable(
|
|
253
|
+
report.plugins.map((plugin) => [
|
|
254
|
+
plugin.packageName,
|
|
255
|
+
plugin.relativePath,
|
|
256
|
+
plugin.status,
|
|
257
|
+
plugin.errorCount,
|
|
258
|
+
plugin.warningCount,
|
|
259
|
+
]),
|
|
260
|
+
["Package", "Path", "Status", "Errors", "Warnings"],
|
|
261
|
+
)
|
|
262
|
+
: "_No plugin roots discovered._",
|
|
263
|
+
].join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
267
|
+
let index = 0;
|
|
268
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
269
|
+
while (index < items.length) {
|
|
270
|
+
const item = items[index];
|
|
271
|
+
index += 1;
|
|
272
|
+
await worker(item);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
await Promise.all(runners);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function isPluginRoot(dir) {
|
|
279
|
+
if (existsSync(path.join(dir, "plugin-inspector.config.json"))) return true;
|
|
280
|
+
if (existsSync(path.join(dir, ".plugin-inspector.json"))) return true;
|
|
281
|
+
if (existsSync(path.join(dir, "openclaw.plugin.json"))) return true;
|
|
282
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
283
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
284
|
+
try {
|
|
285
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
286
|
+
return Boolean(packageJson.openclaw || packageJson.pluginInspector || packageJson["plugin-inspector"]);
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function packageNameFromReport(report) {
|
|
293
|
+
return (
|
|
294
|
+
report.fixtures?.[0]?.package?.packageJson?.name ??
|
|
295
|
+
report.fixtures?.[0]?.package?.name ??
|
|
296
|
+
report.fixtures?.[0]?.name ??
|
|
297
|
+
report.fixtures?.[0]?.id ??
|
|
298
|
+
"plugin"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function slugForPath(value) {
|
|
303
|
+
return (
|
|
304
|
+
String(value)
|
|
305
|
+
.replaceAll(path.sep, "-")
|
|
306
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
307
|
+
.replace(/^-+|-+$/g, "") || "plugin"
|
|
308
|
+
);
|
|
309
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
loadPluginConfig,
|
|
5
5
|
renderTextSummary,
|
|
6
|
+
runBatchAnalysis,
|
|
6
7
|
sanitizeReportArtifact,
|
|
7
8
|
runPluginCheck,
|
|
8
9
|
} from "./index.js";
|
|
@@ -43,6 +44,8 @@ try {
|
|
|
43
44
|
}
|
|
44
45
|
} else if (command === "ci") {
|
|
45
46
|
await runCi(commandArgs);
|
|
47
|
+
} else if (command === "batch") {
|
|
48
|
+
await runBatch(commandArgs);
|
|
46
49
|
} else if (command === "capture") {
|
|
47
50
|
await runCapture(commandArgs);
|
|
48
51
|
} else {
|
|
@@ -53,6 +56,38 @@ try {
|
|
|
53
56
|
process.exitCode = 1;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
async function runBatch(commandArgs) {
|
|
60
|
+
const inputDir = readFirstPositional(commandArgs, new Set(["--out", "--openclaw", "--concurrency"]));
|
|
61
|
+
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
62
|
+
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
63
|
+
const concurrency = Number(readFlag(commandArgs, "--concurrency") ?? "4");
|
|
64
|
+
const json = commandArgs.includes("--json");
|
|
65
|
+
const check = commandArgs.includes("--check");
|
|
66
|
+
const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
|
|
67
|
+
const authorFacing = readAuthorFacingFlag(commandArgs);
|
|
68
|
+
if (!inputDir) {
|
|
69
|
+
throw new Error("batch requires a folder of plugin roots");
|
|
70
|
+
}
|
|
71
|
+
const { report, paths } = await runBatchAnalysis({
|
|
72
|
+
rootDir: inputDir,
|
|
73
|
+
outDir,
|
|
74
|
+
openclawPath,
|
|
75
|
+
concurrency,
|
|
76
|
+
authorFacing,
|
|
77
|
+
keepPluginReports,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (json) {
|
|
81
|
+
console.log(JSON.stringify(report, null, 2));
|
|
82
|
+
} else {
|
|
83
|
+
console.log(renderBatchTextSummary(report, paths));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (check && report.summary.pluginsWithErrors > 0) {
|
|
87
|
+
throw new Error(`plugin-inspector batch found ${report.summary.pluginsWithErrors} plugin(s) with errors`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
56
91
|
async function runConfig(commandArgs) {
|
|
57
92
|
const configPath = readFlag(commandArgs, "--config");
|
|
58
93
|
const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
|
|
@@ -75,8 +110,10 @@ async function runCheck(commandArgs) {
|
|
|
75
110
|
const mockSdk = readMockSdkFlag(commandArgs);
|
|
76
111
|
const allowExecution = readAllowExecutionFlag(commandArgs);
|
|
77
112
|
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
113
|
+
const authorFacing = readAuthorFacingFlag(commandArgs);
|
|
78
114
|
const { report, paths } = await runPluginCheck({
|
|
79
115
|
allowExecution,
|
|
116
|
+
authorFacing,
|
|
80
117
|
capture,
|
|
81
118
|
configPath,
|
|
82
119
|
mockSdk,
|
|
@@ -131,12 +168,18 @@ async function runInit(commandArgs) {
|
|
|
131
168
|
async function runReport(command, commandArgs) {
|
|
132
169
|
const configPath = readFlag(commandArgs, "--config");
|
|
133
170
|
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
171
|
+
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
134
172
|
const check = commandArgs.includes("--check") || command === "ci";
|
|
135
173
|
const json = commandArgs.includes("--json");
|
|
136
174
|
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
175
|
+
const authorFacing = readAuthorFacingFlag(commandArgs);
|
|
137
176
|
const config = await loadInspectorConfig(configPath);
|
|
138
|
-
const report =
|
|
139
|
-
|
|
177
|
+
const report = authorFacing
|
|
178
|
+
? await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath })
|
|
179
|
+
: await inspectFixtureSet(config);
|
|
180
|
+
const paths = authorFacing
|
|
181
|
+
? await writeCompatibilityReport(report, { cwd: config.rootDir, outDir })
|
|
182
|
+
: await writeReport(report, { outDir });
|
|
140
183
|
await writeCiOutputArtifacts(report, {
|
|
141
184
|
...ciOutputs,
|
|
142
185
|
cwd: path.dirname(paths.jsonPath),
|
|
@@ -164,8 +207,10 @@ async function runCi(commandArgs) {
|
|
|
164
207
|
const mockSdk = readMockSdkFlag(commandArgs);
|
|
165
208
|
const allowExecution = readAllowExecutionFlag(commandArgs);
|
|
166
209
|
const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
|
|
210
|
+
const authorFacing = readAuthorFacingFlag(commandArgs);
|
|
167
211
|
const { report, reportDir } = await runCiCompatibilityReport({
|
|
168
212
|
allowExecution,
|
|
213
|
+
authorFacing,
|
|
169
214
|
capture,
|
|
170
215
|
configPath,
|
|
171
216
|
mockSdk,
|
|
@@ -204,10 +249,19 @@ async function runCi(commandArgs) {
|
|
|
204
249
|
}
|
|
205
250
|
}
|
|
206
251
|
|
|
207
|
-
async function runCiCompatibilityReport({
|
|
252
|
+
async function runCiCompatibilityReport({
|
|
253
|
+
allowExecution,
|
|
254
|
+
authorFacing,
|
|
255
|
+
capture,
|
|
256
|
+
configPath,
|
|
257
|
+
mockSdk,
|
|
258
|
+
openclawPath,
|
|
259
|
+
outDir,
|
|
260
|
+
pluginRoot,
|
|
261
|
+
}) {
|
|
208
262
|
if (configPath) {
|
|
209
263
|
const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
|
|
210
|
-
const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
|
|
264
|
+
const report = await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath });
|
|
211
265
|
await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
|
|
212
266
|
return {
|
|
213
267
|
report,
|
|
@@ -215,7 +269,15 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
|
|
|
215
269
|
};
|
|
216
270
|
}
|
|
217
271
|
|
|
218
|
-
const { report } = await runPluginCheck({
|
|
272
|
+
const { report } = await runPluginCheck({
|
|
273
|
+
allowExecution,
|
|
274
|
+
authorFacing,
|
|
275
|
+
capture,
|
|
276
|
+
mockSdk,
|
|
277
|
+
openclawPath,
|
|
278
|
+
outDir,
|
|
279
|
+
pluginRoot,
|
|
280
|
+
});
|
|
219
281
|
return {
|
|
220
282
|
report,
|
|
221
283
|
reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
|
|
@@ -317,6 +379,15 @@ function readAllowExecutionFlag(commandArgs) {
|
|
|
317
379
|
return commandArgs.includes("--allow-execute");
|
|
318
380
|
}
|
|
319
381
|
|
|
382
|
+
function readAuthorFacingFlag(commandArgs) {
|
|
383
|
+
if (commandArgs.includes("--include-inspector-gaps")) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
"--include-inspector-gaps has been replaced by --author-facing; default output now includes internal findings.",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return commandArgs.includes("--author-facing");
|
|
389
|
+
}
|
|
390
|
+
|
|
320
391
|
function renderCiTextSummary(summary) {
|
|
321
392
|
return [
|
|
322
393
|
`Status: ${summary.status.toUpperCase()}`,
|
|
@@ -326,6 +397,34 @@ function renderCiTextSummary(summary) {
|
|
|
326
397
|
].join("\n");
|
|
327
398
|
}
|
|
328
399
|
|
|
400
|
+
function renderBatchTextSummary(report, paths) {
|
|
401
|
+
const lines = [
|
|
402
|
+
"Plugin Inspector Batch",
|
|
403
|
+
`Plugins: ${report.summary.pluginCount}`,
|
|
404
|
+
`Plugins with errors: ${report.summary.pluginsWithErrors}`,
|
|
405
|
+
`Plugins with warnings: ${report.summary.pluginsWithWarnings}`,
|
|
406
|
+
`Finding codes: ${report.summary.findingCodeCount}`,
|
|
407
|
+
"",
|
|
408
|
+
"Reports:",
|
|
409
|
+
`- JSON: ${paths.jsonPath}`,
|
|
410
|
+
`- Markdown: ${paths.markdownPath}`,
|
|
411
|
+
];
|
|
412
|
+
const topFinding = report.findingFrequency[0];
|
|
413
|
+
if (topFinding) {
|
|
414
|
+
lines.push("", `Top finding: ${topFinding.code} (${topFinding.plugins} plugin(s))`);
|
|
415
|
+
}
|
|
416
|
+
return lines.join("\n");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function readFirstPositional(args, valueFlags = new Set()) {
|
|
420
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
421
|
+
const arg = args[index];
|
|
422
|
+
if (!arg.startsWith("-")) return arg;
|
|
423
|
+
if (valueFlags.has(arg)) index += 1;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
329
428
|
function initCommandSummary(result) {
|
|
330
429
|
return {
|
|
331
430
|
dryRun: result.dryRun,
|
|
@@ -353,16 +452,18 @@ function printHelp() {
|
|
|
353
452
|
|
|
354
453
|
Usage:
|
|
355
454
|
plugin-inspector
|
|
356
|
-
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json]
|
|
455
|
+
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json]
|
|
357
456
|
plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
|
|
358
457
|
plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
|
|
359
|
-
plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
|
|
360
|
-
plugin-inspector
|
|
361
|
-
plugin-inspector
|
|
458
|
+
plugin-inspector report --config <path> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json]
|
|
459
|
+
plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--author-facing] [--check] [--json]
|
|
460
|
+
plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
|
|
461
|
+
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json] [--no-sarif] [--no-junit]
|
|
362
462
|
plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
|
|
363
463
|
|
|
364
464
|
Default check runs from the current plugin root and writes reports/ unless --out is set.
|
|
365
465
|
CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
|
|
366
466
|
Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
|
|
467
|
+
Default output includes author-facing and internal findings; pass --author-facing to show only findings with author remediation docs.
|
|
367
468
|
`);
|
|
368
469
|
}
|
|
@@ -250,10 +250,23 @@ function issueBlock(issue, options) {
|
|
|
250
250
|
` - state: ${issueState(issue)}`,
|
|
251
251
|
" - evidence:",
|
|
252
252
|
...evidenceList(issue.evidence, options).map((item) => ` - ${item}`),
|
|
253
|
+
...remediationList(issue),
|
|
253
254
|
...runtimeCoverageList(issue, options),
|
|
254
255
|
].join("\n");
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
function remediationList(issue) {
|
|
259
|
+
const remediation = issue.authorRemediation;
|
|
260
|
+
if (!remediation?.summary) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
return [
|
|
264
|
+
" - author remediation:",
|
|
265
|
+
` - ${remediation.summary}`,
|
|
266
|
+
` - docs: ${remediation.docsUrl}`,
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
257
270
|
function issueState(issue) {
|
|
258
271
|
const flags = [
|
|
259
272
|
issue.status,
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import * as runtimeReconciliationApi from "./runtime-reconciliation.js";
|
|
|
17
17
|
import * as syntheticEntrypointApi from "./synthetic-entrypoint.js";
|
|
18
18
|
import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
|
|
19
19
|
import * as syntheticProbesApi from "./synthetic-probes.js";
|
|
20
|
+
import * as batchApi from "./batch.js";
|
|
20
21
|
|
|
21
22
|
export const pluginRoot = Object.freeze({
|
|
22
23
|
loadConfig: pluginApi.loadPluginConfig,
|
|
@@ -26,6 +27,12 @@ export const pluginRoot = Object.freeze({
|
|
|
26
27
|
setup: pluginApi.setupPluginInspector,
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
export const batch = Object.freeze({
|
|
31
|
+
discoverPluginRoots: batchApi.discoverPluginRoots,
|
|
32
|
+
run: batchApi.runBatchAnalysis,
|
|
33
|
+
writeReport: batchApi.writeBatchReport,
|
|
34
|
+
});
|
|
35
|
+
|
|
29
36
|
export const fixtureSuites = Object.freeze({
|
|
30
37
|
loadConfig: configApi.loadInspectorConfig,
|
|
31
38
|
inspect: pluginApi.inspectCompatibilityFixtureSetConfig,
|
|
@@ -131,6 +138,11 @@ export const synthetic = Object.freeze({
|
|
|
131
138
|
defaultRegistrationArguments: syntheticProbesApi.defaultSyntheticRegistrationArguments,
|
|
132
139
|
});
|
|
133
140
|
|
|
141
|
+
export {
|
|
142
|
+
discoverPluginRoots,
|
|
143
|
+
runBatchAnalysis,
|
|
144
|
+
writeBatchReport,
|
|
145
|
+
} from "./batch.js";
|
|
134
146
|
export {
|
|
135
147
|
capturePluginEntrypoint,
|
|
136
148
|
buildFixtureSetColdImportReadiness,
|
|
@@ -205,7 +217,7 @@ export {
|
|
|
205
217
|
validateImportLoopProfile,
|
|
206
218
|
writeImportLoopProfile,
|
|
207
219
|
} from "./import-loop-profile.js";
|
|
208
|
-
export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
|
|
220
|
+
export { classifyIssueFinding, issueId, isAuthorFacingFinding, knownIssueCodes } from "./issues.js";
|
|
209
221
|
export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
|
|
210
222
|
export { openClawTargetPathCandidates, readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
211
223
|
export {
|
package/src/inspector.js
CHANGED
package/src/issues.js
CHANGED
|
@@ -45,6 +45,12 @@ export const knownIssueCodes = new Set([
|
|
|
45
45
|
"unrecognized-security-manifest",
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
|
+
const authorRemediationDocsUrl = (code) => `https://docs.openclaw.ai/clawhub/plugin-validation-fixes#${code}`;
|
|
49
|
+
|
|
50
|
+
const authorRemediation = (summary) => ({ summary });
|
|
51
|
+
|
|
52
|
+
const migrationRemediation = authorRemediation;
|
|
53
|
+
|
|
48
54
|
export const issueMetadataByCode = {
|
|
49
55
|
"before-tool-call-probe": {
|
|
50
56
|
severity: "P1",
|
|
@@ -63,6 +69,13 @@ export const issueMetadataByCode = {
|
|
|
63
69
|
owner: "core",
|
|
64
70
|
decision: "core-compat-adapter",
|
|
65
71
|
title: "channelEnvVars legacy manifest metadata must stay covered",
|
|
72
|
+
authorRemediation: migrationRemediation(
|
|
73
|
+
"Move legacy channel environment variable metadata into the current setup/config metadata while keeping the old field until your supported OpenClaw range no longer needs it.",
|
|
74
|
+
[
|
|
75
|
+
"Mirror each channel environment variable into the current setup or provider configuration metadata.",
|
|
76
|
+
"Keep channelEnvVars only as backwards compatibility for older OpenClaw versions you still support.",
|
|
77
|
+
],
|
|
78
|
+
),
|
|
66
79
|
},
|
|
67
80
|
"conversation-access-hook": {
|
|
68
81
|
severity: "P1",
|
|
@@ -75,12 +88,27 @@ export const issueMetadataByCode = {
|
|
|
75
88
|
owner: "core",
|
|
76
89
|
decision: "core-compat-adapter",
|
|
77
90
|
title: "legacy before_agent_start hook compatibility is still used",
|
|
91
|
+
authorRemediation: migrationRemediation(
|
|
92
|
+
"Replace the legacy before_agent_start hook with the current prompt/model hooks.",
|
|
93
|
+
[
|
|
94
|
+
"Move model-selection work to before_model_resolve when possible.",
|
|
95
|
+
"Move prompt mutation work to before_prompt_build.",
|
|
96
|
+
"Keep before_agent_start only if your declared compatibility range still includes OpenClaw versions that require it.",
|
|
97
|
+
],
|
|
98
|
+
),
|
|
78
99
|
},
|
|
79
100
|
"legacy-root-sdk-import": {
|
|
80
101
|
severity: "P2",
|
|
81
102
|
owner: "core",
|
|
82
103
|
decision: "core-compat-adapter",
|
|
83
104
|
title: "root plugin SDK barrel is still used by fixtures",
|
|
105
|
+
authorRemediation: migrationRemediation(
|
|
106
|
+
"Prefer focused public plugin SDK subpath imports instead of the legacy root barrel.",
|
|
107
|
+
[
|
|
108
|
+
"Replace imports from openclaw/plugin-sdk with the documented subpath for the API you use.",
|
|
109
|
+
"Keep the root import only while supporting older OpenClaw versions that do not expose the subpath.",
|
|
110
|
+
],
|
|
111
|
+
),
|
|
84
112
|
},
|
|
85
113
|
"sdk-export-missing": {
|
|
86
114
|
severity: "P1",
|
|
@@ -93,12 +121,26 @@ export const issueMetadataByCode = {
|
|
|
93
121
|
owner: "plugin",
|
|
94
122
|
decision: "plugin-upstream-fix",
|
|
95
123
|
title: "plugin imports reserved bundled-plugin SDK compatibility subpaths",
|
|
124
|
+
authorRemediation: authorRemediation(
|
|
125
|
+
"Stop importing reserved bundled-plugin SDK compatibility paths.",
|
|
126
|
+
[
|
|
127
|
+
"Replace reserved OpenClaw internal SDK imports with documented public openclaw/plugin-sdk subpaths.",
|
|
128
|
+
"If no public API exists for the behavior, vendor a plugin-local helper or request a public OpenClaw API.",
|
|
129
|
+
],
|
|
130
|
+
),
|
|
96
131
|
},
|
|
97
132
|
"security-manifest-schema-unavailable": {
|
|
98
133
|
severity: "P3",
|
|
99
134
|
owner: "plugin",
|
|
100
135
|
decision: "plugin-upstream-fix",
|
|
101
136
|
title: "plugin security manifest references an unavailable schema",
|
|
137
|
+
authorRemediation: authorRemediation(
|
|
138
|
+
"Remove or update the unsupported security manifest schema reference.",
|
|
139
|
+
[
|
|
140
|
+
"Delete the schema URL from openclaw.security.json if it is advisory-only.",
|
|
141
|
+
"Use a documented versioned schema once OpenClaw publishes one.",
|
|
142
|
+
],
|
|
143
|
+
),
|
|
102
144
|
},
|
|
103
145
|
"missing-compat-record": {
|
|
104
146
|
severity: "P1",
|
|
@@ -117,18 +159,37 @@ export const issueMetadataByCode = {
|
|
|
117
159
|
owner: "plugin",
|
|
118
160
|
decision: "plugin-upstream-fix",
|
|
119
161
|
title: "manifest display name is missing",
|
|
162
|
+
authorRemediation: authorRemediation(
|
|
163
|
+
"Add a display name to the plugin manifest.",
|
|
164
|
+
["Set a non-empty name field in openclaw.plugin.json."],
|
|
165
|
+
'{\n "name": "My Plugin"\n}',
|
|
166
|
+
),
|
|
120
167
|
},
|
|
121
168
|
"manifest-unknown-contracts": {
|
|
122
169
|
severity: "P1",
|
|
123
170
|
owner: "plugin",
|
|
124
171
|
decision: "plugin-upstream-fix",
|
|
125
172
|
title: "manifest declares unsupported contract keys",
|
|
173
|
+
authorRemediation: authorRemediation(
|
|
174
|
+
"Remove unsupported manifest contract keys or move them to a documented OpenClaw contract field.",
|
|
175
|
+
[
|
|
176
|
+
"Compare the contracts object to the OpenClaw manifest fields supported by your target version.",
|
|
177
|
+
"Delete custom contract keys unless OpenClaw has a versioned schema for them.",
|
|
178
|
+
],
|
|
179
|
+
),
|
|
126
180
|
},
|
|
127
181
|
"manifest-unknown-fields": {
|
|
128
182
|
severity: "P2",
|
|
129
183
|
owner: "plugin",
|
|
130
184
|
decision: "plugin-upstream-fix",
|
|
131
185
|
title: "manifest uses unsupported top-level fields",
|
|
186
|
+
authorRemediation: authorRemediation(
|
|
187
|
+
"Move unsupported top-level manifest fields into supported package metadata or remove them.",
|
|
188
|
+
[
|
|
189
|
+
"Keep openclaw.plugin.json limited to fields supported by the target OpenClaw manifest schema.",
|
|
190
|
+
"Move package-level metadata into package.json openclaw metadata when that field is supported.",
|
|
191
|
+
],
|
|
192
|
+
),
|
|
132
193
|
},
|
|
133
194
|
"package-build-artifact-entrypoint": {
|
|
134
195
|
severity: "P2",
|
|
@@ -147,72 +208,159 @@ export const issueMetadataByCode = {
|
|
|
147
208
|
owner: "plugin",
|
|
148
209
|
decision: "plugin-upstream-fix",
|
|
149
210
|
title: "OpenClaw package entrypoint is missing",
|
|
211
|
+
authorRemediation: authorRemediation(
|
|
212
|
+
"Publish the entrypoint declared in OpenClaw package metadata or update the metadata to point at an existing file.",
|
|
213
|
+
[
|
|
214
|
+
"Check package.json openclaw.extensions and openclaw.runtimeExtensions.",
|
|
215
|
+
"Ensure the referenced file exists in the published artifact, usually under dist/ after build.",
|
|
216
|
+
],
|
|
217
|
+
),
|
|
150
218
|
},
|
|
151
219
|
"package-install-metadata-incomplete": {
|
|
152
220
|
severity: "P2",
|
|
153
221
|
owner: "plugin",
|
|
154
222
|
decision: "plugin-upstream-fix",
|
|
155
223
|
title: "OpenClaw package install metadata is incomplete",
|
|
224
|
+
authorRemediation: authorRemediation(
|
|
225
|
+
"Complete the OpenClaw install metadata so ClawHub can identify the install target.",
|
|
226
|
+
[
|
|
227
|
+
"Fill package.json openclaw.install with the supported release target.",
|
|
228
|
+
"Align clawhubSpec, npmSpec, and defaultChoice with the package you publish.",
|
|
229
|
+
],
|
|
230
|
+
),
|
|
156
231
|
},
|
|
157
232
|
"package-json-missing": {
|
|
158
233
|
severity: "P2",
|
|
159
234
|
owner: "plugin",
|
|
160
235
|
decision: "plugin-upstream-fix",
|
|
161
236
|
title: "package metadata is missing",
|
|
237
|
+
authorRemediation: authorRemediation(
|
|
238
|
+
"Add a package.json to the plugin package.",
|
|
239
|
+
[
|
|
240
|
+
"Include the package name and version.",
|
|
241
|
+
"Add an openclaw metadata block describing extensions, compatibility, and install details.",
|
|
242
|
+
],
|
|
243
|
+
),
|
|
162
244
|
},
|
|
163
245
|
"package-manifest-version-drift": {
|
|
164
246
|
severity: "P2",
|
|
165
247
|
owner: "plugin",
|
|
166
248
|
decision: "plugin-upstream-fix",
|
|
167
249
|
title: "package and manifest versions drift",
|
|
250
|
+
authorRemediation: authorRemediation(
|
|
251
|
+
"Align the plugin version declared in package.json and openclaw.plugin.json.",
|
|
252
|
+
[
|
|
253
|
+
"Use the same version in both files, or remove stale manifest version metadata if package.json is authoritative.",
|
|
254
|
+
"Republish with a new package version after changing published metadata.",
|
|
255
|
+
],
|
|
256
|
+
),
|
|
168
257
|
},
|
|
169
258
|
"package-min-host-version-drift": {
|
|
170
259
|
severity: "P2",
|
|
171
260
|
owner: "plugin",
|
|
172
261
|
decision: "plugin-upstream-fix",
|
|
173
262
|
title: "OpenClaw package minimum host version drifts from build target",
|
|
263
|
+
authorRemediation: authorRemediation(
|
|
264
|
+
"Set the package minimum host version to the OpenClaw version range the plugin was built and tested against.",
|
|
265
|
+
[
|
|
266
|
+
"Update package.json openclaw.install.minHostVersion or compatibility metadata.",
|
|
267
|
+
"Keep it semver-compatible with the target OpenClaw build version.",
|
|
268
|
+
],
|
|
269
|
+
),
|
|
174
270
|
},
|
|
175
271
|
"package-npm-pack-entrypoint-missing": {
|
|
176
272
|
severity: "P1",
|
|
177
273
|
owner: "plugin",
|
|
178
274
|
decision: "plugin-upstream-fix",
|
|
179
275
|
title: "advertised npm artifact is missing OpenClaw entrypoints",
|
|
276
|
+
authorRemediation: authorRemediation(
|
|
277
|
+
"Include the declared OpenClaw entrypoints in the npm-packed artifact.",
|
|
278
|
+
[
|
|
279
|
+
"Run npm pack locally and inspect the tarball contents.",
|
|
280
|
+
"Update package.json files so dist files and manifests are included.",
|
|
281
|
+
"Build before packing if the entrypoint is generated.",
|
|
282
|
+
],
|
|
283
|
+
),
|
|
180
284
|
},
|
|
181
285
|
"package-npm-pack-metadata-missing": {
|
|
182
286
|
severity: "P2",
|
|
183
287
|
owner: "plugin",
|
|
184
288
|
decision: "plugin-upstream-fix",
|
|
185
289
|
title: "advertised npm artifact is missing OpenClaw metadata",
|
|
290
|
+
authorRemediation: authorRemediation(
|
|
291
|
+
"Include OpenClaw metadata files in the npm-packed artifact.",
|
|
292
|
+
[
|
|
293
|
+
"Run npm pack locally and inspect package.json and OpenClaw manifest files.",
|
|
294
|
+
"Update package.json files so required metadata is not excluded.",
|
|
295
|
+
],
|
|
296
|
+
),
|
|
186
297
|
},
|
|
187
298
|
"package-npm-pack-unavailable": {
|
|
188
299
|
severity: "P1",
|
|
189
300
|
owner: "plugin",
|
|
190
301
|
decision: "plugin-upstream-fix",
|
|
191
302
|
title: "advertised npm artifact cannot be packed",
|
|
303
|
+
authorRemediation: authorRemediation(
|
|
304
|
+
"Make the package packable before publishing it through ClawHub.",
|
|
305
|
+
[
|
|
306
|
+
"Remove private:true if this package is intended to publish.",
|
|
307
|
+
"Ensure package.json has a valid name and version.",
|
|
308
|
+
"Fix package scripts or files entries that make npm pack fail.",
|
|
309
|
+
],
|
|
310
|
+
),
|
|
192
311
|
},
|
|
193
312
|
"package-openclaw-entry-missing": {
|
|
194
313
|
severity: "P2",
|
|
195
314
|
owner: "plugin",
|
|
196
315
|
decision: "plugin-upstream-fix",
|
|
197
316
|
title: "OpenClaw package entrypoint metadata is missing",
|
|
317
|
+
authorRemediation: authorRemediation(
|
|
318
|
+
"Declare the plugin runtime entrypoint in package.json OpenClaw metadata.",
|
|
319
|
+
[
|
|
320
|
+
"Add openclaw.extensions for extension entrypoints.",
|
|
321
|
+
"Add openclaw.runtimeExtensions when the plugin has runtime-side code.",
|
|
322
|
+
],
|
|
323
|
+
),
|
|
198
324
|
},
|
|
199
325
|
"package-openclaw-metadata-missing": {
|
|
200
326
|
severity: "P2",
|
|
201
327
|
owner: "plugin",
|
|
202
328
|
decision: "plugin-upstream-fix",
|
|
203
329
|
title: "OpenClaw package metadata is missing",
|
|
330
|
+
authorRemediation: authorRemediation(
|
|
331
|
+
"Add the package.json openclaw metadata block.",
|
|
332
|
+
[
|
|
333
|
+
"Describe extension entrypoints, plugin API compatibility, and install metadata.",
|
|
334
|
+
"Keep package metadata in sync with openclaw.plugin.json when both files are present.",
|
|
335
|
+
],
|
|
336
|
+
),
|
|
204
337
|
},
|
|
205
338
|
"package-openclaw-unsupported-metadata": {
|
|
206
339
|
severity: "P2",
|
|
207
340
|
owner: "plugin",
|
|
208
341
|
decision: "plugin-upstream-fix",
|
|
209
342
|
title: "package declares unsupported OpenClaw metadata",
|
|
343
|
+
authorRemediation: authorRemediation(
|
|
344
|
+
"Remove unsupported OpenClaw package metadata fields.",
|
|
345
|
+
[
|
|
346
|
+
"Delete openclaw.bundle and other fields not accepted by the current package schema.",
|
|
347
|
+
"Move bundle-specific data to documented manifest fields when available.",
|
|
348
|
+
],
|
|
349
|
+
),
|
|
210
350
|
},
|
|
211
351
|
"package-plugin-api-compat-missing": {
|
|
212
352
|
severity: "P2",
|
|
213
353
|
owner: "plugin",
|
|
214
354
|
decision: "plugin-upstream-fix",
|
|
215
355
|
title: "plugin API compatibility range is missing",
|
|
356
|
+
authorRemediation: authorRemediation(
|
|
357
|
+
"Declare the OpenClaw plugin API range this package supports.",
|
|
358
|
+
[
|
|
359
|
+
"Add package.json `openclaw.compat.pluginApi` with the OpenClaw plugin API range you tested.",
|
|
360
|
+
"If known, include the OpenClaw build/version used to produce the package metadata.",
|
|
361
|
+
],
|
|
362
|
+
'"openclaw": {\n "compat": {\n "pluginApi": ">=0.1.0"\n }\n}',
|
|
363
|
+
),
|
|
216
364
|
},
|
|
217
365
|
"package-typescript-source-entrypoint": {
|
|
218
366
|
severity: "P2",
|
|
@@ -225,6 +373,13 @@ export const issueMetadataByCode = {
|
|
|
225
373
|
owner: "core",
|
|
226
374
|
decision: "core-compat-adapter",
|
|
227
375
|
title: "providerAuthEnvVars legacy manifest metadata must stay covered",
|
|
376
|
+
authorRemediation: migrationRemediation(
|
|
377
|
+
"Move legacy provider authentication environment variables into current provider setup metadata.",
|
|
378
|
+
[
|
|
379
|
+
"Mirror providerAuthEnvVars into setup.providers[].envVars or the current provider-choice metadata.",
|
|
380
|
+
"Keep the legacy field only while supporting older OpenClaw versions that still read it.",
|
|
381
|
+
],
|
|
382
|
+
),
|
|
228
383
|
},
|
|
229
384
|
"registration-capture-gap": {
|
|
230
385
|
severity: "P2",
|
|
@@ -255,6 +410,13 @@ export const issueMetadataByCode = {
|
|
|
255
410
|
owner: "plugin",
|
|
256
411
|
decision: "plugin-upstream-fix",
|
|
257
412
|
title: "plugin ships an unsupported security manifest",
|
|
413
|
+
authorRemediation: authorRemediation(
|
|
414
|
+
"Remove unsupported security manifest files until OpenClaw documents a versioned security manifest schema.",
|
|
415
|
+
[
|
|
416
|
+
"Delete openclaw.security.json if it is advisory-only and not consumed by OpenClaw.",
|
|
417
|
+
"Reintroduce it only when the schema and ClawHub behavior are documented.",
|
|
418
|
+
],
|
|
419
|
+
),
|
|
258
420
|
},
|
|
259
421
|
};
|
|
260
422
|
|
|
@@ -284,6 +446,14 @@ export function buildIssues({ breakages = [], warnings = [], suggestions = [], t
|
|
|
284
446
|
evidence: finding.evidence ?? [],
|
|
285
447
|
compatRecord: finding.compatRecord ?? null,
|
|
286
448
|
runtimeCoverage: finding.runtimeCoverage ?? null,
|
|
449
|
+
...(finding.authorRemediation
|
|
450
|
+
? {
|
|
451
|
+
authorRemediation: {
|
|
452
|
+
summary: finding.authorRemediation.summary,
|
|
453
|
+
docsUrl: authorRemediationDocsUrl(finding.code),
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
: {}),
|
|
287
457
|
}));
|
|
288
458
|
}
|
|
289
459
|
|
|
@@ -305,9 +475,18 @@ export function issueMetadata(finding, targetOpenClaw) {
|
|
|
305
475
|
decision: "inspector-follow-up",
|
|
306
476
|
title: finding.message,
|
|
307
477
|
};
|
|
478
|
+
const authorMetadata = metadata.authorRemediation
|
|
479
|
+
? {
|
|
480
|
+
authorRemediation: {
|
|
481
|
+
summary: metadata.authorRemediation.summary,
|
|
482
|
+
docsUrl: authorRemediationDocsUrl(finding.code),
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
: {};
|
|
308
486
|
return {
|
|
309
487
|
...finding,
|
|
310
488
|
...metadata,
|
|
489
|
+
...authorMetadata,
|
|
311
490
|
...classifyIssueFinding(finding, targetOpenClaw, metadata),
|
|
312
491
|
};
|
|
313
492
|
}
|
|
@@ -333,6 +512,14 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
|
|
|
333
512
|
};
|
|
334
513
|
}
|
|
335
514
|
|
|
515
|
+
export function isInspectorGapFinding(finding, targetOpenClaw) {
|
|
516
|
+
return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function isAuthorFacingFinding(finding, targetOpenClaw) {
|
|
520
|
+
return Boolean(issueMetadata(finding, targetOpenClaw).authorRemediation);
|
|
521
|
+
}
|
|
522
|
+
|
|
336
523
|
export function summarizeIssueClasses(issues) {
|
|
337
524
|
const summary = {
|
|
338
525
|
"compat-gap": 0,
|
package/src/report.js
CHANGED
|
@@ -3,7 +3,7 @@ import { renderMarkdownTable, writeArtifacts, writeJsonMarkdownArtifacts } from
|
|
|
3
3
|
import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } from "./compatibility-report.js";
|
|
4
4
|
import { buildContractProbes } from "./contract-probes.js";
|
|
5
5
|
import { classifyCompatibilityFixture } from "./fixture-summary.js";
|
|
6
|
-
import { buildIssues, summarizeIssueClasses } from "./issues.js";
|
|
6
|
+
import { buildIssues, isAuthorFacingFinding, summarizeIssueClasses } from "./issues.js";
|
|
7
7
|
import { sanitizeReportArtifact } from "./report-sanitizer.js";
|
|
8
8
|
import { applyRuntimeExecutionCoverage } from "./runtime-reconciliation.js";
|
|
9
9
|
|
|
@@ -141,18 +141,26 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
141
141
|
decisions,
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
const visibleBreakages = filterVisibleFindings(breakages, targetOpenClaw, options);
|
|
145
|
+
const visibleWarnings = filterVisibleFindings(warnings, targetOpenClaw, options);
|
|
146
|
+
const visibleSuggestions = filterVisibleFindings(suggestions, targetOpenClaw, options);
|
|
147
|
+
const visibleDecisions = options.authorFacing === true ? [] : decisions;
|
|
144
148
|
const runtimeCoverage = applyRuntimeExecutionCoverage({
|
|
145
|
-
findings: [...
|
|
149
|
+
findings: [...visibleWarnings, ...visibleSuggestions],
|
|
146
150
|
executionResults: options.executionResults,
|
|
147
151
|
});
|
|
148
152
|
const issues = buildIssues({
|
|
149
|
-
breakages,
|
|
150
|
-
warnings,
|
|
151
|
-
suggestions,
|
|
153
|
+
breakages: visibleBreakages,
|
|
154
|
+
warnings: visibleWarnings,
|
|
155
|
+
suggestions: visibleSuggestions,
|
|
152
156
|
targetOpenClaw,
|
|
153
157
|
idPrefix: options.issueIdPrefix,
|
|
154
158
|
});
|
|
155
|
-
const contractProbes = buildContractProbes({
|
|
159
|
+
const contractProbes = buildContractProbes({
|
|
160
|
+
warnings: visibleWarnings,
|
|
161
|
+
suggestions: visibleSuggestions,
|
|
162
|
+
fixtures: fixtureReports,
|
|
163
|
+
});
|
|
156
164
|
const issueSummary = summarizeIssueClasses(issues);
|
|
157
165
|
const openIssues = issues.filter((issue) => issue.status !== "runtime-covered");
|
|
158
166
|
const openIssueSummary = summarizeIssueClasses(openIssues);
|
|
@@ -160,14 +168,14 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
160
168
|
return {
|
|
161
169
|
generatedAt: options.generatedAt ?? "deterministic",
|
|
162
170
|
targetOpenClaw,
|
|
163
|
-
status:
|
|
171
|
+
status: visibleBreakages.length === 0 ? "pass" : "fail",
|
|
164
172
|
summary: {
|
|
165
173
|
fixtureCount: fixtureReports.length,
|
|
166
174
|
highPriorityFixtures: fixtureReports.filter((fixture) => fixture.priority === "high").length,
|
|
167
|
-
breakageCount:
|
|
168
|
-
warningCount:
|
|
169
|
-
suggestionCount:
|
|
170
|
-
decisionCount:
|
|
175
|
+
breakageCount: visibleBreakages.length,
|
|
176
|
+
warningCount: visibleWarnings.length,
|
|
177
|
+
suggestionCount: visibleSuggestions.length,
|
|
178
|
+
decisionCount: visibleDecisions.length,
|
|
171
179
|
logCount: logs.length,
|
|
172
180
|
issueCount: issues.length,
|
|
173
181
|
openIssueCount: openIssues.length,
|
|
@@ -189,16 +197,23 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
189
197
|
contractProbeCount: contractProbes.length,
|
|
190
198
|
},
|
|
191
199
|
fixtures: fixtureReports,
|
|
192
|
-
breakages,
|
|
193
|
-
warnings,
|
|
194
|
-
suggestions,
|
|
200
|
+
breakages: visibleBreakages,
|
|
201
|
+
warnings: visibleWarnings,
|
|
202
|
+
suggestions: visibleSuggestions,
|
|
195
203
|
issues,
|
|
196
204
|
contractProbes,
|
|
197
205
|
logs,
|
|
198
|
-
decisions,
|
|
206
|
+
decisions: visibleDecisions,
|
|
199
207
|
};
|
|
200
208
|
}
|
|
201
209
|
|
|
210
|
+
function filterVisibleFindings(findings, targetOpenClaw, options) {
|
|
211
|
+
if (options.authorFacing !== true) {
|
|
212
|
+
return findings;
|
|
213
|
+
}
|
|
214
|
+
return findings.filter((finding) => isAuthorFacingFinding(finding, targetOpenClaw));
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggestions, logs, decisions }) {
|
|
203
218
|
if (targetOpenClaw.status !== "ok") {
|
|
204
219
|
logs.push({
|
package/src/sdk-mock.js
CHANGED
|
@@ -671,6 +671,24 @@ function resolveExistingSourcePath(target) {
|
|
|
671
671
|
` : ""}
|
|
672
672
|
function createMockValue(name) {
|
|
673
673
|
function fn(...args) {
|
|
674
|
+
if (name === "resolveDefaultAgentDir") {
|
|
675
|
+
return mockAgentDir();
|
|
676
|
+
}
|
|
677
|
+
if (name === "resolveAgentDir") {
|
|
678
|
+
return mockAgentDir(args[1]);
|
|
679
|
+
}
|
|
680
|
+
if (name === "resolveUserPath") {
|
|
681
|
+
return typeof args[0] === "string" ? args[0] : mockAgentDir();
|
|
682
|
+
}
|
|
683
|
+
if (name === "resolveAuthProfileOrder") {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
if (name === "resolveWindowsSpawnProgram") {
|
|
687
|
+
return mockWindowsSpawnProgram(args[0]);
|
|
688
|
+
}
|
|
689
|
+
if (name === "materializeWindowsSpawnProgram") {
|
|
690
|
+
return mockWindowsSpawnInvocation(args[0], args[1]);
|
|
691
|
+
}
|
|
674
692
|
if (name === "resolvePreferredOpenClawTmpDir") {
|
|
675
693
|
return process.env.TMPDIR || "/tmp";
|
|
676
694
|
}
|
|
@@ -707,6 +725,80 @@ function createMockValue(name) {
|
|
|
707
725
|
});
|
|
708
726
|
}
|
|
709
727
|
|
|
728
|
+
function mockAgentDir(agentId = "main") {
|
|
729
|
+
const base = process.env.TMPDIR || process.env.TEMP || process.env.TMP || "/tmp";
|
|
730
|
+
const safeAgentId = String(agentId || "main").replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
731
|
+
return base.replace(/[\\/]+$/, "") + "/plugin-inspector-openclaw/agents/" + safeAgentId + "/agent";
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function mockWindowsSpawnProgram(params = {}) {
|
|
735
|
+
return {
|
|
736
|
+
command: typeof params.command === "string" && params.command.trim() ? params.command : process.execPath,
|
|
737
|
+
leadingArgv: [],
|
|
738
|
+
resolution: "mock",
|
|
739
|
+
packageName: typeof params.packageName === "string" ? params.packageName : undefined,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function mockWindowsSpawnInvocation(program = {}, argv = []) {
|
|
744
|
+
const command = typeof program.command === "string" && program.command.trim() ? program.command : process.execPath;
|
|
745
|
+
if (program.packageName === "@openai/codex") {
|
|
746
|
+
return {
|
|
747
|
+
command: process.execPath,
|
|
748
|
+
argv: ["-e", mockCodexAppServerScript()],
|
|
749
|
+
resolution: program.resolution ?? "mock",
|
|
750
|
+
windowsHide: true,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
command,
|
|
755
|
+
argv: [...(Array.isArray(program.leadingArgv) ? program.leadingArgv : []), ...(Array.isArray(argv) ? argv : [])],
|
|
756
|
+
resolution: program.resolution ?? "mock",
|
|
757
|
+
shell: program.shell,
|
|
758
|
+
windowsHide: program.windowsHide,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function mockCodexAppServerScript() {
|
|
763
|
+
return [
|
|
764
|
+
"const readline = require('node:readline');",
|
|
765
|
+
"const rl = readline.createInterface({ input: process.stdin });",
|
|
766
|
+
"let idleTimer;",
|
|
767
|
+
"function scheduleIdleExit() {",
|
|
768
|
+
" if (idleTimer) clearTimeout(idleTimer);",
|
|
769
|
+
" idleTimer = setTimeout(() => process.exit(0), 1000);",
|
|
770
|
+
"}",
|
|
771
|
+
"function write(id, result) { process.stdout.write(JSON.stringify({ id, result }) + String.fromCharCode(10)); }",
|
|
772
|
+
"rl.on('line', (line) => {",
|
|
773
|
+
" let message;",
|
|
774
|
+
" try { message = JSON.parse(line); } catch { return; }",
|
|
775
|
+
" if (message.id === undefined || message.id === null) return;",
|
|
776
|
+
" switch (message.method) {",
|
|
777
|
+
" case 'initialize':",
|
|
778
|
+
" write(message.id, { userAgent: 'openclaw/999.0.0 (plugin-inspector mock)' });",
|
|
779
|
+
" break;",
|
|
780
|
+
" case 'model/list':",
|
|
781
|
+
" write(message.id, { data: [] });",
|
|
782
|
+
" break;",
|
|
783
|
+
" case 'thread/list':",
|
|
784
|
+
" case 'mcpServerStatus/list':",
|
|
785
|
+
" case 'skills/list':",
|
|
786
|
+
" write(message.id, { data: [] });",
|
|
787
|
+
" break;",
|
|
788
|
+
" case 'account/read':",
|
|
789
|
+
" write(message.id, null);",
|
|
790
|
+
" break;",
|
|
791
|
+
" case 'account/rateLimits/read':",
|
|
792
|
+
" write(message.id, null);",
|
|
793
|
+
" break;",
|
|
794
|
+
" default:",
|
|
795
|
+
" process.stdout.write(JSON.stringify({ id: message.id, error: { code: -32601, message: 'mock method not implemented' } }) + String.fromCharCode(10));",
|
|
796
|
+
" }",
|
|
797
|
+
" scheduleIdleExit();",
|
|
798
|
+
"});",
|
|
799
|
+
].join("\\n");
|
|
800
|
+
}
|
|
801
|
+
|
|
710
802
|
function createZNamespace() {
|
|
711
803
|
const namespace = {
|
|
712
804
|
any: () => createSchema(),
|