UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

333 lines (332 loc) 17 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import "./utils-P__uGsPB.js"; import { o as listAgentIds, u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import "./agent-scope-DbtJyKUL.js"; import { n as resolveAuthStorePathForDisplay } from "./paths-DLuhw03H.js"; import { i as formatAuthDoctorHint, n as resolveApiKeyForProfile } from "./oauth-BNreHOU-.js"; import { o as resolveSharedAuthStoreOwnership, s as resolveSharedAuthStorePath } from "./path-resolve-oRkRBkQd.js"; import { c as inspectPersistedSharedAuthProfileStoreRaw, h as resolveAuthProfileDatabasePath } from "./sqlite-MN_7y26V.js"; import { i as classifyOAuthRefreshFailure, n as buildAuthProfileUnusableHint, r as buildOAuthRefreshFailureLoginCommand, s as formatOAuthRefreshFailureLoginCommandMarkdown } from "./oauth-refresh-failure-DP-bO7C0.js"; import { W as shouldUseMainOwnerForLocalOAuthCredential } from "./runtime-snapshots-CHAErv1O.js"; import { f as loadAuthProfileStoreForRuntime, o as findPersistedAuthProfileCredential, r as ensureAuthProfileStore } from "./store-F1B2duCT.js"; import { r as hasLocalAuthProfileStoreSource, t as hasAnyAuthProfileStoreSource } from "./source-check-BuLDxD9s.js"; import { u as resolveProfileUnusableUntilForDisplay } from "./usage-_yfLJGtN.js"; import "./auth-profiles-BdUEhE7u.js"; import { t as buildProviderAuthRecoveryHint } from "./provider-auth-recovery-hint-JMVa2F1c.js"; import { t as note } from "./note-DqHk3fA1.js"; import { n as buildAuthHealthSummary, r as formatRemainingShort, t as DEFAULT_OAUTH_WARN_MS } from "./auth-health-BDRAK0UV.js"; //#region src/commands/doctor-auth.ts /** Doctor notes for auth profile health, OAuth refresh failures, and legacy Codex config. */ const OPENAI_PROVIDER_ID = "openai"; const LEGACY_CODEX_PROVIDER_ID = "openai-codex"; const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth"; const OPENAI_BASE_URL = "https://api.openai.com/v1"; const LEGACY_CODEX_APIS = /* @__PURE__ */ new Set(["openai-responses", "openai-completions"]); const AUTH_PROFILES_CHECK_ID = "core/doctor/auth-profiles"; const DOCTOR_REAUTH_PROVIDER_ALIASES = { [LEGACY_CODEX_PROVIDER_ID]: OPENAI_PROVIDER_ID }; /** Surface the one-time relocation while the legacy shared owner is still active. */ function noteSharedAuthStoreStatus(env = process.env) { if (resolveSharedAuthStoreOwnership(env).location !== "legacy-main" || inspectPersistedSharedAuthProfileStoreRaw(env).status !== "readable") return; note("Shared auth profiles still live in the main agent database. Run `openclaw doctor --fix` to move them into shared SQLite state and make the main agent deletable.", "Shared auth store"); } function hasConfiguredCodexOAuthProfile(cfg) { return Object.values(cfg.auth?.profiles ?? {}).some((profile) => (profile.provider === OPENAI_PROVIDER_ID || profile.provider === LEGACY_CODEX_PROVIDER_ID) && profile.mode === "oauth"); } function hasStoredCodexOAuthProfile() { const store = ensureAuthProfileStore(void 0, { allowKeychainPrompt: false, readOnly: true }); return Object.values(store.profiles).some((profile) => (profile.provider === OPENAI_PROVIDER_ID || profile.provider === LEGACY_CODEX_PROVIDER_ID) && profile.type === "oauth"); } function normalizeCodexOverrideBaseUrl(baseUrl) { if (typeof baseUrl !== "string") return; return baseUrl.trim().replace(/\/+$/, ""); } function isLegacyCodexTransportShape(value, inheritedBaseUrl) { if (!isRecord(value)) return false; const api = typeof value.api === "string" ? value.api : void 0; if (!api || !LEGACY_CODEX_APIS.has(api)) return false; const baseUrl = normalizeCodexOverrideBaseUrl(value.baseUrl ?? inheritedBaseUrl); return !baseUrl || baseUrl === OPENAI_BASE_URL; } function hasLegacyCodexTransportOverride(providerOverride) { if (!isRecord(providerOverride)) return false; if (isLegacyCodexTransportShape(providerOverride)) return true; const models = providerOverride.models; if (!Array.isArray(models)) return false; return models.some((model) => isLegacyCodexTransportShape(model, providerOverride.baseUrl)); } function buildCodexProviderOverrideWarning(providerOverride) { const lines = [`- models.providers.${LEGACY_CODEX_PROVIDER_ID} contains a legacy transport override while Codex OAuth is configured.`, "- Older OpenAI transport settings can shadow the built-in Codex OAuth provider path."]; if (isRecord(providerOverride)) { const record = providerOverride; if (typeof record.api === "string") lines.push(`- models.providers.${LEGACY_CODEX_PROVIDER_ID}.api=${record.api}`); if (typeof record.baseUrl === "string") lines.push(`- models.providers.${LEGACY_CODEX_PROVIDER_ID}.baseUrl=${record.baseUrl}`); } lines.push(`- Remove or rewrite the legacy transport override to restore the built-in Codex OAuth provider path after recent fixes.`); lines.push("- Custom proxies and header-only overrides can stay; this warning only targets old OpenAI transport settings."); return lines.join("\n"); } function legacyCodexProviderOverrideToHealthFinding(providerOverride) { const message = "Legacy openai-codex transport override can shadow configured Codex OAuth credentials."; const details = buildCodexProviderOverrideWarning(providerOverride); return { checkId: AUTH_PROFILES_CHECK_ID, severity: "warning", message, path: `models.providers.${LEGACY_CODEX_PROVIDER_ID}`, target: LEGACY_CODEX_PROVIDER_ID, fixHint: details }; } /** Emits a warning when legacy Codex transport overrides can shadow configured Codex OAuth. */ function noteLegacyCodexProviderOverride(cfg) { const providerOverride = cfg.models?.providers?.[LEGACY_CODEX_PROVIDER_ID]; if (!providerOverride) return; if (!hasLegacyCodexTransportOverride(providerOverride)) return; if (!hasConfiguredCodexOAuthProfile(cfg) && !hasStoredCodexOAuthProfile()) return; note(buildCodexProviderOverrideWarning(providerOverride), CODEX_OAUTH_WARNING_TITLE); } function formatAuthNoteTitle(title, target, labelStores) { return labelStores ? `${title} (${target.label})` : title; } function listAuthProfileHealthTargets(cfg) { const targets = /* @__PURE__ */ new Map(); if (hasAnyAuthProfileStoreSource() || Object.keys(cfg.auth?.profiles ?? {}).length > 0) targets.set(resolveSharedAuthStorePath(), { label: "Shared" }); for (const agentId of listAgentIds(cfg)) { const agentDir = resolveAgentDir(cfg, agentId); const databasePath = resolveAuthProfileDatabasePath(agentDir); if (!targets.has(databasePath) && hasLocalAuthProfileStoreSource(agentDir)) targets.set(databasePath, { label: `Agent ${agentId}`, agentDir }); } return [...targets.values()]; } function formatOAuthRefreshFailureReason(reason) { switch (reason) { case "refresh_token_reused": return "refresh_token_reused"; case "expired": return "expired"; case "invalid_grant": return "invalid_grant"; case "sign_in_again": return "sign in again"; case "invalid_refresh_token": return "invalid refresh token"; case "revoked": return "revoked"; default: return "refresh failed"; } } /** Formats provider OAuth refresh failures as actionable doctor note lines. */ function formatOAuthRefreshFailureDoctorLine(params) { const classified = classifyOAuthRefreshFailure(params.message); if (!classified) return null; const rawProvider = classified.provider ?? params.provider; const provider = rawProvider ? DOCTOR_REAUTH_PROVIDER_ALIASES[rawProvider] ?? rawProvider : null; const command = buildOAuthRefreshFailureLoginCommand(provider, { profileId: provider === rawProvider ? params.profileId : void 0 }); const commandMarkdown = formatOAuthRefreshFailureLoginCommandMarkdown(command); if (classified.reason) return `- ${params.profileId}: re-auth required [${formatOAuthRefreshFailureReason(classified.reason)}] — Run ${commandMarkdown}.`; return `- ${params.profileId}: OAuth refresh failed — Try again; if this persists, run ${commandMarkdown}.`; } async function resolveAuthIssueHint(issue, cfg, store) { if (issue.reasonCode === "invalid_expires") return "Invalid token expires metadata. Set a future Unix ms timestamp or remove expires."; if (issue.reasonCode === "malformed_api_key") return "Paste the API key value, not an OpenClaw onboarding command."; const providerHint = await formatAuthDoctorHint({ cfg, store, provider: issue.provider, profileId: issue.profileId }); if (providerHint.trim()) return providerHint; return buildProviderAuthRecoveryHint({ provider: issue.provider }).replace(/^Run /, "Re-auth via "); } async function formatAuthIssueLine(issue, cfg, store) { const remaining = issue.remainingMs !== void 0 ? ` (${formatRemainingShort(issue.remainingMs)})` : ""; const hint = await resolveAuthIssueHint(issue, cfg, store); const reason = issue.reasonCode ? ` [${issue.reasonCode}]` : ""; return `- ${issue.profileId}: ${issue.status}${reason}${remaining}${hint ? ` — ${hint}` : ""}`; } function resolveAuthProfileStorePath(target) { return resolveAuthStorePathForDisplay(target.agentDir); } function authProfileIssueToHealthFinding(params) { const remaining = params.issue.remainingMs !== void 0 ? ` (${formatRemainingShort(params.issue.remainingMs)})` : ""; const reason = params.issue.reasonCode ? ` [${params.issue.reasonCode}]` : ""; const owner = params.labelStores ? `${params.target.label} auth profile` : "Auth profile"; return { checkId: AUTH_PROFILES_CHECK_ID, severity: "warning", message: `${owner} ${params.issue.profileId} is ${params.issue.status}${reason}${remaining}.`, path: resolveAuthProfileStorePath(params.target), target: params.issue.profileId, ...params.issue.reasonCode ? { requirement: params.issue.reasonCode } : {}, fixHint: params.hint ?? (params.issue.status === "expiring" ? "Run `openclaw doctor --fix` to refresh expiring OAuth profiles, or re-authenticate static tokens." : "Run `openclaw doctor --fix` to refresh OAuth profiles, or re-authenticate this provider.") }; } function collectAuthProfileCooldowns(store) { const cooldowns = []; const now = Date.now(); for (const profileId of Object.keys(store.usageStats ?? {})) { const until = resolveProfileUnusableUntilForDisplay(store, profileId); if (!until || now >= until) continue; const stats = store.usageStats?.[profileId]; const disabledActive = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil; const reason = disabledActive ? stats?.disabledReason : stats?.cooldownReason; const displayReason = disabledActive ? reason : stats?.cooldownClassification ?? reason; cooldowns.push({ profileId, kind: `${disabledActive ? "disabled" : "cooldown"}${displayReason ? `:${displayReason}` : ""}`, remaining: formatRemainingShort(until - now), hint: buildAuthProfileUnusableHint({ kind: disabledActive ? "disabled" : "cooldown", reason, provider: store.profiles[profileId]?.provider ?? findPersistedAuthProfileCredential({ profileId })?.provider ?? profileId, profileId }) }); } return cooldowns; } function authProfileCooldownToHealthFinding(params) { return { checkId: AUTH_PROFILES_CHECK_ID, severity: "warning", message: params.labelStores ? `${params.target.label} auth profile ${params.profileId} is ${params.kind} (${params.remaining}).` : `Auth profile ${params.profileId} is ${params.kind} (${params.remaining}).`, path: resolveAuthProfileStorePath(params.target), target: params.profileId, fixHint: params.hint }; } function isAuthProfileHealthIssue(profile) { if (profile.type === "api_key") return profile.status === "missing"; return (profile.type === "oauth" || profile.type === "token") && (profile.status === "expired" || profile.status === "expiring" || profile.status === "missing"); } function loadAuthProfileHealth(params) { const store = loadAuthProfileStoreForRuntime(params.target.agentDir, { inheritedAuthDir: params.target.agentDir, allowKeychainPrompt: params.allowKeychainPrompt, readOnly: params.readOnly }); const profiles = params.target.agentDir ? Object.fromEntries(Object.entries(store.profiles).filter(([profileId, local]) => local.type !== "oauth" || !shouldUseMainOwnerForLocalOAuthCredential({ local, main: findPersistedAuthProfileCredential({ profileId }) }))) : store.profiles; return { store, summary: buildAuthHealthSummary({ store: { ...store, profiles }, cfg: params.cfg, warnAfterMs: DEFAULT_OAUTH_WARN_MS, allowKeychainPrompt: params.allowKeychainPrompt }) }; } async function collectAuthProfileHealthFindingsForTarget(params) { const { store, summary } = loadAuthProfileHealth({ ...params, readOnly: true }); const findings = []; for (const cooldown of collectAuthProfileCooldowns(store)) findings.push(authProfileCooldownToHealthFinding({ ...cooldown, target: params.target, labelStores: params.labelStores })); const issues = summary.profiles.filter(isAuthProfileHealthIssue); for (const issue of issues) { const authIssue = { profileId: issue.profileId, provider: issue.provider, status: issue.status, reasonCode: issue.reasonCode, remainingMs: issue.remainingMs }; findings.push(authProfileIssueToHealthFinding({ issue: authIssue, target: params.target, labelStores: params.labelStores, hint: await resolveAuthIssueHint(authIssue, params.cfg, store) })); } return findings; } /** Collects read-only structured findings for auth profile health. */ async function collectAuthProfileHealthFindings(params) { const activeTargets = listAuthProfileHealthTargets(params.cfg); const findings = []; const labelStores = activeTargets.length > 1; for (const target of activeTargets) findings.push(...await collectAuthProfileHealthFindingsForTarget({ cfg: params.cfg, allowKeychainPrompt: params.allowKeychainPrompt ?? false, target, labelStores })); const providerOverride = params.cfg.models?.providers?.[LEGACY_CODEX_PROVIDER_ID]; if (providerOverride && hasLegacyCodexTransportOverride(providerOverride) && (hasConfiguredCodexOAuthProfile(params.cfg) || hasStoredCodexOAuthProfile())) findings.push(legacyCodexProviderOverrideToHealthFinding(providerOverride)); return findings; } async function noteAuthProfileHealthForTarget(params) { let { store, summary } = loadAuthProfileHealth(params); const noteTitle = (title) => formatAuthNoteTitle(title, params.target, params.labelStores); const unusable = collectAuthProfileCooldowns(store).map(({ profileId, kind, remaining, hint }) => `- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`); if (unusable.length > 0) note(unusable.join("\n"), noteTitle("Auth profile cooldowns")); const findIssues = () => summary.profiles.filter(isAuthProfileHealthIssue); let issues = findIssues(); if (issues.length === 0) return []; const refreshTargets = issues.filter((issue) => issue.type === "oauth" && [ "expired", "expiring", "missing" ].includes(issue.status)); if (refreshTargets.length > 0 && await params.prompter.confirmAutoFix({ message: "Refresh expiring OAuth tokens now? (static tokens need re-auth)", initialValue: true })) { const errors = []; for (const profile of refreshTargets) try { await resolveApiKeyForProfile({ cfg: params.cfg, store, profileId: profile.profileId, agentDir: params.target.agentDir, forceRefresh: true }); } catch (err) { const message = formatErrorMessage(err); errors.push(formatOAuthRefreshFailureDoctorLine({ profileId: profile.profileId, provider: profile.provider, message }) ?? `- ${profile.profileId}: ${message}`); } if (errors.length > 0) note(errors.join("\n"), noteTitle("OAuth refresh errors")); ({store, summary} = loadAuthProfileHealth({ ...params, allowKeychainPrompt: false })); issues = findIssues(); } return Promise.all(issues.map((issue) => formatAuthIssueLine(issue, params.cfg, store))); } /** Checks configured agent auth stores and emits doctor notes for stale or unusable profiles. */ async function noteAuthProfileHealth(params) { const activeTargets = listAuthProfileHealthTargets(params.cfg); if (activeTargets.length === 0) return; const labelStores = activeTargets.length > 1; const storesByIssueLine = /* @__PURE__ */ new Map(); for (const target of activeTargets) for (const line of await noteAuthProfileHealthForTarget({ ...params, target, labelStores })) { const labels = storesByIssueLine.get(line) ?? /* @__PURE__ */ new Set(); storesByIssueLine.set(line, labels.add(target.label)); } if (storesByIssueLine.size === 0) return; const lines = [...storesByIssueLine.entries()].toSorted(([left], [right]) => left.localeCompare(right)).map(([line, labels]) => labels.size === activeTargets.length ? line : `${line} (stores: ${[...labels].toSorted().join(", ")})`); note(lines.join("\n"), "Model auth"); } //#endregion export { collectAuthProfileHealthFindings, noteAuthProfileHealth, noteLegacyCodexProviderOverride, noteSharedAuthStoreStatus };