@openclaw/plugin-inspector 0.3.4 → 0.3.6

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.
@@ -19,9 +19,12 @@ const channelRegistrations = new Set([
19
19
  "registerChannel",
20
20
  ]);
21
21
  const hostLinkedRuntimeDependencies = new Set(["openclaw"]);
22
+ const unsupportedSecurityManifestName = "openclaw.security.json";
23
+ const unavailableSecurityManifestSchema = "https://openclaw.ai/schemas/plugin-security.json";
22
24
 
23
25
  export async function buildCompatibilityFixtureReport({ fixture, inspection, checkoutPath, sourceRoot, rootDir = process.cwd() }) {
24
26
  const pluginManifests = await readPluginManifests({ checkoutPath, sourceRoot, rootDir });
27
+ const securityManifests = await readSecurityManifests({ checkoutPath, sourceRoot, rootDir });
25
28
  const packageSummaries = await readPackageSummaries({ checkoutPath, sourceRoot, rootDir });
26
29
  const packageJson = selectPrimaryPackage(packageSummaries);
27
30
  const sdkImports = unique((inspection.sdkImports ?? []).map((sdkImport) => sdkImport.specifier));
@@ -41,6 +44,7 @@ export async function buildCompatibilityFixtureReport({ fixture, inspection, che
41
44
  manifestFiles: inspection.manifestFiles ?? [],
42
45
  sourceFiles: inspection.sourceFiles ?? [],
43
46
  pluginManifests,
47
+ securityManifests,
44
48
  package: packageJson,
45
49
  packages: packageSummaries,
46
50
  sdkImports,
@@ -74,6 +78,46 @@ export async function readPluginManifests({ checkoutPath, sourceRoot, rootDir =
74
78
  return manifests;
75
79
  }
76
80
 
81
+ export async function readSecurityManifests({ checkoutPath, sourceRoot, rootDir = process.cwd() }) {
82
+ const candidates = unique(
83
+ [
84
+ path.join(sourceRoot, unsupportedSecurityManifestName),
85
+ path.join(checkoutPath, unsupportedSecurityManifestName),
86
+ ].filter(existsSync),
87
+ );
88
+ const manifests = [];
89
+
90
+ for (const manifestPath of candidates) {
91
+ const relativePath = path.relative(rootDir, manifestPath);
92
+ try {
93
+ const manifest = await readJsonFile(manifestPath);
94
+ manifests.push({
95
+ path: relativePath,
96
+ schema: typeof manifest.$schema === "string" ? manifest.$schema : null,
97
+ version: typeof manifest.version === "string" ? manifest.version : null,
98
+ plugin: typeof manifest.plugin === "string" ? manifest.plugin : null,
99
+ expectedBehaviorCount: Array.isArray(manifest.expectedBehaviors)
100
+ ? manifest.expectedBehaviors.length
101
+ : 0,
102
+ securityNoteCount: Array.isArray(manifest.securityNotes) ? manifest.securityNotes.length : 0,
103
+ validJson: true,
104
+ });
105
+ } catch {
106
+ manifests.push({
107
+ path: relativePath,
108
+ schema: null,
109
+ version: null,
110
+ plugin: null,
111
+ expectedBehaviorCount: 0,
112
+ securityNoteCount: 0,
113
+ validJson: false,
114
+ });
115
+ }
116
+ }
117
+
118
+ return manifests;
119
+ }
120
+
77
121
  export async function readPackageSummaries({ checkoutPath, sourceRoot, rootDir = process.cwd(), maxDepth = 3 }) {
78
122
  const candidates = unique([
79
123
  path.join(sourceRoot, "package.json"),
@@ -108,6 +152,9 @@ export function summarizePackage(packagePath, packageJson, options = {}) {
108
152
  typeof packageJson.openclaw.build?.pluginSdkVersion === "string"
109
153
  ? packageJson.openclaw.build.pluginSdkVersion
110
154
  : null,
155
+ install: summarizeOpenClawInstall(packageJson.openclaw.install),
156
+ release: summarizeOpenClawRelease(packageJson.openclaw.release),
157
+ unsupportedMetadata: unsupportedOpenClawPackageMetadata(packageJson.openclaw),
111
158
  }
112
159
  : null;
113
160
 
@@ -121,6 +168,7 @@ export function summarizePackage(packagePath, packageJson, options = {}) {
121
168
  version: packageJson.version ?? null,
122
169
  type: packageJson.type ?? null,
123
170
  main: typeof packageJson.main === "string" ? packageJson.main : null,
171
+ npmPack: summarizeNpmPack(packageJson, openclaw),
124
172
  dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
125
173
  peerDependencies: Object.keys(packageJson.peerDependencies ?? {}).sort(),
126
174
  optionalDependencies: Object.keys(packageJson.optionalDependencies ?? {}).sort(),
@@ -202,6 +250,79 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
202
250
  });
203
251
  }
204
252
 
253
+ if ((packageSummary.openclaw?.unsupportedMetadata ?? []).length > 0) {
254
+ warnings.push({
255
+ fixture: fixture.id,
256
+ code: "package-openclaw-unsupported-metadata",
257
+ level: "warning",
258
+ message: "package declares unsupported OpenClaw metadata",
259
+ evidence: packageSummary.openclaw.unsupportedMetadata,
260
+ });
261
+ decisions.push({
262
+ fixture: fixture.id,
263
+ decision: "plugin-upstream-fix",
264
+ seam: "package-metadata",
265
+ action: "Remove unsupported OpenClaw metadata; native plugins use openclaw.plugin.json plus supported package openclaw fields.",
266
+ evidence: packageSummary.openclaw.unsupportedMetadata.join(", "),
267
+ });
268
+ }
269
+
270
+ const installMetadataIssues = packageInstallMetadataIssues(packageSummary);
271
+ if (installMetadataIssues.length > 0) {
272
+ warnings.push({
273
+ fixture: fixture.id,
274
+ code: "package-install-metadata-incomplete",
275
+ level: "warning",
276
+ message: "package OpenClaw install metadata does not match advertised release targets",
277
+ evidence: installMetadataIssues,
278
+ });
279
+ decisions.push({
280
+ fixture: fixture.id,
281
+ decision: "plugin-upstream-fix",
282
+ seam: "package-metadata",
283
+ action: "Ask the plugin to align openclaw.install metadata with openclaw.release publishing targets.",
284
+ evidence: installMetadataIssues.join(", "),
285
+ });
286
+ }
287
+
288
+ if (packageMinHostVersionDrift(packageSummary)) {
289
+ warnings.push({
290
+ fixture: fixture.id,
291
+ code: "package-min-host-version-drift",
292
+ level: "warning",
293
+ message: "package openclaw.install.minHostVersion is not a semver floor for the target OpenClaw build version",
294
+ evidence: [
295
+ `minHostVersion:${packageSummary.openclaw.install.minHostVersion}`,
296
+ `buildOpenClawVersion:${packageSummary.openclaw.buildOpenClawVersion}`,
297
+ ],
298
+ });
299
+ decisions.push({
300
+ fixture: fixture.id,
301
+ decision: "plugin-upstream-fix",
302
+ seam: "package-metadata",
303
+ action: "Ask the plugin to publish install.minHostVersion as a semver floor for the OpenClaw package surface it targets.",
304
+ evidence: packageSummary.path,
305
+ });
306
+ }
307
+
308
+ const npmPackIssues = packageNpmPackIssues(packageSummary, fixtureReport);
309
+ for (const finding of npmPackIssues) {
310
+ warnings.push({
311
+ fixture: fixture.id,
312
+ code: finding.code,
313
+ level: "warning",
314
+ message: finding.message,
315
+ evidence: finding.evidence,
316
+ });
317
+ decisions.push({
318
+ fixture: fixture.id,
319
+ decision: "plugin-upstream-fix",
320
+ seam: "package-artifact",
321
+ action: "Ask the plugin to make its advertised npm install artifact match the published OpenClaw metadata.",
322
+ evidence: finding.evidence.join(", "),
323
+ });
324
+ }
325
+
205
326
  if (packageSummary.openclaw && packageSummary.openclaw.entrypoints.length === 0) {
206
327
  warnings.push({
207
328
  fixture: fixture.id,
@@ -351,6 +472,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
351
472
  suggestions.push(...packageContracts.suggestions);
352
473
  logs.push(...packageContracts.logs);
353
474
  decisions.push(...packageContracts.decisions);
475
+ classifySecurityManifestCoverage({ fixture, fixtureReport, warnings, decisions });
354
476
 
355
477
  for (const pluginManifest of fixtureReport.pluginManifests) {
356
478
  const providerAuthKeys = Object.keys(pluginManifest.providerAuthEnvVars ?? {});
@@ -557,6 +679,45 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
557
679
  return { warnings, suggestions, logs, decisions };
558
680
  }
559
681
 
682
+ function classifySecurityManifestCoverage({ fixture, fixtureReport, warnings, decisions }) {
683
+ for (const securityManifest of fixtureReport.securityManifests ?? []) {
684
+ warnings.push({
685
+ fixture: fixture.id,
686
+ code: "unrecognized-security-manifest",
687
+ level: "warning",
688
+ message:
689
+ "openclaw.security.json is not a supported OpenClaw or ClawHub security contract and is ignored by install safety checks",
690
+ evidence: [securityManifest.path],
691
+ });
692
+ decisions.push({
693
+ fixture: fixture.id,
694
+ decision: "plugin-upstream-fix",
695
+ seam: "security-metadata",
696
+ action:
697
+ "Remove the advisory security manifest or replace it with a supported, versioned OpenClaw/ClawHub security contract once one exists.",
698
+ evidence: securityManifest.path,
699
+ });
700
+
701
+ if (securityManifest.schema === unavailableSecurityManifestSchema) {
702
+ warnings.push({
703
+ fixture: fixture.id,
704
+ code: "security-manifest-schema-unavailable",
705
+ level: "warning",
706
+ message: "openclaw.security.json references an OpenClaw schema URL that is not currently published",
707
+ evidence: [`${securityManifest.path}:$schema=${securityManifest.schema}`],
708
+ });
709
+ decisions.push({
710
+ fixture: fixture.id,
711
+ decision: "plugin-upstream-fix",
712
+ seam: "security-metadata",
713
+ action:
714
+ "Do not rely on the schema URL until OpenClaw publishes and documents a real plugin security metadata schema.",
715
+ evidence: securityManifest.schema,
716
+ });
717
+ }
718
+ }
719
+ }
720
+
560
721
  function registrationCaptureGapDetails(inspection, targetOpenClaw) {
561
722
  const apiRegistrationDetails = inspection.registrationDetails.filter((registration) =>
562
723
  registration.name.startsWith("register"),
@@ -787,6 +948,235 @@ function selectPrimaryPackage(packages) {
787
948
  return packages[0] ?? null;
788
949
  }
789
950
 
951
+ function summarizeOpenClawInstall(install) {
952
+ if (!install || typeof install !== "object") {
953
+ return null;
954
+ }
955
+ return {
956
+ clawhubSpec: stringOrNull(install.clawhubSpec),
957
+ npmSpec: stringOrNull(install.npmSpec),
958
+ defaultChoice: stringOrNull(install.defaultChoice),
959
+ minHostVersion: stringOrNull(install.minHostVersion),
960
+ };
961
+ }
962
+
963
+ function summarizeOpenClawRelease(release) {
964
+ if (!release || typeof release !== "object") {
965
+ return null;
966
+ }
967
+ return {
968
+ publishToClawHub: booleanOrNull(release.publishToClawHub),
969
+ publishToNpm: booleanOrNull(release.publishToNpm),
970
+ };
971
+ }
972
+
973
+ function unsupportedOpenClawPackageMetadata(openclaw) {
974
+ if (!openclaw || typeof openclaw !== "object") {
975
+ return [];
976
+ }
977
+ return Object.keys(openclaw)
978
+ .filter((key) => key === "bundle")
979
+ .map((key) => `openclaw.${key}`);
980
+ }
981
+
982
+ function summarizeNpmPack(packageJson, openclaw) {
983
+ const files = arrayValues(packageJson.files).map(normalizePackagePath).filter((item) => item.length > 0);
984
+ return {
985
+ advertised: openclaw?.release?.publishToNpm === true || nonEmptyString(openclaw?.install?.npmSpec),
986
+ private: packageJson.private === true,
987
+ filesMode: files.length > 0 ? "allowlist" : "implicit",
988
+ files,
989
+ invalidFileSpecs: files.filter((item) => invalidPackageFileSpec(item)),
990
+ };
991
+ }
992
+
993
+ function packageInstallMetadataIssues(packageSummary) {
994
+ const openclaw = packageSummary.openclaw;
995
+ if (!openclaw) {
996
+ return [];
997
+ }
998
+
999
+ const issues = [];
1000
+ const install = openclaw.install;
1001
+ const release = openclaw.release;
1002
+ const publishToClawHub = release?.publishToClawHub === true;
1003
+ const publishToNpm = release?.publishToNpm === true;
1004
+
1005
+ if (publishToClawHub && !nonEmptyString(install?.clawhubSpec)) {
1006
+ issues.push("openclaw.release.publishToClawHub requires openclaw.install.clawhubSpec");
1007
+ }
1008
+ if (publishToNpm && !nonEmptyString(install?.npmSpec)) {
1009
+ issues.push("openclaw.release.publishToNpm requires openclaw.install.npmSpec");
1010
+ }
1011
+ if (publishToNpm && nonEmptyString(install?.npmSpec) && nonEmptyString(packageSummary.name) && install.npmSpec !== packageSummary.name) {
1012
+ issues.push(`openclaw.install.npmSpec:${install.npmSpec} does not match package name:${packageSummary.name}`);
1013
+ }
1014
+ if (nonEmptyString(install?.defaultChoice) && !["clawhub", "npm"].includes(install.defaultChoice)) {
1015
+ issues.push(`openclaw.install.defaultChoice:${install.defaultChoice} must be clawhub or npm`);
1016
+ }
1017
+ if (install?.defaultChoice === "clawhub" && !nonEmptyString(install.clawhubSpec)) {
1018
+ issues.push("openclaw.install.defaultChoice clawhub requires openclaw.install.clawhubSpec");
1019
+ }
1020
+ if (install?.defaultChoice === "npm" && !nonEmptyString(install.npmSpec)) {
1021
+ issues.push("openclaw.install.defaultChoice npm requires openclaw.install.npmSpec");
1022
+ }
1023
+
1024
+ return issues;
1025
+ }
1026
+
1027
+ function packageNpmPackIssues(packageSummary, fixtureReport) {
1028
+ if (!packageSummary.npmPack?.advertised) {
1029
+ return [];
1030
+ }
1031
+
1032
+ const findings = [];
1033
+ const unavailable = [];
1034
+ if (packageSummary.npmPack.private) {
1035
+ unavailable.push("package.json private:true");
1036
+ }
1037
+ if (!nonEmptyString(packageSummary.name)) {
1038
+ unavailable.push("package.json name missing");
1039
+ }
1040
+ if (!nonEmptyString(packageSummary.version)) {
1041
+ unavailable.push("package.json version missing");
1042
+ }
1043
+ unavailable.push(...packageSummary.npmPack.invalidFileSpecs.map((item) => `invalid files entry:${item}`));
1044
+
1045
+ if (unavailable.length > 0) {
1046
+ findings.push({
1047
+ code: "package-npm-pack-unavailable",
1048
+ message: "package advertises npm install or publish metadata but cannot produce a usable npm pack artifact",
1049
+ evidence: unavailable,
1050
+ });
1051
+ }
1052
+
1053
+ const missingMetadata = packageNpmPackMissingMetadata(packageSummary, fixtureReport);
1054
+ if (missingMetadata.length > 0) {
1055
+ findings.push({
1056
+ code: "package-npm-pack-metadata-missing",
1057
+ message: "advertised npm artifact would not include required OpenClaw package metadata",
1058
+ evidence: missingMetadata,
1059
+ });
1060
+ }
1061
+
1062
+ const missingEntrypoints = packageSummary.openclaw?.entrypoints
1063
+ .filter((entrypoint) => !repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath))
1064
+ .map((entrypoint) => `${entrypoint.kind}:${entrypoint.specifier} -> ${entrypoint.relativePath}`) ?? [];
1065
+ if (missingEntrypoints.length > 0) {
1066
+ findings.push({
1067
+ code: "package-npm-pack-entrypoint-missing",
1068
+ message: "advertised npm artifact would not include declared OpenClaw entrypoints",
1069
+ evidence: missingEntrypoints,
1070
+ });
1071
+ }
1072
+
1073
+ return findings;
1074
+ }
1075
+
1076
+ function packageNpmPackMissingMetadata(packageSummary, fixtureReport) {
1077
+ const missing = [];
1078
+ if (!repoPathIncludedInNpmPack(packageSummary, packageSummary.path)) {
1079
+ missing.push(packageSummary.path);
1080
+ }
1081
+
1082
+ for (const manifest of fixtureReport.pluginManifests ?? []) {
1083
+ if (repoPathWithinPackage(packageSummary, manifest.path) && !repoPathIncludedInNpmPack(packageSummary, manifest.path)) {
1084
+ missing.push(manifest.path);
1085
+ }
1086
+ }
1087
+
1088
+ return missing;
1089
+ }
1090
+
1091
+ function packageMinHostVersionDrift(packageSummary) {
1092
+ const openclaw = packageSummary.openclaw;
1093
+ if (!nonEmptyString(openclaw?.install?.minHostVersion) || !nonEmptyString(openclaw?.buildOpenClawVersion)) {
1094
+ return false;
1095
+ }
1096
+ return parseMinHostVersionFloor(openclaw.install.minHostVersion) !== openclaw.buildOpenClawVersion;
1097
+ }
1098
+
1099
+ function parseMinHostVersionFloor(value) {
1100
+ const match = /^>=([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(value);
1101
+ return match?.[1] ?? null;
1102
+ }
1103
+
1104
+ function repoPathIncludedInNpmPack(packageSummary, repoPath) {
1105
+ const packageRelativePath = packageRelativeRepoPath(packageSummary, repoPath);
1106
+ if (!packageRelativePath) {
1107
+ return false;
1108
+ }
1109
+ if (npmAlwaysPacksPath(packageRelativePath)) {
1110
+ return true;
1111
+ }
1112
+ if (packageSummary.npmPack?.filesMode !== "allowlist") {
1113
+ return true;
1114
+ }
1115
+ return packageSummary.npmPack.files.some((spec) => packageFileSpecIncludesPath(spec, packageRelativePath));
1116
+ }
1117
+
1118
+ function repoPathWithinPackage(packageSummary, repoPath) {
1119
+ return packageRelativeRepoPath(packageSummary, repoPath) !== null;
1120
+ }
1121
+
1122
+ function packageRelativeRepoPath(packageSummary, repoPath) {
1123
+ const packageDir = path.posix.dirname(normalizeRepoPath(packageSummary.path));
1124
+ const normalized = normalizeRepoPath(repoPath);
1125
+ if (packageDir === ".") {
1126
+ return normalized;
1127
+ }
1128
+ if (normalized === packageDir) {
1129
+ return "";
1130
+ }
1131
+ return normalized.startsWith(`${packageDir}/`) ? normalized.slice(packageDir.length + 1) : null;
1132
+ }
1133
+
1134
+ function npmAlwaysPacksPath(packageRelativePath) {
1135
+ const base = path.posix.basename(packageRelativePath).toLowerCase();
1136
+ return packageRelativePath === "package.json" || /^readme(?:\..*)?$/u.test(base) || /^licen[cs]e(?:\..*)?$/u.test(base);
1137
+ }
1138
+
1139
+ function packageFileSpecIncludesPath(spec, packageRelativePath) {
1140
+ if (spec === "." || spec === packageRelativePath) {
1141
+ return true;
1142
+ }
1143
+ if (spec.includes("*")) {
1144
+ return globLikeSpecIncludesPath(spec, packageRelativePath);
1145
+ }
1146
+ return packageRelativePath.startsWith(`${spec.replace(/\/$/u, "")}/`);
1147
+ }
1148
+
1149
+ function globLikeSpecIncludesPath(spec, packageRelativePath) {
1150
+ return globSegmentsIncludePath(spec.split("/"), packageRelativePath.split("/"));
1151
+ }
1152
+
1153
+ function globSegmentsIncludePath(specSegments, packageSegments) {
1154
+ if (specSegments.length === 0) {
1155
+ return packageSegments.length === 0;
1156
+ }
1157
+ const [head, ...tail] = specSegments;
1158
+ if (head === "**") {
1159
+ return globSegmentsIncludePath(tail, packageSegments) || (packageSegments.length > 0 && globSegmentsIncludePath(specSegments, packageSegments.slice(1)));
1160
+ }
1161
+ if (packageSegments.length === 0) {
1162
+ return false;
1163
+ }
1164
+ return globSegmentIncludesPath(head, packageSegments[0]) && globSegmentsIncludePath(tail, packageSegments.slice(1));
1165
+ }
1166
+
1167
+ function globSegmentIncludesPath(specSegment, packageSegment) {
1168
+ const escaped = specSegment.replace(/[.+?^${}()|[\]\\]/gu, "\\$&").replaceAll("*", ".*");
1169
+ return new RegExp(`^${escaped}$`, "u").test(packageSegment);
1170
+ }
1171
+
1172
+ function invalidPackageFileSpec(spec) {
1173
+ return spec.startsWith("/") || spec === ".." || spec.startsWith("../") || spec.includes("/../");
1174
+ }
1175
+
1176
+ function normalizePackagePath(value) {
1177
+ return normalizeRepoPath(value).replace(/^\.\/+/u, "").replace(/\/$/u, "");
1178
+ }
1179
+
790
1180
  function packageRank(packageSummary) {
791
1181
  if (packageSummary.openclaw?.entrypoints.length > 0) {
792
1182
  return 0;
@@ -801,6 +1191,22 @@ function arrayValues(value) {
801
1191
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
802
1192
  }
803
1193
 
1194
+ function stringOrNull(value) {
1195
+ return typeof value === "string" ? value : null;
1196
+ }
1197
+
1198
+ function booleanOrNull(value) {
1199
+ return typeof value === "boolean" ? value : null;
1200
+ }
1201
+
1202
+ function nonEmptyString(value) {
1203
+ return typeof value === "string" && value.trim().length > 0;
1204
+ }
1205
+
1206
+ function normalizeRepoPath(value) {
1207
+ return String(value ?? "").replaceAll("\\", "/").replace(/^\.\/+/u, "");
1208
+ }
1209
+
804
1210
  function detailEvidence(details, key = "name") {
805
1211
  return unique(details.map((detail) => `${detail[key]} @ ${detail.ref}`));
806
1212
  }