refactor: mechanical dedup batch (protocol types, update-cli bridge, talk fallback) (#114432)

* refactor(onboarding): remove search setup barrel

* refactor(plugins): reuse detected package manifest

* test(update): replace global helper bridges

* refactor(talk): remove dead legacy response fallback

* refactor(protocol): derive root types from schema

The maintainer approved broadening the additive schema-backed type surface without a protocol version bump.

* fix(plugins): drop stale package path import

* test(protocol): type dynamic registry lookups

* docs(talk): explain canonical response boundary

* refactor(talk): enforce canonical response input

* fix(protocol): keep root type exports registry-free

* refactor(update): expose helpers through test facades

* fix(protocol): keep result types on leaf schema modules
This commit is contained in:
Peter Steinberger
2026-07-27 06:55:07 -04:00
committed by GitHub
parent 0f879595d4
commit c6b2ec28c8
27 changed files with 582 additions and 2198 deletions

View File

@@ -1,7 +1,5 @@
// Plugin synchronization and convergence after the core update.
import path from "node:path";
import { confirm, isCancel, text } from "@clack/prompts";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { stripAnsi } from "../../../packages/terminal-core/src/ansi.js";
import { stylePromptMessage } from "../../../packages/terminal-core/src/prompt-style.js";
import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js";
@@ -10,20 +8,13 @@ import { readConfigFileSnapshot } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PluginInstallRecord } from "../../config/types.plugins.js";
import type { ClawHubRiskAcknowledgementRequest } from "../../infra/clawhub-install-trust.js";
import { pathExists } from "../../infra/fs-safe.js";
import type { UpdateChannel } from "../../infra/update-channels.js";
import type { UpdateRunResult } from "../../infra/update-runner.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../../plugins/config-state.js";
import { commitPluginInstallRecordsWithConfig } from "../../plugins/install-record-commit.js";
import {
loadInstalledPluginIndexInstallRecords,
withoutPluginInstallRecords,
withPluginInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
import {
resolveTrustedSourceLinkedOfficialClawHubSpec,
resolveTrustedSourceLinkedOfficialNpmSpec,
} from "../../plugins/official-external-install-records.js";
import { refreshPluginRegistryAfterConfigMutation } from "../../plugins/registry-refresh.js";
import {
isClawHubTrustSkippedOutcome,
@@ -33,46 +24,27 @@ import {
type PluginUpdateOutcome,
} from "../../plugins/update.js";
import { defaultRuntime } from "../../runtime.js";
import { resolveUserPath } from "../../utils.js";
import { listPersistedBundledPluginLocationBridges } from "../plugins-location-bridges.js";
import {
hasNativePackageInstallPayload,
resolveBundleInstallRecordPayload,
validateBundleInstallRecordPayload,
} from "./plugin-payload-validation.js";
import {
convergenceWarningsToOutcomes,
runPostCorePluginConvergence,
} from "./post-core-plugin-convergence.js";
import { readPackageVersion, type UpdateCommandOptions } from "./shared.js";
import {
buildInvalidConfigPostCoreUpdateResult,
collectMissingPluginInstallPayloads,
resolvePostSyncPluginUpdateSkipIds,
type MissingPluginInstallPayload,
type PostCorePluginUpdateResult,
} from "./update-command-plugins-internals.js";
export type { PostCorePluginUpdateResult } from "./update-command-plugins-internals.js";
const POST_UPDATE_PLUGIN_REPAIR_GUIDANCE =
"Run openclaw update repair to retry post-update plugin repair.";
export type PostCorePluginUpdateResult = NonNullable<
NonNullable<UpdateRunResult["postUpdate"]>["plugins"]
>;
type MissingPluginInstallPayload = {
pluginId: string;
installPath?: string;
reason: "missing-install-path" | "missing-package-dir" | "missing-package-json";
};
type PostUpdatePluginWarning = NonNullable<PostCorePluginUpdateResult["warnings"]>[number];
function resolvePostSyncPluginUpdateSkipIds(params: {
switchedToClawHub: readonly string[];
switchedToNpm: readonly string[];
repairedMissingPayloadIds: ReadonlySet<string>;
}): Set<string> {
return new Set([
...params.switchedToClawHub,
...params.switchedToNpm,
...params.repairedMissingPayloadIds,
]);
}
function isClawHubTrustNotice(message: string): boolean {
const trimmed = stripAnsi(message).trimStart();
return (
@@ -133,85 +105,6 @@ function resolveUpdateClawHubRiskAcknowledgementOptions(
};
}
function isTrackedPackageInstallRecord(record: PluginInstallRecord): boolean {
return (
record.source === "npm" ||
record.source === "clawhub" ||
record.source === "git" ||
record.source === "marketplace"
);
}
async function collectMissingPluginInstallPayloads(params: {
records: Record<string, PluginInstallRecord>;
config?: OpenClawConfig;
skipDisabledPlugins?: boolean;
syncOfficialPluginInstalls?: boolean;
env?: NodeJS.ProcessEnv;
}): Promise<MissingPluginInstallPayload[]> {
const env = params.env ?? process.env;
const normalizedPluginConfig =
params.skipDisabledPlugins && params.config
? normalizePluginsConfig(params.config.plugins)
: undefined;
const missing: MissingPluginInstallPayload[] = [];
for (const [pluginId, record] of Object.entries(params.records).toSorted(([left], [right]) =>
left.localeCompare(right),
)) {
if (!isTrackedPackageInstallRecord(record)) {
continue;
}
const officialNpmSpec = params.syncOfficialPluginInstalls
? resolveTrustedSourceLinkedOfficialNpmSpec({ pluginId, record })
: undefined;
const officialClawHubSpec = params.syncOfficialPluginInstalls
? resolveTrustedSourceLinkedOfficialClawHubSpec({ pluginId, record })
: undefined;
if (normalizedPluginConfig && params.config) {
const enableState = resolveEffectiveEnableState({
id: pluginId,
origin: "global",
config: normalizedPluginConfig,
rootConfig: params.config,
});
if (!enableState.enabled && !officialNpmSpec && !officialClawHubSpec) {
continue;
}
}
const rawInstallPath = normalizeOptionalString(record.installPath);
if (!rawInstallPath) {
missing.push({ pluginId, reason: "missing-install-path" });
continue;
}
const installPath = resolveUserPath(rawInstallPath, env);
if (!(await pathExists(installPath))) {
missing.push({ pluginId, installPath, reason: "missing-package-dir" });
continue;
}
const bundlePayload = resolveBundleInstallRecordPayload({ record, installPath });
if (bundlePayload.isBundlePayload) {
if (await hasNativePackageInstallPayload(installPath)) {
continue;
}
const bundleFailure = validateBundleInstallRecordPayload({
pluginId,
installPath,
record,
bundleFormat: bundlePayload.bundleFormat,
});
if (bundleFailure) {
missing.push({ pluginId, installPath, reason: "missing-package-json" });
}
continue;
}
const packageJsonPath = path.join(installPath, "package.json");
if (!(await pathExists(packageJsonPath))) {
missing.push({ pluginId, installPath, reason: "missing-package-json" });
}
}
return missing;
}
function formatMissingPluginPayloadReason(entry: MissingPluginInstallPayload): string {
if (entry.reason === "missing-install-path") {
return "installPath is missing";
@@ -295,58 +188,6 @@ function isActionableSkippedPostUpdateOutcome(outcome: PluginUpdateOutcome): boo
return isDisabledAfterFailureOutcome(outcome) || isClawHubTrustSkippedOutcome(outcome);
}
/**
* Build the post-core-update result we return when the active config cannot
* even be parsed. Mandatory post-core convergence requires a parseable
* config to know which plugins are configured; if one isn't available, we
* refuse to restart the gateway and surface this as a hard error so the
* existing `status === "error"` ⇒ `exit 1` pre-restart gate fires.
*
*/
function buildInvalidConfigPostCoreUpdateResult(): {
message: string;
guidance: string[];
result: PostCorePluginUpdateResult;
} {
const guidance = [
"Run `openclaw doctor` to inspect the config validation errors.",
"Once the config parses, rerun `openclaw update repair`.",
];
const message =
"Plugin post-update convergence skipped because the config is invalid; refusing to restart the gateway with an unverified plugin set.";
return {
message,
guidance,
result: {
status: "error",
reason: "invalid-config",
changed: false,
sync: {
changed: false,
switchedToBundled: [],
switchedToNpm: [],
warnings: [],
errors: [],
},
npm: {
changed: false,
outcomes: [],
},
integrityDrifts: [],
warnings: [{ reason: "invalid-config", message, guidance }],
},
};
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.updateCommandPluginsTestApi")] =
{
buildInvalidConfigPostCoreUpdateResult,
collectMissingPluginInstallPayloads,
resolvePostSyncPluginUpdateSkipIds,
};
}
export async function updatePluginsAfterCoreUpdate(params: {
root: string;
channel: UpdateChannel;