openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,422 lines • 59.7 kB
JavaScript
import { C as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { n as resolveOpenClawPackageRootSync } from "./openclaw-root-CNp1Ofdk.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { n as privateFileStoreSync } from "./private-file-store-BAvApZYp.js";
import { _ as PluginApprovalResolutions } from "./command-registration-Bp1_oQY0.js";
import { r as hasGlobalHooks } from "./hook-runner-global-BnyQREr6.js";
import { t as stableStringify } from "./stable-stringify-BL8fDhrH.js";
import { h as normalizeToolName } from "./tool-policy-CpBMaMTY.js";
import { f as requestDeferredPluginToolApproval, m as runBeforeToolCallHook, r as cancelDeferredPluginToolApproval, s as hasBeforeToolCallPolicy } from "./agent-tools.before-tool-call-BkVrW03y.js";
import { t as callGatewayTool } from "./gateway-Dq9H8-iz.js";
import { t as resolveToolLoopDetectionConfig } from "./tool-loop-detection-config-LHDaKYjp.js";
import { i as runAgentHarnessBeforeAgentFinalizeHook } from "./lifecycle-hook-helpers-BiRlZKs7.js";
import { t as runAgentHarnessAfterToolCallHook } from "./hook-helpers-B1pwlijz.js";
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
import path from "node:path";
import { tmpdir } from "node:os";
import { createHash, randomUUID } from "node:crypto";
import { createServer, request } from "node:http";
//#region src/agents/harness/native-hook-relay.ts
/**
* Bridges native harness hook events through registered relay processes.
*/
const NATIVE_HOOK_RELAY_EVENTS = [
"pre_tool_use",
"post_tool_use",
"permission_request",
"before_agent_finalize"
];
const DEFAULT_RELAY_TTL_MS = 1800 * 1e3;
const DEFAULT_RELAY_TIMEOUT_MS = 5e3;
const DEFAULT_PERMISSION_TIMEOUT_MS = 12e4;
const PERMISSION_ALLOW_ALWAYS_TTL_MS = 1800 * 1e3;
const MAX_NATIVE_HOOK_RELAY_INVOCATIONS = 200;
const MAX_NATIVE_HOOK_RELAY_JSON_DEPTH = 64;
const MAX_NATIVE_HOOK_RELAY_JSON_NODES = 2e4;
const MAX_NATIVE_HOOK_RELAY_STRING_LENGTH = 1e6;
const MAX_NATIVE_HOOK_RELAY_TOTAL_STRING_LENGTH = 4e6;
const MAX_NATIVE_HOOK_RELAY_HISTORY_STRING_LENGTH = 4e3;
const MAX_NATIVE_HOOK_RELAY_HISTORY_TOTAL_STRING_LENGTH = 2e4;
const MAX_NATIVE_HOOK_RELAY_HISTORY_ARRAY_ITEMS = 50;
const MAX_NATIVE_HOOK_RELAY_HISTORY_OBJECT_KEYS = 50;
const MAX_PERMISSION_FALLBACK_KEYS = 200;
const MAX_PERMISSION_FALLBACK_KEY_CHARS = 240;
const MAX_PERMISSION_FINGERPRINT_SORT_KEYS = 200;
const MAX_APPROVAL_TITLE_LENGTH = 80;
const MAX_APPROVAL_DESCRIPTION_LENGTH = 700;
const MAX_PERMISSION_APPROVALS_PER_WINDOW = 12;
const PERMISSION_APPROVAL_WINDOW_MS = 6e4;
const MAX_PERMISSION_ALLOW_ALWAYS_ENTRIES = 512;
const MAX_NATIVE_HOOK_BRIDGE_BODY_BYTES = 5e6;
const MAX_NATIVE_HOOK_BRIDGE_RESPONSE_BYTES = 5e6;
const NATIVE_HOOK_BRIDGE_RETRY_INTERVAL_MS = 25;
const NATIVE_HOOK_BRIDGE_REPLACEMENT_RECORD_GRACE_MS = 250;
const NATIVE_HOOK_RELAY_BRIDGE_STALE_REGISTRATION_ERROR = "native hook relay bridge stale registration";
const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
const log = createSubsystemLogger("agents/harness/native-hook-relay");
function resolveNativeHookRelayExpiresAtMs(ttlMs) {
return resolveExpiresAtMsFromDurationMs(normalizePositiveInteger(ttlMs, DEFAULT_RELAY_TTL_MS));
}
const NATIVE_HOOK_RELAY_STATE_SYMBOL = Symbol.for("openclaw.nativeHookRelay.state");
function getNativeHookRelaySharedState() {
const globalRecord = globalThis;
globalRecord[NATIVE_HOOK_RELAY_STATE_SYMBOL] ??= {
relays: /* @__PURE__ */ new Map(),
relayBridges: /* @__PURE__ */ new Map(),
invocations: [],
pendingPermissionApprovals: /* @__PURE__ */ new Map(),
pendingPreToolUseApprovals: /* @__PURE__ */ new Map(),
permissionApprovalWindows: /* @__PURE__ */ new Map(),
permissionAllowAlwaysApprovals: /* @__PURE__ */ new Map()
};
return globalRecord[NATIVE_HOOK_RELAY_STATE_SYMBOL];
}
const nativeHookRelayState = getNativeHookRelaySharedState();
const relays = nativeHookRelayState.relays;
const relayBridges = nativeHookRelayState.relayBridges;
const invocations = nativeHookRelayState.invocations;
const pendingPermissionApprovals = nativeHookRelayState.pendingPermissionApprovals;
const pendingPreToolUseApprovals = nativeHookRelayState.pendingPreToolUseApprovals;
const permissionApprovalWindows = nativeHookRelayState.permissionApprovalWindows;
const permissionAllowAlwaysApprovals = nativeHookRelayState.permissionAllowAlwaysApprovals;
let nativeHookRelayPermissionApprovalRequester = requestNativeHookRelayPermissionApproval;
let nativeHookRelayDeferredToolApprovalRequester = requestDeferredPluginToolApproval;
const NATIVE_HOOK_TOOL_NAME_ALIASES = { exec_command: "exec" };
const nativeHookRelayProviderAdapters = { codex: {
normalizeMetadata: normalizeCodexHookMetadata,
readToolInput: readCodexToolInput,
readToolResponse: readCodexToolResponse,
renderNoopResponse: () => {
return {
stdout: "",
stderr: "",
exitCode: 0
};
},
renderPreToolUseBlockResponse: (reason) => ({
stdout: `${JSON.stringify({ hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: reason
} })}\n`,
stderr: "",
exitCode: 0
}),
renderBeforeAgentFinalizeReviseResponse: (reason) => ({
stdout: `${JSON.stringify({
decision: "block",
reason
})}\n`,
stderr: "",
exitCode: 0
}),
renderBeforeAgentFinalizeStopResponse: (reason) => ({
stdout: `${JSON.stringify({
continue: false,
...reason?.trim() ? { stopReason: reason.trim() } : {}
})}\n`,
stderr: "",
exitCode: 0
}),
renderPermissionDecisionResponse: (decision, message) => ({
stdout: `${JSON.stringify({ hookSpecificOutput: {
hookEventName: "PermissionRequest",
decision: decision === "allow" ? { behavior: "allow" } : {
behavior: "deny",
message: message?.trim() || "Denied by OpenClaw"
}
} })}\n`,
stderr: "",
exitCode: 0
})
} };
function registerNativeHookRelay(params) {
pruneExpiredNativeHookRelays();
pruneNativeHookRelayPermissionAllowAlways();
const relayId = normalizeRelayId(params.relayId) ?? randomUUID();
const generation = normalizeRelayGeneration(params.generation) ?? randomUUID();
const generationMismatchGraceMs = normalizePositiveInteger(params.generationMismatchGraceMs, 0);
const now = Date.now();
const expiresAtMs = resolveNativeHookRelayExpiresAtMs(params.ttlMs);
if (expiresAtMs === void 0) throw new Error("Native hook relay expiry is outside the supported Date range");
const allowedEvents = normalizeAllowedEvents(params.allowedEvents);
unregisterNativeHookRelay(relayId);
const registration = {
relayId,
provider: params.provider,
generation,
...generationMismatchGraceMs > 0 ? { generationMismatchGraceExpiresAtMs: now + generationMismatchGraceMs } : {},
...params.agentId ? { agentId: params.agentId } : {},
sessionId: params.sessionId,
...params.sessionKey ? { sessionKey: params.sessionKey } : {},
...params.config ? { config: params.config } : {},
runId: params.runId,
...params.channelId ? { channelId: params.channelId } : {},
allowedEvents,
expiresAtMs,
...params.signal ? { signal: params.signal } : {}
};
relays.set(relayId, registration);
registerNativeHookRelayBridge(registration);
const handle = {
...registration,
shouldRelayEvent: (event) => nativeHookRelayEventHasLocalWork(registration, event),
commandForEvent: (event, options) => buildNativeHookRelayCommand({
provider: params.provider,
relayId,
generation: registration.generation,
event,
preToolUseUnavailable: event === "pre_tool_use" && !nativeHookRelayEventHasLocalWork(registration, event) ? "noop" : void 0,
nice: params.command?.nice,
timeoutMs: resolveNativeHookRelayCommandTimeoutMs(params.command?.timeoutMs, options?.timeoutMs),
executable: params.command?.executable,
nodeExecutable: params.command?.nodeExecutable
}),
renew: (ttlMs) => {
const current = relays.get(relayId);
if (current !== registration) return;
const renewedExpiresAtMs = resolveNativeHookRelayExpiresAtMs(ttlMs);
if (renewedExpiresAtMs === void 0) return;
current.expiresAtMs = renewedExpiresAtMs;
handle.expiresAtMs = renewedExpiresAtMs;
const bridge = relayBridges.get(relayId);
if (bridge) writeNativeHookRelayBridgeRecordForRegistration(current, bridge);
},
unregister: () => unregisterNativeHookRelay(relayId, registration)
};
return handle;
}
function unregisterNativeHookRelay(relayId, expectedRegistration) {
if (expectedRegistration && relays.get(relayId) !== expectedRegistration) return;
unregisterNativeHookRelayBridge(relayId);
relays.delete(relayId);
removeNativeHookRelayInvocations(relayId);
removeNativeHookRelayPreToolUseApprovals(relayId);
removeNativeHookRelayPermissionState(relayId);
}
function normalizeRelayId(value) {
const trimmed = value?.trim();
if (!trimmed) return;
if (trimmed.length > 160 || !/^[A-Za-z0-9._:-]+$/u.test(trimmed)) throw new Error("native hook relay id must be non-empty, compact, and URL-safe");
return trimmed;
}
function normalizeRelayGeneration(value) {
const trimmed = value?.trim();
if (!trimmed) return;
if (trimmed.length > 160 || !/^[A-Za-z0-9._:-]+$/u.test(trimmed)) throw new Error("native hook relay generation must be non-empty, compact, and URL-safe");
return trimmed;
}
function resolveNativeHookRelayNicePrefix(value) {
if (process.platform === "win32" || value === false || value === void 0) return [];
const nice = normalizePositiveInteger(value, 0);
if (nice <= 0) return [];
return [
"nice",
"-n",
String(nice)
];
}
function resolveNativeHookRelayCommandTimeoutMs(configuredTimeoutMs, overrideTimeoutMs) {
const configured = normalizeOptionalPositiveInteger(configuredTimeoutMs);
const override = normalizeOptionalPositiveInteger(overrideTimeoutMs);
if (configured === void 0) return override;
if (override === void 0) return configured;
return Math.min(configured, override);
}
function buildNativeHookRelayCommand(params) {
const timeoutMs = normalizePositiveInteger(params.timeoutMs, DEFAULT_RELAY_TIMEOUT_MS);
const executable = params.executable ?? resolveOpenClawCliExecutable();
const argv = executable === "openclaw" ? ["openclaw"] : [params.nodeExecutable ?? process.execPath, executable];
return shellQuoteArgs([
...resolveNativeHookRelayNicePrefix(params.nice),
...argv,
"hooks",
"relay",
"--provider",
params.provider,
"--relay-id",
params.relayId,
...params.generation ? ["--generation", params.generation] : [],
"--event",
params.event,
...params.event === "pre_tool_use" && params.preToolUseUnavailable ? ["--pre-tool-use-unavailable", params.preToolUseUnavailable] : [],
"--timeout",
String(timeoutMs)
]);
}
function nativePreToolUseMayRunLoopDetection(registration) {
if (!registration.sessionKey) return false;
return resolveToolLoopDetectionConfig({
cfg: registration.config,
agentId: registration.agentId
})?.enabled !== false;
}
function nativeHookRelayEventHasLocalWork(registration, event) {
if (event === "pre_tool_use") return hasBeforeToolCallPolicy() || nativePreToolUseMayRunLoopDetection(registration);
if (event === "post_tool_use") return hasGlobalHooks("after_tool_call");
if (event === "before_agent_finalize") return hasGlobalHooks("before_agent_finalize");
return true;
}
async function invokeNativeHookRelay(params) {
const provider = readNativeHookRelayProvider(params.provider);
const relayId = readNonEmptyString(params.relayId, "relayId");
const event = readNativeHookRelayEvent(params.event);
const registration = relays.get(relayId);
if (!registration) {
pruneExpiredNativeHookRelays();
throw new Error("native hook relay not found");
}
if (Date.now() > registration.expiresAtMs) {
unregisterNativeHookRelay(relayId, registration);
throw new Error("native hook relay expired");
}
if (registration.provider !== provider) throw new Error("native hook relay provider mismatch");
if (params.requireGeneration) {
const generation = readNonEmptyString(params.generation, "generation");
if (generation !== registration.generation) {
if (!canAcceptNativeHookRelayGenerationMismatch(registration, generation)) throw new Error(NATIVE_HOOK_RELAY_BRIDGE_STALE_REGISTRATION_ERROR);
log.debug("native hook relay accepted bootstrap generation mismatch", {
relayId,
event,
runId: registration.runId
});
}
}
if (!registration.allowedEvents.includes(event)) throw new Error("native hook relay event not allowed");
if (!isJsonValue(params.rawPayload)) throw new Error("native hook relay payload must be JSON-compatible");
const normalized = normalizeNativeHookInvocation({
registration,
event,
rawPayload: params.rawPayload
});
recordNativeHookRelayInvocation(normalized);
return processNativeHookRelayInvocation({
registration,
invocation: normalized,
adapter: getNativeHookRelayProviderAdapter(provider)
});
}
function hasNativeHookRelayInvocation(params) {
const toolUseId = params.toolUseId?.trim();
if (!toolUseId) return false;
return invocations.some((invocation) => invocation.relayId === params.relayId && invocation.event === params.event && invocation.toolUseId === toolUseId);
}
async function resolveNativeHookRelayDeferredToolApproval(params) {
const pendingApprovalKey = nativeHookRelayPreToolUseApprovalKey({
relayId: params.relayId,
toolUseId: params.toolUseId
});
if (!pendingApprovalKey) return;
const pendingApproval = pendingPreToolUseApprovals.get(pendingApprovalKey);
if (!pendingApproval) return;
pendingApproval.resolutionPromise ??= resolveNativeHookRelayPreToolUseApproval(pendingApproval, params.signal).finally(() => {
if (pendingPreToolUseApprovals.get(pendingApprovalKey) === pendingApproval) pendingPreToolUseApprovals.delete(pendingApprovalKey);
});
return pendingApproval.resolutionPromise;
}
async function resolveNativeHookRelayPreToolUseApproval(pendingApproval, signal) {
const outcome = await nativeHookRelayDeferredToolApprovalRequester({
deferredApproval: pendingApproval.deferredApproval,
signal
});
if (outcome.blocked) return {
handled: true,
outcome: "denied",
reason: outcome.reason
};
if (nativeHookRelayParamsWereRewritten(pendingApproval.originalParamsFingerprint, outcome.params)) return {
handled: true,
outcome: "denied",
reason: "OpenClaw tool policy rewrote Codex app-server approval params; refusing original request."
};
return {
handled: true,
outcome: "approved-once"
};
}
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 = readNativeHookRelayBridgeRecord(relayId);
if (Date.now() > record.expiresAtMs) throw new Error("native hook relay bridge expired");
return await invokeNativeHookRelayBridgeRecord({
record,
timeoutMs: Math.max(1, timeoutMs - (Date.now() - startedAt)),
payload: {
provider,
relayId,
event,
generation: params.generation,
rawPayload: params.rawPayload
}
});
} catch (error) {
lastError = error;
if (error instanceof Error && error.message === "native hook relay bridge not found" && Date.now() - startedAt >= registrationTimeoutMs) break;
if (!isRetryableNativeHookRelayBridgeLookupError({
error,
elapsedMs: Date.now() - startedAt
})) break;
await delay(Math.min(NATIVE_HOOK_BRIDGE_RETRY_INTERVAL_MS, timeoutMs - (Date.now() - startedAt)));
}
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
function renderNativeHookRelayUnavailableResponse(params) {
const provider = readNativeHookRelayProvider(params.provider);
const event = readNativeHookRelayEvent(params.event);
const adapter = getNativeHookRelayProviderAdapter(provider);
const message = params.message?.trim() || "Native hook relay unavailable";
if (event === "pre_tool_use") {
if (params.preToolUseUnavailable === "noop") return adapter.renderNoopResponse(event);
return adapter.renderPreToolUseBlockResponse(message);
}
if (event === "permission_request") return adapter.renderPermissionDecisionResponse("deny", message);
return adapter.renderNoopResponse(event);
}
function isNativeHookRelayBridgeStaleRegistrationError(error) {
return error instanceof Error && error.message === NATIVE_HOOK_RELAY_BRIDGE_STALE_REGISTRATION_ERROR;
}
function recordNativeHookRelayInvocation(invocation) {
invocations.push({
...invocation,
rawPayload: snapshotNativeHookRelayPayload(invocation.rawPayload)
});
if (invocations.length > MAX_NATIVE_HOOK_RELAY_INVOCATIONS) invocations.splice(0, invocations.length - MAX_NATIVE_HOOK_RELAY_INVOCATIONS);
}
function removeNativeHookRelayInvocations(relayId) {
for (let index = invocations.length - 1; index >= 0; index -= 1) if (invocations[index]?.relayId === relayId) invocations.splice(index, 1);
}
function canAcceptNativeHookRelayGenerationMismatch(registration, generation) {
const expiresAtMs = registration.generationMismatchGraceExpiresAtMs;
if (typeof expiresAtMs !== "number" || Date.now() > expiresAtMs) return false;
if (registration.generationMismatchGraceAcceptedGeneration) return registration.generationMismatchGraceAcceptedGeneration === generation;
registration.generationMismatchGraceAcceptedGeneration = generation;
return true;
}
function nativeHookRelayPreToolUseApprovalKey(params) {
const toolUseId = params.toolUseId?.trim();
return toolUseId ? `${params.relayId}:${toolUseId}` : void 0;
}
function setNativeHookRelayPreToolUseApproval(params) {
const key = nativeHookRelayPreToolUseApprovalKey(params);
if (!key) return false;
const previousApproval = pendingPreToolUseApprovals.get(key);
if (previousApproval) cancelDeferredPluginToolApproval(previousApproval.deferredApproval);
pendingPreToolUseApprovals.set(key, {
deferredApproval: params.deferredApproval,
originalParamsFingerprint: params.originalParamsFingerprint
});
if (pendingPreToolUseApprovals.size > MAX_NATIVE_HOOK_RELAY_INVOCATIONS) {
const oldestKey = pendingPreToolUseApprovals.keys().next().value;
if (oldestKey) {
const oldestApproval = pendingPreToolUseApprovals.get(oldestKey);
if (oldestApproval) cancelDeferredPluginToolApproval(oldestApproval.deferredApproval);
pendingPreToolUseApprovals.delete(oldestKey);
}
}
return true;
}
function removeNativeHookRelayPreToolUseApprovals(relayId) {
const prefix = `${relayId}:`;
for (const [key, pendingApproval] of pendingPreToolUseApprovals) if (key.startsWith(prefix)) {
cancelDeferredPluginToolApproval(pendingApproval.deferredApproval);
pendingPreToolUseApprovals.delete(key);
}
}
function pruneExpiredNativeHookRelays(now = Date.now()) {
for (const [relayId, registration] of relays) if (now > registration.expiresAtMs) unregisterNativeHookRelay(relayId, registration);
}
function isNativeHookRelayBridgePidDead(pid) {
try {
process.kill(pid, 0);
return false;
} catch (error) {
return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
}
}
function registerNativeHookRelayBridge(registration) {
try {
const staleDir = ensureNativeHookRelayBridgeDir();
const now = Date.now();
for (const name of readdirSync(staleDir)) {
if (!name.endsWith(".json")) continue;
const full = path.join(staleDir, name);
try {
const rec = JSON.parse(readFileSync(full, "utf8"));
if (!rec || typeof rec.pid !== "number" || rec.pid === process.pid) continue;
const expired = typeof rec.expiresAtMs === "number" && now > rec.expiresAtMs;
const deadPid = !expired && isNativeHookRelayBridgePidDead(rec.pid);
if (!expired && !deadPid) continue;
rmSync(full, { force: true });
log.debug("pruned stale native hook relay bridge file", {
file: name,
stalePid: rec.pid,
currentPid: process.pid,
reason: deadPid ? "dead-pid" : "expired"
});
} catch {}
}
} catch (error) {
log.debug("native hook relay bridge dir prune skipped", { error });
}
unregisterNativeHookRelayBridge(registration.relayId, { deferRegistryRemovalMs: NATIVE_HOOK_BRIDGE_REPLACEMENT_RECORD_GRACE_MS });
const token = randomUUID();
const bridgeDir = ensureNativeHookRelayBridgeDir();
const bridgeKey = nativeHookRelayBridgeKey(registration.relayId);
const registryPath = path.join(bridgeDir, `${bridgeKey}.json`);
const server = createServer();
const bridge = {
relayId: registration.relayId,
registryPath,
token,
server
};
server.on("request", (req, res) => {
handleNativeHookRelayBridgeRequest(req, res, {
provider: registration.provider,
relayId: registration.relayId,
token,
registration,
bridge
});
});
relayBridges.set(registration.relayId, bridge);
server.on("error", (error) => {
log.debug("native hook relay bridge server error", {
error,
relayId: registration.relayId
});
});
server.listen(0, "127.0.0.1", () => {
if (relayBridges.get(registration.relayId) !== bridge) return;
writeNativeHookRelayBridgeRecordForRegistration(registration, bridge);
});
if (relayBridges.get(registration.relayId) === bridge) writeNativeHookRelayBridgeRecordForRegistration(registration, bridge);
server.unref();
}
function writeNativeHookRelayBridgeRecordForRegistration(registration, bridge) {
const address = bridge.server.address();
if (!address || typeof address === "string") {
log.debug("native hook relay bridge server address unavailable", { relayId: registration.relayId });
return;
}
const record = {
version: 1,
relayId: registration.relayId,
pid: process.pid,
hostname: "127.0.0.1",
port: address.port,
token: bridge.token,
expiresAtMs: registration.expiresAtMs
};
writeNativeHookRelayBridgeRecord(bridge.registryPath, record);
}
function unregisterNativeHookRelayBridge(relayId, options) {
const bridge = relayBridges.get(relayId);
if (!bridge) return;
relayBridges.delete(relayId);
bridge.server.close();
if (readNativeHookRelayBridgeRecordIfExists(relayId)?.token === bridge.token) {
const deferRegistryRemovalMs = normalizePositiveInteger(options?.deferRegistryRemovalMs, 0);
if (deferRegistryRemovalMs > 0) {
setTimeout(() => {
if (readNativeHookRelayBridgeRecordIfExists(relayId)?.token === bridge.token) rmSync(bridge.registryPath, { force: true });
}, deferRegistryRemovalMs).unref();
return;
}
rmSync(bridge.registryPath, { force: true });
}
}
async function handleNativeHookRelayBridgeRequest(req, res, auth) {
try {
if (req.method !== "POST" || req.url !== "/invoke") {
writeNativeHookRelayBridgeJson(res, 404, {
ok: false,
error: "not found"
});
return;
}
if (req.headers.authorization !== `Bearer ${auth.token}`) {
writeNativeHookRelayBridgeJson(res, 403, {
ok: false,
error: "forbidden"
});
return;
}
if (!isCurrentNativeHookRelayBridgeRequest(auth)) {
writeNativeHookRelayBridgeJson(res, 410, {
ok: false,
error: NATIVE_HOOK_RELAY_BRIDGE_STALE_REGISTRATION_ERROR
});
return;
}
const body = await readNativeHookRelayBridgeBody(req);
const payload = readNativeHookRelayBridgePayload(JSON.parse(body));
if (payload.provider !== auth.provider || payload.relayId !== auth.relayId) {
writeNativeHookRelayBridgeJson(res, 403, {
ok: false,
error: "native hook relay bridge target mismatch"
});
return;
}
if (!isCurrentNativeHookRelayBridgeRequest(auth)) {
writeNativeHookRelayBridgeJson(res, 410, {
ok: false,
error: NATIVE_HOOK_RELAY_BRIDGE_STALE_REGISTRATION_ERROR
});
return;
}
writeNativeHookRelayBridgeJson(res, 200, {
ok: true,
result: await invokeNativeHookRelay({
...payload,
requireGeneration: true
})
});
} catch (error) {
writeNativeHookRelayBridgeJson(res, isNativeHookRelayBridgeStaleRegistrationError(error) ? 410 : 500, {
ok: false,
error: error instanceof Error ? error.message : String(error)
});
}
}
function isCurrentNativeHookRelayBridgeRequest(auth) {
return relays.get(auth.relayId) === auth.registration && relayBridges.get(auth.relayId) === auth.bridge;
}
async function readNativeHookRelayBridgeBody(req) {
const chunks = [];
let total = 0;
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
total += buffer.byteLength;
if (total > MAX_NATIVE_HOOK_BRIDGE_BODY_BYTES) throw new Error("native hook relay bridge payload too large");
chunks.push(buffer);
}
return Buffer.concat(chunks, total).toString("utf8");
}
function readNativeHookRelayBridgePayload(value) {
if (!isJsonObject(value)) throw new Error("native hook relay bridge payload must be an object");
return {
provider: value.provider,
relayId: value.relayId,
generation: readNonEmptyString(value.generation, "generation"),
event: value.event,
rawPayload: value.rawPayload
};
}
function writeNativeHookRelayBridgeJson(res, statusCode, payload) {
const body = JSON.stringify(payload);
res.writeHead(statusCode, {
"content-type": "application/json",
"content-length": Buffer.byteLength(body)
});
res.end(body);
}
function readNativeHookRelayBridgeRecord(relayId) {
const record = readNativeHookRelayBridgeRecordIfExists(relayId);
if (!record) throw new Error("native hook relay bridge not found");
return record;
}
function readNativeHookRelayBridgeRecordIfExists(relayId) {
const registryPath = nativeHookRelayBridgeRegistryPath(relayId);
try {
const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
if (isNativeHookRelayBridgeRecord(parsed, relayId)) return parsed;
} catch (error) {
if (error.code !== "ENOENT") log.debug("failed to read native hook relay bridge registry", {
error,
relayId
});
}
}
function isNativeHookRelayBridgeRecord(value, relayId) {
return isJsonObject(value) && value.version === 1 && value.relayId === relayId && typeof value.pid === "number" && Number.isInteger(value.pid) && value.hostname === "127.0.0.1" && typeof value.port === "number" && Number.isInteger(value.port) && value.port > 0 && value.port <= 65535 && typeof value.token === "string" && value.token.length > 0 && typeof value.expiresAtMs === "number";
}
async function invokeNativeHookRelayBridgeRecord(params) {
const startedAt = Date.now();
let lastError;
while (Date.now() - startedAt < params.timeoutMs) try {
return await postNativeHookRelayBridgeRecord({
...params,
timeoutMs: Math.max(1, params.timeoutMs - (Date.now() - startedAt))
});
} catch (error) {
lastError = error;
if (!isRetryableNativeHookRelayBridgeError(error)) break;
await delay(Math.min(NATIVE_HOOK_BRIDGE_RETRY_INTERVAL_MS, params.timeoutMs - (Date.now() - startedAt)));
}
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(toLintErrorObject(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 < NATIVE_HOOK_BRIDGE_REPLACEMENT_RECORD_GRACE_MS && isNativeHookRelayBridgeStaleRegistrationError(params.error);
}
function nativeHookRelayBridgeDir() {
const uid = typeof process.getuid === "function" ? process.getuid() : "nouid";
return path.join(tmpdir(), `openclaw-native-hook-relays-${uid}`);
}
function ensureNativeHookRelayBridgeDir() {
const bridgeDir = nativeHookRelayBridgeDir();
mkdirSync(bridgeDir, {
recursive: true,
mode: 448
});
const stats = lstatSync(bridgeDir);
const expectedUid = typeof process.getuid === "function" ? process.getuid() : void 0;
if (!stats.isDirectory() || stats.isSymbolicLink()) throw new Error("unsafe native hook relay bridge directory");
if (expectedUid !== void 0 && stats.uid !== expectedUid) throw new Error("unsafe native hook relay bridge directory owner");
if (process.platform !== "win32" && (stats.mode & 63) !== 0) {
chmodSync(bridgeDir, 448);
if ((lstatSync(bridgeDir).mode & 63) !== 0) throw new Error("unsafe native hook relay bridge directory permissions");
}
return bridgeDir;
}
function writeNativeHookRelayBridgeRecord(registryPath, record) {
privateFileStoreSync(path.dirname(registryPath)).writeText(path.basename(registryPath), `${JSON.stringify(record)}\n`);
}
function nativeHookRelayBridgeRegistryPath(relayId) {
return path.join(nativeHookRelayBridgeDir(), `${nativeHookRelayBridgeKey(relayId)}.json`);
}
function nativeHookRelayBridgeKey(relayId) {
return createHash("sha256").update(relayId).digest("hex").slice(0, 32);
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, Math.max(0, ms));
});
}
async function processNativeHookRelayInvocation(params) {
if (params.invocation.event === "pre_tool_use") return runNativeHookRelayPreToolUse(params);
if (params.invocation.event === "post_tool_use") return runNativeHookRelayPostToolUse(params);
if (params.invocation.event === "before_agent_finalize") return runNativeHookRelayBeforeAgentFinalize(params);
return runNativeHookRelayPermissionRequest(params);
}
async function runNativeHookRelayPreToolUse(params) {
const toolName = normalizeNativeHookToolName(params.invocation.toolName);
const toolInput = params.adapter.readToolInput(params.invocation.rawPayload);
const originalToolInputFingerprint = stableStringify(toolInput);
const approvalMode = readNativeHookRelayApprovalMode(params.invocation.rawPayload);
const outcome = await runBeforeToolCallHook({
toolName,
params: toolInput,
...params.invocation.toolUseId ? { toolCallId: params.invocation.toolUseId } : {},
...approvalMode === "report" ? { approvalMode: "defer" } : {},
signal: params.registration.signal,
ctx: {
...params.registration.agentId ? { agentId: params.registration.agentId } : {},
sessionId: params.registration.sessionId,
...params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {},
...params.registration.config ? { config: params.registration.config } : {},
runId: params.registration.runId,
...params.registration.channelId ? { channelId: params.registration.channelId } : {},
...params.invocation.cwd ? { cwd: params.invocation.cwd } : {}
}
});
if (outcome.blocked) return params.adapter.renderPreToolUseBlockResponse(outcome.reason);
if (outcome.deferredApproval) {
if (!setNativeHookRelayPreToolUseApproval({
relayId: params.registration.relayId,
toolUseId: params.invocation.toolUseId,
deferredApproval: outcome.deferredApproval,
originalParamsFingerprint: originalToolInputFingerprint
})) {
cancelDeferredPluginToolApproval(outcome.deferredApproval);
return params.adapter.renderPreToolUseBlockResponse("Plugin approval required but Codex tool id unavailable.");
}
return params.adapter.renderNoopResponse(params.invocation.event);
}
if (nativeHookRelayParamsWereRewritten(originalToolInputFingerprint, outcome.params)) return params.adapter.renderPreToolUseBlockResponse("OpenClaw tool policy rewrote Codex app-server approval params; refusing original request.");
return params.adapter.renderNoopResponse(params.invocation.event);
}
async function runNativeHookRelayPostToolUse(params) {
await runAgentHarnessAfterToolCallHook({
toolName: normalizeNativeHookToolName(params.invocation.toolName),
toolCallId: params.invocation.toolUseId ?? `${params.invocation.event}:${params.invocation.receivedAt}`,
runId: params.registration.runId,
...params.registration.agentId ? { agentId: params.registration.agentId } : {},
sessionId: params.registration.sessionId,
...params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {},
...params.registration.channelId ? { channelId: params.registration.channelId } : {},
startArgs: params.adapter.readToolInput(params.invocation.rawPayload),
result: params.adapter.readToolResponse(params.invocation.rawPayload)
});
return params.adapter.renderNoopResponse(params.invocation.event);
}
async function runNativeHookRelayPermissionRequest(params) {
const request = {
provider: params.registration.provider,
...params.registration.agentId ? { agentId: params.registration.agentId } : {},
sessionId: params.registration.sessionId,
...params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {},
runId: params.registration.runId,
toolName: normalizeNativeHookToolName(params.invocation.toolName),
...params.invocation.toolUseId ? { toolCallId: params.invocation.toolUseId } : {},
...params.invocation.cwd ? { cwd: params.invocation.cwd } : {},
...params.invocation.model ? { model: params.invocation.model } : {},
toolInput: params.adapter.readToolInput(params.invocation.rawPayload),
...params.registration.signal ? { signal: params.registration.signal } : {}
};
const approvalKey = nativeHookRelayPermissionApprovalKey({
registration: params.registration,
request
});
const allowAlwaysKey = nativeHookRelayPermissionAllowAlwaysKey({
registration: params.registration,
request
});
if (hasNativeHookRelayPermissionAllowAlways(allowAlwaysKey)) return params.adapter.renderPermissionDecisionResponse("allow");
const pendingApproval = pendingPermissionApprovals.get(approvalKey);
try {
const decision = await (pendingApproval ?? startNativeHookRelayPermissionApprovalWithBudget({
registration: params.registration,
approvalKey,
request
}));
if (decision === "allow") return params.adapter.renderPermissionDecisionResponse("allow");
if (decision === "allow-always") {
rememberNativeHookRelayPermissionAllowAlways(allowAlwaysKey);
return params.adapter.renderPermissionDecisionResponse("allow");
}
if (decision === "deny") return params.adapter.renderPermissionDecisionResponse("deny", "Denied by user");
} catch (error) {
log.warn(`native hook permission approval failed; deferring to provider approval path: ${String(error)}`);
}
return params.adapter.renderNoopResponse(params.invocation.event);
}
async function runNativeHookRelayBeforeAgentFinalize(params) {
const outcome = await runAgentHarnessBeforeAgentFinalizeHook({
event: {
runId: params.registration.runId,
sessionId: params.registration.sessionId,
...params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {},
...params.invocation.turnId ? { turnId: params.invocation.turnId } : {},
provider: params.registration.provider,
...params.invocation.model ? { model: params.invocation.model } : {},
...params.invocation.cwd ? { cwd: params.invocation.cwd } : {},
...params.invocation.transcriptPath ? { transcriptPath: params.invocation.transcriptPath } : {},
stopHookActive: params.invocation.stopHookActive === true,
...params.invocation.lastAssistantMessage ? { lastAssistantMessage: params.invocation.lastAssistantMessage } : {}
},
ctx: {
...params.registration.agentId ? { agentId: params.registration.agentId } : {},
sessionId: params.registration.sessionId,
...params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {},
runId: params.registration.runId,
...params.registration.channelId ? { channelId: params.registration.channelId } : {},
...params.invocation.cwd ? { workspaceDir: params.invocation.cwd } : {},
...params.invocation.model ? { modelId: params.invocation.model } : {}
}
});
if (outcome.action === "revise") return params.adapter.renderBeforeAgentFinalizeReviseResponse(outcome.reason);
if (outcome.action === "finalize") return params.adapter.renderBeforeAgentFinalizeStopResponse(outcome.reason);
return params.adapter.renderNoopResponse(params.invocation.event);
}
async function startNativeHookRelayPermissionApprovalWithBudget(params) {
if (!consumeNativeHookRelayPermissionBudget(params.registration.relayId)) {
log.warn(`native hook permission approval rate limit exceeded; deferring to provider approval path: relay=${params.registration.relayId} run=${params.registration.runId}`);
return "defer";
}
const approval = nativeHookRelayPermissionApprovalRequester(params.request).finally(() => {
if (pendingPermissionApprovals.get(params.approvalKey) === approval) pendingPermissionApprovals.delete(params.approvalKey);
});
pendingPermissionApprovals.set(params.approvalKey, approval);
return approval;
}
function nativeHookRelayPermissionApprovalKey(params) {
return [
params.registration.relayId,
params.registration.runId,
params.request.toolCallId ? `call:${params.request.toolCallId}` : permissionRequestFallbackKey(params.request),
permissionRequestContentFingerprint(params.request)
].join(":");
}
function nativeHookRelayPermissionAllowAlwaysKey(params) {
const hash = createHash("sha256");
hash.update("openclaw:native-hook-relay:permission-allow-always:v2");
hash.update("\0");
hash.update(params.registration.relayId);
hash.update("\0");
hash.update(params.request.provider);
hash.update("\0");
hash.update(params.request.agentId ?? "");
hash.update("\0");
hash.update(params.request.sessionKey ?? params.request.sessionId);
hash.update("\0");
hash.update(permissionRequestContentFingerprint(params.request));
return hash.digest("hex");
}
function permissionRequestFallbackKey(request) {
const command = readOptionalString(request.toolInput.command);
if (command) return `${request.toolName}:command:${truncateText(command, 240)}`;
return `${request.toolName}:keys:${permissionRequestToolInputKeyFingerprint(request.toolInput)}`;
}
function permissionRequestToolInputKeyFingerprint(toolInput) {
let fingerprint = "";
const { keys, truncated } = readBoundedOwnKeys(toolInput, MAX_PERMISSION_FALLBACK_KEYS);
for (const key of keys) {
const separator = fingerprint ? "," : "";
const remaining = MAX_PERMISSION_FALLBACK_KEY_CHARS - fingerprint.length - separator.length;
if (remaining <= 0) break;
fingerprint += `${separator}${key.slice(0, remaining)}`;
}
if (truncated && fingerprint.length < MAX_PERMISSION_FALLBACK_KEY_CHARS) fingerprint += `${fingerprint ? "," : ""}...`.slice(0, MAX_PERMISSION_FALLBACK_KEY_CHARS - fingerprint.length);
return fingerprint || "none";
}
function permissionRequestContentFingerprint(request) {
const hash = createHash("sha256");
hash.update(request.toolName);
hash.update("\0");
hash.update(request.cwd ?? "");
hash.update("\0");
updateJsonHash(hash, request.toolInput);
return hash.digest("hex");
}
function updateJsonHash(hash, value) {
if (value === null) {
hash.update("null");
return;
}
if (typeof value === "string") {
hash.update("string:");
hash.update(JSON.stringify(value));
return;
}
if (typeof value === "number") {
hash.update(`number:${String(value)}`);
return;
}
if (typeof value === "boolean") {
hash.update(`boolean:${String(value)}`);
return;
}
if (Array.isArray(value)) {
hash.update("[");
for (const item of value) {
updateJsonHash(hash, item);
hash.update(",");
}
hash.update("]");
return;
}
hash.update("{");
const { keys, truncated } = readBoundedOwnKeys(value, MAX_PERMISSION_FINGERPRINT_SORT_KEYS);
for (const key of keys) {
hash.update(JSON.stringify(key));
hash.update(":");
updateJsonHash(hash, value[key]);
hash.update(",");
}
if (truncated) {
const sortedKeySet = new Set(keys);
hash.update("#object-tail:");
for (const key in value) {
if (!Object.hasOwn(value, key) || sortedKeySet.has(key)) continue;
hash.update(JSON.stringify(key));
hash.update(":");
updateJsonHash(hash, value[key]);
hash.update(",");
}
}
hash.update("}");
}
function readBoundedOwnKeys(value, maxKeys) {
const keys = [];
let truncated = false;
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
if (keys.length >= maxKeys) {
truncated = true;
break;
}
keys.push(key);
}
keys.sort();
return {
keys,
truncated
};
}
function consumeNativeHookRelayPermissionBudget(relayId, now = Date.now()) {
const windowStart = now - PERMISSION_APPROVAL_WINDOW_MS;
const timestamps = (permissionApprovalWindows.get(relayId) ?? []).filter((timestamp) => timestamp >= windowStart);
if (timestamps.length >= MAX_PERMISSION_APPROVALS_PER_WINDOW) {
permissionApprovalWindows.set(relayId, timestamps);
return false;
}
timestamps.push(now);
permissionApprovalWindows.set(relayId, timestamps);
return true;
}
function hasNativeHookRelayPermissionAllowAlways(key, now = Date.now()) {
const validNow = asDateTimestampMs(now);
if (validNow === void 0) return false;
const entry = permissionAllowAlwaysApprovals.get(key);
if (!entry) return false;
const expiresAtMs = asDateTimestampMs(entry.expiresAtMs);
if (expiresAtMs === void 0 || expiresAtMs <= validNow) {
permissionAllowAlwaysApprovals.delete(key);
return false;
}
return true;
}
function rememberNativeHookRelayPermissionAllowAlways(key, now = Date.now()) {
pruneNativeHookRelayPermissionAllowAlways(now);
const expiresAtMs = resolveExpiresAtMsFromDurationMs(PERMISSION_ALLOW_ALWAYS_TTL_MS, { nowMs: now });
if (expiresAtMs === void 0) return;
permissionAllowAlwaysApprovals.set(key, { expiresAtMs });
while (permissionAllowAlwaysApprovals.size > MAX_PERMISSION_ALLOW_ALWAYS_ENTRIES) {
const oldestKey = permissionAllowAlwaysApprovals.keys().next().value;
if (typeof oldestKey !== "string") break;
permissionAllowAlwaysApprovals.delete(oldestKey);
}
}
function pruneNativeHookRelayPermissionAllowAlways(now = Date.now()) {
const validNow = asDateTimestampMs(now);
if (validNow === void 0) return;
for (const [key, entry] of permissionAllowAlwaysApprovals) {
const expiresAtMs = asDateTimestampMs(entry.expiresAtMs);
if (expiresAtMs === void 0 || expiresAtMs <= validNow) permissionAllowAlwaysApprovals.delete(key);
}
}
function removeNativeHookRelayPermissionState(relayId) {
permissionApprovalWindows.delete(relayId);
for (const key of pendingPermissionApprovals.keys()) if (key.startsWith(`${relayId}:`)) pendingPermissionApprovals.delete(key);
}
function snapshotNativeHookRelayPayload(payload) {
return snapshotJsonValue(payload, { remainingStringLength: MAX_NATIVE_HOOK_RELAY_HISTORY_TOTAL_STRING_LENGTH });
}
function snapshotJsonValue(value, state) {
if (value === null || typeof value === "number" || typeof value === "boolean") return value;
if (typeof value === "string") return snapshotString(value, state);
if (Array.isArray(value)) {
const items = value.slice(0, MAX_NATIVE_HOOK_RELAY_HISTORY_ARRAY_ITEMS).map((item) => snapshotJsonValue(item, state));
if (value.length > MAX_NATIVE_HOOK_RELAY_HISTORY_ARRAY_ITEMS) items.push("[truncated]");
return items;
}
const snapshot = {};
const keys = Object.keys(value);
for (const key of keys.slice(0, MAX_NATIVE_HOOK_RELAY_HISTORY_OBJECT_KEYS)) snapshot[snapshotString(key, state)] = snapshotJsonValue(value[key], state);
if (keys.length > MAX_NATIVE_HOOK_RELAY_HISTORY_OBJECT_KEYS) snapshot["[truncated]"] = keys.length - MAX_NATIVE_HOOK_RELAY_HISTORY_OBJECT_KEYS;
return snapshot;
}
function snapshotString(value, state) {
if (state.remainingStringLength <= 0) return "[truncated]";
const limit = Math.min(value.length, MAX_NATIVE_HOOK_RELAY_HISTORY_STRING_LENGTH, state.remainingStringLength);
state.remainingStringLength -= limit;
if (limit >= value.length) return value;
return `${value.slice(0, limit)}...[truncated]`;
}
function normalizeNativeHookInvocation(params) {
const metadata = getNativeHookRelayProviderAdapter(params.registration.provider).normalizeMetadata(params.rawPayload);
return {
provider: params.registration.provider,
relayId: params.registration.relayId,
event: params.event,
...metadata,
...params.registration.agentId ? { agentId: params.registration.agentId } : {},
sessionId: params.registration.sessionId,
...params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {},
runId: params.registration.runId,
rawPayload: params.rawPayload,
receivedAt: (/* @__PURE__ */ new Date()).toISOString()
};
}
function getNativeHookRelayProviderAdapter(provider) {
return nativeHookRelayProviderAdapters[provider];
}
function normalizeCodexHookMetadata(rawPayload) {
const payload = isJsonObject(rawPayload) ? rawPayload : {};
const metadata = {};
const nativeEventName = readOptionalString(payload.hook_event_name);
if (nativeEventName) metadata.nativeEventName = nativeEventName;
const cwd = readOptionalString(payload.cwd);
if (cwd) metadata.cwd = cwd;
const model = readOptionalString(payload.model);
if (model) metadata.model = model;
const turnId = readOptionalString(payload.turn_id);
if (turnId) metadata.turnId = turnId;
const transcriptPath = readOptionalString(payload.transcript_path);
if (transcriptPath) metadata.transcriptPath = transcriptPath;
const permissionMode = readOptionalString(payload.permission_mode);
if (permissionMode) metadata.permissionMode = permissionMode;
const stopHookActive = readOptionalBoolean(payload.stop_hook_active);
if (stopHookActive !== void 0) metadata.stopHookActive = stopHookActive;
const lastAssistantMessage = readOptionalString(payload.last_assistant_message);
if (lastAssistantMessage) metadata.lastAssistantMessage = lastAssistantMessage;
const toolName = readOptionalString(payload.tool_name);
if (toolName) metadata.toolName = toolName;
const toolUseId = readOptionalString(payload.tool_use_id);
if (toolUseId) metadata.toolUseId = toolUseId;
return metadata;
}
function readCodexToolInput(rawPayload) {
const payload = isJsonObject(rawPayload) ? rawPayload : {};
const toolInput = payload.tool_input;
if (isJsonObject(toolInput)) return normalizeCodexToolInput(normalizeNativeHookToolName(readOptionalString(payload.tool_name)), toolInput);
if (toolInput === void 0) return {};
return { value: toolInput };
}
function normalizeCodexToolInput(toolName, toolInput) {
const command = normalizeCodexCommand(toolInput.cmd);
if (toolName !== "exec" || command === void 0) return toolInput;
return {
...toolInput,
command
};
}
function normalizeCodexCommand(value) {
if (typeof value === "string") return value;
if (Array.isArray(value) && value.every((part) => typeof part === "string")) return shellQuoteArgs(value);
}
function nativeHookRelayParamsWereRewritten(originalFingerprint, candidate) {
if (candidate === void 0) return false;
return stableStringify(candidate) !== originalFingerprint;
}
function readCodexToolResponse(rawPayload) {
return (isJsonObject(rawPayload) ? rawPayload : {}).tool_response;
}
function readNativeHookRelayApprovalMode(rawPayload) {
return (isJsonObject(rawPayload) ? rawPayload : {}).openclaw_approval_mode === "report" ? "report" : void 0;
}
function normalizeNativeHookToolName(toolName) {
const normalized = normalizeToolName(toolName ?? "tool");
return NATIVE_HOOK_TOOL_NAME_ALIASES[normalized] ?? normalized;
}
async function requestNativeHookRelayPermissionApproval(request) {
const timeoutMs = DEFAULT_PERMISSION_TIMEOUT_MS;
const requestResult = await callGatewayTool("plugin.approval.request", { timeoutMs: 13e4 }, {
pluginId: `openclaw-native-hook-relay-${request.provider}`,
title: truncateText(`${nativeHookRelayProviderDisplayName(request.provider)} permission request`, MAX_APPROVAL_TITLE_LENGTH),
description: truncateText(formatPermissionApprovalDescription(request), MAX_APPROVAL_DESCRIPTION_LENGTH),
severity: "warning",
toolName: request.toolName,
toolCallId: request.toolCallId,
allowedDecisions: [
PluginApprovalResolutions.ALLOW_ONCE,
PluginApprovalResolutions.ALLOW_ALWAYS,
PluginApprovalResolutions.DENY
],
agentId: request.agentId,
sessionKey: request.sessionKey,
timeoutMs,
twoPhase: true
}, { expectFinal: false });
const approvalId = requestResult?.id;
if (!approvalId) return "defer";
let decision;
if (Object.hasOwn(requestResult ?? {}, "decision")) decision = requestResult.decision;
else decision = (await waitForNativeHookRelayApprovalDecision({
approvalId,
signal: request.signal,
timeoutMs
}))?.decision;
if (decision === PluginApprovalResolutions.ALLOW_ONCE) return "allow";
if (decision === PluginApprovalResolutions.ALLOW_ALWAYS) return "allow-always";
if (decision === PluginApprovalResolutions.DENY) return "deny";
return "defer";
}
async function waitForNativeHookRelayApprovalDecision(params) {
const waitPromise = callGatewayTool("plugin.approval.waitDecision", { timeoutMs: params.timeoutMs + 1e4 }, { id: params.approvalId });
if (!params.signal) return waitPromise;
let onAbort;
const abortPromise = new Promise((_, reject) => {
if (params.signal.aborted) {
reject(toLintErrorObject(params.signal.reason, "Non-Error rejection"));
return;
}
onAbort = () => reject(toLintErrorObject(params.signal.reason, "Non-Error rejection"));
params.signal.addEventListener("abort", onAbort, { once: true });
});
try {
return await Promise.race([waitPromise, abortPromise]);
} finally {
if (onAbort) params.signal.removeEventListener("abort", onAbort);
}
}
function formatPermissionApprovalDescription(request) {
return [
`Tool: ${sanitizeApprovalText(request.toolName)}`,
request.cwd ? `Cwd: ${sanitizeApprovalText(request.cwd)}` : void 0,
request.model ? `Model: ${sanitizeApprovalText(request.model)}` : void 0,
formatToolInputPreview(request.toolInput)
].filter((line) => Boolean(line)).join("\n");
}
function formatToolInputPreview(toolInput) {
const command = readOptionalString(toolInput.command);
if (command) return `Command: ${truncateText(sanitizeApprovalText(command), 240)}`;
const keys = Object.keys(toolInput).map(sanitizeApprovalText).filter(Boolean).toSorted();
if (!keys.length) return;
return `Input keys: ${keys.slice(0, 12).join(", ")}${keys.length > 12 ? ` (${keys.length - 12} omitted)` : ""}`;
}
function sanitizeApprovalText(value) {
let sanitized = "";
for (const char of value.replace(ANSI_ESCAPE_PATTERN, "")) {
const codePoint = char.codePointAt(0);
sanitized += codePoint != null && isUnsafeApprovalCodePoint(codePoint) ? " " : char;
}
return sanitized.replace(/\s+/g, " ").trim();
}
function isUnsafeApprovalCodePoint(codePoint) {
return codePoint >= 0 && codePoint <= 8 || codePoint === 11 || codePoint === 12 || codePoint >= 14 && codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297;
}
function nativeHookRelayProviderDisplayName(provider) {
if (provider === "codex") return "Codex";
return provider;
}
function truncateText(value, maxLength) {
if (value.length <= maxLength) return value;
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
}
function resolveOpenClawCliExecutable() {
const envPath = process.env.OPENCLAW_CLI_PATH?.trim();
if (envPath && existsSync(envPath)) return envPath;
const packageRoot = resolveOpenClawPackageRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd()
});
if (packageRoot) {
for (const candidate of [
path.join(packageRoot, "openclaw.mjs"),
path.join(packageRoot, "dist", "entry.js"),
path.join(packageRoot, "scripts", "run-node.mjs")
]) if (existsSync(candidate)) return candidate;
}
const argvEntry = process.argv[1];
if (argvEntry) {
const resolved = path.resolve(argvEntry);
if (existsSync(resolved)) return resolved;
}
throw new Error("Cannot resolve OpenClaw CLI executable path for native hook relay");
}
function normalizeAllowedEvents(events) {
if (!events?.length) return NATIVE_HOOK_RELAY_EVENTS;
return [...new Set(events)];
}
function normalizePositiveInteger(value, fallback) {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}
function normalizeOptionalPositiveInteger(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
}
function shellQuoteArgs(args) {
return args.map((arg) => shellQuoteArg(arg, process.platform)).join(" ");
}
function shellQuoteArg(value, platform) {
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value;
if (platform === "win32") return `"${value.replaceAll("\"", "\\\"")}"`;
return `'${value.replaceAll("'", "'\\''")}'`;
}
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`);
}
function readOptionalString(value) {
return typeof value === "string" && value.length > 0 ? value : void 0;
}
function readOptionalBoolean(value) {
return typeof value === "boolean" ? value : void 0;
}
function isJsonValue(value) {
const stack = [{
value,
depth: 0
}];
let nodes = 0;
let totalStringLength = 0;
while (stack.length) {
const current = stack.pop();
nodes += 1;
if (nodes > MAX_NATIVE_HOOK_RELAY_JSON_NODES) return false;
if (current.depth > MAX_NATIVE_HOOK_RELAY_JSON_DEPTH) return false;
if (current.value === null) continue;
if (typeof current.value === "string") {
if (current.value.length > MAX_NATIVE_HOOK_RELAY_STRING_LENGTH) return false;
totalStringLength += current.value.length;
if (totalStringLength > MAX_NATIVE_HOOK_RELAY_TOTAL_STRING_LENGTH) return false;
continue;
}
if (typeof current.value === "number") {
if (!Number.isFinite(current.value)) return false;
continue;
}
if (typeof current.value === "boolean") continue;
if (Array.isArray(current.value)) {
for (const valueLocal of current.value) {
if (nodes + stack.length + 1 > MAX_NATIVE_HOOK_RELAY_JSON_NODES) return false;
stack.push({
value: valueLocal,
depth: current.depth + 1
});
}
continue;
}
if (!isJsonObject(current.value)) return false;
try {
for (const key in current.value) {
if (!Object.hasOwn(current.value, key)) continue;
if (key.length > MAX_NATIVE_HOOK_RELAY_STRING_LENGTH) return false;
totalStringLength += key.length;
if (totalStringLength > MAX_NATIVE_HOOK_RELAY_TOTAL_STRING_LENGTH) return false;
if (nodes + stack.length + 1 > MAX_NATIVE_HOOK_RELAY_JSON_NODES) return false;
stack.push({
value: current.value[key],
depth: current.depth + 1
});
}
} catch {
return false;
}
}
return true;
}
function isJsonObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
try {
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
} catch {
return false;
}
}
const testing = {
clearNativeHookRelaysForTests() {
for (const relayId of relayBridges.keys()) unregisterNativeHookRelayBridge(relayId);
relays.clear();
invocations.length = 0;
pendingPermissionApprovals.clear();
for (const pendingApproval of pendingPreToolUseApprovals.values()) cancelDeferredPluginToolApproval(pendingApproval.deferredApproval);
pendingPreToolUseApprovals.clear();
permissionApprovalWindows.clear();
permissionAllowAlwaysApprovals.clear();
nativeHookRelayPermissionApprovalRequester = requestNativeHookRelayPermissionApproval;
nativeHookRelayDeferredToolApprovalRequester = requestDeferredPluginToolApproval;
},
getNativeHookRelayInvocationsForTests() {
return [...invocations];
},
getNativeHookRelayRegistrationForTests(relayId) {
return relays.get(relayId);
},
getNativeHookRelayBridgeDirForTests() {
return nativeHookRelayBridgeDir();
},
getNativeHookRelayBridgeRegistryPathForTests(relayId) {
return nativeHookRelayBridgeRegistryPath(relayId);
},
getNativeHookRelayBridgeRecordForTests(relayId) {
const record = readNativeHookRelayBridgeRecordIfExists(relayId);
return record ? { ...record } : void 0;
},
isNativeHookRelayBridgeLookupRetryableForTests(error, elapsedMs = 0) {
return isRetryableNativeHookRelayBridgeLookupError({
error,
elapsedMs
});
},
formatPermissionApprovalDescriptionForTests(request) {
return formatPermissionApprovalDescription(request);
},
permissionRequestContentFingerprintForTests(request) {
return permissionRequestContentFingerprint(request);
},
permissionRequestToolInputKeyFingerprintForTests(toolInput) {
return permissionRequestToolInputKeyFingerprint(toolInput);
},
setNativeHookRelayPermissionApprovalRequesterForTests(requester) {
nativeHookRelayPermissionApprovalRequester = requester;
},
setNativeHookRelayDeferredToolApprovalRequesterForTests(requester) {
nativeHookRelayDeferredToolApprovalRequester = requester;
}
};
function toLintErrorObject(value, fallbackMessage) {
if (value instanceof Error) return value;
if (typeof value === "string") return new Error(value);
const error = new Error(fallbackMessage, { cause: value });
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
return error;
}
//#endregion
export { isNativeHookRelayBridgeStaleRegistrationError as a, resolveNativeHookRelayDeferredToolApproval as c, invokeNativeHookRelayBridge as i, testing as l, hasNativeHookRelayInvocation as n, registerNativeHookRelay as o, invokeNativeHookRelay as r, renderNativeHookRelayUnavailableResponse as s, buildNativeHookRelayCommand as t };