@openclaw/plugin-inspector 0.1.2 → 0.2.0
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 +28 -0
- package/README.md +56 -8
- package/examples/github-actions-plugin-inspector.yml +1 -2
- package/examples/package-json-plugin-inspector.json +21 -0
- package/package.json +1 -1
- package/src/advanced.js +10 -0
- package/src/api.js +2 -1
- package/src/capture-api.js +20 -3
- package/src/capture-cli.js +61 -0
- package/src/ci-outputs.js +235 -0
- package/src/ci-summary.js +1 -1
- package/src/cli.js +157 -6
- package/src/config.js +30 -3
- package/src/contract-probes.js +5 -0
- package/src/fixture-summary.js +40 -16
- package/src/index.js +1 -0
- package/src/init.js +1 -2
- package/src/issues.js +8 -0
- package/src/openclaw-target.js +27 -0
- package/src/report.js +1 -0
- package/src/sdk-mock.js +50 -5
- package/src/synthetic-probes-cli.js +101 -0
- package/src/synthetic-probes.js +6 -5
- package/src/workspace-plan.js +16 -4
package/src/cli.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import {
|
|
4
|
+
loadPluginConfig,
|
|
3
5
|
renderTextSummary,
|
|
4
6
|
runPluginCheck,
|
|
5
7
|
} from "./index.js";
|
|
6
8
|
import {
|
|
9
|
+
buildCiSummary,
|
|
7
10
|
captureEntrypoint,
|
|
11
|
+
defaultJunitPath,
|
|
12
|
+
defaultSarifPath,
|
|
13
|
+
inspectCompatibilityFixtureSet,
|
|
8
14
|
inspectFixtureSet,
|
|
9
15
|
loadInspectorConfig,
|
|
16
|
+
writeCiOutputArtifacts,
|
|
17
|
+
writeCiSummary,
|
|
18
|
+
writeCompatibilityReport,
|
|
10
19
|
writePluginInspectorInit,
|
|
11
20
|
writeArtifacts,
|
|
12
21
|
writeReport,
|
|
@@ -23,8 +32,16 @@ try {
|
|
|
23
32
|
await runCheck(commandArgs);
|
|
24
33
|
} else if (command === "init") {
|
|
25
34
|
await runInit(commandArgs);
|
|
26
|
-
} else if (command === "
|
|
27
|
-
await
|
|
35
|
+
} else if (command === "config") {
|
|
36
|
+
await runConfig(commandArgs);
|
|
37
|
+
} else if (command === "inspect" || command === "report") {
|
|
38
|
+
if (command === "inspect" && !commandArgs.includes("--config")) {
|
|
39
|
+
await runCheck(commandArgs);
|
|
40
|
+
} else {
|
|
41
|
+
await runReport(command, commandArgs);
|
|
42
|
+
}
|
|
43
|
+
} else if (command === "ci") {
|
|
44
|
+
await runCi(commandArgs);
|
|
28
45
|
} else if (command === "capture") {
|
|
29
46
|
await runCapture(commandArgs);
|
|
30
47
|
} else {
|
|
@@ -35,6 +52,18 @@ try {
|
|
|
35
52
|
process.exitCode = 1;
|
|
36
53
|
}
|
|
37
54
|
|
|
55
|
+
async function runConfig(commandArgs) {
|
|
56
|
+
const configPath = readFlag(commandArgs, "--config");
|
|
57
|
+
const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
|
|
58
|
+
const config = await loadPluginConfig({ configPath, pluginRoot });
|
|
59
|
+
|
|
60
|
+
if (commandArgs.includes("--json")) {
|
|
61
|
+
console.log(JSON.stringify(config, null, 2));
|
|
62
|
+
} else {
|
|
63
|
+
console.log(renderConfigTextSummary(config));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
38
67
|
async function runCheck(commandArgs) {
|
|
39
68
|
const configPath = readFlag(commandArgs, "--config");
|
|
40
69
|
const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
|
|
@@ -43,7 +72,13 @@ async function runCheck(commandArgs) {
|
|
|
43
72
|
const json = commandArgs.includes("--json");
|
|
44
73
|
const capture = readRuntimeFlag(commandArgs);
|
|
45
74
|
const mockSdk = readMockSdkFlag(commandArgs);
|
|
46
|
-
const
|
|
75
|
+
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
76
|
+
const { report, paths } = await runPluginCheck({ configPath, pluginRoot, outDir, openclawPath, capture, mockSdk });
|
|
77
|
+
await writeCiOutputArtifacts(report, {
|
|
78
|
+
...ciOutputs,
|
|
79
|
+
cwd: path.dirname(paths.jsonPath),
|
|
80
|
+
outDir: ".",
|
|
81
|
+
});
|
|
47
82
|
|
|
48
83
|
if (json) {
|
|
49
84
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -80,9 +115,15 @@ async function runReport(command, commandArgs) {
|
|
|
80
115
|
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
81
116
|
const check = commandArgs.includes("--check") || command === "ci";
|
|
82
117
|
const json = commandArgs.includes("--json");
|
|
118
|
+
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
83
119
|
const config = await loadInspectorConfig(configPath);
|
|
84
120
|
const report = await inspectFixtureSet(config);
|
|
85
|
-
await writeReport(report, { outDir });
|
|
121
|
+
const paths = await writeReport(report, { outDir });
|
|
122
|
+
await writeCiOutputArtifacts(report, {
|
|
123
|
+
...ciOutputs,
|
|
124
|
+
cwd: path.dirname(paths.jsonPath),
|
|
125
|
+
outDir: ".",
|
|
126
|
+
});
|
|
86
127
|
|
|
87
128
|
if (json) {
|
|
88
129
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -95,6 +136,72 @@ async function runReport(command, commandArgs) {
|
|
|
95
136
|
}
|
|
96
137
|
}
|
|
97
138
|
|
|
139
|
+
async function runCi(commandArgs) {
|
|
140
|
+
const configPath = readFlag(commandArgs, "--config");
|
|
141
|
+
const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
|
|
142
|
+
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
143
|
+
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
144
|
+
const json = commandArgs.includes("--json");
|
|
145
|
+
const capture = readRuntimeFlag(commandArgs);
|
|
146
|
+
const mockSdk = readMockSdkFlag(commandArgs);
|
|
147
|
+
const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
|
|
148
|
+
const { report, reportDir } = await runCiCompatibilityReport({
|
|
149
|
+
capture,
|
|
150
|
+
configPath,
|
|
151
|
+
mockSdk,
|
|
152
|
+
openclawPath,
|
|
153
|
+
outDir,
|
|
154
|
+
pluginRoot,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const summary = await buildCiSummary({
|
|
158
|
+
artifactBaseDir: reportDir,
|
|
159
|
+
reportPaths: {
|
|
160
|
+
compatibility: "plugin-inspector-report.json",
|
|
161
|
+
},
|
|
162
|
+
reports: {
|
|
163
|
+
compatibility: report,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
await writeCiSummary(summary, {
|
|
167
|
+
jsonPath: path.join(reportDir, "plugin-inspector-ci-summary.json"),
|
|
168
|
+
markdownPath: path.join(reportDir, "plugin-inspector-ci-summary.md"),
|
|
169
|
+
});
|
|
170
|
+
await writeCiOutputArtifacts(report, {
|
|
171
|
+
...ciOutputs,
|
|
172
|
+
cwd: reportDir,
|
|
173
|
+
outDir: ".",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (json) {
|
|
177
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(renderCiTextSummary(summary));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (summary.status !== "pass") {
|
|
183
|
+
throw new Error("plugin-inspector ci summary failed");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function runCiCompatibilityReport({ capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
|
|
188
|
+
if (configPath) {
|
|
189
|
+
const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
|
|
190
|
+
const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
|
|
191
|
+
await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
|
|
192
|
+
return {
|
|
193
|
+
report,
|
|
194
|
+
reportDir: path.resolve(config.rootDir, outDir),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { report } = await runPluginCheck({ pluginRoot, outDir, openclawPath, capture, mockSdk });
|
|
199
|
+
return {
|
|
200
|
+
report,
|
|
201
|
+
reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
98
205
|
async function runCapture(commandArgs) {
|
|
99
206
|
const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
|
|
100
207
|
const outputPath = readFlag(commandArgs, "--output");
|
|
@@ -124,6 +231,26 @@ function readFlag(commandArgs, name) {
|
|
|
124
231
|
return commandArgs[index + 1] ?? null;
|
|
125
232
|
}
|
|
126
233
|
|
|
234
|
+
function readOptionalPathFlag(commandArgs, name, defaultPath) {
|
|
235
|
+
const index = commandArgs.indexOf(name);
|
|
236
|
+
if (index === -1) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const value = commandArgs[index + 1];
|
|
240
|
+
return value && !value.startsWith("-") ? value : defaultPath;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function readCiOutputFlags(commandArgs, options = {}) {
|
|
244
|
+
return {
|
|
245
|
+
sarifPath: commandArgs.includes("--no-sarif")
|
|
246
|
+
? null
|
|
247
|
+
: (readOptionalPathFlag(commandArgs, "--sarif", defaultSarifPath) ?? (options.defaultEnabled ? defaultSarifPath : null)),
|
|
248
|
+
junitPath: commandArgs.includes("--no-junit")
|
|
249
|
+
? null
|
|
250
|
+
: (readOptionalPathFlag(commandArgs, "--junit", defaultJunitPath) ?? (options.defaultEnabled ? defaultJunitPath : null)),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
127
254
|
function readRuntimeFlag(commandArgs) {
|
|
128
255
|
if (commandArgs.includes("--runtime") || commandArgs.includes("--capture")) {
|
|
129
256
|
return true;
|
|
@@ -154,19 +281,43 @@ function readMockSdkFlag(commandArgs) {
|
|
|
154
281
|
return undefined;
|
|
155
282
|
}
|
|
156
283
|
|
|
284
|
+
function renderCiTextSummary(summary) {
|
|
285
|
+
return [
|
|
286
|
+
`Status: ${summary.status.toUpperCase()}`,
|
|
287
|
+
`Breakages: ${summary.summary.breakages}`,
|
|
288
|
+
`Issues: ${summary.summary.issues}`,
|
|
289
|
+
`Artifacts: ${Object.values(summary.artifacts).filter(Boolean).length}`,
|
|
290
|
+
].join("\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function renderConfigTextSummary(config) {
|
|
294
|
+
const fixture = config.fixtures[0];
|
|
295
|
+
return [
|
|
296
|
+
`Plugin: ${fixture.id}`,
|
|
297
|
+
`Root: ${config.rootDir}`,
|
|
298
|
+
`Config: ${config.configPath ?? "auto"}`,
|
|
299
|
+
`Priority: ${fixture.priority}`,
|
|
300
|
+
`Seams: ${fixture.seams.join(", ")}`,
|
|
301
|
+
`Runtime capture: ${config.capture?.runtime === true ? "on" : "off"}`,
|
|
302
|
+
`Mock SDK: ${config.capture?.mockSdk === false ? "off" : "on"}`,
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
157
306
|
function printHelp() {
|
|
158
307
|
console.log(`plugin-inspector
|
|
159
308
|
|
|
160
309
|
Usage:
|
|
161
310
|
plugin-inspector
|
|
162
311
|
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json]
|
|
312
|
+
plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
|
|
163
313
|
plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--package-manager npm|pnpm|yarn|bun] [--force]
|
|
164
314
|
plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
|
|
165
|
-
plugin-inspector inspect --config <path> [--out <dir>] [--check] [--json]
|
|
166
|
-
plugin-inspector ci --config <path> [--out <dir>]
|
|
315
|
+
plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]]
|
|
316
|
+
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--json] [--no-sarif] [--no-junit]
|
|
167
317
|
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--plugin-root <path>] [--output <path>]
|
|
168
318
|
|
|
169
319
|
Default check runs from the current plugin root and writes reports/ unless --out is set.
|
|
320
|
+
CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
|
|
170
321
|
Runtime capture is opt-in because it imports plugin code; use --runtime with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
|
|
171
322
|
`);
|
|
172
323
|
}
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
export const npmPackagePayloadDir = ".crabpot-package";
|
|
6
6
|
export const defaultPluginRootConfigFiles = ["plugin-inspector.config.json", ".plugin-inspector.json"];
|
|
7
|
+
export const packageJsonConfigKeys = ["pluginInspector", "plugin-inspector"];
|
|
7
8
|
|
|
8
9
|
export async function loadInspectorConfig(configPath, options = {}) {
|
|
9
10
|
if (!configPath) {
|
|
@@ -24,16 +25,23 @@ export async function loadInspectorConfig(configPath, options = {}) {
|
|
|
24
25
|
export async function loadPluginRootConfig(configPath = null, options = {}) {
|
|
25
26
|
const rootDir = path.resolve(options.cwd ?? process.cwd());
|
|
26
27
|
const resolvedPath = configPath ? path.resolve(rootDir, configPath) : findPluginRootConfigPath(rootDir);
|
|
27
|
-
|
|
28
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
29
|
+
const packageJson = await readJsonIfExists(packageJsonPath);
|
|
30
|
+
const packageConfig = packageJsonConfig(packageJson);
|
|
31
|
+
|
|
32
|
+
if (!resolvedPath && !packageJson && !existsSync(path.join(rootDir, "openclaw.plugin.json"))) {
|
|
28
33
|
throw new Error("run from a plugin root with package.json/openclaw.plugin.json, or pass --config");
|
|
29
34
|
}
|
|
30
|
-
|
|
35
|
+
|
|
36
|
+
const config = resolvedPath
|
|
37
|
+
? JSON.parse(await readFile(resolvedPath, "utf8"))
|
|
38
|
+
: (packageConfig.config ?? { version: 1 });
|
|
31
39
|
const normalizedConfig = await normalizePluginRootConfig(config, { rootDir });
|
|
32
40
|
validateInspectorConfig(normalizedConfig);
|
|
33
41
|
return {
|
|
34
42
|
...normalizedConfig,
|
|
35
43
|
rootDir,
|
|
36
|
-
configPath: resolvedPath,
|
|
44
|
+
configPath: resolvedPath ?? packageConfig.configPath,
|
|
37
45
|
};
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -164,6 +172,25 @@ function findPluginRootConfigPath(rootDir) {
|
|
|
164
172
|
return defaultPluginRootConfigFiles.map((file) => path.join(rootDir, file)).find(existsSync) ?? null;
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
function packageJsonConfig(packageJson) {
|
|
176
|
+
if (!packageJson) {
|
|
177
|
+
return { config: null, configPath: null };
|
|
178
|
+
}
|
|
179
|
+
for (const key of packageJsonConfigKeys) {
|
|
180
|
+
if (packageJson[key] === undefined) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!packageJson[key] || typeof packageJson[key] !== "object" || Array.isArray(packageJson[key])) {
|
|
184
|
+
throw new Error(`package.json ${key} must be an object`);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
config: packageJson[key],
|
|
188
|
+
configPath: `package.json#${key}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { config: null, configPath: null };
|
|
192
|
+
}
|
|
193
|
+
|
|
167
194
|
async function readJsonIfExists(filePath) {
|
|
168
195
|
if (!existsSync(filePath)) {
|
|
169
196
|
return null;
|
package/src/contract-probes.js
CHANGED
|
@@ -29,6 +29,11 @@ export const contractProbeRules = {
|
|
|
29
29
|
contract: "Every observed OpenClaw plugin SDK import remains exported by the target OpenClaw package.",
|
|
30
30
|
target: "sdk-alias",
|
|
31
31
|
},
|
|
32
|
+
"reserved-sdk-import": {
|
|
33
|
+
id: "sdk.import.reserved-bundled-plugin-boundary",
|
|
34
|
+
contract: "External plugins use documented public SDK subpaths instead of reserved bundled-plugin compatibility shims.",
|
|
35
|
+
target: "sdk-import",
|
|
36
|
+
},
|
|
32
37
|
"provider-auth-env-vars": {
|
|
33
38
|
id: "manifest.compat.provider-auth-env-vars",
|
|
34
39
|
contract: "Legacy provider auth env metadata continues to map into config/help surfaces.",
|
package/src/fixture-summary.js
CHANGED
|
@@ -626,7 +626,12 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
626
626
|
|
|
627
627
|
const sdkExports = new Set(targetOpenClaw.sdkExports);
|
|
628
628
|
const unknownImports = fixtureReport.sdkImportDetails.filter((sdkImport) => !sdkExports.has(sdkImport.specifier));
|
|
629
|
-
|
|
629
|
+
const reservedSdkExports = new Set(targetOpenClaw.reservedSdkExports ?? []);
|
|
630
|
+
const reservedImports = fixtureReport.sdkImportDetails.filter((sdkImport) =>
|
|
631
|
+
reservedSdkExports.has(sdkImport.specifier),
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
if (reservedImports.length === 0 && unknownImports.length === 0) {
|
|
630
635
|
logs.push({
|
|
631
636
|
fixture: fixture.id,
|
|
632
637
|
code: "sdk-exports-present",
|
|
@@ -637,21 +642,40 @@ function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, war
|
|
|
637
642
|
return;
|
|
638
643
|
}
|
|
639
644
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
645
|
+
if (unknownImports.length > 0) {
|
|
646
|
+
warnings.push({
|
|
647
|
+
fixture: fixture.id,
|
|
648
|
+
code: "sdk-export-missing",
|
|
649
|
+
level: "warning",
|
|
650
|
+
message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
|
|
651
|
+
evidence: detailEvidence(unknownImports, "specifier"),
|
|
652
|
+
compatRecord: "plugin-sdk-export-aliases",
|
|
653
|
+
});
|
|
654
|
+
decisions.push({
|
|
655
|
+
fixture: fixture.id,
|
|
656
|
+
decision: "core-compat-adapter",
|
|
657
|
+
seam: "sdk-alias",
|
|
658
|
+
action: "Restore the package export alias or publish a versioned migration map before cold-importing old plugins.",
|
|
659
|
+
evidence: unique(unknownImports.map((sdkImport) => sdkImport.specifier)).join(", "),
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (reservedImports.length > 0) {
|
|
664
|
+
warnings.push({
|
|
665
|
+
fixture: fixture.id,
|
|
666
|
+
code: "reserved-sdk-import",
|
|
667
|
+
level: "warning",
|
|
668
|
+
message: "fixture imports reserved bundled-plugin SDK compatibility subpaths",
|
|
669
|
+
evidence: detailEvidence(reservedImports, "specifier"),
|
|
670
|
+
});
|
|
671
|
+
decisions.push({
|
|
672
|
+
fixture: fixture.id,
|
|
673
|
+
decision: "plugin-upstream-fix",
|
|
674
|
+
seam: "sdk-import",
|
|
675
|
+
action: "Move the plugin to documented public SDK subpaths or plugin-local helpers before relying on this compatibility shim.",
|
|
676
|
+
evidence: unique(reservedImports.map((sdkImport) => sdkImport.specifier)).join(", "),
|
|
677
|
+
});
|
|
678
|
+
}
|
|
655
679
|
}
|
|
656
680
|
|
|
657
681
|
function classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
|
package/src/index.js
CHANGED
package/src/init.js
CHANGED
|
@@ -79,8 +79,7 @@ jobs:
|
|
|
79
79
|
node-version: 24
|
|
80
80
|
cache: ${setup.cache}
|
|
81
81
|
${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
|
|
82
|
-
- run: ${setup.exec} @openclaw/plugin-inspector
|
|
83
|
-
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector check --no-openclaw --runtime --mock-sdk
|
|
82
|
+
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk
|
|
84
83
|
- uses: actions/upload-artifact@v5
|
|
85
84
|
if: always()
|
|
86
85
|
with:
|
package/src/issues.js
CHANGED
|
@@ -32,6 +32,7 @@ export const knownIssueCodes = new Set([
|
|
|
32
32
|
"provider-auth-env-vars",
|
|
33
33
|
"registration-capture-gap",
|
|
34
34
|
"runtime-tool-capture",
|
|
35
|
+
"reserved-sdk-import",
|
|
35
36
|
"sdk-export-missing",
|
|
36
37
|
]);
|
|
37
38
|
|
|
@@ -78,6 +79,12 @@ export const issueMetadataByCode = {
|
|
|
78
79
|
decision: "core-compat-adapter",
|
|
79
80
|
title: "plugin SDK import aliases are missing from target package exports",
|
|
80
81
|
},
|
|
82
|
+
"reserved-sdk-import": {
|
|
83
|
+
severity: "P1",
|
|
84
|
+
owner: "plugin",
|
|
85
|
+
decision: "plugin-upstream-fix",
|
|
86
|
+
title: "plugin imports reserved bundled-plugin SDK compatibility subpaths",
|
|
87
|
+
},
|
|
81
88
|
"missing-compat-record": {
|
|
82
89
|
severity: "P1",
|
|
83
90
|
owner: "core",
|
|
@@ -310,6 +317,7 @@ function issueClassFor(code, options) {
|
|
|
310
317
|
"package-openclaw-entry-missing",
|
|
311
318
|
"package-openclaw-metadata-missing",
|
|
312
319
|
"package-plugin-api-compat-missing",
|
|
320
|
+
"reserved-sdk-import",
|
|
313
321
|
].includes(code)
|
|
314
322
|
) {
|
|
315
323
|
return "upstream-metadata";
|
package/src/openclaw-target.js
CHANGED
|
@@ -31,6 +31,7 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
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
33
|
const manifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
|
|
34
|
+
const pluginSdkEntrypointsPath = path.join(resolvedPath, "src/plugin-sdk/entrypoints.ts");
|
|
34
35
|
const packagePath = path.join(resolvedPath, "package.json");
|
|
35
36
|
|
|
36
37
|
const registrySource = await readFile(registryPath, "utf8");
|
|
@@ -48,6 +49,18 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
48
49
|
const sdkExports = existsSync(packagePath)
|
|
49
50
|
? parsePluginSdkExports(JSON.parse(await readFile(packagePath, "utf8")))
|
|
50
51
|
: [];
|
|
52
|
+
const pluginSdkEntrypointsSource = existsSync(pluginSdkEntrypointsPath)
|
|
53
|
+
? await readFile(pluginSdkEntrypointsPath, "utf8")
|
|
54
|
+
: "";
|
|
55
|
+
const reservedSdkExports = pluginSdkEntrypointsSource
|
|
56
|
+
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "reservedBundledPluginSdkEntrypoints")
|
|
57
|
+
: [];
|
|
58
|
+
const supportedFacadeSdkExports = pluginSdkEntrypointsSource
|
|
59
|
+
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "supportedBundledFacadeSdkEntrypoints")
|
|
60
|
+
: [];
|
|
61
|
+
const publicPluginOwnedSdkExports = pluginSdkEntrypointsSource
|
|
62
|
+
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "publicPluginOwnedSdkEntrypoints")
|
|
63
|
+
: [];
|
|
51
64
|
|
|
52
65
|
return {
|
|
53
66
|
configuredPath: requestedPath,
|
|
@@ -69,6 +82,13 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
69
82
|
packagePath: existsSync(packagePath) ? relativePath(rootDir, packagePath) : null,
|
|
70
83
|
sdkExportCount: sdkExports.length,
|
|
71
84
|
sdkExports,
|
|
85
|
+
pluginSdkEntrypointsPath: existsSync(pluginSdkEntrypointsPath)
|
|
86
|
+
? relativePath(rootDir, pluginSdkEntrypointsPath)
|
|
87
|
+
: null,
|
|
88
|
+
reservedSdkExportCount: reservedSdkExports.length,
|
|
89
|
+
reservedSdkExports,
|
|
90
|
+
supportedFacadeSdkExports,
|
|
91
|
+
publicPluginOwnedSdkExports,
|
|
72
92
|
manifestTypesPath: existsSync(manifestTypesPath) ? relativePath(rootDir, manifestTypesPath) : null,
|
|
73
93
|
manifestFieldCount: manifestFields.length,
|
|
74
94
|
manifestFields,
|
|
@@ -149,11 +169,18 @@ function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status
|
|
|
149
169
|
apiRegistrars: [],
|
|
150
170
|
capturedRegistrars: [],
|
|
151
171
|
sdkExports: [],
|
|
172
|
+
reservedSdkExports: [],
|
|
173
|
+
supportedFacadeSdkExports: [],
|
|
174
|
+
publicPluginOwnedSdkExports: [],
|
|
152
175
|
manifestFields: [],
|
|
153
176
|
manifestContractFields: [],
|
|
154
177
|
};
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
export function parsePluginSdkEntrypointSpecifiers(source, exportName) {
|
|
181
|
+
return parseExportedStringArray(source, exportName).map((entrypoint) => `openclaw/plugin-sdk/${entrypoint}`).sort();
|
|
182
|
+
}
|
|
183
|
+
|
|
157
184
|
function parseCapturedRegistrars(source) {
|
|
158
185
|
return unique([...source.matchAll(/^\s*(register[A-Za-z0-9]+)\s*\(/gm)].map((match) => match[1])).sort();
|
|
159
186
|
}
|
package/src/report.js
CHANGED
|
@@ -160,6 +160,7 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
160
160
|
warningCount: warnings.length,
|
|
161
161
|
suggestionCount: suggestions.length,
|
|
162
162
|
decisionCount: decisions.length,
|
|
163
|
+
logCount: logs.length,
|
|
163
164
|
issueCount: issues.length,
|
|
164
165
|
p0IssueCount: issues.filter((issue) => issue.severity === "P0").length,
|
|
165
166
|
p1IssueCount: issues.filter((issue) => issue.severity === "P1").length,
|
package/src/sdk-mock.js
CHANGED
|
@@ -718,6 +718,37 @@ function mockSdkSource() {
|
|
|
718
718
|
return typeof entry === "function" ? { register: entry } : entry;
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
+
function isPlainObject(value) {
|
|
722
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function parseWithSchema(schema, value) {
|
|
726
|
+
return schema && typeof schema.parse === "function" ? schema.parse(value) : value;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function createConfigSchema(schema = {}) {
|
|
730
|
+
if (schema && typeof schema.parse === "function") {
|
|
731
|
+
return schema;
|
|
732
|
+
}
|
|
733
|
+
const shape = isPlainObject(schema?.shape) ? schema.shape : isPlainObject(schema?.properties) ? schema.properties : schema;
|
|
734
|
+
return {
|
|
735
|
+
...schema,
|
|
736
|
+
parse(value = {}) {
|
|
737
|
+
if (!isPlainObject(shape)) {
|
|
738
|
+
return isPlainObject(value) ? value : {};
|
|
739
|
+
}
|
|
740
|
+
const source = isPlainObject(value) ? value : {};
|
|
741
|
+
const output = { ...source };
|
|
742
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
743
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
744
|
+
output[key] = parseWithSchema(fieldSchema, source[key]);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return output;
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
721
752
|
export function definePluginEntry(entry) {
|
|
722
753
|
return normalizeEntry(entry);
|
|
723
754
|
}
|
|
@@ -759,13 +790,13 @@ export function defineSingleProviderPluginEntry(options) {
|
|
|
759
790
|
}
|
|
760
791
|
|
|
761
792
|
export function buildPluginConfigSchema(schema = {}) {
|
|
762
|
-
return schema;
|
|
793
|
+
return createConfigSchema(schema);
|
|
763
794
|
}
|
|
764
795
|
|
|
765
|
-
export const emptyPluginConfigSchema = { type: "object", properties: {}, additionalProperties: false };
|
|
796
|
+
export const emptyPluginConfigSchema = createConfigSchema({ type: "object", properties: {}, additionalProperties: false });
|
|
766
797
|
|
|
767
798
|
export function buildChannelConfigSchema(schema = {}) {
|
|
768
|
-
return schema;
|
|
799
|
+
return createConfigSchema(schema);
|
|
769
800
|
}
|
|
770
801
|
|
|
771
802
|
export const emptyChannelConfigSchema = emptyPluginConfigSchema;
|
|
@@ -951,14 +982,28 @@ export function createAuthRateLimiter() {
|
|
|
951
982
|
}
|
|
952
983
|
|
|
953
984
|
export function createProviderApiKeyAuthMethod(options = {}) {
|
|
954
|
-
return {
|
|
985
|
+
return {
|
|
986
|
+
id: options.id ?? "apiKey",
|
|
987
|
+
type: "apiKey",
|
|
988
|
+
...options,
|
|
989
|
+
async resolve(ctx = {}) {
|
|
990
|
+
return ctx.apiKey ?? ctx.key ?? ctx.token ?? null;
|
|
991
|
+
},
|
|
992
|
+
};
|
|
955
993
|
}
|
|
956
994
|
|
|
957
995
|
export function buildSingleProviderApiKeyCatalog(options = {}) {
|
|
996
|
+
const auth = options.auth ?? createProviderApiKeyAuthMethod(options.authOptions);
|
|
958
997
|
return {
|
|
998
|
+
auth,
|
|
959
999
|
order: "simple",
|
|
960
1000
|
async run(ctx) {
|
|
961
|
-
|
|
1001
|
+
const provider = (await options.buildProvider?.(ctx)) ?? options.provider ?? { id: options.id ?? "provider", auth };
|
|
1002
|
+
return {
|
|
1003
|
+
provider,
|
|
1004
|
+
providers: [provider],
|
|
1005
|
+
models: (await options.buildModels?.(ctx)) ?? options.models ?? [],
|
|
1006
|
+
};
|
|
962
1007
|
},
|
|
963
1008
|
};
|
|
964
1009
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { register } from "node:module";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { captureEntrypoint, runCapturedSyntheticProbes, writeArtifacts } from "./advanced.js";
|
|
8
|
+
import { createMockSdkPackage } from "./sdk-mock.js";
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await run(args);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(error.message);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function run(commandArgs) {
|
|
20
|
+
const entrypoint = readFlag(commandArgs, "--entrypoint") ?? commandArgs.find((arg) => !arg.startsWith("-"));
|
|
21
|
+
const outputPath = readFlag(commandArgs, "--output");
|
|
22
|
+
const pluginRoot = readFlag(commandArgs, "--plugin-root");
|
|
23
|
+
const includeLifecycle = commandArgs.includes("--include-lifecycle");
|
|
24
|
+
const includeChannelRuntime = commandArgs.includes("--include-channel-runtime");
|
|
25
|
+
const includeProviderCapabilities = commandArgs.includes("--include-provider-capabilities");
|
|
26
|
+
const mockSdk = readMockSdkFlag(commandArgs) ?? true;
|
|
27
|
+
|
|
28
|
+
if (!entrypoint) {
|
|
29
|
+
throw new Error("synthetic probes require --entrypoint <path>");
|
|
30
|
+
}
|
|
31
|
+
if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
|
|
32
|
+
throw new Error("synthetic probes import plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const capture = await captureForSyntheticProbes(entrypoint, {
|
|
36
|
+
mockSdk,
|
|
37
|
+
pluginRoot,
|
|
38
|
+
apiOptions: { retainHandlers: true },
|
|
39
|
+
});
|
|
40
|
+
const results = await runCapturedSyntheticProbes(capture, {
|
|
41
|
+
includeLifecycle,
|
|
42
|
+
includeChannelRuntime,
|
|
43
|
+
includeProviderCapabilities,
|
|
44
|
+
});
|
|
45
|
+
const json = `${JSON.stringify(results, null, 2)}\n`;
|
|
46
|
+
|
|
47
|
+
if (outputPath) {
|
|
48
|
+
await writeArtifacts([{ path: outputPath, content: json }]);
|
|
49
|
+
} else {
|
|
50
|
+
process.stdout.write(json);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function captureForSyntheticProbes(entrypoint, options) {
|
|
55
|
+
if (options.mockSdk !== true) {
|
|
56
|
+
return captureEntrypoint(entrypoint, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resolvedEntrypoint = path.resolve(process.cwd(), entrypoint);
|
|
60
|
+
const pluginRoot = path.resolve(process.cwd(), options.pluginRoot ?? path.dirname(resolvedEntrypoint));
|
|
61
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-mock-sdk-"));
|
|
62
|
+
try {
|
|
63
|
+
const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
|
|
64
|
+
register(pathToFileURL(loaderPath));
|
|
65
|
+
return captureEntrypoint(entrypoint, {
|
|
66
|
+
...options,
|
|
67
|
+
mockSdk: false,
|
|
68
|
+
pluginRoot,
|
|
69
|
+
});
|
|
70
|
+
} finally {
|
|
71
|
+
await rm(workspace, { force: true, recursive: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readFlag(commandArgs, name) {
|
|
76
|
+
const index = commandArgs.indexOf(name);
|
|
77
|
+
if (index === -1) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return commandArgs[index + 1] ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readMockSdkFlag(commandArgs) {
|
|
84
|
+
const sdk = readFlag(commandArgs, "--sdk");
|
|
85
|
+
if (sdk === "mock") {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (sdk === "real") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (sdk && !["mock", "real"].includes(sdk)) {
|
|
92
|
+
throw new Error("--sdk must be mock or real");
|
|
93
|
+
}
|
|
94
|
+
if (commandArgs.includes("--mock-sdk")) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (commandArgs.includes("--real-sdk")) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|