UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

573 lines (572 loc) • 22.2 kB
#!/usr/bin/env node import { s as resolveOpenClawStateSqlitePath, u as disableExitUnsafeCompilers } from "./openclaw-state-db.paths-BENDWC2V.mjs"; import { r as racePromiseWithAbortSignal } from "./src-DTVzOLXp.mjs"; import { d as toErrorObject } from "./errno-jiy5vtf-.mjs"; import { u as parseStrictPositiveInteger } from "./number-coercion-PJEgwXYS.mjs"; import { Y as isSqliteLockError, u as SqliteSchemaVersionError } from "./sqlite-worker-identity-CfwbX7eo.mjs"; import { i as isPidDefinitelyDead } from "./pid-alive-Cnn-Mjdg.mjs"; import { m as sleep } from "./utils-Bv29YgC5.mjs"; import { t as ADMIN_SCOPE } from "./operator-scopes-C844xNSs.mjs"; import { o as resolveSafeTimeoutDelayMs } from "./timeouts-DpuJHBKD.mjs"; import process$1 from "node:process"; import { request } from "node:http"; import { constants } from "node:fs"; import path from "node:path"; import { open } from "node:fs/promises"; import { request as request$1 } from "node:https"; //#region src/process/output-drain.ts const OUTPUT_DRAIN_TIMEOUT_MS = 5e3; /** Drain both output pipes before a bounded owner termination. */ function drainProcessOutput(exit) { let pendingStreams = 2; const fallback = setTimeout(exit, OUTPUT_DRAIN_TIMEOUT_MS); fallback.unref(); const drain = (stream) => { stream.write("", () => { pendingStreams -= 1; if (pendingStreams === 0) { clearTimeout(fallback); setImmediate(exit); } }); }; drain(process.stdout); drain(process.stderr); } //#endregion //#region src/infra/native-error-response.ts function restoreNativeErrorResponse(value) { const cause = value.cause ? Object.assign(new Error(value.cause.message), value.cause) : void 0; return Object.assign(new Error(value.message, cause ? { cause } : void 0), { name: value.name, code: value.code, errcode: value.errcode }); } //#endregion //#region src/agents/harness/native-hook-relay-client-store.ts /** Read one native relay locator without loading the shared-state writer lifecycle. */ async function readNativeHookRelayClientBridgeRecord(params) { const pathname = path.resolve(params.stateDbPath ?? resolveOpenClawStateSqlitePath()); const input = { relayId: params.relayId, stateDbPath: pathname }; if (!process.versions.bun) { const { readNativeHookRelayClientRecord } = await import("./native-hook-relay-client-read-DlwSOYZM.mjs"); return readNativeHookRelayClientRecord(input, 0); } const [{ WorkerTaskPool }, { resolveRuntimeProcessEntrypointUrl }] = await Promise.all([import("./worker-task-pool-DdKZhlAR.mjs"), import("./runtime-process-url-4myuGG5q.mjs")]); const pool = new WorkerTaskPool({ workerUrl: resolveRuntimeProcessEntrypointUrl("nativeHookRelayClient"), workerClass: "singleton" }); try { const result = await pool.run(input, { inputBytes: 2 * (params.relayId.length + pathname.length) }); if (!result.ok) throw result.newerSchema ? new SqliteSchemaVersionError(result.error.message) : restoreNativeErrorResponse(result.error); return result.record; } finally { await pool.close(); } } //#endregion //#region src/agents/harness/native-hook-relay-constants.ts const DEFAULT_RELAY_TIMEOUT_MS = 5e3; //#endregion //#region src/agents/harness/native-hook-relay-response-codec.ts function renderJsonResponse(payload) { return { stdout: `${JSON.stringify(payload)}\n`, stderr: "", exitCode: 0 }; } /** Render the native Codex hook responses shared by server and cold client paths. */ const codexNativeHookRelayResponseCodec = { renderNoopResponse() { return { stdout: "", stderr: "", exitCode: 0 }; }, renderPreToolUseBlockResponse(reason, failureDisposition) { return { ...renderJsonResponse({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }), ...failureDisposition ? { failureDisposition } : {} }; }, renderPermissionDecisionResponse(decision, message) { return renderJsonResponse({ hookSpecificOutput: { hookEventName: "PermissionRequest", decision: decision === "allow" ? { behavior: "allow" } : { behavior: "deny", message: message?.trim() || "Denied by OpenClaw" } } }); }, renderBeforeAgentFinalizeReviseResponse: (reason) => renderJsonResponse({ decision: "block", reason }), renderBeforeAgentFinalizeStopResponse: (reason) => renderJsonResponse({ continue: false, ...reason?.trim() ? { stopReason: reason.trim() } : {} }) }; //#endregion //#region src/agents/harness/native-hook-relay-utils.ts function normalizePositiveInteger(value, fallback) { return normalizeOptionalPositiveInteger(value) ?? fallback; } function normalizeOptionalPositiveInteger(value) { return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0; } function readNativeHookRelayProvider(value) { if (value === "codex") return value; throw new Error("unsupported native hook relay provider"); } function readNativeHookRelayEvent(value) { if (value === "pre_tool_use" || value === "post_tool_use" || value === "permission_request" || value === "before_agent_finalize") return value; throw new Error("unsupported native hook relay event"); } function readNonEmptyString(value, name) { if (typeof value === "string" && value.trim()) return value.trim(); throw new Error(`native hook relay ${name} is required`); } //#endregion //#region src/agents/harness/native-hook-relay-client.ts const MAX_NATIVE_HOOK_BRIDGE_RESPONSE_BYTES = 5e6; const NATIVE_HOOK_BRIDGE_RETRY_INTERVAL_MS = 25; /** Invoke a registered native relay through its read-only SQLite locator. */ async function invokeNativeHookRelayBridge(params) { const provider = readNativeHookRelayProvider(params.provider); const relayId = readNonEmptyString(params.relayId, "relayId"); const event = readNativeHookRelayEvent(params.event); const timeoutMs = normalizePositiveInteger(params.timeoutMs, DEFAULT_RELAY_TIMEOUT_MS); const registrationTimeoutMs = normalizePositiveInteger(params.registrationTimeoutMs, timeoutMs); const startedAt = Date.now(); let lastError = /* @__PURE__ */ new Error("native hook relay bridge not found"); while (Date.now() - startedAt < timeoutMs) try { const record = await readNativeHookRelayClientBridgeRecord({ relayId, stateDbPath: params.stateDbPath }); if (!record) throw new Error("native hook relay bridge not found"); if (isPidDefinitelyDead(record.pid)) throw new Error("native hook relay bridge not found"); if (Date.now() > record.expiresAtMs) throw new Error("native hook relay bridge expired"); const remainingMs = timeoutMs - (Date.now() - startedAt); if (remainingMs <= 0) throw new Error("native hook relay bridge timed out"); return await postNativeHookRelayBridgeRecord({ record, timeoutMs: remainingMs, payload: { provider, relayId, event, generation: params.generation, rawPayload: params.rawPayload } }); } catch (error) { lastError = error; const elapsedMs = Date.now() - startedAt; if (error instanceof Error && error.message === "native hook relay bridge not found" && elapsedMs >= registrationTimeoutMs) break; if (!isRetryableNativeHookRelayBridgeLookupError({ error, elapsedMs })) break; await sleep(Math.min(NATIVE_HOOK_BRIDGE_RETRY_INTERVAL_MS, timeoutMs - elapsedMs)); } throw lastError instanceof Error ? lastError : new Error(String(lastError)); } function postNativeHookRelayBridgeRecord(params) { const body = JSON.stringify(params.payload); return new Promise((resolve, reject) => { let settled = false; const rejectOnce = (error) => { if (!settled) { settled = true; reject(toErrorObject(error, "Non-Error rejection")); } }; const req = request({ hostname: params.record.hostname, method: "POST", path: "/invoke", port: params.record.port, timeout: params.timeoutMs, headers: { authorization: `Bearer ${params.record.token}`, "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => { let responseText = ""; let responseBytes = 0; res.setEncoding("utf8"); res.on("data", (chunk) => { const chunkText = typeof chunk === "string" ? chunk : String(chunk); responseBytes += Buffer.byteLength(chunkText); if (responseBytes > MAX_NATIVE_HOOK_BRIDGE_RESPONSE_BYTES) { rejectOnce(/* @__PURE__ */ new Error("native hook relay bridge response too large")); res.destroy(); return; } responseText += chunkText; }); res.on("error", rejectOnce); res.on("end", () => { if (settled) return; try { const parsed = JSON.parse(responseText); if (parsed.ok) { settled = true; resolve(parsed.result); return; } rejectOnce(new Error(parsed.error || "native hook relay bridge failed")); } catch (error) { rejectOnce(error); } }); }); req.on("timeout", () => { req.destroy(/* @__PURE__ */ new Error("native hook relay bridge timed out")); }); req.on("error", rejectOnce); req.end(body); }); } function isRetryableNativeHookRelayBridgeError(error) { const code = error.code; return code === "ENOENT" || code === "ECONNREFUSED" || code === "EAGAIN" || isSqliteLockError(error) || error instanceof Error && error.message === "native hook relay bridge not found"; } function isRetryableNativeHookRelayBridgeLookupError(params) { return isRetryableNativeHookRelayBridgeError(params.error) || params.elapsedMs < 250 && isNativeHookRelayBridgeStaleRegistrationError(params.error); } /** Detect a stale locator response that must not fall back to the Gateway. */ function isNativeHookRelayBridgeStaleRegistrationError(error) { return error instanceof Error && error.message === "native hook relay bridge stale registration"; } /** Render the provider response used when both relay transports are unavailable. */ function renderNativeHookRelayUnavailableResponse(params) { readNativeHookRelayProvider(params.provider); const event = readNativeHookRelayEvent(params.event); const message = params.message?.trim() || "Native hook relay unavailable"; if (event === "pre_tool_use") { if (params.preToolUseUnavailable === "noop") return codexNativeHookRelayResponseCodec.renderNoopResponse(); return codexNativeHookRelayResponseCodec.renderPreToolUseBlockResponse(message); } if (event === "permission_request") return codexNativeHookRelayResponseCodec.renderPermissionDecisionResponse("deny", message); return codexNativeHookRelayResponseCodec.renderNoopResponse(); } //#endregion //#region src/agents/harness/native-hook-relay-remote-client.ts const MAX_CREDENTIAL_BYTES = 16384; const MAX_RESPONSE_BYTES = 1048576; /** Read a dedicated credential, never a Gateway config or shared state database. */ async function invokeRemoteNativeHookRelay(credentialPath, params, signal) { const file = await open(credentialPath, constants.O_RDONLY | constants.O_NOFOLLOW); let credential; try { const stat = await file.stat(); if (!stat.isFile() || stat.size > MAX_CREDENTIAL_BYTES) throw new Error("Invalid native hook callback credential"); const buffer = Buffer.alloc(16385); const { bytesRead } = await file.read(buffer, 0, buffer.length, 0); if (bytesRead > MAX_CREDENTIAL_BYTES) throw new Error("Invalid native hook callback credential"); try { credential = JSON.parse(buffer.subarray(0, bytesRead).toString("utf8")); } catch { throw new Error("Invalid native hook callback credential"); } } finally { await file.close(); } if (!credential || typeof credential.url !== "string" || typeof credential.token !== "string" || !credential.token) throw new Error("Invalid native hook callback credential"); let url; try { url = new URL(credential.url); } catch { throw new Error("Invalid native hook callback URL"); } if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error("Native hook callback requires an HTTPS URL without credentials or query"); signal.throwIfAborted(); const body = JSON.stringify(params); return await new Promise((resolve, reject) => { const req = request$1(url, { method: "POST", signal, headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => { if (res.statusCode !== 200) { res.destroy(); reject(/* @__PURE__ */ new Error(`Native hook callback rejected (${res.statusCode ?? 0})`)); return; } const chunks = []; let size = 0; res.on("data", (chunk) => { size += chunk.length; if (size > MAX_RESPONSE_BYTES) { reject(/* @__PURE__ */ new Error("Native hook callback response too large")); res.destroy(); return; } chunks.push(chunk); }); res.on("error", () => reject(/* @__PURE__ */ new Error("Native hook callback response failed"))); res.on("end", () => { try { const decoded = JSON.parse(Buffer.concat(chunks).toString("utf8")); const result = decoded?.result; if (decoded?.ok !== true || !result || typeof result.stdout !== "string" || typeof result.stderr !== "string" || !Number.isInteger(result.exitCode)) throw new Error("Invalid response"); resolve(result); } catch { reject(/* @__PURE__ */ new Error("Invalid native hook callback response")); } }); }); req.on("error", () => reject(/* @__PURE__ */ new Error("Native hook callback connection failed"))); req.end(body); }); } //#endregion //#region src/utils/timer-delay.ts function setSafeTimeout(callback, delayMs, opts) { return setTimeout(callback, resolveSafeTimeoutDelayMs(delayMs, opts)); } //#endregion //#region src/cli/parse-timeout.ts function invalidTimeout(flagName, value) { const suffix = value ? ` Received: "${value}".` : ""; return /* @__PURE__ */ new Error(`Invalid ${flagName}. Use a positive millisecond value, e.g. ${flagName} 30000.${suffix}`); } function parseTimeoutMsWithFallback(raw, fallbackMs, options = {}) { const flagName = options.flagName ?? "--timeout"; if (raw === void 0 || raw === null) return fallbackMs; const value = typeof raw === "string" ? raw.trim() : typeof raw === "number" || typeof raw === "bigint" ? String(raw) : null; if (!value) { if (options.invalidType === "error") throw invalidTimeout(flagName); return fallbackMs; } const parsed = parseStrictPositiveInteger(value); if (parsed === void 0) throw invalidTimeout(flagName, value); return parsed; } //#endregion //#region src/cli/native-hook-relay-cli.ts const MAX_NATIVE_HOOK_STDIN_BYTES = 1048576; const NATIVE_HOOK_RELAY_VALUE_FLAGS = { "--provider": "provider", "--relay-id": "relayId", "--state-db": "stateDb", "--remote-credential": "remoteCredential", "--generation": "generation", "--event": "event", "--pre-tool-use-unavailable": "preToolUseUnavailable", "--timeout": "timeout" }; var NativeHookRelayDeadlineError = class extends Error { constructor(timeoutMs) { super(`native hook relay timed out after ${timeoutMs}ms`); this.name = "NativeHookRelayDeadlineError"; } }; /** Parse and run the internal native relay directly from the process argument vector. */ async function runNativeHookRelayCliFromArgv(argv) { return await runNativeHookRelayCli(parseNativeHookRelayCliOptions(argv)); } function parseNativeHookRelayCliOptions(argv) { const relayIndex = argv.findIndex((arg, index) => arg === "relay" && argv[index - 1] === "hooks"); if (relayIndex < 0) throw new Error("native hook relay command path is required"); const opts = {}; for (let index = relayIndex + 1; index < argv.length; index += 1) { const rawFlag = argv[index] ?? ""; const equalsIndex = rawFlag.indexOf("="); const flag = equalsIndex > 0 ? rawFlag.slice(0, equalsIndex) : rawFlag; const key = NATIVE_HOOK_RELAY_VALUE_FLAGS[flag]; if (!key) throw new Error(`unknown native hook relay option: ${rawFlag}`); const value = equalsIndex > 0 ? rawFlag.slice(equalsIndex + 1) : argv[++index]; if (!value) throw new Error(`native hook relay option ${flag} requires a value`); opts[key] = value; } return opts; } /** Run one native hook relay invocation from stdin JSON to stdout/stderr response streams. */ async function runNativeHookRelayCli(opts) { const { stdin, stdout, stderr } = process; const provider = readRequiredOption(opts.provider, "provider"); const relayId = readRequiredOption(opts.relayId, "relay-id"); const generation = opts.generation?.trim() || void 0; const event = readRequiredOption(opts.event, "event"); let timeoutMs; try { timeoutMs = parseTimeoutMsWithFallback(opts.timeout, 5e3, { invalidType: "error" }); } catch (error) { writeText(stderr, formatRelayCliError("invalid native hook timeout", error)); return 1; } const deadline = createNativeHookRelayDeadline(timeoutMs); const writeResponse = (response) => { writeText(stdout, response.stdout); writeText(stderr, response.stderr); return response.exitCode; }; const unavailable = (message = "Native hook relay unavailable") => writeResponse(renderNativeHookRelayUnavailableResponse({ provider, event, preToolUseUnavailable: opts.preToolUseUnavailable, message })); const timedOut = (error) => { writeText(stderr, formatRelayCliError("native hook relay timed out", error)); return unavailable("Native hook relay timed out"); }; try { let rawPayload; try { const rawInput = await readStreamText(stdin, MAX_NATIVE_HOOK_STDIN_BYTES, deadline); rawPayload = rawInput.trim() ? JSON.parse(rawInput) : null; } catch (error) { if (isNativeHookRelayDeadlineError(error)) return timedOut(error); writeText(stderr, formatRelayCliError("failed to read native hook input", error)); return 1; } try { if (opts.remoteCredential) return writeResponse(await withNativeHookRelayDeadline(deadline, invokeRemoteNativeHookRelay(opts.remoteCredential, { provider, relayId, generation, event, rawPayload }, deadline.signal))); try { const remainingMs = remainingNativeHookRelayDeadlineMs(deadline); return writeResponse(await withNativeHookRelayDeadline(deadline, invokeNativeHookRelayBridge({ provider, relayId, stateDbPath: opts.stateDb?.trim() || void 0, generation, event, rawPayload, registrationTimeoutMs: Math.min(100, remainingMs), timeoutMs: remainingMs }))); } catch (error) { if (isNativeHookRelayDeadlineError(error) || isNativeHookRelayBridgeStaleRegistrationError(error)) throw error; } return writeResponse(await withNativeHookRelayDeadline(deadline, callGatewayLazy({ method: "nativeHook.invoke", params: { provider, relayId, generation, event, rawPayload }, timeoutMs: remainingNativeHookRelayDeadlineMs(deadline), signal: deadline.signal, scopes: [ADMIN_SCOPE] }))); } catch (error) { if (isNativeHookRelayDeadlineError(error)) return timedOut(error); writeText(stderr, formatRelayCliError("native hook relay unavailable", error)); return unavailable(); } } finally { deadline.dispose(); } } async function callGatewayLazy(opts) { const { callGateway } = await import("./call-DgSuw3IF.mjs"); return await callGateway(opts); } function readRequiredOption(value, name) { if (typeof value === "string" && value.trim()) return value.trim(); throw new Error(`Missing required option --${name}`); } async function readStreamText(stream, maxBytes, deadline) { const chunks = []; let total = 0; const abortRead = () => { stream.destroy(new NativeHookRelayDeadlineError(deadline.timeoutMs)); }; deadline.signal.addEventListener("abort", abortRead, { once: true }); try { remainingNativeHookRelayDeadlineMs(deadline); for await (const chunk of stream) { remainingNativeHookRelayDeadlineMs(deadline); const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); total += buffer.byteLength; if (total > maxBytes) throw new Error(`native hook input exceeds ${maxBytes} bytes`); chunks.push(buffer); } remainingNativeHookRelayDeadlineMs(deadline); return Buffer.concat(chunks, total).toString("utf8"); } catch (error) { if (isNativeHookRelayDeadlineError(error) || deadline.signal.aborted) throw new NativeHookRelayDeadlineError(deadline.timeoutMs); throw error; } finally { deadline.signal.removeEventListener("abort", abortRead); } } function writeText(stream, value) { if (value) stream.write(value); } function formatRelayCliError(prefix, error) { return `${prefix}: ${error instanceof Error ? error.message : String(error)}\n`; } function createNativeHookRelayDeadline(timeoutMs) { const controller = new AbortController(); const timer = setSafeTimeout(() => controller.abort(), timeoutMs); timer.unref?.(); return { expiresAtMs: performance.now() + timeoutMs, signal: controller.signal, timeoutMs, dispose: () => clearTimeout(timer) }; } function isNativeHookRelayDeadlineError(error) { return error instanceof Error && error.name === "NativeHookRelayDeadlineError"; } function remainingNativeHookRelayDeadlineMs(deadline) { const remainingMs = deadline.expiresAtMs - performance.now(); if (remainingMs <= 0 || deadline.signal.aborted) throw new NativeHookRelayDeadlineError(deadline.timeoutMs); return Math.max(1, remainingMs); } async function withNativeHookRelayDeadline(deadline, promise) { if (deadline.expiresAtMs <= performance.now()) { promise.catch(() => void 0); throw new NativeHookRelayDeadlineError(deadline.timeoutMs); } return await racePromiseWithAbortSignal(promise.then((value) => { remainingNativeHookRelayDeadlineMs(deadline); return value; }, (error) => { throw error instanceof Error ? error : new Error(String(error)); }), deadline.signal, () => new NativeHookRelayDeadlineError(deadline.timeoutMs)); } //#endregion //#region src/cli/native-hook-relay-entry.ts disableExitUnsafeCompilers(); process$1.title = "openclaw-hooks"; let exitCode = 1; try { exitCode = await runNativeHookRelayCliFromArgv(process$1.argv); } catch (error) { process$1.stderr.write(`native hook relay failed: ${error instanceof Error ? error.message : String(error)}\n`); } process$1.exitCode = exitCode; drainProcessOutput(() => process$1.exit(exitCode)); //#endregion export {};