openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
394 lines (393 loc) • 15.2 kB
JavaScript
import { F as resolveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js";
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { n as requestExitAfterOneShotOutput } from "./one-shot-exit-BkFPnjbM.js";
import { Et as array, Qn as url, Rn as string, Tn as object, wn as number } from "./schemas-zxit8y5H.js";
import { t as VERSION } from "./version-v1kuAkGj.js";
import { t as formatDocsLink } from "./links-ClIwBcy4.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { r as formatInvalidPortOption } from "./error-format-Cae7EwnT.js";
import { n as loadDeviceIdentityIfPresent, o as publicKeyRawBase64UrlFromPem } from "./device-identity-J83pn_rP.js";
import "./client-I-RoP1Al.js";
import { t as GatewayClientRequestError } from "./request-error-BSFClQsA.js";
import { t as createPendingRequestRegistry } from "./pending-request-registry-B-sdjY_C.js";
import { t as formatHelpExamples } from "./help-format-CAcwboTs.js";
import { a as loadNodeHostConfig } from "./config-o0eKhywR.js";
import { a as runNodeDaemonStop, c as resolveNodePairGatewayOptions, d as runStartupMigrations, f as prepareNodeHostRuntime, i as runNodeDaemonStatus, n as runNodeDaemonRestart, o as runNodeDaemonUninstall, p as startNodeHostConnection, r as runNodeDaemonStart, s as resolveNodeGatewayOptions, t as runNodeDaemonInstall, u as runNodeHost } from "./daemon-Ck0pm4A2.js";
import { AsyncLocalStorage } from "node:async_hooks";
import { Option } from "commander";
import { createInterface } from "node:readline";
//#region src/node-host/worker-support.ts
const connectionSchema = object({
url: url(),
protocol: number().int().positive(),
capabilities: array(string()),
tlsFingerprint: string().optional(),
cloudflareAccess: object({
clientId: string(),
clientSecret: string()
}).optional()
});
function parseNodeHostWorkerInput(line) {
try {
const parsed = asNullableRecord(JSON.parse(line));
const type = typeof parsed?.type === "string" ? parsed.type : "";
if (type === "stop") return { type };
const generation = parsed?.generation;
if (typeof generation !== "number" || !Number.isSafeInteger(generation) || generation < 0) return null;
if (type === "gateway-connection") return {
type,
generation,
connection: parsed?.connection === null ? null : connectionSchema.parse(parsed?.connection)
};
if (type === "invoke") {
const request = asNullableRecord(parsed?.request);
if (request && typeof request.id === "string" && typeof request.nodeId === "string" && typeof request.command === "string") return {
type,
generation,
request
};
return null;
}
if (type === "gateway-response") {
const id = typeof parsed?.id === "string" ? parsed.id : "";
if (!id) return null;
return parsed?.ok === true ? {
type,
generation,
id,
ok: true,
result: parsed.result
} : {
type,
generation,
id,
ok: false,
error: object({
code: string(),
message: string()
}).parse(parsed?.error)
};
}
if (type === "invoke-input") {
const invokeId = typeof parsed?.invokeId === "string" ? parsed.invokeId : "";
const seq = typeof parsed?.seq === "number" ? parsed.seq : -1;
const payloadJSON = typeof parsed?.payloadJSON === "string" ? parsed.payloadJSON : null;
return invokeId && Number.isInteger(seq) && seq >= 0 && payloadJSON !== null ? {
type,
generation,
invokeId,
seq,
payloadJSON
} : null;
}
if (type === "invoke-cancel") {
const invokeId = typeof parsed?.invokeId === "string" ? parsed.invokeId : "";
return invokeId ? {
type,
generation,
invokeId
} : null;
}
return null;
} catch {
return null;
}
}
var NodeHostWorkerBridgeClient = class {
setConnection(generation, connected) {
this.pending.rejectAll(/* @__PURE__ */ new Error("node-host Gateway route changed"));
this.generation = generation;
this.connected = connected;
}
withConnection(generation, run) {
return this.invocationGeneration.run(generation, run);
}
constructor(writeMessage) {
this.writeMessage = writeMessage;
this.nextRequestId = 1;
this.generation = 0;
this.connected = false;
this.invocationGeneration = new AsyncLocalStorage();
this.pending = createPendingRequestRegistry();
}
async request(method, params, opts) {
const generation = this.invocationGeneration.getStore() ?? this.generation;
if (!this.connected || generation !== this.generation) throw new Error("node-host Gateway route is closed");
if (method === "node.invoke.result") {
this.writeMessage({
type: "invoke-result",
generation,
result: params ?? {}
});
return {};
}
if (method === "node.event") {
this.writeMessage({
type: "node-event",
generation,
event: params ?? {}
});
return {};
}
const id = `gateway-${this.nextRequestId++}`;
const timeoutMs = resolveTimerTimeoutMs(opts?.timeoutMs, 15e3);
const pending = this.pending.add(id, {
value: void 0,
timeoutMs,
timeoutError: () => /* @__PURE__ */ new Error(`Gateway request timed out: ${method}`)
});
if (!pending) throw new Error(`Gateway request id collision: ${id}`);
this.writeMessage({
type: "gateway-request",
generation,
id,
method,
params: params ?? {},
timeoutMs
});
return await pending.promise;
}
handleResponse(message) {
if (message.generation !== this.generation) return false;
const pending = this.pending.take(message.id);
if (!pending) return false;
if (message.ok) pending.resolve(message.result);
else pending.reject(new GatewayClientRequestError({
code: message.error.code,
message: message.error.message
}));
return true;
}
close() {
this.connected = false;
this.pending.rejectAll(/* @__PURE__ */ new Error("node-host worker stopped"));
}
};
async function stopNodeHostWorkerFromSignal(input, stop, exitCode) {
const stopped = stop(exitCode);
input.close();
await stopped;
}
//#endregion
//#region src/node-host/worker.ts
/** Private JSONL worker exposing the CLI node-host runtime to the macOS app. */
function writeMessage(message) {
process.stdout.write(`${JSON.stringify(message)}\n`);
}
function writeStderrLine(message) {
process.stderr.write(`${message}\n`);
}
async function runNodeHostWorker() {
await runStartupMigrations({ log: {
info: writeStderrLine,
warn: writeStderrLine
} });
const nodeConfig = await loadNodeHostConfig();
const prepared = await prepareNodeHostRuntime({
enableDuplexPluginCommands: true,
enableWorkerRuns: true,
installedAppsSharingEnabled: nodeConfig?.installedAppsSharing === true
});
const client = new NodeHostWorkerBridgeClient(writeMessage);
let stopping = false;
let resolveStopped;
const stopped = new Promise((resolve) => {
resolveStopped = resolve;
});
const stop = async (exitCode) => {
if (stopping) return;
stopping = true;
try {
client.close();
await runtime.close();
process.exitCode = exitCode;
} finally {
resolveStopped?.();
}
};
let generation = 0;
let connected = false;
let readySent = false;
let currentManifest = prepared.manifest;
const runtime = startNodeHostConnection({
prepared,
client,
writeStderrLine,
onManifestChanged: (manifest) => {
currentManifest = manifest;
if (readySent) {
connected = false;
client.setConnection(generation, false);
runtime.disconnect();
writeMessage({
type: "manifest",
version: VERSION,
manifest
});
}
}
});
writeMessage({
type: "ready",
version: VERSION,
manifest: currentManifest
});
readySent = true;
const input = createInterface({
input: process.stdin,
crlfDelay: Infinity
});
input.on("line", (line) => {
const message = parseNodeHostWorkerInput(line);
if (!message) {
writeMessage({
type: "protocol-error",
error: "invalid worker request"
});
return;
}
if (message.type === "gateway-connection") {
if (message.generation <= generation) return;
generation = message.generation;
connected = message.connection !== null;
client.setConnection(generation, connected);
runtime.disconnect();
if (message.connection) {
const connectionGeneration = generation;
runtime.connect(message.connection, { request: (...args) => client.withConnection(connectionGeneration, () => client.request(...args)) });
}
return;
}
if (message.type === "gateway-response") {
client.handleResponse(message);
return;
}
if (message.type === "stop") {
input.close();
stop(0);
return;
}
if (!connected || message.generation !== generation) return;
if (message.type === "invoke-input") {
runtime.handleInput(message.invokeId, message.seq, message.payloadJSON);
return;
}
if (message.type === "invoke-cancel") {
runtime.cancel(message.invokeId);
return;
}
client.withConnection(generation, () => runtime.invoke(message.request));
});
input.on("close", () => void stop(0));
const onInterrupt = () => void stopNodeHostWorkerFromSignal(input, stop, 130);
const onTerminate = () => void stopNodeHostWorkerFromSignal(input, stop, 143);
process.once("SIGINT", onInterrupt);
process.once("SIGTERM", onTerminate);
try {
await stopped;
} finally {
process.off("SIGINT", onInterrupt);
process.off("SIGTERM", onTerminate);
requestExitAfterOneShotOutput();
}
}
//#endregion
//#region src/cli/node-cli/identity.ts
/**
* Read-only by design: the SSH-verified pairing probe calls this remotely and
* must never mint a fresh identity on a host that has not run the node host.
*/
function runNodeIdentityShow(opts) {
const identity = loadDeviceIdentityIfPresent();
if (!identity) {
defaultRuntime.error("no node device identity found (start the node host once with `openclaw node run` or `openclaw node install`)");
defaultRuntime.exit(1);
return;
}
const payload = {
deviceId: identity.deviceId,
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem)
};
if (opts.json) {
writeRuntimeJson(defaultRuntime, payload, 0);
return;
}
defaultRuntime.log(`deviceId: ${payload.deviceId}`);
defaultRuntime.log(`publicKey: ${payload.publicKey}`);
}
//#endregion
//#region src/cli/node-cli/register.ts
function registerNodeCli(program) {
const node = program.command("node").description("Run and manage the headless node host service").addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([
["openclaw node run --host 127.0.0.1 --port 18789", "Run the node host in the foreground."],
["openclaw node status", "Check node host service status."],
["openclaw node install", "Install the node host service."],
["openclaw node start", "Start the installed node host service."],
["openclaw node restart", "Restart the installed node host service."]
])}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/node", "docs.openclaw.ai/cli/node")}\n`);
node.command("worker", { hidden: true }).description("Run the private macOS app node-host worker").action(async () => {
await runNodeHostWorker();
});
node.command("run").description("Run the headless node host (foreground)").option("--pair <code-or-url>", "Pair with a setup code or oc-pair URL; explicit gateway flags take precedence").option("--host <host>", "Gateway host").option("--port <port>", "Gateway port").option("--context-path <path>", "Gateway WebSocket context path (e.g. /openclaw-gw)").option("--tls", "Use TLS for the gateway connection").option("--no-tls", "Disable TLS for the gateway connection").option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)").option("--node-id <id>", "Override the generated node instance id").option("--display-name <name>", "Override node display name").addOption(new Option("--ephemeral").hideHelp()).option("--share-installed-apps", "Share installed macOS applications with the Gateway").option("--no-share-installed-apps", "Disable installed application sharing").action(async (opts) => {
let pair;
let gatewayOptions;
try {
pair = opts.pair ? resolveNodePairGatewayOptions(opts.pair) : void 0;
const existing = await loadNodeHostConfig();
gatewayOptions = resolveNodeGatewayOptions(opts, existing, pair);
} catch (error) {
defaultRuntime.error(error instanceof Error ? error.message : String(error));
defaultRuntime.exit(1);
return;
}
const { host, port, contextPath, tls, tlsFingerprint, cloudflareAccess, gatewayCandidates } = gatewayOptions;
if (port === null) {
defaultRuntime.error(formatInvalidPortOption("--port"));
defaultRuntime.exit(1);
return;
}
if (opts.tls === false && opts.tlsFingerprint !== void 0) {
defaultRuntime.error("--no-tls cannot be combined with --tls-fingerprint");
defaultRuntime.exit(1);
return;
}
await runNodeHost({
gatewayHost: host,
gatewayPort: port,
gatewayTls: tls,
gatewayTlsFingerprint: tlsFingerprint,
gatewayContextPath: contextPath,
gatewayCloudflareAccess: cloudflareAccess,
gatewayCandidates,
gatewayBootstrapToken: pair?.bootstrapToken,
preferGatewayBootstrapToken: pair !== void 0,
...opts.ephemeral === true ? {
forceWorkerRuns: true,
ephemeral: true
} : {},
nodeId: opts.nodeId,
displayName: opts.displayName,
installedAppsSharing: opts.shareInstalledApps
});
});
node.command("status").description("Show node host status").option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonStatus(opts);
});
node.command("identity").description("Print the node host device identity (device id + public key)").option("--json", "Output JSON", false).action((opts) => {
runNodeIdentityShow(opts);
});
node.command("install").description("Install the node host service (launchd/systemd/schtasks)").option("--host <host>", "Gateway host").option("--port <port>", "Gateway port").option("--context-path <path>", "Gateway WebSocket context path (e.g. /openclaw-gw)").option("--tls", "Use TLS for the gateway connection").option("--no-tls", "Disable TLS for the gateway connection").option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)").option("--node-id <id>", "Override the generated node instance id").option("--display-name <name>", "Override node display name").option("--share-installed-apps", "Share installed macOS applications with the Gateway").option("--no-share-installed-apps", "Disable installed application sharing").option("--runtime <runtime>", "Service runtime (node|bun). Default: node").option("--force", "Reinstall/overwrite if already installed", false).option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonInstall(opts);
});
for (const [name, action] of [
["uninstall", runNodeDaemonUninstall],
["stop", runNodeDaemonStop],
["start", runNodeDaemonStart],
["restart", runNodeDaemonRestart]
]) node.command(name).description(`${name.charAt(0).toUpperCase()}${name.slice(1)} the node host service (launchd/systemd/schtasks)`).option("--json", "Output JSON", false).action(async (opts) => {
await action(opts);
});
}
//#endregion
export { registerNodeCli };