openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
276 lines (275 loc) • 10.4 kB
JavaScript
import { n as collectErrorGraphCandidates } from "./error-coercion-D_-xJ90S.js";
import { n as isTruthyEnvValue } from "./env-M3R40TOb.js";
import "./error-runtime-Bz9Tw57Z.js";
import "./runtime-env-0Q-gCyUb.js";
import fs from "node:fs";
import os from "node:os";
//#region extensions/bonjour/src/errors.ts
/**
* Bonjour error formatting helper. It normalizes Error and non-Error values
* into concise messages for gateway discovery logs.
*/
/** Format an unknown Bonjour/ciao error value for logs. */
function formatBonjourError(err) {
if (err instanceof Error) {
const msg = err.message.trim() || err.name || String(err).trim();
if (err.name && err.name !== "Error") return msg === err.name ? err.name : `${err.name}: ${msg}`;
return msg;
}
return String(err);
}
//#endregion
//#region extensions/bonjour/src/ciao.ts
/**
* Ciao process-error classifier. It recognizes known noisy ciao failures so
* the Bonjour plugin can suppress or repair expected mDNS lifecycle issues.
*/
const CIAO_NETMASK_ASSERTION_MESSAGE_RE = /IP ADDRESS VERSION MUST MATCH\.\s+NETMASK CANNOT HAVE A VERSION DIFFERENT FROM THE ADDRESS!?/u;
const CIAO_INTERFACE_ENUMERATION_FAILURE_RE = /\bUV_INTERFACE_ADDRESSES\b/u;
/** Classify a ciao error/rejection chain into a known category. */
function classifyCiaoProcessError(reason) {
for (const candidate of collectErrorGraphCandidates(reason, (current) => [
current.cause,
current.reason,
current.original,
current.error,
current.data,
...Array.isArray(current.errors) ? current.errors : []
])) {
const formatted = formatBonjourError(candidate);
const message = formatted.toUpperCase();
if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) return {
kind: "netmask-assertion",
formatted
};
if (CIAO_INTERFACE_ENUMERATION_FAILURE_RE.test(message)) return {
kind: "interface-enumeration-failure",
formatted
};
}
return null;
}
//#endregion
//#region extensions/bonjour/src/advertiser.ts
/** Publishes gateway/SSH records through one ciao-owned advertisement lifecycle. */
const CIAO_SELF_PROBE_RETRY_FRAGMENT = "failed probing with reason: Error: Can't probe for a service which is announced already.";
const defaultLogger = {
info: (_msg) => {},
warn: (_msg) => {},
debug: (_msg) => {}
};
function readBonjourDisableOverride() {
const raw = process.env.OPENCLAW_DISABLE_BONJOUR;
const normalized = raw?.trim().toLowerCase();
if (!normalized) return null;
if (isTruthyEnvValue(raw)) return true;
switch (normalized) {
case "0":
case "false":
case "no":
case "off": return false;
default: return null;
}
}
function isContainerEnvironment() {
if (process.env.FLY_MACHINE_ID?.trim() && process.env.FLY_APP_NAME?.trim()) return true;
for (const sentinelPath of [
"/.dockerenv",
"/run/.containerenv",
"/var/run/.containerenv"
]) try {
if (fs.existsSync(sentinelPath)) return true;
} catch {}
try {
const cgroup = fs.readFileSync("/proc/1/cgroup", "utf8");
return /\/docker\/|cri-containerd-[0-9a-f]|containerd\/[0-9a-f]{64}|\/kubepods[/.]|\blxc\b/u.test(cgroup);
} catch {
return false;
}
}
function isDisabledByEnv() {
if (process.env.VITEST) return true;
const envOverride = readBonjourDisableOverride();
if (envOverride !== null) return envOverride;
if (isContainerEnvironment()) return true;
return false;
}
function resolveSystemMdnsHostname() {
let raw;
try {
raw = os.hostname();
} catch {
return null;
}
const trimmed = raw.trim();
if (!trimmed) return null;
const firstLabel = trimmed.replace(/\.local$/i, "").split(".")[0]?.trim() ?? "";
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(firstLabel)) return null;
return firstLabel;
}
const MAX_DNS_LABEL_BYTES = 63;
const utf8Encoder = new TextEncoder();
function truncateToDnsLabel(name, fallback = "OpenClaw") {
const encoded = utf8Encoder.encode(name);
if (encoded.byteLength <= MAX_DNS_LABEL_BYTES) return name;
for (let end = MAX_DNS_LABEL_BYTES; end > 0; end -= 1) try {
return new TextDecoder("utf-8", { fatal: true }).decode(encoded.subarray(0, end)).replace(/-+$/, "").trim() || fallback;
} catch {}
return fallback;
}
function safeServiceName(name) {
const trimmed = name.trim();
return trimmed.length > 0 ? truncateToDnsLabel(trimmed) : "OpenClaw";
}
function prettifyInstanceName(name) {
const normalized = name.trim().replace(/\s+/g, " ");
return normalized.replace(/\s+\(OpenClaw\)\s*$/i, "").trim() || normalized;
}
function serviceSummary(label, svc) {
return `${label} fqdn=${svc.getFQDN()} host=${svc.getHostname()} port=${svc.getPort()} state=${svc.serviceState}`;
}
function shouldSuppressCiaoConsoleLog(args) {
return args.some((arg) => typeof arg === "string" && arg.includes(CIAO_SELF_PROBE_RETRY_FRAGMENT));
}
function installCiaoConsoleNoiseFilter() {
const previousConsoleLog = console.log;
const wrapper = ((...args) => {
if (shouldSuppressCiaoConsoleLog(args)) return;
previousConsoleLog(...args);
});
console.log = wrapper;
return () => {
if (console.log === wrapper) console.log = previousConsoleLog;
};
}
/** Start Bonjour advertisements for the local gateway services. */
async function startGatewayBonjourAdvertiser(opts, deps) {
if (isDisabledByEnv()) return { stop: async () => {} };
const logger = {
info: deps.logger?.info ?? defaultLogger.info,
warn: deps.logger?.warn ?? defaultLogger.warn,
debug: deps.logger?.debug ?? defaultLogger.debug
};
let restoreConsoleLog = () => {};
let cleanupUnhandledRejection;
let cleanupUncaughtException;
let processHandlersCleaned = false;
function cleanupProcessHandlers() {
if (processHandlersCleaned) return;
processHandlersCleaned = true;
cleanupUncaughtException?.();
cleanupUnhandledRejection?.();
}
try {
const { getResponder } = await import("@homebridge/ciao");
restoreConsoleLog = installCiaoConsoleNoiseFilter();
const handleCiaoProcessError = (reason) => {
const classification = classifyCiaoProcessError(reason);
if (!classification) return false;
if (classification.kind === "interface-enumeration-failure") logger.warn(`bonjour: disabling mDNS — networkInterfaces() unavailable in this environment: ${classification.formatted}`);
else logger.warn(`bonjour: suppressing ciao netmask assertion: ${classification.formatted}`);
return true;
};
cleanupUnhandledRejection = deps.registerUnhandledRejectionHandler(handleCiaoProcessError);
cleanupUncaughtException = deps.registerUncaughtExceptionHandler(handleCiaoProcessError);
const hostnameWithoutLocal = (process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || resolveSystemMdnsHostname() || "openclaw").replace(/\.local$/i, "");
const dotIndex = hostnameWithoutLocal.indexOf(".");
const labelEnd = dotIndex === -1 ? hostnameWithoutLocal.length : dotIndex;
const hostname = truncateToDnsLabel(hostnameWithoutLocal.slice(0, labelEnd).trim() || "openclaw", "openclaw");
const instanceName = typeof opts.instanceName === "string" && opts.instanceName.trim() ? opts.instanceName.trim() : `${hostname} (OpenClaw)`;
const displayName = prettifyInstanceName(instanceName);
const txtBase = {
role: "gateway",
gatewayPort: String(opts.gatewayPort),
lanHost: `${hostname}.local`,
displayName
};
if (opts.gatewayTlsEnabled) {
txtBase.gatewayTls = "1";
if (opts.gatewayTlsFingerprintSha256) txtBase.gatewayTlsSha256 = opts.gatewayTlsFingerprintSha256;
}
if (opts.gatewayDirectReachable) txtBase.gatewayDirectReachable = "1";
if (!opts.minimal && typeof opts.tailnetDns === "string" && opts.tailnetDns.trim()) txtBase.tailnetDns = opts.tailnetDns.trim();
if (!opts.minimal && typeof opts.cliPath === "string" && opts.cliPath.trim()) txtBase.cliPath = opts.cliPath.trim();
const gatewayTxt = {
...txtBase,
transport: "gateway"
};
if (!opts.minimal) gatewayTxt.sshPort = String(opts.sshPort ?? 22);
const responder = getResponder();
function createServices() {
const services = [];
const gateway = responder.createService({
name: safeServiceName(instanceName),
type: "openclaw-gw",
port: opts.gatewayPort,
domain: "local",
hostname,
txt: gatewayTxt
});
services.push({
label: "gateway",
svc: gateway
});
return services;
}
async function stopServices(services) {
for (const { svc } of services) try {
await svc.destroy();
} catch {}
try {
await responder.shutdown();
} catch {}
}
function attachConflictListeners(services) {
for (const { label, svc } of services) try {
svc.on("name-change", (name) => {
logger.warn(`bonjour: ${label} name conflict resolved; newName=${JSON.stringify(name)}`);
});
svc.on("hostname-change", (nextHostname) => {
logger.warn(`bonjour: ${label} hostname conflict resolved; newHostname=${JSON.stringify(nextHostname)}`);
});
} catch (err) {
logger.debug(`bonjour: failed to attach listeners for ${label}: ${String(err)}`);
}
}
function handleAdvertiseFailure(label, svc, err, action) {
const classification = classifyCiaoProcessError(err);
if (classification) {
logger.warn(`bonjour: advertise ${action} with ciao ${classification.kind} (${serviceSummary(label, svc)}): ${classification.formatted}`);
return;
}
logger.warn(`bonjour: advertise ${action} (${serviceSummary(label, svc)}): ${formatBonjourError(err)}`);
}
function startAdvertising(services) {
for (const { label, svc } of services) try {
svc.advertise().then(() => {
logger.info(`bonjour: advertised ${serviceSummary(label, svc)}`);
}).catch((err) => {
handleAdvertiseFailure(label, svc, err, "failed");
});
} catch (err) {
handleAdvertiseFailure(label, svc, err, "threw");
}
}
logger.debug(`bonjour: starting (hostname=${hostname}, instance=${JSON.stringify(safeServiceName(instanceName))}, gatewayPort=${opts.gatewayPort}${opts.minimal ? ", minimal=true" : `, sshPort=${opts.sshPort ?? 22}`})`);
const services = createServices();
attachConflictListeners(services);
startAdvertising(services);
let stopPromise = null;
return { stop: () => {
stopPromise ??= (async () => {
await stopServices(services);
restoreConsoleLog();
cleanupProcessHandlers();
})();
return stopPromise;
} };
} catch (err) {
restoreConsoleLog();
cleanupProcessHandlers();
throw err;
}
}
//#endregion
export { startGatewayBonjourAdvertiser };