refactor: consolidate coercion contracts (#122458)

* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
Peter Steinberger
2026-08-11 23:26:37 -07:00
committed by GitHub
parent 66fe424590
commit b080dd1e76
276 changed files with 1685 additions and 1663 deletions
+5 -12
View File
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { describe, expect, it } from "vitest";
import { expectNoReaddirSyncDuring } from "../test-utils/fs-scan-assertions.js";
import { listGitTrackedFiles, toRepoRelativePath } from "../test-utils/repo-files.js";
@@ -50,14 +51,6 @@ function readJsonFile(filePath: string): unknown {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function normalizeText(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function listBundledPluginDirs(): string[] {
const externalDirs = listExternalBundledPluginDirs();
if (externalDirs) {
@@ -146,8 +139,8 @@ function readBundledPluginRecords(): BundledPluginRecord[] {
const manifest = readJsonFile(manifestPath) as PluginManifestShape;
const pkg = readJsonFile(packagePath) as OpenClawPackageShape;
const manifestId = normalizeText(manifest.id);
const packageName = normalizeText(pkg.name);
const manifestId = normalizeOptionalString(manifest.id);
const packageName = normalizeOptionalString(pkg.name);
if (!manifestId || !packageName) {
return [];
}
@@ -157,8 +150,8 @@ function readBundledPluginRecords(): BundledPluginRecord[] {
dirName,
packageName,
manifestId,
installNpmSpec: normalizeText(pkg.openclaw?.install?.npmSpec),
channelId: normalizeText(pkg.openclaw?.channel?.id),
installNpmSpec: normalizeOptionalString(pkg.openclaw?.install?.npmSpec),
channelId: normalizeOptionalString(pkg.openclaw?.channel?.id),
},
];
});
@@ -14,7 +14,7 @@ import {
type PluginManifest,
} from "../../manifest.js";
import { resolveLoaderPackageRoot } from "../../sdk-alias.js";
import { uniqueStrings } from "../shared.js";
import { normalizeContractStringValues } from "../shared.js";
// Build/test inventory only.
// Runtime code should prefer manifest/runtime registry queries instead of these snapshots.
@@ -111,7 +111,7 @@ function normalizeSetupProviderEnvVars(setup: PluginManifest["setup"]): Record<s
(provider) =>
[
provider.id.trim(),
uniqueStrings(provider.envVars ?? [], (value) =>
normalizeContractStringValues(provider.envVars ?? [], (value) =>
typeof value === "string" ? value.trim() : "",
),
] as const,
@@ -126,57 +126,68 @@ function buildBundledPluginContractSnapshot(
): BundledPluginContractSnapshot {
return {
pluginId: manifest.id,
cliBackendIds: uniqueStrings(manifest.cliBackends, (value) => value.trim()),
providerIds: uniqueStrings(manifest.providers, (value) => value.trim()),
cliBackendIds: normalizeContractStringValues(manifest.cliBackends, (value) => value.trim()),
providerIds: normalizeContractStringValues(manifest.providers, (value) => value.trim()),
providerEnvVars: normalizeSetupProviderEnvVars(manifest.setup),
workerProviderIds: uniqueStrings(manifest.contracts?.workerProviders, (value) => value.trim()),
embeddingProviderIds: uniqueStrings(manifest.contracts?.embeddingProviders, (value) =>
workerProviderIds: normalizeContractStringValues(manifest.contracts?.workerProviders, (value) =>
value.trim(),
),
speechProviderIds: uniqueStrings(manifest.contracts?.speechProviders, (value) => value.trim()),
realtimeTranscriptionProviderIds: uniqueStrings(
embeddingProviderIds: normalizeContractStringValues(
manifest.contracts?.embeddingProviders,
(value) => value.trim(),
),
speechProviderIds: normalizeContractStringValues(manifest.contracts?.speechProviders, (value) =>
value.trim(),
),
realtimeTranscriptionProviderIds: normalizeContractStringValues(
manifest.contracts?.realtimeTranscriptionProviders,
(value) => value.trim(),
),
realtimeVoiceProviderIds: uniqueStrings(manifest.contracts?.realtimeVoiceProviders, (value) =>
value.trim(),
realtimeVoiceProviderIds: normalizeContractStringValues(
manifest.contracts?.realtimeVoiceProviders,
(value) => value.trim(),
),
mediaUnderstandingProviderIds: uniqueStrings(
mediaUnderstandingProviderIds: normalizeContractStringValues(
manifest.contracts?.mediaUnderstandingProviders,
(value) => value.trim(),
),
transcriptSourceProviderIds: uniqueStrings(
transcriptSourceProviderIds: normalizeContractStringValues(
manifest.contracts?.transcriptSourceProviders,
(value) => value.trim(),
),
documentExtractorIds: uniqueStrings(manifest.contracts?.documentExtractors, (value) =>
value.trim(),
documentExtractorIds: normalizeContractStringValues(
manifest.contracts?.documentExtractors,
(value) => value.trim(),
),
imageGenerationProviderIds: uniqueStrings(
imageGenerationProviderIds: normalizeContractStringValues(
manifest.contracts?.imageGenerationProviders,
(value) => value.trim(),
),
videoGenerationProviderIds: uniqueStrings(
videoGenerationProviderIds: normalizeContractStringValues(
manifest.contracts?.videoGenerationProviders,
(value) => value.trim(),
),
musicGenerationProviderIds: uniqueStrings(
musicGenerationProviderIds: normalizeContractStringValues(
manifest.contracts?.musicGenerationProviders,
(value) => value.trim(),
),
webContentExtractorIds: uniqueStrings(manifest.contracts?.webContentExtractors, (value) =>
value.trim(),
webContentExtractorIds: normalizeContractStringValues(
manifest.contracts?.webContentExtractors,
(value) => value.trim(),
),
webFetchProviderIds: uniqueStrings(manifest.contracts?.webFetchProviders, (value) =>
value.trim(),
webFetchProviderIds: normalizeContractStringValues(
manifest.contracts?.webFetchProviders,
(value) => value.trim(),
),
webSearchProviderIds: uniqueStrings(manifest.contracts?.webSearchProviders, (value) =>
value.trim(),
webSearchProviderIds: normalizeContractStringValues(
manifest.contracts?.webSearchProviders,
(value) => value.trim(),
),
migrationProviderIds: uniqueStrings(manifest.contracts?.migrationProviders, (value) =>
value.trim(),
migrationProviderIds: normalizeContractStringValues(
manifest.contracts?.migrationProviders,
(value) => value.trim(),
),
toolNames: uniqueStrings(manifest.contracts?.tools, (value) => value.trim()),
toolNames: normalizeContractStringValues(manifest.contracts?.tools, (value) => value.trim()),
};
}
+41 -21
View File
@@ -10,7 +10,7 @@ import {
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS,
type BundledPluginContractSnapshot,
} from "./inventory/bundled-capability-metadata.js";
import { uniqueStrings } from "./shared.js";
import { normalizeContractStringValues } from "./shared.js";
type BundledCapabilityRuntimeRegistry = ReturnType<typeof loadBundledCapabilityRuntimeRegistry>;
type CapabilityContractEntry<T> = {
@@ -34,7 +34,7 @@ function normalizeProviderEnvVars(
return Object.fromEntries(
Object.entries(providerEnvVars ?? {}).map(([providerId, envVars]) => [
providerId,
uniqueStrings(envVars),
normalizeContractStringValues(envVars),
]),
);
}
@@ -44,7 +44,7 @@ function resolvePluginProviderEnvVars(plugin: {
}): Record<string, string[]> {
const envVars: Record<string, string[]> = {};
for (const provider of plugin.setup?.providers ?? []) {
envVars[provider.id] = uniqueStrings(provider.envVars ?? []);
envVars[provider.id] = normalizeContractStringValues(provider.envVars ?? []);
}
return normalizeProviderEnvVars(envVars);
}
@@ -99,29 +99,49 @@ function resolveBundledManifestContracts(): PluginRegistrationContractEntry[] {
)
.map((plugin) => ({
pluginId: plugin.id,
cliBackendIds: uniqueStrings(plugin.cliBackends),
providerIds: uniqueStrings(plugin.providers),
cliBackendIds: normalizeContractStringValues(plugin.cliBackends),
providerIds: normalizeContractStringValues(plugin.providers),
providerEnvVars: resolvePluginProviderEnvVars(plugin),
workerProviderIds: uniqueStrings(plugin.contracts?.workerProviders ?? []),
embeddingProviderIds: uniqueStrings(plugin.contracts?.embeddingProviders ?? []),
speechProviderIds: uniqueStrings(plugin.contracts?.speechProviders ?? []),
realtimeTranscriptionProviderIds: uniqueStrings(
workerProviderIds: normalizeContractStringValues(plugin.contracts?.workerProviders ?? []),
embeddingProviderIds: normalizeContractStringValues(
plugin.contracts?.embeddingProviders ?? [],
),
speechProviderIds: normalizeContractStringValues(plugin.contracts?.speechProviders ?? []),
realtimeTranscriptionProviderIds: normalizeContractStringValues(
plugin.contracts?.realtimeTranscriptionProviders ?? [],
),
realtimeVoiceProviderIds: uniqueStrings(plugin.contracts?.realtimeVoiceProviders ?? []),
mediaUnderstandingProviderIds: uniqueStrings(
realtimeVoiceProviderIds: normalizeContractStringValues(
plugin.contracts?.realtimeVoiceProviders ?? [],
),
mediaUnderstandingProviderIds: normalizeContractStringValues(
plugin.contracts?.mediaUnderstandingProviders ?? [],
),
transcriptSourceProviderIds: uniqueStrings(plugin.contracts?.transcriptSourceProviders ?? []),
documentExtractorIds: uniqueStrings(plugin.contracts?.documentExtractors ?? []),
imageGenerationProviderIds: uniqueStrings(plugin.contracts?.imageGenerationProviders ?? []),
videoGenerationProviderIds: uniqueStrings(plugin.contracts?.videoGenerationProviders ?? []),
musicGenerationProviderIds: uniqueStrings(plugin.contracts?.musicGenerationProviders ?? []),
webContentExtractorIds: uniqueStrings(plugin.contracts?.webContentExtractors ?? []),
webFetchProviderIds: uniqueStrings(plugin.contracts?.webFetchProviders ?? []),
webSearchProviderIds: uniqueStrings(plugin.contracts?.webSearchProviders ?? []),
migrationProviderIds: uniqueStrings(plugin.contracts?.migrationProviders ?? []),
toolNames: uniqueStrings(plugin.contracts?.tools ?? []),
transcriptSourceProviderIds: normalizeContractStringValues(
plugin.contracts?.transcriptSourceProviders ?? [],
),
documentExtractorIds: normalizeContractStringValues(
plugin.contracts?.documentExtractors ?? [],
),
imageGenerationProviderIds: normalizeContractStringValues(
plugin.contracts?.imageGenerationProviders ?? [],
),
videoGenerationProviderIds: normalizeContractStringValues(
plugin.contracts?.videoGenerationProviders ?? [],
),
musicGenerationProviderIds: normalizeContractStringValues(
plugin.contracts?.musicGenerationProviders ?? [],
),
webContentExtractorIds: normalizeContractStringValues(
plugin.contracts?.webContentExtractors ?? [],
),
webFetchProviderIds: normalizeContractStringValues(plugin.contracts?.webFetchProviders ?? []),
webSearchProviderIds: normalizeContractStringValues(
plugin.contracts?.webSearchProviders ?? [],
),
migrationProviderIds: normalizeContractStringValues(
plugin.contracts?.migrationProviders ?? [],
),
toolNames: normalizeContractStringValues(plugin.contracts?.tools ?? []),
}));
}
@@ -1,6 +1,7 @@
// Shared upstream model contract tests keep capability flags aligned across bundled catalogs.
import fs from "node:fs";
import path from "node:path";
import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
import { describe, expect, it } from "vitest";
import { listGitTrackedFiles } from "../../test-utils/repo-files.js";
@@ -57,12 +58,6 @@ function normalizeSharedModelId(modelId: string): string {
return (separator === -1 ? modelId : modelId.slice(separator + 1)).toLowerCase();
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function collectCatalogEntries(): CatalogEntry[] {
const entries: CatalogEntry[] = [];
for (const manifest of readBundledManifests()) {
+1 -1
View File
@@ -1,5 +1,5 @@
/** Returns unique normalized string values while preserving first-seen order. */
export function uniqueStrings(
export function normalizeContractStringValues(
values: readonly string[] | undefined,
normalize: (value: string) => string = (value) => value,
): string[] {
+2 -3
View File
@@ -1,5 +1,5 @@
/** Converts loaded plugin registries into stable plugin records for status and diagnostics. */
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { parseBooleanValue } from "../utils/boolean.js";
import type { PluginCompatCode } from "./compat/registry.js";
import type { PluginActivationState } from "./config-state.js";
import type { PluginBundleFormat, PluginDiagnosticCode, PluginFormat } from "./manifest-types.js";
@@ -204,8 +204,7 @@ export function formatPluginFailureSummary(failedPlugins: PluginRecord[]): strin
}
function isPluginLoadDebugEnabled(env: NodeJS.ProcessEnv): boolean {
const normalized = normalizeLowercaseStringOrEmpty(env.OPENCLAW_PLUGIN_LOAD_DEBUG);
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
return parseBooleanValue(env.OPENCLAW_PLUGIN_LOAD_DEBUG) === true;
}
function describePluginModuleExportShape(