openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
533 lines (532 loc) • 20 kB
JavaScript
import { s as toErrorObject } from "./error-coercion-CwIUUFnu.mjs";
import { i as isPidDefinitelyDead } from "./pid-alive-C45jDwYk.mjs";
import { a as resolveRuntimeProcessEntrypointUrl } from "./worker-cpu-BzVGEcKM.mjs";
import { l as SqliteSchemaVersionError, o as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-JnUZZAu1.mjs";
import { w as parseStrictPositiveInteger } from "./utils-Dor5uhSv.mjs";
import { t as WorkerTaskPool } from "./worker-task-pool-Csp_W8Qg.mjs";
import { t as ADMIN_SCOPE } from "./operator-scopes-C844xNSs.mjs";
import { o as resolveSafeTimeoutDelayMs } from "./timeouts-B9zvH0xD.mjs";
import process$1 from "node:process";
import { setFlagsFromString } from "node:v8";
import { request } from "node:http";
import path from "node:path";
//#region src/bootstrap/node-exit-safe-compilers.ts
const EXIT_UNSAFE_COMPILERS = [{
disable: "--no-maglev",
enable: "--maglev"
}, {
disable: "--no-concurrent-sparkplug",
enable: "--concurrent-sparkplug"
}];
/** Keeps V8 compile jobs that can deadlock process.exit() off background threads. */
function disableExitUnsafeCompilers() {
if (process.versions.bun) return;
const explicit = new Set(process.execArgv.map((arg) => arg.replaceAll("_", "-")));
for (const { disable, enable } of EXIT_UNSAFE_COMPILERS) if (!explicit.has(enable)) setFlagsFromString(disable);
}
//#endregion
//#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 pool = new WorkerTaskPool({
workerUrl: resolveRuntimeProcessEntrypointUrl("nativeHookRelayClient"),
maxWorkers: 1
});
try {
const result = await pool.run({
relayId: params.relayId,
stateDbPath: pathname
}, { 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
/** 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 {
stdout: `${JSON.stringify({ hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: reason
} })}\n`,
stderr: "",
exitCode: 0,
...failureDisposition ? { failureDisposition } : {}
};
},
renderPermissionDecisionResponse(decision, message) {
return {
stdout: `${JSON.stringify({ hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: decision === "allow" ? { behavior: "allow" } : {
behavior: "deny",
message: message?.trim() || "Denied by OpenClaw"
}
} })}\n`,
stderr: "",
exitCode: 0
};
}
};
//#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 delay(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 resolveOnce = (value) => {
if (!settled) {
settled = true;
resolve(value);
}
};
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) {
resolveOnce(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" || 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();
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, Math.max(0, ms));
});
}
//#endregion
//#region src/utils/timer-delay.ts
/** Wrapper around setTimeout that clamps unsafe or invalid delays before arming the timer. */
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}`);
}
/** Parse a positive timeout or return the supplied fallback for missing values. */
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",
"--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, deps = {}) {
return await runNativeHookRelayCli(parseNativeHookRelayCliOptions(argv), deps);
}
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, deps = {}) {
const stdin = deps.stdin ?? process.stdin;
const stdout = deps.stdout ?? process.stdout;
const stderr = deps.stderr ?? process.stderr;
const invokeBridge = deps.invokeBridge ?? invokeNativeHookRelayBridge;
const callGatewayFn = deps.callGateway ?? callGatewayLazy;
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 {
const remainingMs = remainingNativeHookRelayDeadlineMs(deadline);
return writeResponse(await withNativeHookRelayDeadline(deadline, invokeBridge({
provider,
relayId,
stateDbPath: opts.stateDb?.trim() || void 0,
generation,
event,
rawPayload,
registrationTimeoutMs: Math.min(100, remainingMs),
timeoutMs: remainingMs
})));
} catch (error) {
if (isNativeHookRelayDeadlineError(error)) return timedOut(error);
if (isNativeHookRelayBridgeStaleRegistrationError(error)) {
writeText(stderr, formatRelayCliError("native hook relay unavailable", error));
return unavailable();
}
}
try {
return writeResponse(await withNativeHookRelayDeadline(deadline, callGatewayFn({
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-uKWsa4MR.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 = () => {
destroyReadableStream(stream, 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);
}
function destroyReadableStream(stream, error) {
const destroy = stream.destroy;
if (typeof destroy === "function") {
destroy.call(stream, error);
return;
}
stream.pause();
}
async function withNativeHookRelayDeadline(deadline, promise) {
return await new Promise((resolve, reject) => {
let settled = false;
const cleanup = () => deadline.signal.removeEventListener("abort", abort);
const abort = () => {
if (settled) return;
settled = true;
cleanup();
reject(new NativeHookRelayDeadlineError(deadline.timeoutMs));
};
deadline.signal.addEventListener("abort", abort, { once: true });
promise.then((value) => {
if (settled) return;
if (deadline.signal.aborted || deadline.expiresAtMs <= performance.now()) {
abort();
return;
}
settled = true;
cleanup();
resolve(value);
}, (error) => {
if (settled) return;
settled = true;
cleanup();
reject(error instanceof Error ? error : new Error(String(error)));
});
if (deadline.signal.aborted || deadline.expiresAtMs <= performance.now()) abort();
});
}
//#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 {};