UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

450 lines (449 loc) • 22.2 kB
import "./src-CdRlGRnW.mjs"; import { t as expectDefined } from "./expect-lbe3Hgrh.mjs"; import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs"; import { n as resolvePathViaExistingAncestorSync } from "./boundary-path-CAv_plX7.mjs"; import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import { a as resolveAgentDir, v as resolveMutableAgentEntry } from "./agent-scope-config-CDYpzA0r.mjs"; import { M as parseAgentSessionKey } from "./session-key-C09ow4h0.mjs"; import { i as listAgentIds } from "./agent-roster-BXD1uMJD.mjs"; import { l as toAgentModelListLike, o as resolveAgentModelFallbackValues } from "./model-input-C9K4a_6k.mjs"; import { t as splitTrailingAuthProfile } from "./model-ref-profile-BNjB2hED.mjs"; import { u as resolveAgentModelFallbacksOverride } from "./agent-scope-CG17uXFn.mjs"; import { i as resolveProviderIdForAuth } from "./provider-auth-aliases-DGVQj_99.mjs"; import { C as attachRuntimeConfigWriteApplication, T as createRuntimeConfigWriteApplication } from "./io.runtime-BsUXSsD4.mjs"; import { c as resolveSharedAuthStorePath } from "./path-resolve-CIRhdX6A.mjs"; import { c as prepareOpenClawAgentDatabaseRegistrySnapshotRead } from "./openclaw-agent-db-registry-listing-BmvvQ59E.mjs"; import { g as resolveAuthProfileDatabasePath } from "./sqlite-CW8wNN4G.mjs"; import { i as findPersistedAuthProfileCredential, u as resolvePersistedAuthProfileOwnerAgentDir } from "./store-Bn_NKkGr.mjs"; import { g as listCandidateAuthProfileStores, i as removeAuthProfilesAcrossOwnerStores } from "./profiles-zTWlOxGK.mjs"; import { i as captureGatewayRootWorkAdmissionContinuationScope } from "./gateway-work-admission-rc4ZgKq_.mjs"; import { f as listProfilesForProvider } from "./order-BJ9-s7yV.mjs"; import { _ as resolveProviderConfigSecretInput, y as resolveProviderEntryApiKeyProfileReference } from "./model-auth-provider-config-C6nCoXKi.mjs"; import { i as ensureAuthProfileStoreWithoutExternalProfiles, l as loadAuthProfileStoreWithoutExternalProfiles } from "./store-runtime-DZk7O5wt.mjs"; import "./auth-profiles-DnH-eEC0.mjs"; import { d as withSessionHistoryWorkerReadCandidates, s as withSessionHistoryWorkerDatabase } from "./session-transcript-worker-runtime-Dty5DpKL.mjs"; import { c as patchSessionEntryTarget } from "./session-accessor.sqlite-entry-Dn_MsDKD.mjs"; import { r as prepareSessionStoreTargetInventory } from "./session-store-target-inventory-Dj6Sq0En.mjs"; import "./session-accessor-BDmTPdtx.mjs"; import { i as clearRemovedQueuedAuthProfiles } from "./state-22A-PMUF.mjs"; import "./queue-CXufbg1P.mjs"; import { i as removeAuthProfileConfig, r as configReferencesAuthProfile } from "./provider-auth-helpers-CMstK74G.mjs"; import { t as createClackPrompter } from "./clack-prompter-D76Rt341.mjs"; import { r as logConfigUpdated } from "./logging-BBc-wU1i.mjs"; import { l as resolveModelsTargetAgent, u as updateConfig } from "./shared-zVYpFkgx.mjs"; import { t as loadModelsConfig } from "./load-config-CI2enNTl.mjs"; import { n as refreshRunningGatewayAuthState } from "./auth-refresh-B8Oo71H-.mjs"; import { isDeepStrictEqual } from "node:util"; //#region src/sessions/auth-profile-removal.ts /** Release only references to credentials whose owning store confirmed removal. */ async function clearRemovedSessionAuthProfiles(params) { clearRemovedQueuedAuthProfiles(params); if (params.removedByAgent.size === 0) return; const { candidates, ...inventory } = prepareSessionStoreTargetInventory(params.cfg, [...params.removedByAgent.keys()]); const registry = prepareOpenClawAgentDatabaseRegistrySnapshotRead({ env: inventory.env }); await withSessionHistoryWorkerReadCandidates(candidates, async (discovery) => { let sources = await discovery.readTargetInventory({ ...inventory, registeredDatabases: { status: "deferred" } }); if (sources.kind === "session-target-registry-required") { const current = await registry.read(); sources = await discovery.readTargetInventory({ ...inventory, registeredDatabases: current.result.status === "available" ? current.result.entries : { status: "unavailable" } }); } if (sources.kind !== "session-target-inventory") throw new Error("Session stores could not be resolved after removing the account."); for (const { agentId, result, reads } of sources.agents) { if (!result.available) { if (result.reason === "database-missing") continue; throw new Error(`Sessions for agent ${agentId} could not be read after removing the account.`); } const removed = expectDefined(params.removedByAgent.get(agentId), "removed agent profiles"); const agentDir = resolveAgentDir(params.cfg, agentId); const isRemoved = (profileId) => profileId !== void 0 && removed.has(profileId); for (const { database } of reads) await withSessionHistoryWorkerDatabase({ ...database, env: inventory.env }, async (owner) => { const assertCurrent = () => { discovery.assertCurrent(); registry.assertCurrent(); owner.assertCurrent(); }; const sessionKeys = (await owner.readEntries({ agentId: database.agentId, storePath: database.path, env: inventory.env, projection: "list", readConsistency: "latest" })).filter(({ sessionKey, entry }) => normalizeAgentId(parseAgentSessionKey(sessionKey)?.agentId ?? database.agentId) === agentId && (isRemoved(entry.authProfileOverride) || isRemoved(entry.modelFallback?.prevAuthProfileOverride))).map(({ sessionKey }) => sessionKey); if (sessionKeys.length === 0) return; const selected = await owner.readExactEntries({ env: inventory.env, sessionKeys, includeAuthorization: true }); assertCurrent(); if (selected.entries.length === 0) return; const identity = expectDefined(selected.databaseIdentity, "session database identity"); for (const { sessionKey, entry } of selected.entries) { const profileIds = new Set([entry.authProfileOverride, entry.modelFallback?.prevAuthProfileOverride].filter(isRemoved)); for (const profileId of profileIds) await patchSessionEntryTarget({ agentId, storePath: database.path, env: inventory.env, readSource: { ...database, databaseIdentity: identity.identity, databaseBirthtime: identity.birthtime }, target: { canonicalKey: sessionKey, storeKeys: [sessionKey] } }, (current) => { const clearCurrent = current.authProfileOverride === profileId; const fallback = current.modelFallback; const clearPrevious = fallback?.prevAuthProfileOverride === profileId; if (!clearCurrent && !clearPrevious) return null; return { ...clearCurrent ? { authProfileOverride: void 0, authProfileOverrideSource: void 0, authProfileOverrideCompactionCount: void 0 } : {}, ...clearPrevious ? { modelFallback: { ...fallback, prevAuthProfileOverride: void 0, prevAuthProfileOverrideSource: void 0, prevAuthProfileOverrideCompactionCount: void 0 } } : {} }; }, { preserveActivity: true, skipMaintenance: true, shouldCommit: () => !findPersistedAuthProfileCredential({ agentDir, profileId }), assertCommitAllowed: assertCurrent }); } }); } }); } //#endregion //#region src/commands/models/auth-logout-selections.ts /** Capture logical consumers while the selected credential still has its physical owner. */ async function resolveModelAuthProfileRemoval(cfg, scopes) { const owners = /* @__PURE__ */ new Map(); for (const scope of scopes) { const ids = owners.get(scope.databasePath) ?? /* @__PURE__ */ new Set(); for (const id of scope.profileIds) ids.add(id); owners.set(scope.databasePath, ids); } const sharedPath = resolvePathViaExistingAncestorSync(resolveSharedAuthStorePath()); const shared = owners.get(sharedPath) ?? /* @__PURE__ */ new Set(); const agents = /* @__PURE__ */ new Map(); if (owners.size === 0) return { shared, agents }; const agentDirs = new Map(listAgentIds(cfg).map((id) => [id, resolveAgentDir(cfg, id)])); for (const candidate of await listCandidateAuthProfileStores({ cfg })) if (!agentDirs.has(candidate.agentId)) agentDirs.set(candidate.agentId, candidate.agentDir); const profileIds = new Set(scopes.flatMap((scope) => [...scope.profileIds])); for (const [agentId, agentDir] of agentDirs) { const removed = /* @__PURE__ */ new Set(); for (const profileId of profileIds) { if (!findPersistedAuthProfileCredential({ agentDir, profileId })) continue; const owner = resolvePersistedAuthProfileOwnerAgentDir({ agentDir, profileId }); const databasePath = owner ? resolvePathViaExistingAncestorSync(resolveAuthProfileDatabasePath(owner)) : sharedPath; if (owners.get(databasePath)?.has(profileId)) removed.add(profileId); } if (removed.size > 0) agents.set(agentId, removed); } return { shared, agents }; } function excludeSurvivingModelAuthProfiles(planned, surviving) { return { shared: new Set([...planned.shared].filter((id) => !surviving.shared.has(id))), agents: new Map([...planned.agents].map(([agentId, ids]) => [agentId, new Set([...ids].filter((id) => !surviving.agents.get(agentId)?.has(id)))]).filter(([, ids]) => ids.size > 0)) }; } /** Revalidate after removal awaits, before synchronous queued-work cleanup. */ function excludeReconnectedModelAuthProfiles(cfg, planned) { const surviving = (ids, agentDir) => new Set([...ids].filter((profileId) => findPersistedAuthProfileCredential({ agentDir, profileId }))); return excludeSurvivingModelAuthProfiles(planned, { shared: surviving(planned.shared), agents: new Map([...planned.agents].map(([id, ids]) => [id, surviving(ids, resolveAgentDir(cfg, id))])) }); } /** Release only deleted account selections; model choices and unrelated accounts survive. */ function removeModelAuthProfileSelections(cfg, removed) { if (!cfg.agents) return cfg; const strip = (ref, ids) => { if (!ref || !ids?.size) return ref; const { model, profile } = splitTrailingAuthProfile(ref); return profile && ids.has(profile) ? model : ref; }; const stripModel = (model, ids) => { if (typeof model === "string" || model === void 0) return strip(model, ids); return { ...model, ...model.primary !== void 0 ? { primary: strip(model.primary, ids) } : {}, ...model.fallbacks ? { fallbacks: model.fallbacks.map((ref) => strip(ref, ids)) } : {} }; }; const next = structuredClone(cfg); const beforeDefaults = cfg.agents.defaults; const defaults = next.agents?.defaults; if (defaults) { if (defaults.model !== void 0) defaults.model = stripModel(defaults.model, removed.shared); if (defaults.utilityModel !== void 0) defaults.utilityModel = strip(defaults.utilityModel, removed.shared); } for (const agentId of listAgentIds(cfg)) { let entry = resolveMutableAgentEntry(next, agentId); const implicitAgent = !entry; if (!entry && next.agents) { next.agents.entries = { [agentId]: {} }; entry = next.agents.entries[agentId]; } if (!entry) continue; const ids = removed.agents.get(agentId); const originalModel = entry.model; const own = toAgentModelListLike(originalModel); const inherited = toAgentModelListLike(beforeDefaults?.model); const nextInherited = toAgentModelListLike(defaults?.model); const model = toAgentModelListLike(stripModel(originalModel, ids)) ?? {}; if (own?.primary === void 0 && inherited?.primary !== void 0) { const primary = strip(inherited.primary, ids); if (primary !== nextInherited?.primary) model.primary = primary; } if (Object.keys(model).length > 0) entry.model = typeof originalModel === "string" && model.fallbacks === void 0 ? model.primary : model; const fallbacks = (resolveAgentModelFallbacksOverride(cfg, agentId) ?? resolveAgentModelFallbackValues(beforeDefaults?.model)).map((ref) => strip(ref, ids)); const nextFallbacks = resolveAgentModelFallbacksOverride(next, agentId) ?? resolveAgentModelFallbackValues(defaults?.model); if (!isDeepStrictEqual(fallbacks, nextFallbacks)) entry.model = { ...toAgentModelListLike(entry.model), fallbacks }; const utility = strip(entry.utilityModel ?? beforeDefaults?.utilityModel, ids); if (entry.utilityModel !== void 0 || utility !== defaults?.utilityModel) entry.utilityModel = utility; if (implicitAgent && Object.keys(entry).length === 0 && next.agents) delete next.agents.entries; } return isDeepStrictEqual(cfg, next) ? cfg : next; } //#endregion //#region src/commands/models/auth-logout.ts const MISSING_CONFIG_VALUE = Symbol("missing-config-value"); function restoreConfigMutationValue(current, before, after) { if (isDeepStrictEqual(before, after)) return current; if (isDeepStrictEqual(current, after)) return before; const currentRecord = asOptionalRecord(current); const beforeRecord = asOptionalRecord(before); const afterRecord = asOptionalRecord(after); if (!currentRecord || !beforeRecord || !afterRecord) return current; const restored = { ...currentRecord }; const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)]); for (const key of keys) { const value = restoreConfigMutationValue(Object.hasOwn(currentRecord, key) ? currentRecord[key] : MISSING_CONFIG_VALUE, Object.hasOwn(beforeRecord, key) ? beforeRecord[key] : MISSING_CONFIG_VALUE, Object.hasOwn(afterRecord, key) ? afterRecord[key] : MISSING_CONFIG_VALUE); if (value === MISSING_CONFIG_VALUE) delete restored[key]; else restored[key] = value; } return restored; } function restoreCredentialConfigMutation(params) { return restoreConfigMutationValue(params.current, params.before, params.after); } function restoreSurvivingProfileOrder(params) { const order = { ...params.after.auth?.order }; for (const [provider, beforeOrder] of Object.entries(params.before.auth?.order ?? {})) { const afterOrder = params.after.auth?.order?.[provider] ?? []; const restored = beforeOrder.filter((profileId) => afterOrder.includes(profileId) || params.survivingProfileIds.has(profileId)); for (const profileId of afterOrder) if (!restored.includes(profileId)) restored.push(profileId); if (restored.length > 0 || Object.hasOwn(order, provider)) order[provider] = restored; } if (Object.keys(order).length === 0 && !params.desired.auth) return params.desired; const { order: _removedOrder, ...auth } = params.desired.auth ?? {}; return { ...params.desired, auth: { ...auth, ...Object.keys(order).length > 0 ? { order } : {} } }; } function removeCredentialConfigReferences(params) { let next = params.current; for (const id of params.profileIds) next = removeAuthProfileConfig(next, id); if (params.apiKeyProvider === void 0 || !next.models?.providers) return next; const owner = resolveProviderIdForAuth(params.apiKeyProvider, { config: next }); const providers = { ...next.models.providers }; for (const [provider, entry] of Object.entries(providers)) if (resolveProviderIdForAuth(provider, { config: next }) === owner && resolveProviderEntryApiKeyProfileReference({ cfg: params.runtimeConfig, sourceConfig: params.runtimeConfig, provider, store: params.store }).kind === "literal") { const { apiKey: _removed, ...connection } = entry; providers[provider] = connection; } return { ...next, models: { ...next.models, providers } }; } /** Clears selected config references before deleting the credentials they name. */ async function removeModelAuthCredentials(params) { const apiKeyProvider = params.apiKeyProvider; const keyBindings = (cfg, sourceConfig) => { const owner = apiKeyProvider === void 0 ? void 0 : resolveProviderIdForAuth(apiKeyProvider, { config: cfg }); return { owner, bindings: Object.fromEntries(Object.entries(cfg.models?.providers ?? {}).filter(([provider, entry]) => entry.apiKey !== void 0 && resolveProviderIdForAuth(provider, { config: cfg }) === owner).map(([provider]) => { const { providerConfig, ref } = resolveProviderConfigSecretInput(cfg, provider, sourceConfig); return [provider, ref ?? providerConfig?.apiKey]; })) }; }; const expectedBindings = apiKeyProvider === void 0 ? void 0 : keyBindings(params.cfg); const application = createRuntimeConfigWriteApplication(captureGatewayRootWorkAdmissionContinuationScope()?.run); let configChanged = false; let confirmedSelections; let cleanup; const beforeRemove = async (profileIds, scopes = []) => { await updateConfig(async (current, { runtimeConfig }) => { if (expectedBindings && !isDeepStrictEqual(keyBindings(runtimeConfig, runtimeConfig), expectedBindings)) throw new Error("The key changed while removing it. Nothing was removed. Reload Models and retry removal."); const store = loadAuthProfileStoreWithoutExternalProfiles(params.agentDir, { allowKeychainPrompt: false }); if (apiKeyProvider !== void 0 && profileIds.some((id) => { const credential = store.profiles[id]; return credential?.type !== "api_key" || Boolean(credential.keyRef) || resolveProviderIdForAuth(credential.provider, { config: current, storedCredential: true }) !== resolveProviderIdForAuth(apiKeyProvider, { config: current }); })) throw new Error("The selected API key changed. Reload Models and retry removal."); const selections = await resolveModelAuthProfileRemoval(runtimeConfig, scopes); const next = removeModelAuthProfileSelections(removeCredentialConfigReferences({ current, runtimeConfig, profileIds, store, ...apiKeyProvider !== void 0 ? { apiKeyProvider } : {} }), selections); cleanup = { before: current, after: next, profileIds, selections }; configChanged = !isDeepStrictEqual(current, next); return next; }, void 0, void 0, attachRuntimeConfigWriteApplication({}, application)); }; const restoreIncompleteRemoval = async (survivingProfiles, scopes = []) => { const cleanupState = cleanup; if (!cleanupState) return; const survivingProfileIds = new Set(survivingProfiles.keys()); let withoutSurvivors = cleanupState.before; for (const profileId of cleanupState.profileIds) if (survivingProfileIds.has(profileId)) withoutSurvivors = removeAuthProfileConfig(withoutSurvivors, profileId); let desired = restoreSurvivingProfileOrder({ desired: restoreCredentialConfigMutation({ current: cleanupState.after, before: cleanupState.before, after: withoutSurvivors }), before: cleanupState.before, after: cleanupState.after, survivingProfileIds }); const removedSelections = excludeSurvivingModelAuthProfiles(cleanupState.selections, await resolveModelAuthProfileRemoval(cleanupState.before, scopes)); confirmedSelections = removedSelections; desired = restoreCredentialConfigMutation({ current: desired, before: removeModelAuthProfileSelections(cleanupState.before, removedSelections), after: removeModelAuthProfileSelections(cleanupState.before, cleanupState.selections) }); await updateConfig((current) => restoreCredentialConfigMutation({ current, before: desired, after: cleanupState.after })); cleanupState.selections = removedSelections; }; let removed; try { removed = await removeAuthProfilesAcrossOwnerStores({ cfg: params.cfg, agentDir: params.agentDir, profileIds: params.profileIds, beforeRemove, onIncomplete: restoreIncompleteRemoval, ...params.provider !== void 0 ? { provider: params.provider } : {} }); if (removed && cleanup) confirmedSelections = cleanup.selections; } finally { if (cleanup && confirmedSelections) { const selections = excludeReconnectedModelAuthProfiles(cleanup.after, confirmedSelections); await clearRemovedSessionAuthProfiles({ cfg: cleanup.after, removedByAgent: selections.agents, rewriteConfig: (cfg) => removeModelAuthProfileSelections(cfg, selections) }); } } if (!removed) throw new Error("Saved credentials could not be removed. Wait a moment and retry."); if (configChanged && !(application.claimed && await application.result === "applied")) return "Credentials were removed, but the Gateway has not confirmed applying the change. Run `openclaw gateway restart` to apply it."; } async function modelsAuthLogoutCommand(opts, runtime) { const profileId = opts.profileId?.trim(); if (!profileId) throw new Error(`Missing profile id. Run ${formatCliCommand("openclaw models auth list")} to see saved profile ids.`); const cfg = await loadModelsConfig({ commandName: "models auth logout", runtime }); const { agentId, agentDir } = resolveModelsTargetAgent(cfg, opts.agent, { kind: "mutation" }); const store = ensureAuthProfileStoreWithoutExternalProfiles(agentDir); const credential = store.profiles[profileId]; if (!credential) throw new Error(`Auth profile "${profileId}" not found for agent "${agentId}". Run ${formatCliCommand(`openclaw models auth list --agent ${agentId}`)} to see saved profile ids.`); const description = `${profileId} (${credential.provider}/${credential.type})`; if (!opts.yes) { if (!process.stdin.isTTY) throw new Error(`Refusing to remove auth profile ${description} without confirmation. Pass --yes to remove it non-interactively.`); if (!await createClackPrompter().confirm({ message: `Remove auth profile ${description} from agent ${agentId}?`, initialValue: false })) { runtime.log("Cancelled."); return; } } const warning = await removeModelAuthCredentials({ cfg, agentDir, profileIds: [profileId] }); if (configReferencesAuthProfile(cfg, profileId)) logConfigUpdated(runtime); await refreshRunningGatewayAuthState(agentId, "logout", runtime); if (warning) runtime.error(warning); runtime.log(`Agent: ${agentId}`); runtime.log(`Removed auth profile: ${description}`); if (listProfilesForProvider(store, credential.provider).filter((id) => id !== profileId).length === 0) runtime.log(`No auth profiles remain for ${credential.provider}. Run ${formatCliCommand(`openclaw models auth login --provider ${credential.provider}`)} to sign in again.`); } //#endregion export { modelsAuthLogoutCommand, removeModelAuthCredentials };