import type { AdapterEnvironmentCheck, AdapterEnvironmentTestContext, AdapterEnvironmentTestResult, } from "@paperclip/adapter-utils"; import { asString, parseObject } from "@paperclip/adapter-utils/server-utils"; function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] { if (checks.some((check) => check.level === "error")) return "fail"; if (checks.some((check) => check.level === "warn")) return "warn"; return "pass"; } function isLoopbackHost(hostname: string): boolean { const value = hostname.trim().toLowerCase(); return value === "localhost" || value === "127.0.0.1" || value === "::1"; } export async function testEnvironment( ctx: AdapterEnvironmentTestContext, ): Promise { const checks: AdapterEnvironmentCheck[] = []; const config = parseObject(ctx.config); const urlValue = asString(config.url, ""); if (!urlValue) { checks.push({ code: "openclaw_url_missing", level: "error", message: "OpenClaw adapter requires a webhook URL.", hint: "Set adapterConfig.url to your OpenClaw webhook endpoint.", }); return { adapterType: ctx.adapterType, status: summarizeStatus(checks), checks, testedAt: new Date().toISOString(), }; } let url: URL | null = null; try { url = new URL(urlValue); } catch { checks.push({ code: "openclaw_url_invalid", level: "error", message: `Invalid URL: ${urlValue}`, }); } if (url && url.protocol !== "http:" && url.protocol !== "https:") { checks.push({ code: "openclaw_url_protocol_invalid", level: "error", message: `Unsupported URL protocol: ${url.protocol}`, hint: "Use an http:// or https:// endpoint.", }); } if (url) { checks.push({ code: "openclaw_url_valid", level: "info", message: `Configured endpoint: ${url.toString()}`, }); if (isLoopbackHost(url.hostname)) { checks.push({ code: "openclaw_loopback_endpoint", level: "warn", message: "Endpoint uses loopback hostname. Remote OpenClaw workers cannot reach localhost on the Paperclip host.", hint: "Use a reachable hostname/IP (for example Tailscale/private hostname or public domain).", }); } } const method = asString(config.method, "POST").trim().toUpperCase() || "POST"; checks.push({ code: "openclaw_method_configured", level: "info", message: `Configured method: ${method}`, }); if (url && (url.protocol === "http:" || url.protocol === "https:")) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); try { const response = await fetch(url, { method: "HEAD", signal: controller.signal }); if (!response.ok && response.status !== 405 && response.status !== 501) { checks.push({ code: "openclaw_endpoint_probe_unexpected_status", level: "warn", message: `Endpoint probe returned HTTP ${response.status}.`, hint: "Verify OpenClaw webhook reachability and auth/network settings.", }); } else { checks.push({ code: "openclaw_endpoint_probe_ok", level: "info", message: "Endpoint responded to a HEAD probe.", }); } } catch (err) { checks.push({ code: "openclaw_endpoint_probe_failed", level: "warn", message: err instanceof Error ? err.message : "Endpoint probe failed", hint: "This may be expected in restricted networks; validate from the Paperclip server host.", }); } finally { clearTimeout(timeout); } } return { adapterType: ctx.adapterType, status: summarizeStatus(checks), checks, testedAt: new Date().toISOString(), }; }