Download src/cli/update-cli/schema-preflight.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 7.91 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/update-cli/schema-preflight.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cli/update-cli/schema-preflight.ts
-
curl -L -o schema-preflight.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/update-cli/schema-preflight.ts
7.91 kB
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { isDeepStrictEqual } from "node:util"; | |
| import type { LegacyConfigUpdatePlan } from "../../commands/doctor/legacy-config-repair.js"; | |
| import { cloneEnvWithPlatformSemantics } from "../../config/env-vars.js"; | |
| import { createConfigIO } from "../../config/io.js"; | |
| import { formatConfigIssueLines } from "../../config/issue-format.js"; | |
| import { resolveConfiguredAgentDatabaseCandidatePaths } from "../../config/sessions/targets.js"; | |
| import type { OpenClawConfig } from "../../config/types.openclaw.js"; | |
| import { formatErrorMessage } from "../../infra/errors.js"; | |
| import { | |
| OPENCLAW_DATABASE_SCHEMA_DOCS_URL, | |
| preflightOpenClawDatabaseSchemas, | |
| type IncompatibleOpenClawDatabase, | |
| type IndeterminateOpenClawDatabase, | |
| type OpenClawDatabaseSchemaPreflight, | |
| } from "../../state/openclaw-database-preflight.js"; | |
| import type { OpenClawSchemaVersions } from "../../state/openclaw-schema-versions.js"; | |
| import { UpdatePreMutationError } from "./shared.js"; | |
| type TargetDatabaseSchemaContext = { | |
| config: OpenClawConfig; | |
| env: NodeJS.ProcessEnv; | |
| }; | |
| export function formatSchemaRefusalLines( | |
| schemas: { | |
| incompatible: readonly IncompatibleOpenClawDatabase[]; | |
| indeterminate: readonly IndeterminateOpenClawDatabase[]; | |
| }, | |
| dryRun = false, | |
| ): string[] { | |
| const prefix = dryRun ? "Would refuse update" : "Update refused"; | |
| return [ | |
| ...schemas.incompatible.map((database) => { | |
| const agent = database.agentId ? ` (agent ${database.agentId})` : ""; | |
| return `${prefix}: ${database.kind} database${agent} ${database.path} has schema ${database.foundVersion}; target supports ${database.supportedVersion}; writer build ${database.writerAppVersion ?? "unknown"}.`; | |
| }), | |
| ...schemas.indeterminate.map( | |
| (database) => | |
| `${prefix}: could not inspect ${database.kind} database ${database.path}: ${database.reason}; retry once the gateway releases it.`, | |
| ), | |
| OPENCLAW_DATABASE_SCHEMA_DOCS_URL, | |
| "Installing manually via npm bypasses this guard; back up first and verify compatibility.", | |
| ]; | |
| } | |
| async function checkTargetDatabaseSchemas( | |
| supportedVersions: OpenClawSchemaVersions, | |
| context: TargetDatabaseSchemaContext, | |
| ): Promise<OpenClawDatabaseSchemaPreflight> { | |
| let configuredAgentDatabaseCandidatePaths: string[]; | |
| try { | |
| configuredAgentDatabaseCandidatePaths = resolveConfiguredAgentDatabaseCandidatePaths( | |
| context.config, | |
| { env: context.env }, | |
| ); | |
| } catch (error) { | |
| throw new UpdatePreMutationError( | |
| "database-schema-preflight", | |
| `Update refused: could not inspect configured database paths: ${formatErrorMessage(error)}`, | |
| ); | |
| } | |
| return preflightOpenClawDatabaseSchemas({ | |
| env: context.env, | |
| supportedVersions, | |
| // Include default on-disk stores that update-time Doctor can later touch, | |
| // without resolving configured candidates into writable migration owners. | |
| configuredAgentDatabaseTargets: [], | |
| // Inspection keeps registered paths without adopting their migration ownership. | |
| configuredAgentDatabaseCandidatePaths, | |
| }); | |
| } | |
| export async function captureTargetDatabaseSchemaContext( | |
| env: NodeJS.ProcessEnv, | |
| options?: { legacyConfigPlan?: LegacyConfigUpdatePlan }, | |
| ) { | |
| // Discover stores without plugins, recovery, observations, or caller environment changes. | |
| const inspectionEnv = cloneEnvWithPlatformSemantics(env); | |
| const readEnv = cloneEnvWithPlatformSemantics(env); | |
| const { snapshot, writeOptions } = await createConfigIO({ | |
| env: inspectionEnv, | |
| observe: false, | |
| pluginValidation: "core-only", | |
| }).readConfigFileSnapshotForWrite(); | |
| // A Doctor-validated projection is usable only for its exact authored source. | |
| // Keep the original snapshot intact for checkpointing and later revalidation; | |
| // a caller's plan must not authorize a different managed service's config. | |
| const planned = options?.legacyConfigPlan; | |
| const before = planned?.snapshot; | |
| const legacyConfigPlan = | |
| before && | |
| before.path === snapshot.path && | |
| before.exists === snapshot.exists && | |
| before.raw === snapshot.raw && | |
| before.hash === snapshot.hash && | |
| isDeepStrictEqual(before.includedPaths ?? [], snapshot.includedPaths ?? []) && | |
| isDeepStrictEqual(before.includeProvenance ?? [], snapshot.includeProvenance ?? []) && | |
| isDeepStrictEqual(before.sourceConfig, snapshot.sourceConfig) && | |
| isDeepStrictEqual( | |
| planned.includeIdentity.includeFileHashesForWrite ?? {}, | |
| writeOptions.includeFileHashesForWrite ?? {}, | |
| ) && | |
| isDeepStrictEqual( | |
| planned.includeIdentity.includeFileTargetsForWrite ?? {}, | |
| writeOptions.includeFileTargetsForWrite ?? {}, | |
| ) | |
| ? planned | |
| : undefined; | |
| if (before?.path === snapshot.path && !legacyConfigPlan) { | |
| throw new UpdatePreMutationError( | |
| "database-schema-preflight", | |
| `Update refused: planned configuration changed at ${snapshot.path}. Retry against the current source.`, | |
| ); | |
| } | |
| if ((!snapshot.valid && !legacyConfigPlan) || snapshot.readError) { | |
| throw new UpdatePreMutationError( | |
| "invalid-config", | |
| [ | |
| `Update refused: configuration is invalid or unreadable at ${snapshot.path}.`, | |
| ...formatConfigIssueLines( | |
| // Validator messages can contain config values, including misplaced secrets. | |
| snapshot.issues.map(({ path: issuePath, pathSegments }) => ({ | |
| path: issuePath, | |
| pathSegments, | |
| message: "Invalid configuration field", | |
| })), | |
| "-", | |
| { normalizeRoot: true }, | |
| ), | |
| "Run `openclaw doctor --fix` to repair retired or unrecognized configuration fields, then correct any remaining errors before retrying.", | |
| ].join("\n"), | |
| ); | |
| } | |
| return { | |
| env: inspectionEnv, | |
| config: legacyConfigPlan?.config ?? snapshot.sourceConfig ?? snapshot.config, | |
| configSnapshot: snapshot, | |
| readEnv, | |
| ...(legacyConfigPlan ? { legacyConfigPlan } : {}), | |
| }; | |
| } | |
| function canonicalDatabaseIdentity(database: { kind: "agent" | "state"; path: string }): string { | |
| let canonical: string; | |
| try { | |
| // Native traversal must see the original locator before any lexical | |
| // normalization: link/../file can name a different database from resolve(). | |
| canonical = fs.realpathSync.native(database.path); | |
| } catch { | |
| canonical = path.resolve(database.path); | |
| } | |
| const comparable = process.platform === "win32" ? canonical.toLowerCase() : canonical; | |
| return `${database.kind}\0${comparable}`; | |
| } | |
| /** Inspect the union of caller/service stores without granting migration ownership. */ | |
| export async function checkTargetDatabaseSchemasForContexts( | |
| supportedVersions: OpenClawSchemaVersions | undefined, | |
| contexts: readonly TargetDatabaseSchemaContext[], | |
| ): Promise<OpenClawDatabaseSchemaPreflight> { | |
| if (!supportedVersions) { | |
| return { incompatible: [], indeterminate: [] }; | |
| } | |
| const incompatible = new Map<string, IncompatibleOpenClawDatabase>(); | |
| const indeterminate = new Map<string, IndeterminateOpenClawDatabase>(); | |
| for (const context of contexts) { | |
| const result = await checkTargetDatabaseSchemas(supportedVersions, context); | |
| for (const database of result.incompatible) { | |
| const identity = canonicalDatabaseIdentity(database); | |
| incompatible.set(identity, incompatible.get(identity) ?? database); | |
| indeterminate.delete(identity); | |
| } | |
| for (const database of result.indeterminate) { | |
| const identity = canonicalDatabaseIdentity(database); | |
| if (!incompatible.has(identity) && !indeterminate.has(identity)) { | |
| indeterminate.set(identity, database); | |
| } | |
| } | |
| } | |
| return { incompatible: [...incompatible.values()], indeterminate: [...indeterminate.values()] }; | |
| } | |
| export function hasSchemaRefusal(schemas: OpenClawDatabaseSchemaPreflight): boolean { | |
| return schemas.incompatible.length > 0 || schemas.indeterminate.length > 0; | |
| } | |