UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

783 lines (782 loc) • 34.6 kB
import { w as resolveStateDir } from "./paths-D2sRr1a_.js"; import "./state-paths-2IWTvznc.js"; import { t as BROWSER_NATIVE_HOST_NAME } from "./extension-native-host-BDsbHgRh.js"; import { constants } from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; import os from "node:os"; import crypto from "node:crypto"; //#region extensions/browser/src/browser/extension-install-layout.ts const EXTENSION_ID_PATTERN = /^[a-p]{32}$/; const UNPACKED_MANIFEST_LOCATION = 4; const OWNED_COPY_MARKER = ".openclaw-owned.json"; const PREFERENCES_MAX_BYTES = 33554432; /** Chromium crx_file::id_util::GenerateIdForPath for a canonical absolute path. */ function generateChromeExtensionIdForPath(canonicalPath, platform = process.platform) { if (!(platform === "win32" ? path.win32.isAbsolute(canonicalPath) : path.isAbsolute(canonicalPath))) throw new Error("Chrome extension ID paths must be canonical absolute paths"); let nativePath = canonicalPath; if (platform === "win32" && /^[a-z]:/u.test(nativePath)) nativePath = `${nativePath[0]?.toUpperCase()}${nativePath.slice(1)}`; const bytes = Buffer.from(nativePath, platform === "win32" ? "utf16le" : "utf8"); return crypto.createHash("sha256").update(bytes).digest("hex").slice(0, 32).replace(/[0-9a-f]/gu, (nibble) => String.fromCharCode("a".charCodeAt(0) + Number.parseInt(nibble, 16))); } function homeDirectory(deps) { const value = deps.homeDir ?? deps.env?.HOME ?? deps.env?.USERPROFILE ?? os.homedir(); if (!value.trim()) throw new Error("Could not resolve the user home directory."); return path.resolve(value); } /** Chromium-derived default user-data and user native-host roots. */ function chromeProductRoots(deps = {}) { const platform = deps.platform ?? process.platform; const env = deps.env ?? process.env; const home = homeDirectory({ ...deps, env }); if (platform === "darwin") { const appSupport = path.join(home, "Library", "Application Support"); const testingData = path.join(appSupport, "Google", "Chrome for Testing"); return [ { product: "chrome", label: "Google Chrome", userDataDir: path.join(appSupport, "Google", "Chrome"), nativeManifestDir: path.join(appSupport, "Google", "Chrome", "NativeMessagingHosts") }, { product: "chrome-for-testing", label: "Google Chrome for Testing", userDataDir: testingData, nativeManifestDir: path.join(testingData, "NativeMessagingHosts") }, { product: "chrome-for-testing", label: "Google Chrome for Testing (documented host root)", userDataDir: testingData, nativeManifestDir: path.join(appSupport, "Google", "ChromeForTesting", "NativeMessagingHosts") }, { product: "chromium", label: "Chromium", userDataDir: path.join(appSupport, "Chromium"), nativeManifestDir: path.join(appSupport, "Chromium", "NativeMessagingHosts") } ]; } if (platform === "linux") { const configHome = path.resolve(env.CHROME_CONFIG_HOME?.trim() || env.XDG_CONFIG_HOME?.trim() || path.join(home, ".config")); return [ [ "chrome", "Google Chrome", "google-chrome" ], [ "chrome-for-testing", "Google Chrome for Testing", "google-chrome-for-testing" ], [ "chromium", "Chromium", "chromium" ] ].map(([product, label, basename]) => ({ product, label, userDataDir: path.join(configHome, basename), nativeManifestDir: path.join(configHome, basename, "NativeMessagingHosts") })); } if (platform === "win32") { const localAppData = env.LOCALAPPDATA?.trim(); if (!localAppData) return []; return [ [ "chrome", "Google Chrome", path.join("Google", "Chrome", "User Data") ], [ "chrome-for-testing", "Google Chrome for Testing", path.join("Google", "Chrome for Testing", "User Data") ], [ "chromium", "Chromium", path.join("Chromium", "User Data") ] ].map(([product, label, suffix]) => ({ product, label, userDataDir: path.join(localAppData, suffix), nativeManifestDir: "" })); } return []; } function stableChromeExtensionDir(deps = {}) { return path.join(path.resolve(deps.stateDir ?? resolveStateDir(deps.env)), "browser", "chrome-extension"); } async function pathInfo(target) { try { return await fs$1.lstat(target); } catch (error) { if (error.code === "ENOENT") return null; throw error; } } async function assertOwnedPath(target, kind, policy = {}) { const info = await fs$1.lstat(target); if (info.isSymbolicLink() || (kind === "file" ? !info.isFile() : !info.isDirectory())) throw new Error(`Unsafe ${kind} at ${target}`); if (process.platform !== "win32") { const uid = process.getuid?.(); if (!(uid === void 0 || info.uid === uid || policy.allowRootOwner === true && info.uid === 0)) throw new Error(`Refusing foreign owner at ${target}`); if ((info.mode & 18) !== 0) throw new Error(`Refusing group/world-writable path at ${target}`); } if (await fs$1.realpath(target) !== path.resolve(target)) throw new Error(`Refusing non-canonical path at ${target}`); } async function ensurePrivateDirectory(target) { await fs$1.mkdir(target, { recursive: true, mode: 448 }); await assertOwnedPath(target, "directory"); if (process.platform !== "win32") await fs$1.chmod(target, 448); } async function inspectInstalledCopy(target) { if (!await pathInfo(target)) return { present: false, owned: false }; await assertOwnedPath(target, "directory"); const markerPath = path.join(target, OWNED_COPY_MARKER); try { await assertOwnedPath(markerPath, "file"); const marker = JSON.parse(await fs$1.readFile(markerPath, "utf8")); return { present: true, owned: Boolean(marker) && typeof marker === "object" && !Array.isArray(marker) && marker.v === 1 }; } catch { return { present: true, owned: false }; } } async function copyRuntimeTree(source, target) { await ensurePrivateDirectory(target); for (const entry of await fs$1.readdir(source, { withFileTypes: true })) { if (/(?:sidepanel|copilot|page-share)/iu.test(entry.name) || entry.name.endsWith(".test.ts") || entry.name.endsWith(".test-support.ts") || entry.name.endsWith(".test-harness.ts") || entry.name.endsWith(".d.ts")) continue; const sourcePath = path.join(source, entry.name); const targetPath = path.join(target, entry.name); if (entry.isSymbolicLink()) throw new Error(`Refusing symlink in bundled Chrome extension: ${sourcePath}`); if (entry.isDirectory()) await copyRuntimeTree(sourcePath, targetPath); else if (entry.isFile()) { await fs$1.copyFile(sourcePath, targetPath, constants.COPYFILE_EXCL); if (process.platform !== "win32") await fs$1.chmod(targetPath, 384); } } } /** Copy/update the bundled extension with rollback-safe same-directory renames. */ async function installStableChromeExtension(bundledDir, deps = {}) { const source = await fs$1.realpath(path.resolve(bundledDir)); await assertOwnedPath(source, "directory", { allowRootOwner: true }); const target = stableChromeExtensionDir(deps); await ensurePrivateDirectory(path.resolve(deps.stateDir ?? resolveStateDir(deps.env))); await ensurePrivateDirectory(path.dirname(target)); const existing = await inspectInstalledCopy(target); if (existing.present && !existing.owned) throw new Error(`Refusing to overwrite foreign Chrome extension directory: ${target}`); const suffix = `${process.pid}-${crypto.randomBytes(6).toString("hex")}`; const temporary = `${target}.tmp-${suffix}`; const previous = `${target}.previous-${suffix}`; try { await copyRuntimeTree(source, temporary); await fs$1.writeFile(path.join(temporary, OWNED_COPY_MARKER), `${JSON.stringify({ v: 1, owner: "openclaw" })}\n`, { mode: 384, flag: "wx" }); if (existing.present) await fs$1.rename(target, previous); try { await fs$1.rename(temporary, target); } catch (error) { if (existing.present) await fs$1.rename(previous, target).catch(() => void 0); throw error; } if (existing.present) await fs$1.rm(previous, { recursive: true, force: true }); return await fs$1.realpath(target); } finally { await fs$1.rm(temporary, { recursive: true, force: true }).catch(() => void 0); } } function comparablePath(value, platform) { const resolved = path.resolve(value); return platform === "win32" ? resolved.toLowerCase() : resolved; } async function approvedRealpaths(paths) { const resolved = await Promise.all(paths.map(async (candidate) => await fs$1.realpath(candidate).catch(() => null))); return [...new Set(resolved.filter((value) => value !== null))]; } /** Discover exact Store identity separately from approved unpacked path records. */ async function discoverChromeExtensionIds(params) { const deps = params.deps ?? {}; const platform = deps.platform ?? process.platform; const approved = new Set((await approvedRealpaths(params.approvedDirs)).map((value) => comparablePath(value, platform))); const discovered = []; const storeDiscovered = []; const issues = []; const identityMismatches = []; for (const root of chromeProductRoots(deps)) { if (!await pathInfo(root.userDataDir)) continue; try { await assertOwnedPath(root.userDataDir, "directory"); } catch (error) { issues.push(`${root.label}: ${error instanceof Error ? error.message : String(error)}`); continue; } let profiles; try { profiles = await fs$1.readdir(root.userDataDir, { withFileTypes: true }); } catch (error) { issues.push(`${root.label}: could not list profiles (${String(error)})`); continue; } for (const profileEntry of profiles) { if (!profileEntry.isDirectory() || profileEntry.isSymbolicLink()) continue; const profileDir = path.join(root.userDataDir, profileEntry.name); for (const filename of ["Preferences", "Secure Preferences"]) { const preferencesPath = path.join(profileDir, filename); const preferencesInfo = await pathInfo(preferencesPath); if (!preferencesInfo) continue; try { await assertOwnedPath(profileDir, "directory"); await assertOwnedPath(preferencesPath, "file"); if (preferencesInfo.size > PREFERENCES_MAX_BYTES) throw new Error(`${filename} exceeds the 32 MiB inspection limit`); const settings = JSON.parse(await fs$1.readFile(preferencesPath, "utf8"))?.extensions?.settings; if (!settings || typeof settings !== "object" || Array.isArray(settings)) continue; for (const [extensionId, rawEntry] of Object.entries(settings)) { if (!EXTENSION_ID_PATTERN.test(extensionId) || !rawEntry || typeof rawEntry !== "object") continue; const entry = rawEntry; if (extensionId === params.storeExtensionId && entry.from_webstore === true && entry.location !== UNPACKED_MANIFEST_LOCATION) { storeDiscovered.push({ product: root.product, browser: root.label, userDataDir: root.userDataDir, profile: profileEntry.name, securePreferencesPath: preferencesPath, extensionId }); continue; } if (entry.location !== UNPACKED_MANIFEST_LOCATION || typeof entry.path !== "string") continue; const recordedPath = path.isAbsolute(entry.path) ? entry.path : path.join(profileDir, "Extensions", entry.path); const canonicalPath = await fs$1.realpath(recordedPath).catch(() => null); if (!canonicalPath || !approved.has(comparablePath(canonicalPath, platform))) continue; const predictedId = generateChromeExtensionIdForPath(canonicalPath, platform); if (extensionId !== predictedId) { const issue = `${root.label} profile ${profileEntry.name}: unpacked extension ID ${extensionId} does not match predicted ID ${predictedId} for ${canonicalPath}`; issues.push(issue); identityMismatches.push(issue); continue; } discovered.push({ product: root.product, browser: root.label, userDataDir: root.userDataDir, profile: profileEntry.name, securePreferencesPath: preferencesPath, extensionId, extensionPath: canonicalPath }); } } catch (error) { issues.push(`${root.label} profile ${profileEntry.name} (${filename}): ${error instanceof Error ? error.message : String(error)}`); } } } } const unique = new Map(discovered.map((entry) => [`${entry.product}\0${entry.profile}\0${entry.extensionId}\0${entry.extensionPath}`, entry])); const uniqueStore = new Map(storeDiscovered.map((entry) => [`${entry.product}\0${entry.profile}\0${entry.extensionId}`, entry])); return { discovered: [...unique.values()].toSorted((a, b) => `${a.product}/${a.profile}/${a.extensionId}`.localeCompare(`${b.product}/${b.profile}/${b.extensionId}`)), storeDiscovered: [...uniqueStore.values()].toSorted((a, b) => `${a.product}/${a.profile}/${a.extensionId}`.localeCompare(`${b.product}/${b.profile}/${b.extensionId}`)), issues, identityMismatches }; } //#endregion //#region extensions/browser/src/browser/extension-install.ts const OWNED_LAUNCHER_MARKER = "# OpenClaw native messaging bootstrap v1"; const BROWSER_EXTENSION_INSTALL_WAIT_DEFAULT_MS = 3e4; const BROWSER_EXTENSION_INSTALL_WAIT_MIN_MS = 1e3; const BROWSER_EXTENSION_INSTALL_WAIT_MAX_MS = 12e4; const NATIVE_HOST_DESCRIPTION = "OpenClaw browser extension bootstrap"; const FOUNDATION_CHROME_WEB_STORE_EXTENSION_ID = "kcdjddhmeafeomebliikmbpblkmkfoig"; const FOUNDATION_CHROME_WEB_STORE_URL = `https://chromewebstore.google.com/detail/openclaw/${FOUNDATION_CHROME_WEB_STORE_EXTENSION_ID}`; function nativeMessagingRoot(deps = {}) { return path.join(resolveInstallStateDir(deps), "browser", "native-messaging"); } function resolveInstallStateDir(deps) { return path.resolve(deps.stateDir ?? resolveStateDir(deps.env)); } function resolveInstallConfigPath(deps) { const env = deps.env ?? process.env; const explicit = env.OPENCLAW_CONFIG_PATH?.trim(); return explicit ? resolveStateDir({ ...env, OPENCLAW_STATE_DIR: explicit }) : void 0; } function shellQuote(value) { return `'${value.replaceAll("'", `'"'"'`)}'`; } async function resolveNativeHostPath(pluginRoot, explicit) { if (explicit) return await fs$1.realpath(explicit); const resolvedPluginRoot = path.resolve(pluginRoot); const candidates = [path.join(resolvedPluginRoot, "native-host-entry.js")]; let cursor = resolvedPluginRoot; for (;;) { candidates.push(path.join(cursor, "dist", "extensions", "browser", "native-host-entry.js")); const parent = path.dirname(cursor); if (parent === cursor) break; cursor = parent; } for (const candidate of candidates) if (await pathInfo(candidate)) return await fs$1.realpath(candidate); throw new Error("Could not resolve the built browser native-host entrypoint; run pnpm build."); } function launcherPathForManifest(manifestPath, deps) { const suffix = crypto.createHash("sha256").update(manifestPath).digest("hex").slice(0, 16); return path.join(nativeMessagingRoot(deps), `${BROWSER_NATIVE_HOST_NAME}.${suffix}.sh`); } function expectedExtensionIds(extensionIds) { return [.../* @__PURE__ */ new Set([...extensionIds, FOUNDATION_CHROME_WEB_STORE_EXTENSION_ID])].toSorted(); } function expectedOriginsForExtensionIds(extensionIds) { return expectedExtensionIds(extensionIds).map((extensionId) => `chrome-extension://${extensionId}/`); } function pathDerivedExtensionIds(extensionIds) { return extensionIds.filter((extensionId) => extensionId !== FOUNDATION_CHROME_WEB_STORE_EXTENSION_ID); } function isSafeOriginMigration(existingIds, desiredPathIds) { const existingPathIds = pathDerivedExtensionIds(existingIds).toSorted(); const desiredIds = [...new Set(desiredPathIds)].toSorted(); if (JSON.stringify(existingPathIds) === JSON.stringify(desiredIds)) return true; const removed = existingPathIds.filter((id) => !desiredIds.includes(id)); const added = desiredIds.filter((id) => !existingPathIds.includes(id)); const overlap = existingPathIds.some((id) => desiredIds.includes(id)); return existingPathIds.length === desiredIds.length && removed.length === 1 && added.length === 1 && overlap; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } function parseOwnedLauncherTargets(params) { const quotedValue = String.raw`'(?:[^'\r\n]|'"'"')*'`; const command = [ `(${quotedValue})`, `(${quotedValue})`, escapeRegExp(shellQuote("--manifest")), escapeRegExp(shellQuote(params.manifestPath)), escapeRegExp(shellQuote("--launcher")), escapeRegExp(shellQuote(params.launcherPath)), ...params.origins.flatMap((origin) => [escapeRegExp(shellQuote("--expected-origin")), escapeRegExp(shellQuote(origin))]) ].join(" "); return new RegExp(`^#!/bin/sh\\n${escapeRegExp(OWNED_LAUNCHER_MARKER)}\\nexport OPENCLAW_STATE_DIR=${quotedValue}\\n(?:export OPENCLAW_CONFIG_PATH=${quotedValue}\\n)?exec ${command} "\\$@"\\n$`, "u").exec(params.content)?.slice(1).map((value) => value.slice(1, -1).replaceAll(`'"'"'`, "'")); } async function assertPrivateNativeHostFile(target, executable, platform) { await assertOwnedPath(target, "file"); if (platform === "win32") return; const mode = (await fs$1.lstat(target)).mode & 511; if ((mode & 63) !== 0 || executable && (mode & 64) === 0) throw new Error("native host file has unsafe mode"); } async function assertNativeHostTarget(target, accessMode) { if (!path.isAbsolute(target)) throw new Error("native host target must be an absolute path"); await assertOwnedPath(target, "file", { allowRootOwner: true }); await fs$1.access(target, accessMode); } async function resolveLauncherInstall(params) { const launcherPath = launcherPathForManifest(params.manifestPath, params.deps); const nodePath = await fs$1.realpath(params.deps.nodePath ?? process.execPath); const nativeHostPath = await resolveNativeHostPath(params.pluginRoot, params.deps.nativeHostPath); await assertNativeHostTarget(nodePath, fs$1.constants.X_OK); await assertNativeHostTarget(nativeHostPath, fs$1.constants.R_OK); const command = [ nodePath, nativeHostPath, "--manifest", params.manifestPath, "--launcher", launcherPath, ...expectedOriginsForExtensionIds(params.extensionIds).flatMap((origin) => ["--expected-origin", origin]) ]; const configPath = resolveInstallConfigPath(params.deps); return { path: launcherPath, content: [ "#!/bin/sh", OWNED_LAUNCHER_MARKER, `export OPENCLAW_STATE_DIR=${shellQuote(resolveInstallStateDir(params.deps))}`, ...configPath ? [`export OPENCLAW_CONFIG_PATH=${shellQuote(configPath)}`] : [], `exec ${command.map(shellQuote).join(" ")} "$@"`, "" ].join("\n") }; } async function inspectRegistration(root, deps, expectedPathExtensionIds) { const manifestPath = path.join(root.nativeManifestDir, `${BROWSER_NATIVE_HOST_NAME}.json`); if (!await pathInfo(manifestPath)) return { product: root.product, browser: root.label, manifestPath, extensionIds: [], state: "missing" }; try { await assertPrivateNativeHostFile(manifestPath, false, deps.platform ?? process.platform); const parsed = JSON.parse(await fs$1.readFile(manifestPath, "utf8")); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("manifest is not an object"); const manifest = parsed; const expectedLauncher = launcherPathForManifest(manifestPath, deps); const origins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins : []; const ids = origins.flatMap((origin) => { const match = /^chrome-extension:\/\/([a-p]{32})\/$/.exec(String(origin)); return match?.[1] ? [match[1]] : []; }); if (manifest.name !== "ai.openclaw.browser_bootstrap" || manifest.path !== expectedLauncher) return { product: root.product, browser: root.label, manifestPath, extensionIds: ids, state: "foreign", issue: "same host name is registered to a foreign manifest or launcher" }; const exactKeys = [ "name", "description", "path", "type", "allowed_origins" ]; const stringOrigins = origins.filter((origin) => typeof origin === "string"); const validOrigins = origins.length > 0 && origins.length === stringOrigins.length && stringOrigins.every((origin) => /^chrome-extension:\/\/[a-p]{32}\/$/u.test(origin)) && new Set(origins).size === origins.length; const canonicalOrigins = [...stringOrigins].toSorted(); const expectedOrigins = expectedPathExtensionIds ? expectedOriginsForExtensionIds(expectedPathExtensionIds) : null; if (Object.keys(manifest).length !== exactKeys.length || !exactKeys.every((key) => Object.hasOwn(manifest, key)) || manifest.description !== NATIVE_HOST_DESCRIPTION || manifest.type !== "stdio" || !validOrigins || JSON.stringify(origins) !== JSON.stringify(canonicalOrigins) || expectedOrigins !== null && JSON.stringify(origins) !== JSON.stringify(expectedOrigins)) throw new Error("native host manifest does not contain exact allowed origins"); await assertPrivateNativeHostFile(expectedLauncher, true, deps.platform ?? process.platform); const launcherTargets = parseOwnedLauncherTargets({ content: await fs$1.readFile(expectedLauncher, "utf8"), manifestPath, launcherPath: expectedLauncher, origins: stringOrigins }); if (!launcherTargets) throw new Error("native host launcher and manifest origins do not match"); let issue; try { for (const [index, target] of launcherTargets.entries()) await assertNativeHostTarget(target, index === 0 ? fs$1.constants.X_OK : fs$1.constants.R_OK); } catch { issue = "registered native host runtime or entry is unavailable or unsafe; run openclaw browser extension install"; } return { product: root.product, browser: root.label, manifestPath, extensionIds: ids.toSorted(), state: "owned", issue }; } catch (error) { return { product: root.product, browser: root.label, manifestPath, extensionIds: [], state: "invalid", issue: error instanceof Error ? error.message : String(error) }; } } async function installRegistration(params) { const { root, extensionIds, deps } = params; const manifestPath = path.join(root.nativeManifestDir, `${BROWSER_NATIVE_HOST_NAME}.json`); const existing = await inspectRegistration(root, deps); if (existing.state === "foreign" || existing.state === "invalid") throw new Error(`Refusing to overwrite ${existing.state} native host: ${manifestPath}`); const desiredOrigins = expectedOriginsForExtensionIds(extensionIds); if (existing.state === "owned" && JSON.stringify(existing.extensionIds.map((id) => `chrome-extension://${id}/`)) !== JSON.stringify(desiredOrigins) && !isSafeOriginMigration(existing.extensionIds, extensionIds)) throw new Error(`Refusing to overwrite owned native host with unexpected allowed origins`); await ensurePrivateDirectory(nativeMessagingRoot(deps)); await ensurePrivateDirectory(root.nativeManifestDir); const launcher = await resolveLauncherInstall({ manifestPath, pluginRoot: params.pluginRoot, extensionIds, deps }); const launcherPath = launcher.path; if (await pathInfo(launcherPath)) { await assertOwnedPath(launcherPath, "file"); const existingLauncher = await fs$1.readFile(launcherPath, "utf8"); if (!existingLauncher.includes(OWNED_LAUNCHER_MARKER)) throw new Error(`Refusing to overwrite foreign native host launcher: ${launcherPath}`); if (existingLauncher !== launcher.content) { const replacement = `${launcherPath}.tmp-${process.pid}`; await fs$1.writeFile(replacement, launcher.content, { mode: 448, flag: "wx" }); await fs$1.rename(replacement, launcherPath); } } else await fs$1.writeFile(launcherPath, launcher.content, { mode: 448, flag: "wx" }); if (process.platform !== "win32") await fs$1.chmod(launcherPath, 448); const manifest = { name: BROWSER_NATIVE_HOST_NAME, description: NATIVE_HOST_DESCRIPTION, path: launcherPath, type: "stdio", allowed_origins: expectedOriginsForExtensionIds(extensionIds) }; const temporary = `${manifestPath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`; await fs$1.writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 384, flag: "wx" }); await fs$1.rename(temporary, manifestPath); if (process.platform !== "win32") await fs$1.chmod(manifestPath, 384); return await inspectRegistration(root, deps, extensionIds); } async function approvedInstallRealpaths(installed, bundled) { const installedPath = await fs$1.realpath(installed); const bundledPath = await fs$1.realpath(bundled); await assertOwnedPath(installedPath, "directory"); await assertOwnedPath(bundledPath, "directory", { allowRootOwner: true }); return [.../* @__PURE__ */ new Set([installedPath, bundledPath])]; } function normalizeExtensionInstallWaitMs(value) { if (value === void 0) return BROWSER_EXTENSION_INSTALL_WAIT_DEFAULT_MS; const parsed = typeof value === "number" || typeof value === "string" && /^\d+$/u.test(value) ? Number(value) : NaN; if (!Number.isInteger(parsed) || parsed < BROWSER_EXTENSION_INSTALL_WAIT_MIN_MS || parsed > BROWSER_EXTENSION_INSTALL_WAIT_MAX_MS) throw new Error(`--wait-ms must be an integer from ${BROWSER_EXTENSION_INSTALL_WAIT_MIN_MS} to ${BROWSER_EXTENSION_INSTALL_WAIT_MAX_MS}`); return parsed; } /** Copy, pre-register Store plus deterministic IDs, then verify Chrome's recorded identity. */ async function installChromeExtensionBootstrap(params) { const deps = params.deps ?? {}; const platform = deps.platform ?? process.platform; const installed = await installStableChromeExtension(params.bundledDir, deps); if (platform === "win32") return await browserExtensionStatus({ bundledDir: params.bundledDir, deps }); const approvedPaths = await approvedInstallRealpaths(installed, params.bundledDir); const predictedIds = [...new Set(approvedPaths.map((candidate) => generateChromeExtensionIdForPath(candidate, platform)))].toSorted(); const preRegistrationIssues = []; let preRegisteredRoots = 0; for (const root of chromeProductRoots(deps)) { if (!await pathInfo(root.userDataDir)) continue; try { await assertOwnedPath(root.userDataDir, "directory"); await installRegistration({ root, extensionIds: predictedIds, pluginRoot: params.pluginRoot, deps }); preRegisteredRoots += 1; params.onProgress?.(`Pre-registered the native host for ${root.label}.`); } catch (error) { preRegistrationIssues.push(`${root.label}: native host pre-registration refused (${error instanceof Error ? error.message : String(error)})`); } } if (preRegisteredRoots > 0) params.onProgress?.(`Native bootstrap is ready. Add OpenClaw from the Chrome Web Store: ${FOUNDATION_CHROME_WEB_STORE_URL}. For development, load unpacked from ${installed}.`); else preRegistrationIssues.push("No native host was pre-registered. Resolve any pre-registration refusals above; if Chrome has not been launched yet, launch it first. Then run install again before loading the extension."); const waitMs = normalizeExtensionInstallWaitMs(params.waitMs); const now = deps.now ?? Date.now; const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => { setTimeout(resolve, ms); })); const deadline = now() + waitMs; let discovery = await discoverChromeExtensionIds({ approvedDirs: approvedPaths, storeExtensionId: FOUNDATION_CHROME_WEB_STORE_EXTENSION_ID, deps }); let announcedWait = false; while (discovery.discovered.length === 0 && discovery.storeDiscovered.length === 0 && now() < deadline) { if (!announcedWait) { params.onProgress?.("Waiting for Chrome to verify the OpenClaw extension…"); announcedWait = true; } await sleep(Math.min(500, Math.max(1, deadline - now()))); discovery = await discoverChromeExtensionIds({ approvedDirs: approvedPaths, storeExtensionId: FOUNDATION_CHROME_WEB_STORE_EXTENSION_ID, deps }); } const status = await browserExtensionStatus({ bundledDir: params.bundledDir, deps }); return { ...status, issues: [.../* @__PURE__ */ new Set([...preRegistrationIssues, ...status.issues])] }; } /** Read-only extension copy, profile discovery, and native registration report. */ async function browserExtensionStatus(params) { const deps = params.deps ?? {}; const platform = deps.platform ?? process.platform; const installedPath = stableChromeExtensionDir(deps); const installedCopy = await inspectInstalledCopy(installedPath); const bundledPath = await fs$1.realpath(params.bundledDir); await assertOwnedPath(bundledPath, "directory", { allowRootOwner: true }); const approvedPaths = installedCopy.owned ? await approvedInstallRealpaths(installedPath, bundledPath) : [bundledPath]; const discovery = await discoverChromeExtensionIds({ approvedDirs: approvedPaths, storeExtensionId: FOUNDATION_CHROME_WEB_STORE_EXTENSION_ID, deps }); const predictedIds = [...new Set(approvedPaths.map((candidate) => generateChromeExtensionIdForPath(candidate, platform)))].toSorted(); const registrations = platform === "win32" ? [] : await Promise.all(chromeProductRoots(deps).map((root) => inspectRegistration(root, deps, predictedIds))); const unavailableRegistration = registrations.some((registration) => { return (discovery.discovered.some((entry) => entry.product === registration.product) || discovery.storeDiscovered.some((entry) => entry.product === registration.product)) && (registration.state !== "owned" || Boolean(registration.issue)); }); return { platform, platformSupport: platform === "win32" ? "manual_required" : "automatic", installedCopy: { path: installedPath, ...installedCopy }, bundledPath: path.resolve(params.bundledDir), approvedPaths, discovered: discovery.discovered, storeDiscovered: discovery.storeDiscovered, registrations, manualSetupRequired: platform === "win32" || installedCopy.present && !installedCopy.owned || discovery.discovered.length === 0 && discovery.storeDiscovered.length === 0 || discovery.identityMismatches.length > 0 || unavailableRegistration, issues: [ ...installedCopy.present && !installedCopy.owned ? [`Chrome extension copy is not OpenClaw-owned: ${installedPath}`] : [], ...discovery.issues, ...registrations.flatMap((entry) => entry.issue ? [`${entry.browser}: ${entry.issue}`] : []) ] }; } /** Remove only registrations and launchers that carry OpenClaw ownership. */ async function uninstallChromeExtensionNativeHosts(params = {}) { const deps = params.deps ?? {}; if ((deps.platform ?? process.platform) === "win32") return { removed: [], refused: [], manualRequired: true }; const removed = []; const refused = []; for (const root of chromeProductRoots(deps)) { const status = await inspectRegistration(root, deps); if (status.state === "missing") continue; if (status.state !== "owned") { refused.push(status.manifestPath); continue; } const launcherPath = launcherPathForManifest(status.manifestPath, deps); const launcher = await pathInfo(launcherPath); if (launcher) { await assertOwnedPath(launcherPath, "file"); if (!(await fs$1.readFile(launcherPath, "utf8")).includes(OWNED_LAUNCHER_MARKER)) { refused.push(launcherPath); continue; } } await fs$1.unlink(status.manifestPath); removed.push(status.manifestPath); if (launcher) { await fs$1.unlink(launcherPath); removed.push(launcherPath); } } return { removed, refused, manualRequired: false }; } /** Resolve the installed stable copy when present, bundled source otherwise. */ async function resolveChromeExtensionLoadPath(bundledDir, deps = {}) { const installedPath = stableChromeExtensionDir(deps); const installed = await inspectInstalledCopy(installedPath); if (installed.present) { if (!installed.owned) throw new Error(`Refusing foreign Chrome extension directory: ${installedPath}`); return await fs$1.realpath(installedPath); } const bundledPath = await fs$1.realpath(path.resolve(bundledDir)); await assertOwnedPath(bundledPath, "directory", { allowRootOwner: true }); return bundledPath; } /** Repair drift only when both the copy and existing registration are already owned. */ async function repairOwnedChromeExtensionNativeHosts(params) { const deps = params.deps ?? {}; if ((deps.platform ?? process.platform) === "win32") return { changes: [], warnings: [] }; const before = await browserExtensionStatus({ bundledDir: params.bundledDir, deps }); if (!before.installedCopy.owned || before.discovered.length === 0 && before.storeDiscovered.length === 0) return { changes: [], warnings: [] }; const changes = []; const warnings = []; const predictedIds = before.approvedPaths.map((candidate) => generateChromeExtensionIdForPath(candidate, deps.platform ?? process.platform)).toSorted(); for (const root of chromeProductRoots(deps)) { const manifestPath = path.join(root.nativeManifestDir, `${BROWSER_NATIVE_HOST_NAME}.json`); const registration = await inspectRegistration(root, deps); if (!(before.discovered.some((entry) => entry.product === root.product) || before.storeDiscovered.some((entry) => entry.product === root.product))) continue; if (registration?.state === "foreign" || registration?.state === "invalid") { warnings.push(`${root.label} native host repair refused: ${registration.issue ?? registration.state}`); continue; } if (registration?.state !== "owned") continue; try { const idsAreCurrent = JSON.stringify(registration.extensionIds) === JSON.stringify(expectedExtensionIds(predictedIds)); if (!idsAreCurrent && !isSafeOriginMigration(registration.extensionIds, predictedIds)) { warnings.push(`${root.label} native host repair refused: unexpected allowed origins`); continue; } const launcher = await resolveLauncherInstall({ manifestPath, pluginRoot: params.pluginRoot, extensionIds: predictedIds, deps }); await assertOwnedPath(launcher.path, "file"); const launcherIsCurrent = await fs$1.readFile(launcher.path, "utf8") === launcher.content; if (idsAreCurrent && launcherIsCurrent) continue; await installRegistration({ root, extensionIds: predictedIds, pluginRoot: params.pluginRoot, deps }); changes.push(`Repaired ${root.label} OpenClaw native messaging registration.`); } catch (error) { warnings.push(`${root.label} native host repair failed: ${String(error)}`); } } return { changes, warnings }; } //#endregion export { repairOwnedChromeExtensionNativeHosts as a, normalizeExtensionInstallWaitMs as i, browserExtensionStatus as n, resolveChromeExtensionLoadPath as o, installChromeExtensionBootstrap as r, uninstallChromeExtensionNativeHosts as s, FOUNDATION_CHROME_WEB_STORE_URL as t };