@openclaw/plugin-inspector 0.0.0 → 0.1.1
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 +21 -0
- package/LICENSE +21 -0
- package/README.md +151 -2
- package/examples/github-actions-plugin-inspector.yml +24 -0
- package/examples/plugin-inspector.config.json +15 -0
- package/package.json +56 -3
- package/src/advanced.js +186 -0
- package/src/api.js +85 -0
- package/src/artifacts.js +113 -0
- package/src/capture-api.js +115 -0
- package/src/ci-policy.js +259 -0
- package/src/ci-summary.js +223 -0
- package/src/cli.js +113 -0
- package/src/cold-import-readiness.js +271 -0
- package/src/compatibility-report.js +356 -0
- package/src/config.js +182 -0
- package/src/contract-capture.js +288 -0
- package/src/contract-coverage.js +167 -0
- package/src/contract-probes.js +156 -0
- package/src/execution-results.js +297 -0
- package/src/fixture-summary.js +780 -0
- package/src/import-loop-profile.js +169 -0
- package/src/index.js +10 -0
- package/src/inspector.js +405 -0
- package/src/issues.js +366 -0
- package/src/json-file.js +10 -0
- package/src/mock-sdk-capture-runner.js +69 -0
- package/src/openclaw-target.js +179 -0
- package/src/path-utils.js +28 -0
- package/src/platform-probes.js +238 -0
- package/src/process-profile.js +117 -0
- package/src/profile-diff.js +222 -0
- package/src/ref-diff.js +335 -0
- package/src/report.js +435 -0
- package/src/runtime-capture-report.js +134 -0
- package/src/runtime-profile.js +289 -0
- package/src/sdk-mock.js +61 -0
- package/src/stats.js +13 -0
- package/src/synthetic-probes.js +544 -0
- package/src/workspace-plan.js +496 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readJsonFile } from "./json-file.js";
|
|
5
|
+
|
|
6
|
+
const conversationAccessHooks = new Set(["agent_end", "llm_input", "llm_output"]);
|
|
7
|
+
const captureGapRegistrations = new Set([
|
|
8
|
+
"registerChannel",
|
|
9
|
+
"registerCommand",
|
|
10
|
+
"registerGatewayMethod",
|
|
11
|
+
"registerHttpRoute",
|
|
12
|
+
"registerInteractiveHandler",
|
|
13
|
+
"registerService",
|
|
14
|
+
]);
|
|
15
|
+
const channelRegistrations = new Set([
|
|
16
|
+
"createChatChannelPlugin",
|
|
17
|
+
"defineChannelPluginEntry",
|
|
18
|
+
"registerChannel",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export async function buildCompatibilityFixtureReport({ fixture, inspection, checkoutPath, sourceRoot, rootDir = process.cwd() }) {
|
|
22
|
+
const pluginManifests = await readPluginManifests({ checkoutPath, sourceRoot, rootDir });
|
|
23
|
+
const packageSummaries = await readPackageSummaries({ checkoutPath, sourceRoot, rootDir });
|
|
24
|
+
const packageJson = selectPrimaryPackage(packageSummaries);
|
|
25
|
+
const sdkImports = unique((inspection.sdkImports ?? []).map((sdkImport) => sdkImport.specifier));
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
id: fixture.id,
|
|
29
|
+
name: fixture.name,
|
|
30
|
+
priority: fixture.priority,
|
|
31
|
+
seams: fixture.seams,
|
|
32
|
+
why: fixture.why,
|
|
33
|
+
status: inspection.status,
|
|
34
|
+
hooks: inspection.hooks,
|
|
35
|
+
hookDetails: inspection.hookDetails ?? [],
|
|
36
|
+
registrations: inspection.registrations,
|
|
37
|
+
registrationDetails: inspection.registrationDetails ?? [],
|
|
38
|
+
manifestContracts: inspection.manifestContracts,
|
|
39
|
+
manifestFiles: inspection.manifestFiles ?? [],
|
|
40
|
+
sourceFiles: inspection.sourceFiles ?? [],
|
|
41
|
+
pluginManifests,
|
|
42
|
+
package: packageJson,
|
|
43
|
+
packages: packageSummaries,
|
|
44
|
+
sdkImports,
|
|
45
|
+
sdkImportDetails: inspection.sdkImports ?? [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function readPluginManifests({ checkoutPath, sourceRoot, rootDir = process.cwd() }) {
|
|
50
|
+
const candidates = unique(
|
|
51
|
+
[path.join(sourceRoot, "openclaw.plugin.json"), path.join(checkoutPath, "openclaw.plugin.json")].filter(
|
|
52
|
+
existsSync,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const manifests = [];
|
|
56
|
+
|
|
57
|
+
for (const manifestPath of candidates) {
|
|
58
|
+
const manifest = await readJsonFile(manifestPath);
|
|
59
|
+
manifests.push({
|
|
60
|
+
path: path.relative(rootDir, manifestPath),
|
|
61
|
+
id: manifest.id ?? null,
|
|
62
|
+
name: manifest.name ?? null,
|
|
63
|
+
version: manifest.version ?? null,
|
|
64
|
+
keys: Object.keys(manifest).sort(),
|
|
65
|
+
contracts: Object.keys(manifest.contracts ?? {}).sort(),
|
|
66
|
+
providerAuthEnvVars: manifest.providerAuthEnvVars ?? {},
|
|
67
|
+
channelEnvVars: manifest.channelEnvVars ?? {},
|
|
68
|
+
activation: manifest.activation ?? null,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return manifests;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readPackageSummaries({ checkoutPath, sourceRoot, rootDir = process.cwd(), maxDepth = 3 }) {
|
|
76
|
+
const candidates = unique([
|
|
77
|
+
path.join(sourceRoot, "package.json"),
|
|
78
|
+
path.join(checkoutPath, "package.json"),
|
|
79
|
+
...(await findPackageFiles(checkoutPath, { maxDepth })),
|
|
80
|
+
].filter(existsSync));
|
|
81
|
+
const summaries = [];
|
|
82
|
+
|
|
83
|
+
for (const packagePath of candidates) {
|
|
84
|
+
const packageJson = await readJsonFile(packagePath);
|
|
85
|
+
summaries.push(summarizePackage(packagePath, packageJson, { rootDir }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return summaries.sort((left, right) => packageRank(left) - packageRank(right) || left.path.localeCompare(right.path));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function summarizePackage(packagePath, packageJson, options = {}) {
|
|
92
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
93
|
+
const packageDir = path.dirname(packagePath);
|
|
94
|
+
const openclaw = packageJson.openclaw
|
|
95
|
+
? {
|
|
96
|
+
extensions: arrayValues(packageJson.openclaw.extensions),
|
|
97
|
+
runtimeExtensions: arrayValues(packageJson.openclaw.runtimeExtensions),
|
|
98
|
+
setupEntry: typeof packageJson.openclaw.setupEntry === "string" ? packageJson.openclaw.setupEntry : null,
|
|
99
|
+
compatPluginApi:
|
|
100
|
+
typeof packageJson.openclaw.compat?.pluginApi === "string" ? packageJson.openclaw.compat.pluginApi : null,
|
|
101
|
+
buildOpenClawVersion:
|
|
102
|
+
typeof packageJson.openclaw.build?.openclawVersion === "string"
|
|
103
|
+
? packageJson.openclaw.build.openclawVersion
|
|
104
|
+
: null,
|
|
105
|
+
buildPluginSdkVersion:
|
|
106
|
+
typeof packageJson.openclaw.build?.pluginSdkVersion === "string"
|
|
107
|
+
? packageJson.openclaw.build.pluginSdkVersion
|
|
108
|
+
: null,
|
|
109
|
+
}
|
|
110
|
+
: null;
|
|
111
|
+
|
|
112
|
+
if (openclaw) {
|
|
113
|
+
openclaw.entrypoints = collectOpenClawEntrypoints(packageDir, openclaw, { rootDir });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
path: path.relative(rootDir, packagePath),
|
|
118
|
+
name: packageJson.name ?? null,
|
|
119
|
+
version: packageJson.version ?? null,
|
|
120
|
+
type: packageJson.type ?? null,
|
|
121
|
+
main: typeof packageJson.main === "string" ? packageJson.main : null,
|
|
122
|
+
dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
|
|
123
|
+
peerDependencies: Object.keys(packageJson.peerDependencies ?? {}).sort(),
|
|
124
|
+
optionalDependencies: Object.keys(packageJson.optionalDependencies ?? {}).sort(),
|
|
125
|
+
openclaw,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function classifyPackageContracts({ fixture, inspection, fixtureReport }) {
|
|
130
|
+
const warnings = [];
|
|
131
|
+
const suggestions = [];
|
|
132
|
+
const logs = [];
|
|
133
|
+
const decisions = [];
|
|
134
|
+
const packageSummary = fixtureReport.package;
|
|
135
|
+
if (!packageSummary) {
|
|
136
|
+
warnings.push({
|
|
137
|
+
fixture: fixture.id,
|
|
138
|
+
code: "package-json-missing",
|
|
139
|
+
level: "warning",
|
|
140
|
+
message: "fixture has no package.json to describe install and plugin entrypoint metadata",
|
|
141
|
+
evidence: [fixture.path],
|
|
142
|
+
});
|
|
143
|
+
decisions.push({
|
|
144
|
+
fixture: fixture.id,
|
|
145
|
+
decision: "plugin-upstream-fix",
|
|
146
|
+
seam: "package-metadata",
|
|
147
|
+
action: "Ask the plugin to publish package metadata before treating install/cold-import checks as covered.",
|
|
148
|
+
evidence: fixture.path,
|
|
149
|
+
});
|
|
150
|
+
return { warnings, suggestions, logs, decisions };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
logs.push({
|
|
154
|
+
fixture: fixture.id,
|
|
155
|
+
code: "package-metadata",
|
|
156
|
+
level: "log",
|
|
157
|
+
message: "selected package metadata for plugin contract checks",
|
|
158
|
+
evidence: [
|
|
159
|
+
packageSummary.path,
|
|
160
|
+
packageSummary.name ?? "unnamed",
|
|
161
|
+
packageSummary.version ? `version:${packageSummary.version}` : "version:missing",
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const manifestVersions = fixtureReport.pluginManifests
|
|
166
|
+
.map((manifest) => manifest.version)
|
|
167
|
+
.filter((version) => typeof version === "string" && version.length > 0);
|
|
168
|
+
const mismatchedManifestVersions = manifestVersions.filter((version) => version !== packageSummary.version);
|
|
169
|
+
if (packageSummary.version && mismatchedManifestVersions.length > 0) {
|
|
170
|
+
warnings.push({
|
|
171
|
+
fixture: fixture.id,
|
|
172
|
+
code: "package-manifest-version-drift",
|
|
173
|
+
level: "warning",
|
|
174
|
+
message: "package.json and openclaw.plugin.json publish different versions",
|
|
175
|
+
evidence: [`package:${packageSummary.version}`, ...mismatchedManifestVersions.map((version) => `manifest:${version}`)],
|
|
176
|
+
});
|
|
177
|
+
decisions.push({
|
|
178
|
+
fixture: fixture.id,
|
|
179
|
+
decision: "plugin-upstream-fix",
|
|
180
|
+
seam: "package-metadata",
|
|
181
|
+
action: "Ask the plugin to keep package and manifest versions aligned before relying on release compatibility signals.",
|
|
182
|
+
evidence: `${packageSummary.version} != ${mismatchedManifestVersions.join(", ")}`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (packageSummary.openclaw && !packageSummary.openclaw.compatPluginApi) {
|
|
187
|
+
warnings.push({
|
|
188
|
+
fixture: fixture.id,
|
|
189
|
+
code: "package-plugin-api-compat-missing",
|
|
190
|
+
level: "warning",
|
|
191
|
+
message: "package openclaw metadata does not declare compat.pluginApi",
|
|
192
|
+
evidence: [packageSummary.path],
|
|
193
|
+
});
|
|
194
|
+
decisions.push({
|
|
195
|
+
fixture: fixture.id,
|
|
196
|
+
decision: "plugin-upstream-fix",
|
|
197
|
+
seam: "package-metadata",
|
|
198
|
+
action: "Ask the plugin to declare the plugin API range it was built against.",
|
|
199
|
+
evidence: packageSummary.path,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (packageSummary.openclaw && packageSummary.openclaw.entrypoints.length === 0) {
|
|
204
|
+
warnings.push({
|
|
205
|
+
fixture: fixture.id,
|
|
206
|
+
code: "package-openclaw-entry-missing",
|
|
207
|
+
level: "warning",
|
|
208
|
+
message: "package openclaw metadata does not declare plugin entrypoints",
|
|
209
|
+
evidence: [packageSummary.path],
|
|
210
|
+
});
|
|
211
|
+
decisions.push({
|
|
212
|
+
fixture: fixture.id,
|
|
213
|
+
decision: "plugin-upstream-fix",
|
|
214
|
+
seam: "package-entrypoint",
|
|
215
|
+
action: "Ask the plugin to declare openclaw.extensions or runtimeExtensions so cold import can target the correct entrypoint.",
|
|
216
|
+
evidence: packageSummary.path,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const missingEntrypoints = packageSummary.openclaw?.entrypoints.filter((entrypoint) => !entrypoint.exists) ?? [];
|
|
221
|
+
const buildEntrypoints = missingEntrypoints.filter((entrypoint) => entrypoint.requiresBuild);
|
|
222
|
+
const plainMissingEntrypoints = missingEntrypoints.filter((entrypoint) => !entrypoint.requiresBuild);
|
|
223
|
+
|
|
224
|
+
if (buildEntrypoints.length > 0) {
|
|
225
|
+
suggestions.push({
|
|
226
|
+
fixture: fixture.id,
|
|
227
|
+
code: "package-build-artifact-entrypoint",
|
|
228
|
+
level: "suggestion",
|
|
229
|
+
message: "package OpenClaw entrypoint points at build output that is not present in the source fixture checkout",
|
|
230
|
+
evidence: buildEntrypoints.map((entrypoint) => `${entrypoint.kind}:${entrypoint.specifier} -> ${entrypoint.relativePath}`),
|
|
231
|
+
});
|
|
232
|
+
decisions.push({
|
|
233
|
+
fixture: fixture.id,
|
|
234
|
+
decision: "inspector-follow-up",
|
|
235
|
+
seam: "cold-import",
|
|
236
|
+
action: "Run the plugin build or resolve source entrypoint aliases before cold-importing this fixture.",
|
|
237
|
+
evidence: buildEntrypoints.map((entrypoint) => entrypoint.specifier).join(", "),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (plainMissingEntrypoints.length > 0) {
|
|
242
|
+
warnings.push({
|
|
243
|
+
fixture: fixture.id,
|
|
244
|
+
code: "package-entrypoint-missing",
|
|
245
|
+
level: "warning",
|
|
246
|
+
message: "package OpenClaw entrypoint does not exist in the fixture checkout",
|
|
247
|
+
evidence: plainMissingEntrypoints.map((entrypoint) => `${entrypoint.kind}:${entrypoint.specifier} -> ${entrypoint.relativePath}`),
|
|
248
|
+
});
|
|
249
|
+
decisions.push({
|
|
250
|
+
fixture: fixture.id,
|
|
251
|
+
decision: "plugin-upstream-fix",
|
|
252
|
+
seam: "package-entrypoint",
|
|
253
|
+
action: "Ask the plugin to publish a valid OpenClaw entrypoint or update package metadata.",
|
|
254
|
+
evidence: plainMissingEntrypoints.map((entrypoint) => entrypoint.specifier).join(", "),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const sourceEntrypoints =
|
|
259
|
+
packageSummary.openclaw?.entrypoints.filter((entrypoint) => entrypoint.exists && entrypoint.relativePath.endsWith(".ts")) ?? [];
|
|
260
|
+
if (sourceEntrypoints.length > 0) {
|
|
261
|
+
suggestions.push({
|
|
262
|
+
fixture: fixture.id,
|
|
263
|
+
code: "package-typescript-source-entrypoint",
|
|
264
|
+
level: "suggestion",
|
|
265
|
+
message: "package OpenClaw entrypoint resolves to TypeScript source in this fixture checkout",
|
|
266
|
+
evidence: sourceEntrypoints.map((entrypoint) => `${entrypoint.kind}:${entrypoint.relativePath}`),
|
|
267
|
+
});
|
|
268
|
+
decisions.push({
|
|
269
|
+
fixture: fixture.id,
|
|
270
|
+
decision: "inspector-follow-up",
|
|
271
|
+
seam: "cold-import",
|
|
272
|
+
action: "Compile TypeScript source or run a loader before cold-importing this fixture entrypoint.",
|
|
273
|
+
evidence: sourceEntrypoints.map((entrypoint) => entrypoint.relativePath).join(", "),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const runtimeDependencies = unique([
|
|
278
|
+
...packageSummary.dependencies,
|
|
279
|
+
...packageSummary.peerDependencies,
|
|
280
|
+
...packageSummary.optionalDependencies,
|
|
281
|
+
]);
|
|
282
|
+
if (packageSummary.openclaw?.entrypoints.length > 0 && runtimeDependencies.length > 0) {
|
|
283
|
+
suggestions.push({
|
|
284
|
+
fixture: fixture.id,
|
|
285
|
+
code: "package-dependency-install-required",
|
|
286
|
+
level: "suggestion",
|
|
287
|
+
message: "package declares runtime dependencies that must be installed before cold import",
|
|
288
|
+
evidence: runtimeDependencies.map((dependency) => `${dependency} @ ${packageSummary.path}`),
|
|
289
|
+
});
|
|
290
|
+
decisions.push({
|
|
291
|
+
fixture: fixture.id,
|
|
292
|
+
decision: "inspector-follow-up",
|
|
293
|
+
seam: "cold-import",
|
|
294
|
+
action: "Install runtime dependencies in an isolated workspace before executing this fixture entrypoint.",
|
|
295
|
+
evidence: runtimeDependencies.join(", "),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (inspection.registrations.length > 0 && !packageSummary.openclaw) {
|
|
300
|
+
warnings.push({
|
|
301
|
+
fixture: fixture.id,
|
|
302
|
+
code: "package-openclaw-metadata-missing",
|
|
303
|
+
level: "warning",
|
|
304
|
+
message: "fixture registers plugin APIs but the selected package.json has no openclaw metadata",
|
|
305
|
+
evidence: [packageSummary.path, ...inspection.registrations],
|
|
306
|
+
});
|
|
307
|
+
decisions.push({
|
|
308
|
+
fixture: fixture.id,
|
|
309
|
+
decision: "plugin-upstream-fix",
|
|
310
|
+
seam: "package-metadata",
|
|
311
|
+
action: "Ask the plugin to declare OpenClaw install and entrypoint metadata in package.json.",
|
|
312
|
+
evidence: packageSummary.path,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { warnings, suggestions, logs, decisions };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function classifyTargetOpenClawCoverage({ fixture, inspection, fixtureReport, targetOpenClaw }) {
|
|
320
|
+
const warnings = [];
|
|
321
|
+
const logs = [];
|
|
322
|
+
const decisions = [];
|
|
323
|
+
|
|
324
|
+
classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs });
|
|
325
|
+
classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs });
|
|
326
|
+
classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions });
|
|
327
|
+
classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions });
|
|
328
|
+
|
|
329
|
+
return { warnings, logs, decisions };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function classifyCompatibilityFixture({ fixture, inspection, fixtureReport, targetOpenClaw }) {
|
|
333
|
+
const warnings = [];
|
|
334
|
+
const suggestions = [];
|
|
335
|
+
const logs = [];
|
|
336
|
+
const decisions = [];
|
|
337
|
+
|
|
338
|
+
if (inspection.status !== "ok") {
|
|
339
|
+
return { warnings, suggestions, logs, decisions };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const targetCoverage = classifyTargetOpenClawCoverage({ fixture, inspection, fixtureReport, targetOpenClaw });
|
|
343
|
+
warnings.push(...targetCoverage.warnings);
|
|
344
|
+
logs.push(...targetCoverage.logs);
|
|
345
|
+
decisions.push(...targetCoverage.decisions);
|
|
346
|
+
|
|
347
|
+
const packageContracts = classifyPackageContracts({ fixture, inspection, fixtureReport });
|
|
348
|
+
warnings.push(...packageContracts.warnings);
|
|
349
|
+
suggestions.push(...packageContracts.suggestions);
|
|
350
|
+
logs.push(...packageContracts.logs);
|
|
351
|
+
decisions.push(...packageContracts.decisions);
|
|
352
|
+
|
|
353
|
+
for (const pluginManifest of fixtureReport.pluginManifests) {
|
|
354
|
+
const providerAuthKeys = Object.keys(pluginManifest.providerAuthEnvVars ?? {});
|
|
355
|
+
if (providerAuthKeys.length > 0) {
|
|
356
|
+
warnings.push({
|
|
357
|
+
fixture: fixture.id,
|
|
358
|
+
code: "provider-auth-env-vars",
|
|
359
|
+
level: "warning",
|
|
360
|
+
message: "manifest uses providerAuthEnvVars legacy compatibility metadata",
|
|
361
|
+
evidence: providerAuthKeys,
|
|
362
|
+
compatRecord: "provider-auth-env-vars",
|
|
363
|
+
});
|
|
364
|
+
decisions.push({
|
|
365
|
+
fixture: fixture.id,
|
|
366
|
+
decision: "core-compat-adapter",
|
|
367
|
+
seam: "env-auth",
|
|
368
|
+
action: "Keep providerAuthEnvVars compatibility active while the inspector recommends manifest-schema migration upstream.",
|
|
369
|
+
evidence: providerAuthKeys.join(", "),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const channelEnvKeys = Object.keys(pluginManifest.channelEnvVars ?? {});
|
|
374
|
+
if (channelEnvKeys.length > 0) {
|
|
375
|
+
warnings.push({
|
|
376
|
+
fixture: fixture.id,
|
|
377
|
+
code: "channel-env-vars",
|
|
378
|
+
level: "warning",
|
|
379
|
+
message: "manifest uses channelEnvVars legacy compatibility metadata",
|
|
380
|
+
evidence: channelEnvKeys,
|
|
381
|
+
compatRecord: "channel-env-vars",
|
|
382
|
+
});
|
|
383
|
+
decisions.push({
|
|
384
|
+
fixture: fixture.id,
|
|
385
|
+
decision: "core-compat-adapter",
|
|
386
|
+
seam: "channel-env",
|
|
387
|
+
action: "Keep channelEnvVars compatibility active until channel setup metadata has a stable replacement path.",
|
|
388
|
+
evidence: channelEnvKeys.join(", "),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const conversationHooks = inspection.hooks.filter((hook) => conversationAccessHooks.has(hook));
|
|
394
|
+
const conversationHookDetails = inspection.hookDetails.filter((hook) => conversationAccessHooks.has(hook.name));
|
|
395
|
+
if (conversationHooks.length > 0) {
|
|
396
|
+
warnings.push({
|
|
397
|
+
fixture: fixture.id,
|
|
398
|
+
code: "conversation-access-hook",
|
|
399
|
+
level: "warning",
|
|
400
|
+
message: "fixture observes raw model or conversation content and needs privacy-boundary contract probes",
|
|
401
|
+
evidence: detailEvidence(conversationHookDetails),
|
|
402
|
+
});
|
|
403
|
+
decisions.push({
|
|
404
|
+
fixture: fixture.id,
|
|
405
|
+
decision: "inspector-follow-up",
|
|
406
|
+
seam: "conversation-access",
|
|
407
|
+
action: "Add synthetic llm_input/llm_output/agent_end probes before tightening hook payloads or redaction behavior.",
|
|
408
|
+
evidence: conversationHooks.join(", "),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const rootSdkImports = fixtureReport.sdkImports.filter((specifier) => specifier === "openclaw/plugin-sdk");
|
|
413
|
+
const rootSdkImportDetails = fixtureReport.sdkImportDetails.filter(
|
|
414
|
+
(sdkImport) => sdkImport.specifier === "openclaw/plugin-sdk",
|
|
415
|
+
);
|
|
416
|
+
if (rootSdkImports.length > 0) {
|
|
417
|
+
warnings.push({
|
|
418
|
+
fixture: fixture.id,
|
|
419
|
+
code: "legacy-root-sdk-import",
|
|
420
|
+
level: "warning",
|
|
421
|
+
message: "fixture imports the root plugin SDK barrel",
|
|
422
|
+
evidence: detailEvidence(rootSdkImportDetails, "specifier"),
|
|
423
|
+
compatRecord: "legacy-root-sdk-import",
|
|
424
|
+
});
|
|
425
|
+
decisions.push({
|
|
426
|
+
fixture: fixture.id,
|
|
427
|
+
decision: "core-compat-adapter",
|
|
428
|
+
seam: "sdk-import",
|
|
429
|
+
action: "Keep the root SDK barrel stable or expose a machine-readable migration map before removing aliases.",
|
|
430
|
+
evidence: unique(rootSdkImports).join(", "),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const legacyBeforeAgentStartDetails = inspection.hookDetails.filter((hook) => hook.name === "before_agent_start");
|
|
435
|
+
if (legacyBeforeAgentStartDetails.length > 0) {
|
|
436
|
+
warnings.push({
|
|
437
|
+
fixture: fixture.id,
|
|
438
|
+
code: "legacy-before-agent-start",
|
|
439
|
+
level: "warning",
|
|
440
|
+
message: "fixture uses deprecated before_agent_start hook compatibility",
|
|
441
|
+
evidence: detailEvidence(legacyBeforeAgentStartDetails),
|
|
442
|
+
compatRecord: "legacy-before-agent-start",
|
|
443
|
+
});
|
|
444
|
+
decisions.push({
|
|
445
|
+
fixture: fixture.id,
|
|
446
|
+
decision: "core-compat-adapter",
|
|
447
|
+
seam: "hook-compat",
|
|
448
|
+
action: "Keep before_agent_start wired while plugin authors migrate to before_model_resolve and before_prompt_build.",
|
|
449
|
+
evidence: detailEvidence(legacyBeforeAgentStartDetails).join(", "),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const captureGapRegistrationDetails = registrationCaptureGapDetails(inspection, targetOpenClaw);
|
|
454
|
+
const captureGapRegistrationNames = unique(captureGapRegistrationDetails.map((registration) => registration.name));
|
|
455
|
+
if (captureGapRegistrationNames.length > 0) {
|
|
456
|
+
suggestions.push({
|
|
457
|
+
fixture: fixture.id,
|
|
458
|
+
code: "registration-capture-gap",
|
|
459
|
+
level: "suggestion",
|
|
460
|
+
message: "future inspector capture API should record lifecycle, route, gateway, command, and interactive registrations",
|
|
461
|
+
evidence: detailEvidence(captureGapRegistrationDetails),
|
|
462
|
+
});
|
|
463
|
+
decisions.push({
|
|
464
|
+
fixture: fixture.id,
|
|
465
|
+
decision: "inspector-follow-up",
|
|
466
|
+
seam: "registration-capture",
|
|
467
|
+
action: "Expose or mirror a full public API capture shim before treating these runtime-only seams as covered.",
|
|
468
|
+
evidence: captureGapRegistrationNames.join(", "),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (inspection.hooks.includes("before_tool_call")) {
|
|
473
|
+
const hookDetails = inspection.hookDetails.filter((hook) => hook.name === "before_tool_call");
|
|
474
|
+
suggestions.push({
|
|
475
|
+
fixture: fixture.id,
|
|
476
|
+
code: "before-tool-call-probe",
|
|
477
|
+
level: "suggestion",
|
|
478
|
+
message: "add contract probes for before_tool_call terminal, block, and approval semantics",
|
|
479
|
+
evidence: detailEvidence(hookDetails),
|
|
480
|
+
});
|
|
481
|
+
decisions.push({
|
|
482
|
+
fixture: fixture.id,
|
|
483
|
+
decision: "inspector-follow-up",
|
|
484
|
+
seam: "tool-policy",
|
|
485
|
+
action: "Probe before_tool_call return shapes before changing tool-call approval or block behavior.",
|
|
486
|
+
evidence: "before_tool_call",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const observedChannelRegistrations = inspection.registrations.filter((registration) =>
|
|
491
|
+
channelRegistrations.has(registration),
|
|
492
|
+
);
|
|
493
|
+
const channelRegistrationDetails = inspection.registrationDetails.filter((registration) =>
|
|
494
|
+
channelRegistrations.has(registration.name),
|
|
495
|
+
);
|
|
496
|
+
if (observedChannelRegistrations.length > 0) {
|
|
497
|
+
suggestions.push({
|
|
498
|
+
fixture: fixture.id,
|
|
499
|
+
code: "channel-contract-probe",
|
|
500
|
+
level: "suggestion",
|
|
501
|
+
message: "add channel envelope, config-schema, and runtime metadata probes",
|
|
502
|
+
evidence: detailEvidence(channelRegistrationDetails),
|
|
503
|
+
});
|
|
504
|
+
decisions.push({
|
|
505
|
+
fixture: fixture.id,
|
|
506
|
+
decision: "inspector-follow-up",
|
|
507
|
+
seam: "channel-runtime",
|
|
508
|
+
action: "Probe channel setup and message envelope contracts before changing channel runtime payloads.",
|
|
509
|
+
evidence: observedChannelRegistrations.join(", "),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const runtimeToolOnly = inspection.registrations.includes("registerTool") && !inspection.manifestContracts.includes("tools");
|
|
514
|
+
if (runtimeToolOnly) {
|
|
515
|
+
const toolRegistrationDetails = inspection.registrationDetails.filter(
|
|
516
|
+
(registration) => registration.name === "registerTool",
|
|
517
|
+
);
|
|
518
|
+
suggestions.push({
|
|
519
|
+
fixture: fixture.id,
|
|
520
|
+
code: "runtime-tool-capture",
|
|
521
|
+
level: "suggestion",
|
|
522
|
+
message: "tool shape is only visible after runtime registration capture",
|
|
523
|
+
evidence: detailEvidence(toolRegistrationDetails),
|
|
524
|
+
});
|
|
525
|
+
decisions.push({
|
|
526
|
+
fixture: fixture.id,
|
|
527
|
+
decision: "inspector-follow-up",
|
|
528
|
+
seam: "tool-schema",
|
|
529
|
+
action: "Capture registered tool schemas from plugin register() before judging tool compatibility.",
|
|
530
|
+
evidence: "registerTool without manifest contracts.tools",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (inspection.manifestContracts.length > 0) {
|
|
535
|
+
logs.push({
|
|
536
|
+
fixture: fixture.id,
|
|
537
|
+
code: "declarative-contracts",
|
|
538
|
+
level: "log",
|
|
539
|
+
message: "fixture declares manifest contracts that can be checked without executing plugin code",
|
|
540
|
+
evidence: inspection.manifestContracts,
|
|
541
|
+
});
|
|
542
|
+
decisions.push({
|
|
543
|
+
fixture: fixture.id,
|
|
544
|
+
decision: "no-action",
|
|
545
|
+
seam: "manifest-contract",
|
|
546
|
+
action: "Keep checking this declarative contract in default offline CI.",
|
|
547
|
+
evidence: inspection.manifestContracts.join(", "),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { warnings, suggestions, logs, decisions };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function registrationCaptureGapDetails(inspection, targetOpenClaw) {
|
|
555
|
+
const apiRegistrationDetails = inspection.registrationDetails.filter((registration) =>
|
|
556
|
+
registration.name.startsWith("register"),
|
|
557
|
+
);
|
|
558
|
+
if (targetOpenClaw.status === "ok" && targetOpenClaw.capturedRegistrars.length > 0) {
|
|
559
|
+
const captured = new Set(targetOpenClaw.capturedRegistrars);
|
|
560
|
+
return apiRegistrationDetails.filter((registration) => !captured.has(registration.name));
|
|
561
|
+
}
|
|
562
|
+
return apiRegistrationDetails.filter((registration) => captureGapRegistrations.has(registration.name));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function classifyHookNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs }) {
|
|
566
|
+
if (targetOpenClaw.status !== "ok" || targetOpenClaw.hookNames.length === 0) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const knownHookNames = new Set(targetOpenClaw.hookNames);
|
|
571
|
+
const unknownHooks = inspection.hookDetails.filter((hook) => !knownHookNames.has(hook.name));
|
|
572
|
+
if (unknownHooks.length === 0) {
|
|
573
|
+
logs.push({
|
|
574
|
+
fixture: fixture.id,
|
|
575
|
+
code: "hook-names-present",
|
|
576
|
+
level: "log",
|
|
577
|
+
message: "all observed hooks exist in the target OpenClaw hook registry",
|
|
578
|
+
evidence: inspection.hooks,
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
warnings.push({
|
|
584
|
+
fixture: fixture.id,
|
|
585
|
+
code: "unknown-hook-name",
|
|
586
|
+
level: "warning",
|
|
587
|
+
message: "fixture registers hooks that are not present in the target OpenClaw hook registry",
|
|
588
|
+
evidence: detailEvidence(unknownHooks),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function classifyRegistrationNameCoverage({ fixture, inspection, targetOpenClaw, warnings, logs }) {
|
|
593
|
+
if (targetOpenClaw.status !== "ok" || targetOpenClaw.apiRegistrars.length === 0) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const knownRegistrars = new Set(targetOpenClaw.apiRegistrars);
|
|
598
|
+
const apiRegistrations = inspection.registrationDetails.filter((registration) =>
|
|
599
|
+
registration.name.startsWith("register"),
|
|
600
|
+
);
|
|
601
|
+
const unknownRegistrations = apiRegistrations.filter((registration) => !knownRegistrars.has(registration.name));
|
|
602
|
+
if (unknownRegistrations.length === 0) {
|
|
603
|
+
logs.push({
|
|
604
|
+
fixture: fixture.id,
|
|
605
|
+
code: "api-registrars-present",
|
|
606
|
+
level: "log",
|
|
607
|
+
message: "all observed api.register* calls exist in the target OpenClaw plugin API builder",
|
|
608
|
+
evidence: unique(apiRegistrations.map((registration) => registration.name)).sort(),
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
warnings.push({
|
|
614
|
+
fixture: fixture.id,
|
|
615
|
+
code: "unknown-registration-name",
|
|
616
|
+
level: "warning",
|
|
617
|
+
message: "fixture calls api.register* methods that are not present in the target OpenClaw plugin API builder",
|
|
618
|
+
evidence: detailEvidence(unknownRegistrations),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function classifySdkImportCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
|
|
623
|
+
if (targetOpenClaw.status !== "ok" || targetOpenClaw.sdkExports.length === 0 || fixtureReport.sdkImports.length === 0) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const sdkExports = new Set(targetOpenClaw.sdkExports);
|
|
628
|
+
const unknownImports = fixtureReport.sdkImportDetails.filter((sdkImport) => !sdkExports.has(sdkImport.specifier));
|
|
629
|
+
if (unknownImports.length === 0) {
|
|
630
|
+
logs.push({
|
|
631
|
+
fixture: fixture.id,
|
|
632
|
+
code: "sdk-exports-present",
|
|
633
|
+
level: "log",
|
|
634
|
+
message: "all observed plugin SDK imports exist in target OpenClaw package exports",
|
|
635
|
+
evidence: fixtureReport.sdkImports,
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
warnings.push({
|
|
641
|
+
fixture: fixture.id,
|
|
642
|
+
code: "sdk-export-missing",
|
|
643
|
+
level: "warning",
|
|
644
|
+
message: "fixture imports plugin SDK aliases that are not exported by the target OpenClaw package",
|
|
645
|
+
evidence: detailEvidence(unknownImports, "specifier"),
|
|
646
|
+
compatRecord: "plugin-sdk-export-aliases",
|
|
647
|
+
});
|
|
648
|
+
decisions.push({
|
|
649
|
+
fixture: fixture.id,
|
|
650
|
+
decision: "core-compat-adapter",
|
|
651
|
+
seam: "sdk-alias",
|
|
652
|
+
action: "Restore the package export alias or publish a versioned migration map before cold-importing old plugins.",
|
|
653
|
+
evidence: unique(unknownImports.map((sdkImport) => sdkImport.specifier)).join(", "),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function classifyManifestFieldCoverage({ fixture, fixtureReport, targetOpenClaw, warnings, logs, decisions }) {
|
|
658
|
+
if (targetOpenClaw.status !== "ok" || targetOpenClaw.manifestFields.length === 0) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const manifestFields = new Set(targetOpenClaw.manifestFields);
|
|
663
|
+
const contractFields = new Set(targetOpenClaw.manifestContractFields);
|
|
664
|
+
for (const pluginManifest of fixtureReport.pluginManifests) {
|
|
665
|
+
const unknownFields = pluginManifest.keys.filter((key) => !manifestFields.has(key));
|
|
666
|
+
if (unknownFields.length > 0) {
|
|
667
|
+
warnings.push({
|
|
668
|
+
fixture: fixture.id,
|
|
669
|
+
code: "manifest-unknown-fields",
|
|
670
|
+
level: "warning",
|
|
671
|
+
message: "manifest uses top-level fields that are not present in the target OpenClaw PluginManifest type",
|
|
672
|
+
evidence: unknownFields.map((field) => `${field} @ ${pluginManifest.path}`),
|
|
673
|
+
});
|
|
674
|
+
decisions.push({
|
|
675
|
+
fixture: fixture.id,
|
|
676
|
+
decision: "plugin-upstream-fix",
|
|
677
|
+
seam: "manifest-schema",
|
|
678
|
+
action: "Move unknown manifest metadata into supported package openclaw metadata or add a versioned OpenClaw manifest field.",
|
|
679
|
+
evidence: unknownFields.join(", "),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const unknownContractFields = pluginManifest.contracts.filter((field) => !contractFields.has(field));
|
|
684
|
+
if (unknownContractFields.length > 0) {
|
|
685
|
+
warnings.push({
|
|
686
|
+
fixture: fixture.id,
|
|
687
|
+
code: "manifest-unknown-contracts",
|
|
688
|
+
level: "warning",
|
|
689
|
+
message: "manifest declares contract keys that are not present in the target OpenClaw PluginManifestContracts type",
|
|
690
|
+
evidence: unknownContractFields.map((field) => `${field} @ ${pluginManifest.path}`),
|
|
691
|
+
});
|
|
692
|
+
decisions.push({
|
|
693
|
+
fixture: fixture.id,
|
|
694
|
+
decision: "plugin-upstream-fix",
|
|
695
|
+
seam: "manifest-contract",
|
|
696
|
+
action: "Use a supported manifest contract key or add a versioned OpenClaw contract field.",
|
|
697
|
+
evidence: unknownContractFields.join(", "),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (fixtureReport.pluginManifests.length > 0) {
|
|
703
|
+
logs.push({
|
|
704
|
+
fixture: fixture.id,
|
|
705
|
+
code: "manifest-fields-checked",
|
|
706
|
+
level: "log",
|
|
707
|
+
message: "plugin manifest fields were compared with target OpenClaw manifest types",
|
|
708
|
+
evidence: fixtureReport.pluginManifests.map((manifest) => manifest.path),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function collectOpenClawEntrypoints(packageDir, openclaw, options) {
|
|
714
|
+
const entrypoints = [
|
|
715
|
+
...openclaw.extensions.map((specifier) => ({ kind: "extension", specifier })),
|
|
716
|
+
...openclaw.runtimeExtensions.map((specifier) => ({ kind: "runtimeExtension", specifier })),
|
|
717
|
+
...(openclaw.setupEntry ? [{ kind: "setupEntry", specifier: openclaw.setupEntry }] : []),
|
|
718
|
+
];
|
|
719
|
+
|
|
720
|
+
return entrypoints.map((entrypoint) => {
|
|
721
|
+
const resolvedPath = path.resolve(packageDir, entrypoint.specifier);
|
|
722
|
+
const relativePath = path.relative(options.rootDir, resolvedPath);
|
|
723
|
+
return {
|
|
724
|
+
...entrypoint,
|
|
725
|
+
relativePath,
|
|
726
|
+
exists: existsSync(resolvedPath),
|
|
727
|
+
requiresBuild: /(^|\/)dist\//.test(entrypoint.specifier) || /(^|\/)build\//.test(entrypoint.specifier),
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function findPackageFiles(root, options, depth = 0) {
|
|
733
|
+
if (!existsSync(root) || depth > options.maxDepth) {
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const files = [];
|
|
738
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
739
|
+
const entryPath = path.join(root, entry.name);
|
|
740
|
+
if (entry.isFile() && entry.name === "package.json") {
|
|
741
|
+
files.push(entryPath);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (!entry.isDirectory() || shouldSkipPackageDir(entry.name)) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
files.push(...(await findPackageFiles(entryPath, options, depth + 1)));
|
|
748
|
+
}
|
|
749
|
+
return files;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function shouldSkipPackageDir(name) {
|
|
753
|
+
return name === ".git" || name === "node_modules" || name === "dist" || name === "build" || name === "coverage";
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function selectPrimaryPackage(packages) {
|
|
757
|
+
return packages[0] ?? null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function packageRank(packageSummary) {
|
|
761
|
+
if (packageSummary.openclaw?.entrypoints.length > 0) {
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
if (packageSummary.openclaw) {
|
|
765
|
+
return 1;
|
|
766
|
+
}
|
|
767
|
+
return 2;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function arrayValues(value) {
|
|
771
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function detailEvidence(details, key = "name") {
|
|
775
|
+
return unique(details.map((detail) => `${detail[key]} @ ${detail.ref}`));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function unique(values) {
|
|
779
|
+
return [...new Set(values)];
|
|
780
|
+
}
|