UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

308 lines (307 loc) • 16.6 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import "./utils-DgZn2dLu.mjs"; import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import { a as resolveAgentDir } from "./agent-scope-config-CDYpzA0r.mjs"; import { i as listAgentIds } from "./agent-roster-BXD1uMJD.mjs"; import { n as readConfigMachineState } from "./config-machine-state-DR7LQrw2.mjs"; import { r as updateConfigMachineState } from "./config-machine-state-write-pMcF7uIi.mjs"; import "./agent-scope-CG17uXFn.mjs"; import { i as formatAuthDoctorHint, n as resolveApiKeyForProfile } from "./oauth-BEBXh1nm.mjs"; import { c as resolveSharedAuthStorePath, o as resolveSharedAuthStoreOwnership } from "./path-resolve-CIRhdX6A.mjs"; import { g as resolveAuthProfileDatabasePath, s as inspectPersistedSharedAuthProfileStoreRaw } from "./sqlite-CW8wNN4G.mjs"; import { m as resolveProfileUnusableUntilForDisplay } from "./profile-usage-stats-BLwxBqRR.mjs"; import { i as findPersistedAuthProfileCredential } from "./store-Bn_NKkGr.mjs"; import { b as resolveAuthStorePathForDisplay } from "./profiles-zTWlOxGK.mjs"; import { a as buildOAuthRefreshFailureLoginCommand, i as buildAuthProfileUnusableHint, l as formatOAuthRefreshFailureLoginCommandMarkdown, o as classifyOAuthRefreshFailure } from "./oauth-refresh-failure-B2gKOXl5.mjs"; import { S as shouldUseMainOwnerForLocalOAuthCredential } from "./runtime-snapshot-owner-BFahRX4y.mjs"; import { r as hasLocalAuthProfileStoreSource, t as hasAnyAuthProfileStoreSource } from "./source-check-BwAkkwWI.mjs"; import { n as ensureAuthProfileStore, o as loadAuthProfileStoreForRuntime } from "./store-runtime-DZk7O5wt.mjs"; import "./auth-profiles-DnH-eEC0.mjs"; import { n as buildAuthHealthSummary, r as formatRemainingShort, t as DEFAULT_OAUTH_WARN_MS } from "./auth-health-gS8rupA3.mjs"; import { t as note } from "./note-Byxgt4MB.mjs"; import { t as buildProviderAuthRecoveryHint } from "./provider-auth-recovery-hint-DqLNwb84.mjs"; //#region src/commands/doctor-auth.ts 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 COPILOT_NOTICE_KEY = "doctor.githubCopilotAmbientTokenNotice"; const DOCTOR_REAUTH_PROVIDER_ALIASES = { [LEGACY_CODEX_PROVIDER_ID]: OPENAI_PROVIDER_ID }; /** Explain the retired ambient-token activation once per state directory. */ function noteCopilotAmbientToken(cfg, env = process.env) { if (!(env.GH_TOKEN?.trim() || env.GITHUB_TOKEN?.trim()) || env.COPILOT_GITHUB_TOKEN?.trim() || cfg.models?.providers?.["github-copilot"] || Object.values(cfg.auth?.profiles ?? {}).some((profile) => profile.provider === "github-copilot") || readConfigMachineState(COPILOT_NOTICE_KEY, { env })) return; const agentDirs = [void 0, ...listAgentIds(cfg).map((id) => resolveAgentDir(cfg, id, env))]; for (const agentDir of agentDirs) { const store = loadAuthProfileStoreForRuntime(agentDir, { readOnly: true, allowKeychainPrompt: false }, env); if (Object.values(store.profiles).some((profile) => profile.provider === "github-copilot")) return; } let claimed = false; updateConfigMachineState(COPILOT_NOTICE_KEY, (shown) => { claimed = shown !== true; return true; }, { env }); if (claimed) note("GitHub Copilot is no longer enabled by GH_TOKEN/GITHUB_TOKEN. To use Copilot, run `openclaw models auth login --provider github-copilot` or set COPILOT_GITHUB_TOKEN.", "GitHub Copilot"); } /** 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 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 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": case "expired": case "invalid_grant": case "revoked": return reason; case "sign_in_again": return "sign in again"; case "invalid_refresh_token": return "invalid refresh token"; default: return "refresh failed"; } } 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 "); } 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 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({ profileId, local, main: findPersistedAuthProfileCredential({ profileId }) }))) : store.profiles; return { store, summary: buildAuthHealthSummary({ store: { ...store, profiles }, cfg: params.cfg, warnAfterMs: DEFAULT_OAUTH_WARN_MS, allowKeychainPrompt: params.allowKeychainPrompt }) }; } /** 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) { const { store, summary } = loadAuthProfileHealth({ cfg: params.cfg, allowKeychainPrompt: params.allowKeychainPrompt ?? false, readOnly: true, target }); const owner = labelStores ? `${target.label} auth profile` : "Auth profile"; for (const cooldown of collectAuthProfileCooldowns(store)) findings.push({ checkId: AUTH_PROFILES_CHECK_ID, severity: "warning", message: `${owner} ${cooldown.profileId} is ${cooldown.kind} (${cooldown.remaining}).`, path: resolveAuthStorePathForDisplay(target.agentDir), target: cooldown.profileId, fixHint: cooldown.hint }); for (const issue of summary.profiles.filter(isAuthProfileHealthIssue)) { const hint = await resolveAuthIssueHint(issue, params.cfg, store); const remaining = issue.remainingMs !== void 0 ? ` (${formatRemainingShort(issue.remainingMs)})` : ""; const reason = issue.reasonCode ? ` [${issue.reasonCode}]` : ""; findings.push({ checkId: AUTH_PROFILES_CHECK_ID, severity: "warning", message: `${owner} ${issue.profileId} is ${issue.status}${reason}${remaining}.`, path: resolveAuthStorePathForDisplay(target.agentDir), target: issue.profileId, ...issue.reasonCode ? { requirement: issue.reasonCode } : {}, fixHint: hint ?? (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.") }); } } const providerOverride = params.cfg.models?.providers?.[LEGACY_CODEX_PROVIDER_ID]; if (providerOverride && hasLegacyCodexTransportOverride(providerOverride) && (hasConfiguredCodexOAuthProfile(params.cfg) || hasStoredCodexOAuthProfile())) findings.push({ checkId: AUTH_PROFILES_CHECK_ID, severity: "warning", message: "Legacy openai-codex transport override can shadow configured Codex OAuth credentials.", path: `models.providers.${LEGACY_CODEX_PROVIDER_ID}`, target: LEGACY_CODEX_PROVIDER_ID, fixHint: buildCodexProviderOverrideWarning(providerOverride) }); return findings; } async function noteAuthProfileHealthForTarget(params) { let { store, summary } = loadAuthProfileHealth(params); const noteTitle = (title) => params.labelStores ? `${title} (${params.target.label})` : title; 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")); let issues = summary.profiles.filter(isAuthProfileHealthIssue); if (issues.length === 0) return []; const refreshTargets = issues.filter((issue) => issue.type === "oauth"); 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 = summary.profiles.filter(isAuthProfileHealthIssue); } return Promise.all(issues.map(async (issue) => { const remaining = issue.remainingMs !== void 0 ? ` (${formatRemainingShort(issue.remainingMs)})` : ""; const hint = await resolveAuthIssueHint(issue, params.cfg, store); const reason = issue.reasonCode ? ` [${issue.reasonCode}]` : ""; return `- ${issue.profileId}: ${issue.status}${reason}${remaining}${hint ? ` — ${hint}` : ""}`; })); } 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, noteCopilotAmbientToken, noteLegacyCodexProviderOverride, noteSharedAuthStoreStatus };