import { SEEDED_DEFAULT_BACKEND_ID, makeDefaultLocalBackend, makeLockedCloudBackend, } from "./default-backend"; import type { Backend, BackendAuthMode, BackendKind, BackendSelection, } from "./types"; export const BACKENDS_STORAGE_KEY = "openhands-backends"; export const ACTIVE_BACKEND_STORAGE_KEY = "openhands-active-backend"; function isValidKind(value: unknown): value is BackendKind { return value === "local" || value === "cloud"; } function isValidAuthMode(value: unknown): value is BackendAuthMode { return value === undefined || value === "api-key" || value === "cookie"; } function isValidBackend(value: unknown): value is Backend { if (typeof value !== "object" || value === null) return false; const v = value as Partial; return ( typeof v.id === "string" && v.id.length > 0 && typeof v.name === "string" && typeof v.host === "string" && typeof v.apiKey === "string" && isValidKind(v.kind) && isValidAuthMode(v.authMode) && (v.connectionRevision === undefined || (typeof v.connectionRevision === "number" && Number.isSafeInteger(v.connectionRevision) && v.connectionRevision >= 0)) ); } function isLoopbackUrl(value: string): boolean { try { const { hostname } = new URL(value); return ( hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" ); } catch { return false; } } function shouldSyncLauncherDefaultLocalBackend( backend: Backend, defaultBackend: Backend, ): boolean { if (backend.id !== SEEDED_DEFAULT_BACKEND_ID || backend.kind !== "local") { return false; } return ( backend.host === defaultBackend.host || (isLoopbackUrl(backend.host) && isLoopbackUrl(defaultBackend.host)) ); } function syncLauncherDefaultLocalBackend(backends: Backend[]): Backend[] { const defaultBackend = makeDefaultLocalBackend(); if (!defaultBackend) return backends; let didSync = false; const syncedBackends = backends.map((backend) => { if (!shouldSyncLauncherDefaultLocalBackend(backend, defaultBackend)) { return backend; } if (backend.apiKey === defaultBackend.apiKey) return backend; didSync = true; return { ...backend, apiKey: defaultBackend.apiKey, }; }); if (!didSync) return backends; writeStoredBackends(syncedBackends); return syncedBackends; } export function writeStoredBackends(backends: Backend[]): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(BACKENDS_STORAGE_KEY, JSON.stringify(backends)); } catch { /* ignore quota / serialization errors */ } } export function readStoredBackends(): Backend[] { if (typeof window === "undefined") return []; try { const lockedCloudBackend = makeLockedCloudBackend(); if (lockedCloudBackend) { writeStoredBackends([lockedCloudBackend]); const activeSelection = readStoredActiveBackend(); if (activeSelection?.backendId !== lockedCloudBackend.id) { writeStoredActiveBackend({ backendId: lockedCloudBackend.id }); } return [lockedCloudBackend]; } const raw = window.localStorage.getItem(BACKENDS_STORAGE_KEY); // First install: seed only when the launcher supplied enough information // for a usable local backend. if (raw === null) { const defaultBackend = makeDefaultLocalBackend(); if (!defaultBackend) return []; writeStoredBackends([defaultBackend]); return [defaultBackend]; } const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; const valid = parsed.filter(isValidBackend); // If the stored array is empty (or everything in it failed validation), // only re-seed when the launcher supplied both a host and API key. if (valid.length === 0) { const defaultBackend = makeDefaultLocalBackend(); if (!defaultBackend) return []; writeStoredBackends([defaultBackend]); return [defaultBackend]; } const synced = syncLauncherDefaultLocalBackend(valid); return synced; } catch { return []; } } function parseBackendSelection(raw: string | null): BackendSelection | null { if (!raw) return null; try { const parsed = JSON.parse(raw); if ( typeof parsed !== "object" || parsed === null || typeof (parsed as BackendSelection).backendId !== "string" ) { return null; } const orgIdRaw = (parsed as BackendSelection).orgId; return { backendId: (parsed as BackendSelection).backendId, orgId: typeof orgIdRaw === "string" && orgIdRaw.length > 0 ? orgIdRaw : null, }; } catch { return null; } } function readStorageItem( storage: Storage | undefined, key: string, ): string | null { try { return storage?.getItem(key) ?? null; } catch { return null; } } function writeStorageItem( storage: Storage | undefined, key: string, value: string, ): void { try { storage?.setItem(key, value); } catch { /* ignore */ } } function removeStorageItem(storage: Storage | undefined, key: string): void { try { storage?.removeItem(key); } catch { /* ignore */ } } export function readStoredActiveBackend(): BackendSelection | null { if (typeof window === "undefined") return null; // Active backend is tab-scoped so reloading tab A does not adopt tab B's // backend. localStorage remains a last-used fallback for fresh tabs and old // persisted state. const sessionSelection = parseBackendSelection( readStorageItem(window.sessionStorage, ACTIVE_BACKEND_STORAGE_KEY), ); if (sessionSelection) return sessionSelection; return parseBackendSelection( readStorageItem(window.localStorage, ACTIVE_BACKEND_STORAGE_KEY), ); } export function writeStoredActiveBackend( selection: BackendSelection | null, ): void { if (typeof window === "undefined") return; if (!selection) { removeStorageItem(window.sessionStorage, ACTIVE_BACKEND_STORAGE_KEY); removeStorageItem(window.localStorage, ACTIVE_BACKEND_STORAGE_KEY); return; } const serialized = JSON.stringify({ backendId: selection.backendId, orgId: selection.orgId ?? null, }); // Mirror to localStorage only as the default for new tabs/backward // compatibility; reads in existing tabs prefer sessionStorage. writeStorageItem( window.sessionStorage, ACTIVE_BACKEND_STORAGE_KEY, serialized, ); writeStorageItem(window.localStorage, ACTIVE_BACKEND_STORAGE_KEY, serialized); }