UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

776 lines (775 loc) 34 kB
import { t as sanitizeForLog } from "./ansi-DrXAcdMD.js"; import "./src-vebZIeLe.js"; import { t as expectDefined } from "./expect-CyE8FADM.js"; import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js"; import { c as normalizeOptionalLowercaseString } from "./string-coerce-CIXf7egm.js"; import { h as normalizeUniqueStringEntries } from "./string-normalization-DsCfAx8q.js"; import { r as isMissingPathError } from "./errno-CkbDOfLk.js"; import { d as pathExists, w as root } from "./fs-safe-B6pvPGnf.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { O as tryResolveSoleAgentId, m as resolveAgentWorkspaceDir, o as listAgentIds } from "./agent-scope-config-DcbEhP0R.js"; import { w as resolveStateDir } from "./paths-D2sRr1a_.js"; import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js"; import { n as normalizeAccountId } from "./account-id-CETVCrTz.js"; import "./errors-Db3Ymjlb.js"; import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { E as tableExists } from "./openclaw-state-db-cache-C7ljO0xP.js"; import { i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js"; import { a as loadInstalledPluginIndex } from "./installed-plugin-index-wrDsjyMD.js"; import { a as GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA, i as normalizeChatChannelId } from "./ids-BVZRYG0I.js"; import { a as loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot-w-4EjxI4.js"; import "./agent-scope-DbtJyKUL.js"; import { h as validateConfigObjectWithPlugins } from "./io.types-BUCjdS5v.js"; import { a as resolveChannelDmAllowFrom, i as resolveChannelDmAccess, s as setCanonicalDmAllowFrom } from "./dm-access-DF6nqjVk.js"; import { t as normalizeAnyChannelId } from "./registry-normalize-CY8V_nbx.js"; import "./registry-COqNvmCg.js"; import { h as ensureUserProfilesSchema } from "./user-profiles-internal-D0HRUN8X.js"; import { n as materializePluginAutoEnableCandidates, t as applyPluginAutoEnable } from "./plugin-auto-enable-Tv_C18cS.js"; import { v as classifyTailscaleLogin, y as githubAuthenticationSubject } from "./user-profiles-4AB7AmiH.js"; import { t as removePathWithinRoot } from "./fs-safe-remove-BLsdpUDO.js"; import { g as validateSkillProposalRollback, h as validateSkillProposalRecord } from "./store-sqlite-record-9Z9Ph1pe.js"; import { E as hashSkillProposalContent, a as readSkillProposal, r as importLegacySkillProposal, s as readSkillProposalRecord, w as readSkillProposalRollback } from "./store-X7p_X4MX.js"; import { s as readChannelAllowFromStore } from "./pairing-store-CxXcVQPq.js"; import { r as maybeRepairCodexRoutes } from "./codex-route-warnings-BVqoTxlz.js"; import { n as findDoctorLegacyConfigIssues } from "./legacy-config-issues-Bz1S8JF8.js"; import { r as VERSION_BOUND_RUNTIME_PLUGIN_POLICY_IDS_BY_SURFACE } from "./configured-runtime-plugin-installs-DWgjbBZ4.js"; import { d as isUpdatePackageSwapInProgress } from "./update-phase-J3w3q1-f.js"; import { r as maybeRepairStaleManagedNpmBundledPlugins } from "./doctor-plugin-registry-ByqlEVhW.js"; import { n as maybeRepairPluginOpenClawHostLinks } from "./doctor-plugin-host-links-D0NqP-RD.js"; import { t as repairMissingConfiguredPluginInstalls } from "./missing-configured-plugin-install-B3eZm5tD.js"; import { n as maybeMigrateAuthProfileJsonStoresToSqlite, r as maybeRepairOpenAICodexAuthConfig, t as collectOpenAICodexAuthProfileStoreIdMap } from "./doctor-auth-flat-profiles-Cd5w9SgG.js"; import { t as maybeRepairLegacyOAuthSidecarProfiles } from "./doctor-auth-oauth-sidecar-DbD6mUOC.js"; import { r as resolveConfigWideDoctorPluginMetadataSnapshot } from "./plugin-metadata-snapshot-scope-BQf8TDa9.js"; import { t as applyDoctorConfigMutation } from "./config-mutation-state-CymdR0Nh.js"; import { t as repairMergedGatewayOwnerProfile } from "./user-profiles-owner-migration-BVfxZUSW.js"; import { t as getDoctorChannelCapabilities } from "./channel-capabilities-CPJ2QEpt.js"; import { n as hasAllowFromEntries, t as scanEmptyAllowlistPolicyWarnings } from "./empty-allowlist-scan-D4-5MuGI.js"; import { n as maybeRepairBundledPluginLoadPaths } from "./bundled-plugin-load-paths-BmwYAAja.js"; import { a as collectChannelDoctorRepairMutations, s as createChannelDoctorEmptyAllowlistPolicyHooks, t as collectChannelDoctorCompatibilityMutations } from "./channel-doctor-Bu2JUlGB.js"; import { n as maybeRepairContextEngineHostCompatibility } from "./context-engine-host-compat-Cmmn2Tma.js"; import { r as maybeRepairExecSafeBinProfiles } from "./exec-safe-bins-DRNwp0r6.js"; import { n as maybeRepairLegacyToolsBySenderKeys } from "./legacy-tools-by-sender-CAEU9HcZ.js"; import { n as maybeRepairOpenPolicyAllowFrom } from "./open-policy-allowfrom-B9SWMu5q.js"; import { r as removeStalePluginRuntimeSymlinks } from "./plugin-runtime-symlinks-B-cu8Ou0.js"; import { t as repairStaleAgentModelRefs } from "./stale-agent-model-ref-repair-BlWIG_PD.js"; import { n as maybeRepairStaleConfiguredAuthOrders } from "./stale-auth-order-Dgz2WHuR.js"; import { n as repairStaleOAuthProfileShadows } from "./stale-oauth-profile-shadows-DJsJ9yLq.js"; import { r as maybeRepairStalePluginConfig } from "./stale-plugin-config-C7GHJBbE.js"; import { n as maybeRepairStaleSubagentAllowlists } from "./stale-subagent-allowlist-BD_YXRE5.js"; import path from "node:path"; import { randomUUID } from "node:crypto"; //#region src/state/user-profiles-tailscale-migration.ts function migrateLegacyTailscaleProfileIdentities(options = {}) { const database = openOpenClawStateDatabase(options); if (!tableExists(database.db, "user_profile_emails")) return { changes: [], warnings: [] }; const kysely = getNodeSqliteKysely(database.db); const legacyRows = executeSqliteQuerySync(database.db, kysely.selectFrom("user_profile_emails").select([ "email", "profile_id", "created_at" ]).orderBy("email", "asc")).rows.flatMap((row) => { const classified = classifyTailscaleLogin(row.email); return classified.kind === "provider" ? [{ ...row, ...classified }] : []; }); if (legacyRows.length === 0) return { changes: [], warnings: [] }; ensureUserProfilesSchema(options); return runOpenClawStateWriteTransaction(({ db }) => { const transactionKysely = getNodeSqliteKysely(db); let migrated = 0; const warnings = []; for (const row of legacyRows) { const subject = row.provider === "github" ? githubAuthenticationSubject(row.subject) : row.subject; executeSqliteQuerySync(db, transactionKysely.insertInto("user_profile_identities").values({ provider: row.provider, subject, profile_id: row.profile_id, canonical_login: null, created_at: row.created_at }).onConflict((conflict) => conflict.columns(["provider", "subject"]).doNothing())); if (executeSqliteQueryTakeFirstSync(db, transactionKysely.selectFrom("user_profile_identities").select("profile_id").where("provider", "=", row.provider).where("subject", "=", subject))?.profile_id !== row.profile_id) { warnings.push(`Kept legacy profile login ${row.email}: ${row.provider} identity is already linked to another profile.`); continue; } executeSqliteQuerySync(db, transactionKysely.deleteFrom("user_profile_emails").where("email", "=", row.email).where("profile_id", "=", row.profile_id)); migrated += 1; } return { changes: migrated > 0 ? [`Moved ${migrated} legacy Tailscale provider ${migrated === 1 ? "identity" : "identities"} out of user profile email aliases.`] : [], warnings }; }, options, { operationLabel: "user-profiles.migrate-legacy-identities" }); } //#endregion //#region src/commands/doctor-skill-workshop-sqlite.ts /** Doctor-owned migration of Skill Workshop proposal metadata into shared SQLite. */ const WORKSHOP_DIR = "skill-workshop"; const PROPOSALS_DIR = `${WORKSHOP_DIR}/proposals`; const MANIFEST_PATH = `${WORKSHOP_DIR}/proposals.json`; const RECOVERY_PROPOSALS_DIR = `${`${WORKSHOP_DIR}/recovery`}/proposals`; const MAX_RECORD_BYTES = 1048576; const MAX_ROLLBACK_BYTES = 134217728; const PROPOSAL_ID_PATTERN = /^[a-z0-9][a-z0-9-]{5,120}$/; async function readJson(rootDir, relativePath, maxBytes) { const read = await rootDir.read(relativePath, { hardlinks: "reject", maxBytes, symlinks: "reject" }); return JSON.parse(read.buffer.toString("utf8")); } function proposalWorkspace(record) { return path.dirname(path.dirname(path.resolve(record.target.skillDir))); } function configuredAgentIds(config) { return listAgentIds(config); } function inferOwnerAgentId(params) { if (params.record.origin?.agentId) return normalizeAgentId(params.record.origin.agentId); if (params.record.origin?.sessionKey) { const sessionAgentId = parseAgentSessionKey(params.record.origin.sessionKey)?.agentId; if (sessionAgentId) return normalizeAgentId(sessionAgentId); } const agentIds = configuredAgentIds(params.config); const workspaceMatches = agentIds.filter((agentId) => path.resolve(resolveAgentWorkspaceDir(params.config, agentId, params.env)) === path.resolve(params.workspaceDir)); if (workspaceMatches.length === 1) return workspaceMatches[0]; return agentIds.length === 1 ? agentIds[0] : void 0; } async function readLegacyRollback(stateRoot, proposalId) { try { const rollback = validateSkillProposalRollback(await readJson(stateRoot, `${PROPOSALS_DIR}/${proposalId}/rollback.json`, MAX_ROLLBACK_BYTES)); if (!rollback.ok) throw new Error(rollback.error.message); if (rollback.value.proposalId !== proposalId) throw new Error("invalid rollback metadata"); return rollback.value; } catch (error) { if (isMissingPathError(error)) return; throw error; } } async function verifyImportedProposal(params) { const imported = (await readSkillProposal(params.record.id, { env: params.env }, {}, { reconcile: false }))?.record; if (!imported || imported.draftHash !== params.record.draftHash || imported.target.skillFile !== params.record.target.skillFile) throw new Error("SQLite verification failed"); if (params.rollback && !await readSkillProposalRollback(params.record.id, { env: params.env })) throw new Error("SQLite rollback verification failed"); } async function migrateProposal(params) { const proposalDir = `${PROPOSALS_DIR}/${params.proposalId}`; const record = validateSkillProposalRecord(await readJson(params.stateRoot, `${proposalDir}/proposal.json`, MAX_RECORD_BYTES)); if (!record.ok) throw new Error(record.error.message); if (record.value.id !== params.proposalId) throw new Error("invalid proposal metadata"); const draft = await params.stateRoot.read(`${proposalDir}/PROPOSAL.md`, { hardlinks: "reject", maxBytes: MAX_RECORD_BYTES, symlinks: "reject" }); if (hashSkillProposalContent(draft.buffer.toString("utf8")) !== record.value.draftHash) throw new Error("proposal draft hash does not match proposal metadata"); const rollback = await readLegacyRollback(params.stateRoot, params.proposalId); const workspaceDir = proposalWorkspace(record.value); const ownerAgentId = inferOwnerAgentId({ config: params.config, env: params.env, record: record.value, workspaceDir }); if (!ownerAgentId) throw new Error("owning agent could not be inferred; legacy metadata was retained for manual recovery"); const result = importLegacySkillProposal({ record: record.value, rollback, ownerAgentId, workspaceDir, store: { env: params.env } }); await verifyImportedProposal({ env: params.env, record: record.value, rollback }); if (rollback) await params.stateRoot.remove(`${proposalDir}/rollback.json`); await params.stateRoot.remove(`${proposalDir}/proposal.json`); return result; } /** * Reconcile a confirmed-incomplete legacy proposal directory that cannot be * imported so Doctor converges on the next run. Empty directories are removed * directly; non-empty directories are relocated into the Doctor-owned recovery * archive under the state directory, preserving any remaining artifacts. */ async function reconcileIncompleteProposal(params) { if ((await params.stateRoot.list(params.proposalDir, { withFileTypes: true })).length === 0) { await params.stateRoot.remove(params.proposalDir); return { kind: "removed-empty" }; } await params.stateRoot.mkdir(RECOVERY_PROPOSALS_DIR); const recoveryPath = `${RECOVERY_PROPOSALS_DIR}/${params.proposalId}-${randomUUID()}`; await params.stateRoot.move(params.proposalDir, recoveryPath, { overwrite: true }); return { kind: "quarantined", recoveryPath }; } /** Import verified legacy proposal sidecars, then remove only the imported JSON metadata. */ async function migrateLegacySkillWorkshopProposals(params) { const env = params.env ?? process.env; const stateDir = resolveStateDir(env); if (!await pathExists(path.join(stateDir, PROPOSALS_DIR))) { if (!await pathExists(path.join(stateDir, MANIFEST_PATH))) return { changes: [], warnings: [], detected: 0, migrated: 0 }; await removePathWithinRoot({ rootDir: stateDir, relativePath: MANIFEST_PATH }); return { changes: ["Removed the empty legacy Skill Workshop proposal index."], warnings: [], detected: 0, migrated: 0 }; } const stateRoot = await root(stateDir); let entries; try { entries = await stateRoot.list(PROPOSALS_DIR, { withFileTypes: true }); } catch (error) { if (error.code === "not-found") return { changes: [], warnings: [], detected: 0, migrated: 0 }; return { changes: [], warnings: [`Failed to inspect legacy Skill Workshop proposals: ${String(error)}`], detected: 0, migrated: 0 }; } const proposalIds = entries.filter((entry) => entry.isDirectory && PROPOSAL_ID_PATTERN.test(entry.name)).map((entry) => entry.name).toSorted((left, right) => left.localeCompare(right)); const warnings = []; const changes = []; let migrated = 0; for (const proposalId of proposalIds) { const proposalDir = `${PROPOSALS_DIR}/${proposalId}`; try { await migrateProposal({ config: params.config, env, proposalId, stateRoot }); migrated += 1; continue; } catch (error) { if (!isMissingPathError(error)) { warnings.push(`Failed to migrate Skill Workshop proposal ${proposalId}: ${String(error)}`); continue; } if (await readSkillProposalRecord(proposalId, { env }, {}, { reconcile: false })) continue; try { const disposition = await reconcileIncompleteProposal({ proposalId, proposalDir, stateRoot }); changes.push(disposition.kind === "removed-empty" ? `Removed empty legacy Skill Workshop proposal directory ${proposalId}.` : `Quarantined incomplete Skill Workshop proposal ${proposalId} to ${disposition.recoveryPath} for manual recovery.`); } catch (reconcileError) { warnings.push(`Could not quarantine incomplete Skill Workshop proposal ${proposalId}: ${String(reconcileError)}. Manually move ${proposalDir} to ${RECOVERY_PROPOSALS_DIR} to recover it.`); } } } await removePathWithinRoot({ rootDir: stateDir, relativePath: MANIFEST_PATH }).catch((error) => { if (!isMissingPathError(error)) warnings.push(`Failed to remove legacy Skill Workshop proposal index: ${String(error)}`); }); const migrationChange = migrated > 0 ? `Migrated ${migrated} Skill Workshop proposal${migrated === 1 ? "" : "s"} into shared SQLite.` : null; return { changes: migrationChange ? [migrationChange, ...changes] : changes, warnings, detected: proposalIds.length, migrated }; } //#endregion //#region src/commands/doctor/shared/allowfrom-fallback-migration.ts const PSEUDO_CHANNEL_KEYS = /* @__PURE__ */ new Set([ "defaults", "modelByChannel", "tools" ]); const ACCOUNT_SCHEMA_WILDCARD = "*"; const CHANNEL_GROUP_ALLOW_FROM_PATH = ["groupAllowFrom"]; const ACCOUNT_GROUP_ALLOW_FROM_PATH = [ "accounts", ACCOUNT_SCHEMA_WILDCARD, "groupAllowFrom" ]; function isDisabled(record) { return record.enabled === false; } function normalizeAllowFrom(raw) { return normalizeUniqueStringEntries(Array.isArray(raw) ? raw : []); } function readGroupAllowFrom(record) { return normalizeAllowFrom(record.groupAllowFrom); } function readDmAllowFrom(params) { return normalizeAllowFrom(resolveChannelDmAllowFrom({ account: params.account, parent: params.parent, mode: getDoctorChannelCapabilities(params.channelName).dmAllowFromMode })); } function readOwnDmAllowFrom(params) { return normalizeAllowFrom(resolveChannelDmAllowFrom({ account: params.account, mode: getDoctorChannelCapabilities(params.channelName).dmAllowFromMode })); } function findGeneratedChannelConfigSchema(channelName) { const normalizedChannelId = normalizeAnyChannelId(channelName); return GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA.find((entry) => entry.channelId === channelName || entry.channelId === normalizedChannelId)?.schema; } function schemaAllowsConfigPath(schema, path) { if (path.length === 0) return true; const node = asNullableRecord(schema); if (!node) return true; const anyOf = Array.isArray(node.anyOf) ? node.anyOf : void 0; if (anyOf) return anyOf.some((branch) => schemaAllowsConfigPath(branch, path)); const oneOf = Array.isArray(node.oneOf) ? node.oneOf : void 0; if (oneOf) return oneOf.some((branch) => schemaAllowsConfigPath(branch, path)); const allOf = Array.isArray(node.allOf) ? node.allOf : void 0; if (allOf) return allOf.every((branch) => schemaAllowsConfigPath(branch, path)); const segment = expectDefined(path[0], "schema path segment"); const rest = path.slice(1); const properties = asNullableRecord(node.properties); if (segment !== ACCOUNT_SCHEMA_WILDCARD && properties && Object.hasOwn(properties, segment)) return schemaAllowsConfigPath(expectDefined(properties[segment], "schema property"), rest); const additionalProperties = node.additionalProperties; if (additionalProperties === false) return false; if (additionalProperties && typeof additionalProperties === "object") return schemaAllowsConfigPath(additionalProperties, rest); return true; } function generatedSchemaAllowsGroupAllowFrom(channelName, path) { const schema = findGeneratedChannelConfigSchema(channelName); return schema !== void 0 && schemaAllowsConfigPath(schema, path); } function migrateRecord(params) { if (!params.canWriteGroupAllowFrom) return false; if (readGroupAllowFrom(params.account).length > 0) return false; if (params.parent && params.parentHadGroupAllowFrom) return false; const ownAllowFrom = readOwnDmAllowFrom(params); if (params.parent && ownAllowFrom.length === 0 && readGroupAllowFrom(params.parent).length > 0) return false; const allowFrom = readDmAllowFrom(params); if (allowFrom.length === 0) return false; params.account.groupAllowFrom = allowFrom; const noun = allowFrom.length === 1 ? "entry" : "entries"; params.changes.push(`${params.prefix}.groupAllowFrom: copied ${allowFrom.length} sender ${noun} from allowFrom for explicit group allowlist.`); return true; } /** Copy legacy allowFrom entries into groupAllowFrom where channel metadata permits fallback. */ function maybeRepairGroupAllowFromFallback(cfg) { if (!asNullableRecord(cfg.channels)) return { config: cfg, changes: [] }; const next = structuredClone(cfg); const nextChannels = next.channels; const changes = []; for (const [channelName, channelConfig] of Object.entries(nextChannels)) { if (PSEUDO_CHANNEL_KEYS.has(channelName) || !channelConfig || typeof channelConfig !== "object") continue; if (isDisabled(channelConfig)) continue; if (!getDoctorChannelCapabilities(channelName).groupAllowFromFallbackToAllowFrom) continue; const hadGroupAllowFrom = readGroupAllowFrom(channelConfig).length > 0; migrateRecord({ account: channelConfig, canWriteGroupAllowFrom: generatedSchemaAllowsGroupAllowFrom(channelName, CHANNEL_GROUP_ALLOW_FROM_PATH), channelName, changes, prefix: `channels.${channelName}` }); const accounts = asNullableRecord(channelConfig.accounts); if (!accounts) continue; const canWriteAccountGroupAllowFrom = generatedSchemaAllowsGroupAllowFrom(channelName, ACCOUNT_GROUP_ALLOW_FROM_PATH); for (const [accountId, accountConfig] of Object.entries(accounts)) { const account = asNullableRecord(accountConfig); if (!account || isDisabled(account)) continue; migrateRecord({ account, canWriteGroupAllowFrom: canWriteAccountGroupAllowFrom, channelName, changes, parent: channelConfig, parentHadGroupAllowFrom: hadGroupAllowFrom, prefix: `channels.${channelName}.accounts.${accountId}` }); } } if (changes.length === 0) return { config: cfg, changes: [] }; return { config: next, changes }; } //#endregion //#region src/commands/doctor/shared/allow-from-mode.ts /** Return the allowFrom interpretation mode advertised by a channel's doctor metadata. */ function resolveAllowFromMode(channelName) { return getDoctorChannelCapabilities(channelName).dmAllowFromMode; } //#endregion //#region src/commands/doctor/shared/allowlist-policy-repair.ts /** Restore missing allowFrom entries for allowlist DM policies from persisted pairing stores. */ async function maybeRepairAllowlistPolicyAllowFrom(cfg) { const channels = cfg.channels; if (!channels || typeof channels !== "object") return { config: cfg, changes: [] }; const next = structuredClone(cfg); const changes = []; const applyRecoveredAllowFrom = (params) => { const count = params.allowFrom.length; const noun = count === 1 ? "entry" : "entries"; setCanonicalDmAllowFrom({ entry: params.account, mode: params.mode, allowFrom: params.allowFrom, pathPrefix: params.prefix, changes, reason: `restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").` }); }; const recoverAllowFromForAccount = async (params) => { const { mode } = params; const { dmPolicy, allowFrom } = resolveChannelDmAccess({ account: params.account, parent: params.parent, mode }); if (dmPolicy !== "allowlist" || hasAllowFromEntries(allowFrom)) return; const normalizedChannelId = normalizeOptionalLowercaseString(normalizeChatChannelId(params.channelName) ?? params.channelName); if (!normalizedChannelId) return; const normalizedAccountId = normalizeAccountId(params.accountId) || "default"; const fromStore = await readChannelAllowFromStore(normalizedChannelId, process.env, normalizedAccountId).catch(() => []); const recovered = normalizeUniqueStringEntries(fromStore); if (recovered.length === 0) return; applyRecoveredAllowFrom({ account: params.account, allowFrom: recovered, mode, prefix: params.prefix }); }; const nextChannels = next.channels; for (const [channelName, channelConfig] of Object.entries(nextChannels)) { if (!channelConfig || typeof channelConfig !== "object") continue; if (channelConfig.enabled === false) continue; const mode = resolveAllowFromMode(channelName); await recoverAllowFromForAccount({ channelName, mode, account: channelConfig, prefix: `channels.${channelName}` }); const accounts = asNullableRecord(channelConfig.accounts); if (!accounts) continue; for (const [accountId, accountConfig] of Object.entries(accounts)) { if (!accountConfig || typeof accountConfig !== "object") continue; if (accountConfig.enabled === false) continue; await recoverAllowFromForAccount({ channelName, mode, account: accountConfig, parent: channelConfig, accountId, prefix: `channels.${channelName}.accounts.${accountId}` }); } } if (changes.length === 0) return { config: cfg, changes: [] }; return { config: next, changes }; } //#endregion //#region src/commands/doctor/shared/invalid-plugin-config.ts const PLUGIN_CONFIG_ISSUE_RE = /^plugins\.entries\.([^.]+)\.config(?:\.|$)/; function scanInvalidPluginConfig(cfg) { const hits = /* @__PURE__ */ new Set(); const validation = validateConfigObjectWithPlugins(cfg); if (validation.ok) return hits; const legacyIssues = findDoctorLegacyConfigIssues(cfg); for (const issue of validation.issues) { if (!issue.message.startsWith("invalid config:")) continue; const pluginId = issue.path.match(PLUGIN_CONFIG_ISSUE_RE)?.[1]; if (!pluginId || hits.has(pluginId)) continue; const configPath = `plugins.entries.${pluginId}.config`; if (legacyIssues.some((legacy) => legacy.path === configPath || legacy.path.startsWith(`${configPath}.`))) continue; hits.add(pluginId); } return hits; } /** Disable plugin entries and clear config when plugin validation marks their config invalid. */ function maybeRepairInvalidPluginConfig(cfg) { const hits = scanInvalidPluginConfig(cfg); if (hits.size === 0) return { config: cfg, changes: [] }; const next = structuredClone(cfg); const entries = asNullableRecord(next.plugins?.entries); if (!entries) return { config: cfg, changes: [] }; const quarantined = []; for (const pluginId of hits) { const entry = asNullableRecord(entries[pluginId]); if (!entry) continue; if ("config" in entry) delete entry.config; entry.enabled = false; quarantined.push(pluginId); } if (quarantined.length === 0) return { config: cfg, changes: [] }; return { config: next, changes: [sanitizeForLog(`- plugins.entries: quarantined ${quarantined.length} invalid plugin config${quarantined.length === 1 ? "" : "s"} (${quarantined.join(", ")})`)] }; } //#endregion //#region src/commands/doctor/repair-sequencing.ts /** Run doctor auto-repairs in dependency order and collect sanitized user notes. */ async function runDoctorRepairSequence(params) { let state = params.state; const pluginMetadataSnapshotState = params.pluginMetadataSnapshotState ?? {}; const changeNotes = []; const configChangeNotes = []; const warningNotes = []; const env = params.env ?? process.env; const resolveCurrentPluginMetadataScope = () => { const config = state.candidate; const soleAgentId = tryResolveSoleAgentId(config); return { config, workspaceDir: soleAgentId ? resolveAgentWorkspaceDir(config, soleAgentId, env) : void 0 }; }; const sanitizeLines = (lines) => lines.map((line) => sanitizeForLog(line)).join("\n"); const appendNotes = (notes, lines) => { if (lines && lines.length > 0) notes.push(sanitizeLines(lines)); }; const appendRepairNotes = (repair) => { appendNotes(changeNotes, repair.changes); appendNotes(warningNotes, repair.warnings); appendNotes(warningNotes, repair.notices); }; const runWithCurrentPluginMetadata = (run) => { if (!params.runWithPluginMetadataSnapshot) return run(); return params.runWithPluginMetadataSnapshot(resolveCurrentPluginMetadataScope(), run); }; const applyMutation = (mutation) => { if (mutation.changes.length > 0) { appendNotes(configChangeNotes, mutation.changes); state = applyDoctorConfigMutation({ state, mutation, shouldRepair: true }); } appendNotes(warningNotes, mutation.warnings); }; const applyRepairStages = async (stages) => { for (const repair of stages) applyMutation(await runWithCurrentPluginMetadata(() => repair(state.candidate))); }; const initialChannelRepairs = await runWithCurrentPluginMetadata(() => collectChannelDoctorRepairMutations({ cfg: state.candidate, doctorFixCommand: params.doctorFixCommand, env })); for (const mutation of initialChannelRepairs) applyMutation(mutation); applyMutation(maybeRepairBundledPluginLoadPaths(state.candidate, env)); const staleManagedNpmBundledPluginRepair = maybeRepairStaleManagedNpmBundledPlugins({ config: state.candidate, env, prompter: { shouldRepair: true } }); const repairedPluginOpenClawHostLinks = await maybeRepairPluginOpenClawHostLinks({ env, prompter: { shouldRepair: true } }); const codexRouteRepair = runWithCurrentPluginMetadata(() => maybeRepairCodexRoutes({ cfg: state.candidate, env, shouldRepair: true, blockedProviderPlan: params.blockedCodexProviderPlan })); applyMutation({ config: codexRouteRepair.cfg, changes: codexRouteRepair.changes, warnings: codexRouteRepair.warnings }); const openAICodexAuthProfileIdMap = collectOpenAICodexAuthProfileStoreIdMap({ cfg: state.candidate, env }); applyMutation(maybeRepairOpenAICodexAuthConfig(state.candidate, { profileIdMap: openAICodexAuthProfileIdMap })); applyMutation(await runWithCurrentPluginMetadata(() => maybeRepairContextEngineHostCompatibility({ cfg: state.candidate, doctorFixCommand: params.doctorFixCommand, env }))); const missingConfiguredPluginInstallRepair = await runWithCurrentPluginMetadata(() => repairMissingConfiguredPluginInstalls({ cfg: state.candidate, env, ...params.onCapabilityConsent ? { onCapabilityConsent: params.onCapabilityConsent } : {}, ...staleManagedNpmBundledPluginRepair ? { baselineRecords: staleManagedNpmBundledPluginRepair.installRecords } : {} })); const repairedPluginIds = missingConfiguredPluginInstallRepair.repairedPluginIds ?? []; if (staleManagedNpmBundledPluginRepair || repairedPluginOpenClawHostLinks || missingConfiguredPluginInstallRepair.pluginInventoryChanged) { const currentScope = resolveCurrentPluginMetadataScope(); pluginMetadataSnapshotState.current = runWithCurrentPluginMetadata(() => resolveConfigWideDoctorPluginMetadataSnapshot({ snapshot: loadPluginMetadataSnapshot({ config: currentScope.config, env, workspaceDir: currentScope.workspaceDir, index: loadInstalledPluginIndex({ config: currentScope.config, env, workspaceDir: currentScope.workspaceDir, installRecords: missingConfiguredPluginInstallRepair.records }) }), config: currentScope.config, env })); } if (missingConfiguredPluginInstallRepair.changes.length > 0) { appendNotes(changeNotes, missingConfiguredPluginInstallRepair.changes); applyMutation(applyPluginAutoEnable({ config: state.candidate, env, manifestRegistry: pluginMetadataSnapshotState.current?.manifestRegistry })); if (repairedPluginIds.length > 0) { applyMutation(materializePluginAutoEnableCandidates({ config: state.candidate, env, manifestRegistry: pluginMetadataSnapshotState.current?.manifestRegistry, candidates: repairedPluginIds.map((pluginId) => ({ pluginId, kind: "configured-plugin-repaired" })) })); const channelCompatibilityMutations = runWithCurrentPluginMetadata(() => collectChannelDoctorCompatibilityMutations(state.candidate, { env })); for (const mutation of channelCompatibilityMutations) applyMutation(mutation); const channelRepairs = await runWithCurrentPluginMetadata(() => collectChannelDoctorRepairMutations({ cfg: state.candidate, doctorFixCommand: params.doctorFixCommand, env })); for (const mutation of channelRepairs) applyMutation(mutation); } } appendNotes(warningNotes, missingConfiguredPluginInstallRepair.warnings); appendNotes(warningNotes, missingConfiguredPluginInstallRepair.notices); const failedPluginIds = missingConfiguredPluginInstallRepair.failedPluginIds ?? []; const hasUnscopedInstallRepairWarnings = missingConfiguredPluginInstallRepair.warnings.length > 0 && failedPluginIds.length === 0; const packageSwapInProgress = isUpdatePackageSwapInProgress(env); if (!packageSwapInProgress && failedPluginIds.length === 0 && !hasUnscopedInstallRepairWarnings) applyMutation(repairStaleAgentModelRefs(state.candidate, { env, pluginMetadataSnapshot: pluginMetadataSnapshotState.current })); if (!packageSwapInProgress && !hasUnscopedInstallRepairWarnings) applyMutation(runWithCurrentPluginMetadata(() => maybeRepairStalePluginConfig(state.candidate, env, { preservePluginIds: failedPluginIds, surfacePreservePluginIds: VERSION_BOUND_RUNTIME_PLUGIN_POLICY_IDS_BY_SURFACE }))); await applyRepairStages([ maybeRepairInvalidPluginConfig, maybeRepairAllowlistPolicyAllowFrom, maybeRepairOpenPolicyAllowFrom, maybeRepairGroupAllowFromFallback, maybeRepairStaleSubagentAllowlists ]); appendNotes(warningNotes, runWithCurrentPluginMetadata(() => scanEmptyAllowlistPolicyWarnings(state.candidate, { doctorFixCommand: params.doctorFixCommand, ...createChannelDoctorEmptyAllowlistPolicyHooks({ cfg: state.candidate, env }) }))); await applyRepairStages([maybeRepairLegacyToolsBySenderKeys, maybeRepairExecSafeBinProfiles]); appendRepairNotes(await migrateLegacySkillWorkshopProposals({ config: state.candidate, env })); appendRepairNotes(migrateLegacyTailscaleProfileIdentities({ env })); appendRepairNotes(repairMergedGatewayOwnerProfile({ env, shouldRepair: true })); appendRepairNotes(await removeStalePluginRuntimeSymlinks()); const legacyOAuthSidecarRepair = await maybeRepairLegacyOAuthSidecarProfiles({ cfg: state.candidate, prompter: { confirmAutoFix: async () => true }, emitNotes: false, env }); appendRepairNotes(legacyOAuthSidecarRepair); const staleOAuthShadowRepair = await repairStaleOAuthProfileShadows({ cfg: state.candidate, env }); appendRepairNotes(staleOAuthShadowRepair); const authProfileSqliteMigration = await maybeMigrateAuthProfileJsonStoresToSqlite({ cfg: state.candidate, prompter: { confirmAutoFix: async () => true }, env, openAICodexAuthProfileIdMap }); if (authProfileSqliteMigration.configChanged) state = applyDoctorConfigMutation({ state, mutation: { config: state.candidate, changes: ["Auth profile SQLite migration updated auth.profiles."] }, shouldRepair: true }); appendRepairNotes(authProfileSqliteMigration); applyMutation(maybeRepairStaleConfiguredAuthOrders({ cfg: state.candidate, env })); const authProfilesRepaired = legacyOAuthSidecarRepair.changes.length > 0 || staleOAuthShadowRepair.changes.length > 0 || authProfileSqliteMigration.changes.length > 0; return { state, changeNotes, configChangeNotes, warningNotes, authProfilesRepaired, ...openAICodexAuthProfileIdMap.size > 0 ? { openAICodexAuthProfileIdMap } : {}, ...pluginMetadataSnapshotState.current ? { pluginMetadataSnapshot: pluginMetadataSnapshotState.current } : {} }; } //#endregion export { runDoctorRepairSequence };