import { resolveBrowserConfig } from "../browser/config.js"; import { loadConfig } from "../config/config.js"; import { GatewayClient } from "../gateway/client.js"; import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; import { getMachineDisplayName } from "../infra/machine-name.js"; import { ensureOpenClawCliOnPath } from "../infra/path-env.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { VERSION } from "../version.js"; import { ensureNodeHostConfig, saveNodeHostConfig, type NodeHostGatewayConfig } from "./config.js"; import { coerceNodeInvokePayload, handleInvoke, type SkillBinsProvider, buildNodeInvokeResultParams, } from "./invoke.js"; export { buildNodeInvokeResultParams }; type NodeHostRunOptions = { gatewayHost: string; gatewayPort: number; gatewayTls?: boolean; gatewayTlsFingerprint?: string; nodeId?: string; displayName?: string; }; const DEFAULT_NODE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; class SkillBinsCache implements SkillBinsProvider { private bins = new Set(); private lastRefresh = 0; private readonly ttlMs = 90_000; private readonly fetch: () => Promise; constructor(fetch: () => Promise) { this.fetch = fetch; } async current(force = false): Promise> { if (force || Date.now() - this.lastRefresh > this.ttlMs) { await this.refresh(); } return this.bins; } private async refresh() { try { const bins = await this.fetch(); this.bins = new Set(bins); this.lastRefresh = Date.now(); } catch { if (!this.lastRefresh) { this.bins = new Set(); } } } } function ensureNodePathEnv(): string { ensureOpenClawCliOnPath({ pathEnv: process.env.PATH ?? "" }); const current = process.env.PATH ?? ""; if (current.trim()) { return current; } process.env.PATH = DEFAULT_NODE_PATH; return DEFAULT_NODE_PATH; } export async function runNodeHost(opts: NodeHostRunOptions): Promise { const config = await ensureNodeHostConfig(); const nodeId = opts.nodeId?.trim() || config.nodeId; if (nodeId !== config.nodeId) { config.nodeId = nodeId; } const displayName = opts.displayName?.trim() || config.displayName || (await getMachineDisplayName()); config.displayName = displayName; const gateway: NodeHostGatewayConfig = { host: opts.gatewayHost, port: opts.gatewayPort, tls: opts.gatewayTls ?? loadConfig().gateway?.tls?.enabled ?? false, tlsFingerprint: opts.gatewayTlsFingerprint, }; config.gateway = gateway; await saveNodeHostConfig(config); const cfg = loadConfig(); const resolvedBrowser = resolveBrowserConfig(cfg.browser, cfg); const browserProxyEnabled = cfg.nodeHost?.browserProxy?.enabled !== false && resolvedBrowser.enabled; const isRemoteMode = cfg.gateway?.mode === "remote"; const token = process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || (isRemoteMode ? cfg.gateway?.remote?.token : cfg.gateway?.auth?.token); const password = process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || (isRemoteMode ? cfg.gateway?.remote?.password : cfg.gateway?.auth?.password); const host = gateway.host ?? "127.0.0.1"; const port = gateway.port ?? 18789; const scheme = gateway.tls ? "wss" : "ws"; const url = `${scheme}://${host}:${port}`; const pathEnv = ensureNodePathEnv(); // eslint-disable-next-line no-console console.log(`node host PATH: ${pathEnv}`); const client = new GatewayClient({ url, token: token?.trim() || undefined, password: password?.trim() || undefined, instanceId: nodeId, clientName: GATEWAY_CLIENT_NAMES.NODE_HOST, clientDisplayName: displayName, clientVersion: VERSION, platform: process.platform, mode: GATEWAY_CLIENT_MODES.NODE, role: "node", scopes: [], caps: ["system", ...(browserProxyEnabled ? ["browser"] : [])], commands: [ "system.run", "system.which", "system.execApprovals.get", "system.execApprovals.set", ...(browserProxyEnabled ? ["browser.proxy"] : []), ], pathEnv, permissions: undefined, deviceIdentity: loadOrCreateDeviceIdentity(), tlsFingerprint: gateway.tlsFingerprint, onEvent: (evt) => { if (evt.event !== "node.invoke.request") { return; } const payload = coerceNodeInvokePayload(evt.payload); if (!payload) { return; } void handleInvoke(payload, client, skillBins); }, onConnectError: (err) => { // keep retrying (handled by GatewayClient) // eslint-disable-next-line no-console console.error(`node host gateway connect failed: ${err.message}`); }, onClose: (code, reason) => { // eslint-disable-next-line no-console console.error(`node host gateway closed (${code}): ${reason}`); }, }); const skillBins = new SkillBinsCache(async () => { const res = await client.request<{ bins: Array }>("skills.bins", {}); const bins = Array.isArray(res?.bins) ? res.bins.map((bin) => String(bin)) : []; return bins; }); client.start(); await new Promise(() => {}); }