openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
240 lines (239 loc) • 16.7 kB
JavaScript
import { r as defaultRuntime } from "./runtime-DlO3PTAd.mjs";
import { r as theme } from "./theme-DzaUZY4q.mjs";
import { r as getRuntimeConfig } from "./io.runtime-BsUXSsD4.mjs";
import { n as info } from "./globals-DyT306Z_.mjs";
import "./runtime-env-C0XPA9NZ.mjs";
import "./runtime-config-snapshot-A-BTduWa.mjs";
import { n as runCommandWithRuntime } from "./cli-utils-ClVfZRCr.mjs";
import "./cli-runtime-BSsYr5WY.mjs";
import { a as resolveFirstExtensionProfileName, i as resolveBrowserConfig, s as resolveProfile } from "./config-DQZ6ubGi.mjs";
import { t as ensureExtensionRelayToken } from "./relay-auth-M8sdXxJv.mjs";
import { g as relayKeyIdFromHex, n as BROWSER_RELAY_AUTH_COMPLETE_PATH, t as BROWSER_RELAY_AUTH_CHALLENGE_PATH, v as RELAY_AUTH_LABEL } from "./auth-v2-RxNpen8C.mjs";
import { a as repairChromeExtensionNativeHosts, c as FOUNDATION_CHROME_WEB_STORE_URL, i as resolveChromeExtensionLoadPath, l as removeChromeStoreInstallRequests, n as runBrowserExtensionSetup, o as resolveNativeHostPath, r as normalizeExtensionInstallWaitMs, s as uninstallChromeExtensionNativeHosts, t as observeBrowserExtensionSetup, u as NativeHostSetupContextError } from "./extension-setup-CL0QFNY7.mjs";
import { t as buildBrowserExtensionPairing } from "./extension-pairing-BH5jcjYb.mjs";
import { s as runBrowserCliCommand } from "./browser-cli-shared-BTrZ1xSP.mjs";
import { fileURLToPath, pathToFileURL } from "node:url";
import path from "node:path";
//#region extensions/browser/src/cli/browser-cli-extension.ts
/** Resolve safe v2 metadata, with an explicit gated legacy credential escape hatch. */
async function buildCdpEndpoint(legacyBearer) {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const token = await ensureExtensionRelayToken();
const profileName = resolveFirstExtensionProfileName(resolved);
const relayPort = (profileName ? resolveProfile(resolved, profileName) : null)?.cdpPort ?? resolved.extensionRelayDefaultPort;
const browserUrl = `http://127.0.0.1:${relayPort}`;
const metadata = {
browserUrl,
wsEndpoint: `ws://127.0.0.1:${relayPort}/cdp`,
auth: {
label: RELAY_AUTH_LABEL,
version: 2,
keyId: relayKeyIdFromHex(token),
challengeUrl: new URL(BROWSER_RELAY_AUTH_CHALLENGE_PATH, browserUrl).toString(),
completeUrl: new URL(BROWSER_RELAY_AUTH_COMPLETE_PATH, browserUrl).toString(),
role: "cdp",
transport: "connection",
method: "SEQUENCE",
resource: "/json/version -> /cdp",
flow: "cdp"
}
};
if (legacyBearer && !resolved.extensionRelay.allowLegacyAuth) throw new Error("Legacy browser relay auth is disabled; remove --legacy-bearer and use Browser Relay Authentication v2.");
return {
...metadata,
...legacyBearer ? { headers: { Authorization: `Bearer ${token}` } } : {}
};
}
function registerBrowserExtensionCommands(browser, parentOpts, pluginRoot) {
const defaultPluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const resolvedPluginRoot = pluginRoot ?? defaultPluginRoot;
const bundledDir = path.join(pluginRoot || defaultPluginRoot, "chrome-extension");
const extension = browser.command("extension").description("Install and inspect the OpenClaw Chrome extension bootstrap");
extension.command("native-host", { hidden: true }).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
try {
const entry = await resolveNativeHostPath(resolvedPluginRoot);
await import(pathToFileURL(entry).href);
} catch {
process.exitCode = 1;
}
});
extension.command("setup").description("Inspect, prepare, or verify automatic Chrome setup on this host").option("--action <action>", "inspect, install, or verify", "inspect").option("--native-host-executable <path>", "Local self-contained Windows bootstrap executable").option("--browser-profile <name>", "Local extension profile").option("--wait-ms <ms>", "Bounded Chrome discovery wait", "1000").option("--json", "Print the redacted setup result").action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
if (opts.action !== "inspect" && opts.action !== "install" && opts.action !== "verify") throw new Error("--action must be inspect, install, or verify");
const result = await runBrowserExtensionSetup({
action: opts.action,
nativeHostExecutable: opts.nativeHostExecutable,
bundledDir,
pluginRoot: resolvedPluginRoot,
cfg: getRuntimeConfig(),
profile: opts.browserProfile ?? parentOpts(command).browserProfile,
waitMs: normalizeExtensionInstallWaitMs(opts.waitMs)
});
if (opts.json || parentOpts(command).json) defaultRuntime.writeJson(result);
else defaultRuntime.log(`${result.target.hostname} · ${result.target.profile}: ${result.phase} (${result.reason}); next: ${result.nextAction}`);
}, (error) => {
defaultRuntime.error(error instanceof NativeHostSetupContextError ? error.message : "Chrome setup could not finish. Check the action, local profile, and native host installation. If automatic Windows selection is unverified, repair the intended existing profile with --browser-profile <name> --action install.");
defaultRuntime.exit(1);
});
});
extension.command("path").description("Print the unpacked Chrome extension directory (Load unpacked)").action(async () => {
await runCommandWithRuntime(defaultRuntime, async () => {
defaultRuntime.log(await resolveChromeExtensionLoadPath(bundledDir));
});
});
extension.command("install").description("Set up the Chrome extension and request supported Store installation").option("--browser-profile <name>", "Local extension profile; repair preserves an owned selector").option("--native-host-executable <path>", "Local self-contained Windows bootstrap executable").option("--no-store", "Prepare native bootstrap and development files without requesting Store installation").option("--json", "Print a machine-readable status report").option("--wait-ms <ms>", "How long to wait after pre-registration for Chrome to verify the extension", String(3e4)).action(async (opts, command) => {
await runBrowserCliCommand(async () => {
const json = opts.json === true || parentOpts(command).json === true;
const waitMs = normalizeExtensionInstallWaitMs(opts.waitMs);
if (!json) defaultRuntime.log(info("Preparing the OpenClaw Chrome extension…"));
const status = await observeBrowserExtensionSetup({
action: "install",
nativeHostExecutable: opts.nativeHostExecutable,
bundledDir,
pluginRoot: resolvedPluginRoot,
waitMs,
requestStoreInstall: opts.store !== false,
profile: opts.browserProfile ?? parentOpts(command).browserProfile,
onProgress: json ? void 0 : (message) => defaultRuntime.log(info(message))
});
if (json) defaultRuntime.writeJson(status);
else {
for (const issue of status.issues) defaultRuntime.error(theme.warn(issue));
defaultRuntime.log(status.manualSetupRequired ? theme.warn(status.platformSupport === "manual_required" ? "Automatic native bootstrap is not supported on this platform; use Settings for manual pairing." : status.storeInstallRequests.some((entry) => entry.state === "requested") ? `Store installation requested. Enable OpenClaw in chrome://extensions and approve Chrome's prompt. If it has not appeared, restart Chrome when convenient or add it from ${FOUNDATION_CHROME_WEB_STORE_URL}. Run extension status to check setup again.` : `Setup needs attention. Add OpenClaw from ${FOUNDATION_CHROME_WEB_STORE_URL} after native registration succeeds. For development, load the printed unpacked path. If the extension attempted setup before the native host existed, restart Chrome once.`) : info(`Native host and extension identity verified for ${status.discovered.length + status.storeDiscovered.length} profile registration(s). Check the extension popup for Connected before using browser automation.`));
}
if (status.manualSetupRequired) defaultRuntime.exit(1);
});
});
extension.command("repair").description("Inspect or repair existing native hosts without browser profile discovery").option("--from <entrypoint>", "Repair only registrations referencing this exact absolute entrypoint").option("--dry-run", "Inspect registered targets without changing files", false).option("--json", "Print a machine-readable repair report").action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await repairChromeExtensionNativeHosts({
bundledDir,
pluginRoot: resolvedPluginRoot,
fromNativeHostPath: opts.from,
dryRun: opts.dryRun === true
});
if (opts.json === true || parentOpts(command).json === true) defaultRuntime.writeJson(result);
else {
for (const message of [...result.changes, ...result.warnings]) defaultRuntime.log(message);
defaultRuntime.log(`Registered native entries: ${result.retainedNativeHostPaths.join(", ") || "none"}`);
}
if (result.manualRequired || !result.retentionSafe || result.warnings.length > 0) defaultRuntime.exit(1);
});
});
extension.command("status").description("Inspect extension copies, Chrome IDs, and native-host registrations").option("--native-host-executable <path>", "Local self-contained Windows bootstrap executable").option("--browser-profile <name>", "Local extension profile").option("--json", "Print a machine-readable status report").action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const json = opts.json === true || parentOpts(command).json === true;
const status = await observeBrowserExtensionSetup({
action: "inspect",
profile: opts.browserProfile ?? parentOpts(command).browserProfile,
nativeHostExecutable: opts.nativeHostExecutable,
pluginRoot: resolvedPluginRoot,
bundledDir
});
if (json) {
defaultRuntime.writeJson(status);
return;
}
defaultRuntime.log([
`Extension copy: ${status.installedCopy.owned ? "installed" : "bundled fallback"}`,
`Store request: ${status.storeInstallRequests.length > 0 ? status.storeInstallRequests.map((entry) => `${entry.browser}: ${entry.state ?? "unknown"}`).join(", ") : "use the Chrome Web Store"}`,
`Store: ${status.storeDiscovered.length > 0 ? status.storeDiscovered.map((entry) => `${entry.extensionId} (${entry.browser}/${entry.profile}; ${entry.enabled ? "enabled" : entry.awaitingApproval ? "awaiting approval" : "disabled"})`).join(", ") : "not detected"}`,
`Development: ${status.discovered.length > 0 ? status.discovered.map((entry) => `${entry.extensionId} (${entry.browser}/${entry.profile})`).join(", ") : "none detected"}`,
`Load unpacked: ${status.installedCopy.owned ? status.installedCopy.path : status.bundledPath}`,
`Native hosts: ${status.registrations.filter((entry) => entry.state === "owned").length} owned`,
`Setup: ${status.manualSetupRequired ? "manual action required" : "automatic bootstrap ready"}`
].join("\n"));
});
});
extension.command("uninstall-store").description("Remove OpenClaw-owned Store install requests; Chrome may remove the extension on restart").option("--json", "Print a machine-readable removal report").action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
if (process.platform === "win32") throw new Error("Windows Store removal belongs to uninstall-host --remove-store; no standalone Store writer is available.");
const result = await removeChromeStoreInstallRequests();
if (opts.json === true || parentOpts(command).json === true) defaultRuntime.writeJson(result);
else {
defaultRuntime.log(info(`Removed ${result.removed.length} owned Store install request(s). Chrome may remove externally installed copies on its next start. Native hosts are unchanged.`));
for (const refused of result.refused) defaultRuntime.error(theme.warn(`Refused foreign Store registration: ${refused}`));
}
if (result.refused.length > 0) defaultRuntime.exit(1);
});
});
extension.command("uninstall-host").description("Remove only OpenClaw-owned Chrome native-host registrations").option("--native-host-executable <path>", "Local self-contained Windows bootstrap executable").option("--browser-profile <name>", "Local extension profile").option("--remove-store", "Remove owned Windows Store requests before native registration").option("--json", "Print a machine-readable removal report").action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const json = opts.json === true || parentOpts(command).json === true;
const result = await uninstallChromeExtensionNativeHosts({
pluginRoot: resolvedPluginRoot,
nativeHostExecutable: opts.nativeHostExecutable,
browserProfile: opts.browserProfile ?? parentOpts(command).browserProfile,
removeStore: opts.removeStore === true
});
if (json) defaultRuntime.writeJson(result);
else {
defaultRuntime.log(result.manualRequired ? theme.warn("Windows native-host removal is manual; no registry key was changed.") : info(`Removed ${result.removed.length} owned native-host artifact(s).`));
for (const refused of result.refused) defaultRuntime.error(theme.warn(`Refused registration removal: ${refused}`));
}
if (result.refused.length) defaultRuntime.exit(1);
});
});
extension.command("pair").description("Print an advanced manual pairing string").option("--json", "Print the pairing string as JSON").option("--local-gateway", "Pair through this host’s local Gateway for desktop native helpers").option("--gateway-url <url>", "Print a remote pairing string for a Chrome on another machine (e.g. wss://gateway.example.com)").action(async (opts, command) => {
await runBrowserCliCommand(async () => {
const json = opts.json === true || parentOpts(command).json === true;
const cfg = getRuntimeConfig();
const localGateway = opts.localGateway === true;
if (localGateway && opts.gatewayUrl !== void 0) throw new Error("--local-gateway cannot be combined with --gateway-url");
if (localGateway && cfg.gateway?.mode === "remote") throw new Error("--local-gateway requires a local Gateway configuration");
const result = await buildBrowserExtensionPairing({
cfg,
gatewayUrl: opts.gatewayUrl,
localTransport: localGateway ? "gateway" : void 0
});
const remote = result.topology === "direct-remote";
if (json) {
defaultRuntime.writeJson({
pairingString: result.pairingString,
relayPort: result.relayPort,
remote
});
return;
}
const setupLine = remote ? info("Remote pairing: load and pair the extension on the machine running Chrome; it connects to this gateway over wss://.") : info("Run this on the machine that hosts the browser (gateway host or browser node).");
defaultRuntime.log([
setupLine,
info("1. Load the extension: chrome://extensions → Developer mode → Load unpacked →"),
` ${bundledDir}`,
info("2. Open the OpenClaw popup and paste this pairing string:"),
"",
theme.heading(result.pairingString),
"",
info("The relay key is a host-local secret; keep it private.")
].join("\n"));
});
});
extension.command("cdp").description("Print non-secret Browser Relay Authentication v2 CDP metadata").option("--json", "Print the endpoint as JSON").option("--legacy-bearer", "Print the legacy Bearer header while browser.extensionRelay.allowLegacyAuth is enabled").action(async (opts, command) => {
await runBrowserCliCommand(async () => {
const json = opts.json === true || parentOpts(command).json === true;
const legacyBearer = opts.legacyBearer === true;
const endpoint = await buildCdpEndpoint(legacyBearer);
if (legacyBearer) defaultRuntime.error(theme.warn("Warning: --legacy-bearer reveals the relay key in an authorization header. Migrate this client to Browser Relay Authentication v2."));
if (json) {
defaultRuntime.writeJson(endpoint);
return;
}
const lines = [
info("Relay CDP endpoint (pair the extension first):"),
`browserUrl: ${endpoint.browserUrl}`,
`wsEndpoint: ${endpoint.wsEndpoint}`,
`auth: ${endpoint.auth.label} v${endpoint.auth.version}`,
`keyId: ${endpoint.auth.keyId}`,
`challenge: POST ${endpoint.auth.challengeUrl}`,
`complete: POST ${endpoint.auth.completeUrl}`,
`sequence: ${endpoint.auth.resource}`
];
if (endpoint.headers) lines.push(`legacy: Authorization: ${endpoint.headers.Authorization}`);
else lines.push("", info("No relay key or authorization header is printed."));
defaultRuntime.log(lines.join("\n"));
});
});
}
//#endregion
export { registerBrowserExtensionCommands };