mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
fix(plugins): add doctor registry repair
This commit is contained in:
@@ -20,6 +20,9 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/Crestodian: open interactive Crestodian in the full OpenClaw TUI shell instead of a basic readline prompt.
|
||||
- CLI/Crestodian: shorten the startup greeting to the active planner/model, config state, Gateway probe result, and next debug action instead of dumping every discovered backend.
|
||||
- Plugins: migrate the local plugin registry automatically during package install/update, preserving legacy config and install-ledger state while indexing existing plugin manifests for the new cold registry path. Thanks @vincentkoc.
|
||||
- Plugins/doctor: make `openclaw doctor --fix` move legacy `plugins.installs`
|
||||
config records into the managed plugin install ledger and refresh the cold
|
||||
registry index when needed. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: align model-call GenAI span attributes with OpenTelemetry stability opt-in semantics, keeping legacy `gen_ai.system` by default while emitting `gen_ai.provider.name` under `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`. Thanks @vincentkoc.
|
||||
- Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc.
|
||||
- Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc.
|
||||
|
||||
@@ -920,6 +920,9 @@ source-plane diagnostics without adding a second raw filesystem-path disclosure
|
||||
surface. Legacy `plugins.installs` config entries are still read as a
|
||||
compatibility fallback while the state-managed `plugins/installs.json` ledger
|
||||
becomes the install source of truth.
|
||||
`openclaw doctor --fix` migrates those legacy config entries into the managed
|
||||
ledger and refreshes the cold registry index without loading plugin runtime
|
||||
modules.
|
||||
|
||||
## Context engine plugins
|
||||
|
||||
|
||||
@@ -266,6 +266,7 @@ openclaw plugins info <id> # inspect alias
|
||||
openclaw plugins doctor # diagnostics
|
||||
openclaw plugins registry # inspect persisted registry state
|
||||
openclaw plugins registry --refresh # rebuild persisted registry
|
||||
openclaw doctor --fix # repair registry/ledger migration state
|
||||
|
||||
openclaw plugins install <package> # install (ClawHub first, then npm)
|
||||
openclaw plugins install clawhub:<pkg> # install from ClawHub only
|
||||
@@ -279,7 +280,7 @@ openclaw plugins install <spec> --dangerously-force-unsafe-install
|
||||
openclaw plugins update <id-or-npm-spec> # update one plugin
|
||||
openclaw plugins update <id-or-npm-spec> --dangerously-force-unsafe-install
|
||||
openclaw plugins update --all # update all
|
||||
openclaw plugins uninstall <id> # remove config/install records
|
||||
openclaw plugins uninstall <id> # remove config and install ledger records
|
||||
openclaw plugins uninstall <id> --keep-files
|
||||
openclaw plugins marketplace list <source>
|
||||
openclaw plugins marketplace list <source> --json
|
||||
@@ -307,6 +308,9 @@ uninstall, enable, and disable flows refresh that registry after changing plugin
|
||||
state. If the registry is missing, stale, or invalid, `openclaw plugins registry
|
||||
--refresh` rebuilds it from the durable install ledger, config policy, and
|
||||
manifest/package metadata without loading plugin runtime modules.
|
||||
If a machine still has legacy `plugins.installs` records in config, run
|
||||
`openclaw doctor --fix` to move them into the managed
|
||||
`plugins/installs.json` ledger and remove the config copy.
|
||||
|
||||
`openclaw plugins update <id-or-npm-spec>` applies to tracked installs. Passing
|
||||
an npm package spec with a dist-tag or exact version resolves the package name
|
||||
|
||||
149
src/commands/doctor-plugin-registry.test.ts
Normal file
149
src/commands/doctor-plugin-registry.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginCandidate } from "../plugins/discovery.js";
|
||||
import { readPersistedPluginInstallLedger } from "../plugins/install-ledger-store.js";
|
||||
import {
|
||||
readPersistedInstalledPluginIndex,
|
||||
writePersistedInstalledPluginIndex,
|
||||
} from "../plugins/installed-plugin-index-store.js";
|
||||
import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "../plugins/test-helpers/fs-fixtures.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { maybeRepairPluginRegistryState } from "./doctor-plugin-registry.js";
|
||||
|
||||
vi.mock("../terminal/note.js", () => ({
|
||||
note: vi.fn(),
|
||||
}));
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(note).mockReset();
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
function makeTempDir() {
|
||||
return makeTrackedTempDir("openclaw-doctor-plugin-registry", tempDirs);
|
||||
}
|
||||
|
||||
function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
||||
return {
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createCandidate(rootDir: string, id = "demo"): PluginCandidate {
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "index.ts"),
|
||||
"throw new Error('runtime entry should not load during doctor registry repair');\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id,
|
||||
name: id,
|
||||
configSchema: { type: "object" },
|
||||
providers: [id],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
return {
|
||||
idHint: id,
|
||||
source: path.join(rootDir, "index.ts"),
|
||||
rootDir,
|
||||
origin: "global",
|
||||
};
|
||||
}
|
||||
|
||||
function createCurrentIndex(): InstalledPluginIndex {
|
||||
return {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 2,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("maybeRepairPluginRegistryState", () => {
|
||||
it("reports legacy config install records without mutating state outside repair mode", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const nextConfig = await maybeRepairPluginRegistryState({
|
||||
stateDir,
|
||||
env: hermeticEnv(),
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter: { shouldRepair: false },
|
||||
});
|
||||
|
||||
expect(nextConfig.plugins?.installs?.demo?.resolvedName).toBe("@vendor/demo");
|
||||
await expect(readPersistedPluginInstallLedger({ stateDir })).resolves.toBeNull();
|
||||
expect(vi.mocked(note).mock.calls.join("\n")).toContain("plugins.installs");
|
||||
});
|
||||
|
||||
it("moves legacy config install records into the ledger and refreshes an existing registry", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir });
|
||||
|
||||
const nextConfig = await maybeRepairPluginRegistryState({
|
||||
stateDir,
|
||||
candidates: [createCandidate(pluginDir)],
|
||||
env: hermeticEnv(),
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter: { shouldRepair: true },
|
||||
});
|
||||
|
||||
expect(nextConfig.plugins?.installs).toBeUndefined();
|
||||
await expect(readPersistedPluginInstallLedger({ stateDir })).resolves.toMatchObject({
|
||||
records: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
});
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||
refreshReason: "migration",
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
installRecord: expect.objectContaining({
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
158
src/commands/doctor-plugin-registry.ts
Normal file
158
src/commands/doctor-plugin-registry.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import {
|
||||
readPersistedPluginInstallLedger,
|
||||
resolvePluginInstallLedgerStorePath,
|
||||
withoutPluginInstallRecords,
|
||||
writePersistedPluginInstallLedger,
|
||||
type PluginInstallLedgerStoreOptions,
|
||||
} from "../plugins/install-ledger-store.js";
|
||||
import { refreshPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
import {
|
||||
DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV,
|
||||
migratePluginRegistryForInstall,
|
||||
preflightPluginRegistryInstallMigration,
|
||||
type PluginRegistryInstallMigrationParams,
|
||||
} from "./doctor/shared/plugin-registry-migration.js";
|
||||
|
||||
type PluginRegistryDoctorRepairParams = Omit<PluginRegistryInstallMigrationParams, "config"> &
|
||||
PluginInstallLedgerStoreOptions & {
|
||||
config: OpenClawConfig;
|
||||
prompter: Pick<DoctorPrompter, "shouldRepair">;
|
||||
};
|
||||
|
||||
type LegacyInstallLedgerMigrationResult = {
|
||||
config: OpenClawConfig;
|
||||
migrated: boolean;
|
||||
recordCount: number;
|
||||
};
|
||||
|
||||
function countRecords(records: Record<string, unknown> | undefined): number {
|
||||
return Object.keys(records ?? {}).length;
|
||||
}
|
||||
|
||||
function mergeInstallRecords(
|
||||
legacyRecords: Record<string, PluginInstallRecord>,
|
||||
ledgerRecords: Record<string, PluginInstallRecord> | undefined,
|
||||
): Record<string, PluginInstallRecord> {
|
||||
return {
|
||||
...legacyRecords,
|
||||
...(ledgerRecords ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function maybeMigrateLegacyInstallLedger(
|
||||
params: PluginRegistryDoctorRepairParams,
|
||||
): Promise<LegacyInstallLedgerMigrationResult> {
|
||||
const legacyRecords = params.config.plugins?.installs;
|
||||
const legacyCount = countRecords(legacyRecords);
|
||||
if (!legacyRecords || legacyCount === 0) {
|
||||
return {
|
||||
config: params.config,
|
||||
migrated: false,
|
||||
recordCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const ledgerPath = resolvePluginInstallLedgerStorePath(params);
|
||||
if (!params.prompter.shouldRepair) {
|
||||
note(
|
||||
[
|
||||
`Legacy plugin install records still live in config at \`plugins.installs\`.`,
|
||||
`Repair with ${formatCliCommand("openclaw doctor --fix")} to move them to ${shortenHomePath(ledgerPath)} and remove the config copy.`,
|
||||
].join("\n"),
|
||||
"Plugin registry",
|
||||
);
|
||||
return {
|
||||
config: params.config,
|
||||
migrated: false,
|
||||
recordCount: legacyCount,
|
||||
};
|
||||
}
|
||||
|
||||
const existingLedger = await readPersistedPluginInstallLedger(params);
|
||||
const nextRecords = mergeInstallRecords(legacyRecords, existingLedger?.records);
|
||||
await writePersistedPluginInstallLedger(nextRecords, params);
|
||||
const nextConfig = withoutPluginInstallRecords(params.config);
|
||||
note(
|
||||
[
|
||||
`Moved ${legacyCount} legacy plugin install record${legacyCount === 1 ? "" : "s"} from config to ${shortenHomePath(ledgerPath)}.`,
|
||||
"Removed the legacy `plugins.installs` config copy.",
|
||||
].join("\n"),
|
||||
"Plugin registry",
|
||||
);
|
||||
return {
|
||||
config: nextConfig,
|
||||
migrated: true,
|
||||
recordCount: legacyCount,
|
||||
};
|
||||
}
|
||||
|
||||
export async function maybeRepairPluginRegistryState(
|
||||
params: PluginRegistryDoctorRepairParams,
|
||||
): Promise<OpenClawConfig> {
|
||||
let nextConfig = params.config;
|
||||
const ledgerMigration = await maybeMigrateLegacyInstallLedger(params);
|
||||
nextConfig = ledgerMigration.config;
|
||||
|
||||
const migrationParams = {
|
||||
...params,
|
||||
config: nextConfig,
|
||||
};
|
||||
const preflight = preflightPluginRegistryInstallMigration(migrationParams);
|
||||
for (const warning of preflight.deprecationWarnings) {
|
||||
note(warning, "Plugin registry");
|
||||
}
|
||||
if (preflight.action === "disabled") {
|
||||
note(
|
||||
`${DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV} is set; skipping plugin registry repair.`,
|
||||
"Plugin registry",
|
||||
);
|
||||
return nextConfig;
|
||||
}
|
||||
|
||||
if (!params.prompter.shouldRepair) {
|
||||
if (preflight.action === "migrate") {
|
||||
note(
|
||||
[
|
||||
"Persisted plugin registry is missing or stale.",
|
||||
`Repair with ${formatCliCommand("openclaw doctor --fix")} to rebuild ${shortenHomePath(preflight.filePath)} from enabled plugins.`,
|
||||
].join("\n"),
|
||||
"Plugin registry",
|
||||
);
|
||||
}
|
||||
return nextConfig;
|
||||
}
|
||||
|
||||
if (preflight.action === "migrate") {
|
||||
const result = await migratePluginRegistryForInstall(migrationParams);
|
||||
if (result.migrated) {
|
||||
const total = result.current.plugins.length;
|
||||
const enabled = result.current.plugins.filter((plugin) => plugin.enabled).length;
|
||||
note(
|
||||
`Plugin registry rebuilt: ${enabled}/${total} enabled plugins indexed.`,
|
||||
"Plugin registry",
|
||||
);
|
||||
}
|
||||
return nextConfig;
|
||||
}
|
||||
|
||||
if (ledgerMigration.migrated) {
|
||||
const index = await refreshPluginRegistry({
|
||||
...migrationParams,
|
||||
reason: "migration",
|
||||
});
|
||||
const total = index.plugins.length;
|
||||
const enabled = index.plugins.filter((plugin) => plugin.enabled).length;
|
||||
note(
|
||||
`Plugin registry refreshed: ${enabled}/${total} enabled plugins indexed.`,
|
||||
"Plugin registry",
|
||||
);
|
||||
}
|
||||
|
||||
return nextConfig;
|
||||
}
|
||||
@@ -13,4 +13,11 @@ describe("doctor health contributions", () => {
|
||||
ids.indexOf("doctor:startup-channel-maintenance"),
|
||||
);
|
||||
});
|
||||
|
||||
it("runs plugin registry repair before final config writes", () => {
|
||||
const ids = resolveDoctorHealthContributions().map((entry) => entry.id);
|
||||
|
||||
expect(ids.indexOf("doctor:plugin-registry")).toBeGreaterThan(-1);
|
||||
expect(ids.indexOf("doctor:plugin-registry")).toBeLessThan(ids.indexOf("doctor:write-config"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,6 +223,15 @@ async function runLegacyPluginManifestHealth(ctx: DoctorHealthFlowContext): Prom
|
||||
});
|
||||
}
|
||||
|
||||
async function runPluginRegistryHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
const { maybeRepairPluginRegistryState } = await import("../commands/doctor-plugin-registry.js");
|
||||
ctx.cfg = await maybeRepairPluginRegistryState({
|
||||
config: ctx.cfg,
|
||||
env: process.env,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
}
|
||||
|
||||
async function runBundledPluginRuntimeDepsHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
const { maybeRepairBundledPluginRuntimeDeps } =
|
||||
await import("../commands/doctor-bundled-plugin-runtime-deps.js");
|
||||
@@ -548,6 +557,11 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] {
|
||||
label: "Legacy plugin manifests",
|
||||
run: runLegacyPluginManifestHealth,
|
||||
}),
|
||||
createDoctorHealthContribution({
|
||||
id: "doctor:plugin-registry",
|
||||
label: "Plugin registry",
|
||||
run: runPluginRegistryHealth,
|
||||
}),
|
||||
createDoctorHealthContribution({
|
||||
id: "doctor:state-integrity",
|
||||
label: "State integrity",
|
||||
|
||||
@@ -202,6 +202,8 @@ function matchRule(path: string): ReloadRule | null {
|
||||
}
|
||||
|
||||
function isPluginInstallTimestampPath(path: string): boolean {
|
||||
// Legacy compatibility only: new plugin install metadata lives in the
|
||||
// managed install ledger, but old config writes may still touch this path.
|
||||
return /^plugins\.installs\..+\.(installedAt|resolvedAt)$/.test(path);
|
||||
}
|
||||
|
||||
@@ -213,6 +215,8 @@ function getPluginInstallRecords(config: unknown): Record<string, unknown> {
|
||||
if (!isPlainObject(plugins)) {
|
||||
return {};
|
||||
}
|
||||
// Keep legacy config install records out of gateway restart decisions while
|
||||
// migration/doctor moves them into the managed plugin install ledger.
|
||||
const installs = plugins.installs;
|
||||
return isPlainObject(installs) ? installs : {};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import { writePersistedPluginInstallLedger } from "./install-ledger-store.js";
|
||||
import {
|
||||
diffInstalledPluginIndexInvalidationReasons,
|
||||
getInstalledPluginRecord,
|
||||
@@ -412,6 +413,42 @@ describe("installed plugin index", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("indexes persisted install ledger records from an explicit state directory", async () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
const stateDir = makeTempDir();
|
||||
await writePersistedPluginInstallLedger(
|
||||
{
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "@vendor/demo-plugin@1.2.3",
|
||||
installPath: fixture.rootDir,
|
||||
resolvedName: "@vendor/demo-plugin",
|
||||
resolvedVersion: "1.2.3",
|
||||
integrity: "sha512-installed",
|
||||
},
|
||||
},
|
||||
{ stateDir },
|
||||
);
|
||||
|
||||
const index = loadInstalledPluginIndex({
|
||||
candidates: [fixture.candidate],
|
||||
env: hermeticEnv(),
|
||||
stateDir,
|
||||
});
|
||||
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
pluginId: "demo",
|
||||
installRecord: {
|
||||
source: "npm",
|
||||
spec: "@vendor/demo-plugin@1.2.3",
|
||||
installPath: fixture.rootDir,
|
||||
resolvedName: "@vendor/demo-plugin",
|
||||
resolvedVersion: "1.2.3",
|
||||
integrity: "sha512-installed",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("indexes local fallback install ledger records written before a process reload", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
const cfg = recordPluginInstall(
|
||||
|
||||
@@ -139,6 +139,8 @@ export type LoadInstalledPluginIndexParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
pluginInstallLedgerFilePath?: string;
|
||||
cache?: boolean;
|
||||
candidates?: PluginCandidate[];
|
||||
diagnostics?: PluginDiagnostic[];
|
||||
@@ -473,6 +475,8 @@ function buildInstalledPluginIndex(
|
||||
const installRecords = loadPluginInstallRecordsSync({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
stateDir: params.stateDir,
|
||||
filePath: params.pluginInstallLedgerFilePath,
|
||||
});
|
||||
const plugins = registry.plugins.map((record): InstalledPluginIndexRecord => {
|
||||
const candidate = candidateByRootDir.get(record.rootDir);
|
||||
|
||||
@@ -431,10 +431,12 @@ export async function collectPluginsTrustFindings(params: {
|
||||
.map(([pluginId, record]) => `${pluginId} (${record.spec})`);
|
||||
if (unpinned.length > 0) {
|
||||
findings.push({
|
||||
// Keep the legacy checkId stable for downstream audit consumers while
|
||||
// plugin install metadata moves from config to the managed ledger.
|
||||
checkId: "plugins.installs_unpinned_npm_specs",
|
||||
severity: "warn",
|
||||
title: "Plugin installs include unpinned npm specs",
|
||||
detail: `Unpinned plugin install records:\n${unpinned.map((entry) => `- ${entry}`).join("\n")}`,
|
||||
title: "Plugin install ledger includes unpinned npm specs",
|
||||
detail: `Unpinned plugin install ledger records:\n${unpinned.map((entry) => `- ${entry}`).join("\n")}`,
|
||||
remediation:
|
||||
"Pin install specs to exact versions (for example, `@scope/pkg@1.2.3`) for higher supply-chain stability.",
|
||||
});
|
||||
@@ -449,8 +451,8 @@ export async function collectPluginsTrustFindings(params: {
|
||||
findings.push({
|
||||
checkId: "plugins.installs_missing_integrity",
|
||||
severity: "warn",
|
||||
title: "Plugin installs are missing integrity metadata",
|
||||
detail: `Plugin install records missing integrity:\n${missingIntegrity.map((entry) => `- ${entry}`).join("\n")}`,
|
||||
title: "Plugin install ledger is missing integrity metadata",
|
||||
detail: `Plugin install ledger records missing integrity:\n${missingIntegrity.map((entry) => `- ${entry}`).join("\n")}`,
|
||||
remediation:
|
||||
"Reinstall or update plugins to refresh install metadata with resolved integrity hashes.",
|
||||
});
|
||||
@@ -475,7 +477,7 @@ export async function collectPluginsTrustFindings(params: {
|
||||
findings.push({
|
||||
checkId: "plugins.installs_version_drift",
|
||||
severity: "warn",
|
||||
title: "Plugin install records drift from installed package versions",
|
||||
title: "Plugin install ledger records drift from installed package versions",
|
||||
detail: `Detected plugin install metadata drift:\n${pluginVersionDrift.map((entry) => `- ${entry}`).join("\n")}`,
|
||||
remediation:
|
||||
"Run `openclaw plugins update --all` (or reinstall affected plugins) to refresh install metadata.",
|
||||
|
||||
Reference in New Issue
Block a user