openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
776 lines (775 loc) • 34 kB
JavaScript
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 };