|
|
|
|
Quelle native-hook-relay.ts
Sprache: JAVA
|
|
Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import path from "node:path";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { PluginApprovalResolutions } from "../../plugins/types.js";
import { runBeforeToolCallHook } from "../pi-tools.before-tool-call.js";
import { normalizeToolName } from "../tool-policy.js";
import { callGatewayTool } from "../tools/gateway.js";
import { runAgentHarnessAfterToolCallHook } from "./hook-helpers.js";
export type JsonValue =
| null
| boolean
| number
| string
| JsonValue[]
| { [key: string]: JsonValue };
export const NATIVE_HOOK_RELAY_EVENTS = [
"pre_tool_use",
"post_tool_use",
"permission_request",
] as const;
export const NATIVE_HOOK_RELAY_PROVIDERS = ["codex"] as const;
export type NativeHookRelayEvent = (typeof NATIVE_HOOK_RELAY_EVENTS)[number];
export type NativeHookRelayProvider = (typeof NATIVE_HOOK_RELAY_PROVIDERS)[number];
export type NativeHookRelayInvocation = {
provider: NativeHookRelayProvider;
relayId: string;
event: NativeHookRelayEvent;
nativeEventName?: string;
agentId?: string;
sessionId: string;
sessionKey?: string;
runId: string;
cwd?: string;
model?: string;
toolName?: string;
toolUseId?: string;
rawPayload: JsonValue;
receivedAt: string;
};
export type NativeHookRelayProcessResponse = {
stdout: string;
stderr: string;
exitCode: number;
};
export type NativeHookRelayRegistration = {
relayId: string;
provider: NativeHookRelayProvider;
agentId?: string;
sessionId: string;
sessionKey?: string;
runId: string;
allowedEvents: readonly NativeHookRelayEvent[];
expiresAtMs: number;
signal?: AbortSignal;
};
export type NativeHookRelayRegistrationHandle = NativeHookRelayRegistration & {
commandForEvent: (event: NativeHookRelayEvent) => string;
unregister: () => void;
};
export type RegisterNativeHookRelayParams = {
provider: NativeHookRelayProvider;
agentId?: string;
sessionId: string;
sessionKey?: string;
runId: string;
allowedEvents?: readonly NativeHookRelayEvent[];
ttlMs?: number;
command?: NativeHookRelayCommandOptions;
signal?: AbortSignal;
};
export type NativeHookRelayCommandOptions = {
executable?: string;
nodeExecutable?: string;
timeoutMs?: number;
};
export type InvokeNativeHookRelayParams = {
provider: unknown;
relayId: unknown;
event: unknown;
rawPayload: unknown;
};
type NativeHookRelayInvocationMetadata = Partial<
Pick<NativeHookRelayInvocation, "nativeEventName" | "cwd" | "model" | "toolName" | "toolUseId">
>;
type NativeHookRelayProviderAdapter = {
normalizeMetadata: (rawPayload: JsonValue) => NativeHookRelayInvocationMetadata;
readToolInput: (rawPayload: JsonValue) => Record<string, unknown>;
readToolResponse: (rawPayload: JsonValue) => unknown;
renderNoopResponse: (event: NativeHookRelayEvent) => NativeHookRelayProcessResponse;
renderPreToolUseBlockResponse: (reason: string) => NativeHookRelayProcessResponse;
renderPermissionDecisionResponse: (
decision: NativeHookRelayPermissionDecision,
message?: string,
) => NativeHookRelayProcessResponse;
};
const DEFAULT_RELAY_TTL_MS = 30 * 60 * 1000;
const DEFAULT_RELAY_TIMEOUT_MS = 5_000;
const DEFAULT_PERMISSION_TIMEOUT_MS = 120_000;
const MAX_NATIVE_HOOK_RELAY_INVOCATIONS = 200;
const MAX_NATIVE_HOOK_RELAY_JSON_DEPTH = 64;
const MAX_NATIVE_HOOK_RELAY_JSON_NODES = 20_000;
const MAX_APPROVAL_TITLE_LENGTH = 80;
const MAX_APPROVAL_DESCRIPTION_LENGTH = 700;
const MAX_PERMISSION_APPROVALS_PER_WINDOW = 12;
const PERMISSION_APPROVAL_WINDOW_MS = 60_000;
const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
const relays = new Map<string, NativeHookRelayRegistration>();
const invocations: NativeHookRelayInvocation[] = [];
const pendingPermissionApprovals = new Map<
string,
Promise<NativeHookRelayPermissionApprovalResult>
>();
const permissionApprovalWindows = new Map<string, number[]>();
const log = createSubsystemLogger("agents/harness/native-hook-relay");
type NativeHookRelayPermissionDecision = "allow" | "deny";
type NativeHookRelayPermissionApprovalResult = NativeHookRelayPermissionDecision | "defer";
type NativeHookRelayPermissionApprovalRequest = {
provider: NativeHookRelayProvider;
agentId?: string;
sessionId: string;
sessionKey?: string;
runId: string;
toolName: string;
toolCallId?: string;
cwd?: string;
model?: string;
toolInput: Record<string, unknown>;
signal?: AbortSignal;
};
type NativeHookRelayPermissionApprovalRequester = (
request: NativeHookRelayPermissionApprovalRequest,
) => Promise<NativeHookRelayPermissionApprovalResult>;
let nativeHookRelayPermissionApprovalRequester: NativeHookRelayPermissionApprovalRequester =
requestNativeHookRelayPermissionApproval;
const nativeHookRelayProviderAdapters: Record<
NativeHookRelayProvider,
NativeHookRelayProviderAdapter
> = {
codex: {
normalizeMetadata: normalizeCodexHookMetadata,
readToolInput: readCodexToolInput,
readToolResponse: readCodexToolResponse,
renderNoopResponse: () => {
// Codex treats empty stdout plus exit 0 as no decision/no additional context.
return { stdout: "", stderr: "", exitCode: 0 };
},
renderPreToolUseBlockResponse: (reason) => ({
stdout: `${JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: reason,
},
})}\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,
}),
},
};
export function registerNativeHookRelay(
params: RegisterNativeHookRelayParams,
): NativeHookRelayRegistrationHandle {
pruneExpiredNativeHookRelays();
const relayId = randomUUID();
const allowedEvents = normalizeAllowedEvents(params.allowedEvents);
const registration: NativeHookRelayRegistration = {
relayId,
provider: params.provider,
...(params.agentId ? { agentId: params.agentId } : {}),
sessionId: params.sessionId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
runId: params.runId,
allowedEvents,
expiresAtMs: Date.now() + normalizePositiveInteger(params.ttlMs, DEFAULT_RELAY_TTL_MS),
...(params.signal ? { signal: params.signal } : {}),
};
relays.set(relayId, registration);
return {
...registration,
commandForEvent: (event) =>
buildNativeHookRelayCommand({
provider: params.provider,
relayId,
event,
timeoutMs: params.command?.timeoutMs,
executable: params.command?.executable,
nodeExecutable: params.command?.nodeExecutable,
}),
unregister: () => unregisterNativeHookRelay(relayId),
};
}
export function unregisterNativeHookRelay(relayId: string): void {
relays.delete(relayId);
removeNativeHookRelayInvocations(relayId);
removeNativeHookRelayPermissionState(relayId);
}
export function buildNativeHookRelayCommand(params: {
provider: NativeHookRelayProvider;
relayId: string;
event: NativeHookRelayEvent;
timeoutMs?: number;
executable?: string;
nodeExecutable?: string;
}): string {
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([
...argv,
"hooks",
"relay",
"--provider",
params.provider,
"--relay-id",
params.relayId,
"--event",
params.event,
"--timeout",
String(timeoutMs),
]);
}
export async function invokeNativeHookRelay(
params: InvokeNativeHookRelayParams,
): Promise<NativeHookRelayProcessResponse> {
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) {
relays.delete(relayId);
removeNativeHookRelayInvocations(relayId);
throw new Error("native hook relay expired");
}
if (registration.provider !== provider) {
throw new Error("native hook relay provider mismatch");
}
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),
});
}
export function renderNativeHookRelayUnavailableResponse(params: {
provider: unknown;
event: unknown;
message?: string;
}): NativeHookRelayProcessResponse {
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") {
return adapter.renderPreToolUseBlockResponse(message);
}
if (event === "permission_request") {
return adapter.renderPermissionDecisionResponse("deny", message);
}
return adapter.renderNoopResponse(event);
}
function recordNativeHookRelayInvocation(invocation: NativeHookRelayInvocation): void {
invocations.push(invocation);
if (invocations.length > MAX_NATIVE_HOOK_RELAY_INVOCATIONS) {
invocations.splice(0, invocations.length - MAX_NATIVE_HOOK_RELAY_INVOCATIONS);
}
}
function removeNativeHookRelayInvocations(relayId: string): void {
for (let index = invocations.length - 1; index >= 0; index -= 1) {
if (invocations[index]?.relayId === relayId) {
invocations.splice(index, 1);
}
}
}
function pruneExpiredNativeHookRelays(now = Date.now()): void {
for (const [relayId, registration] of relays) {
if (now > registration.expiresAtMs) {
relays.delete(relayId);
removeNativeHookRelayInvocations(relayId);
}
}
}
async function processNativeHookRelayInvocation(params: {
registration: NativeHookRelayRegistration;
invocation: NativeHookRelayInvocation;
adapter: NativeHookRelayProviderAdapter;
}): Promise<NativeHookRelayProcessResponse> {
if (params.invocation.event === "pre_tool_use") {
return runNativeHookRelayPreToolUse(params);
}
if (params.invocation.event === "post_tool_use") {
return runNativeHookRelayPostToolUse(params);
}
return runNativeHookRelayPermissionRequest(params);
}
async function runNativeHookRelayPreToolUse(params: {
registration: NativeHookRelayRegistration;
invocation: NativeHookRelayInvocation;
adapter: NativeHookRelayProviderAdapter;
}): Promise<NativeHookRelayProcessResponse> {
const toolName = normalizeNativeHookToolName(params.invocation.toolName);
const toolInput = params.adapter.readToolInput(params.invocation.rawPayload);
const outcome = await runBeforeToolCallHook({
toolName,
params: toolInput,
...(params.invocation.toolUseId ? { toolCallId: params.invocation.toolUseId } : {}),
signal: params.registration.signal,
ctx: {
...(params.registration.agentId ? { agentId: params.registration.agentId } : {}),
sessionId: params.registration.sessionId,
...(params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {}),
runId: params.registration.runId,
},
});
if (outcome.blocked) {
return params.adapter.renderPreToolUseBlockResponse(outcome.reason);
}
// Codex PreToolUse supports block/allow, not argument mutation. If an
// OpenClaw plugin returns adjusted params here, we intentionally ignore them.
return params.adapter.renderNoopResponse(params.invocation.event);
}
async function runNativeHookRelayPostToolUse(params: {
registration: NativeHookRelayRegistration;
invocation: NativeHookRelayInvocation;
adapter: NativeHookRelayProviderAdapter;
}): Promise<NativeHookRelayProcessResponse> {
const toolName = normalizeNativeHookToolName(params.invocation.toolName);
const toolCallId =
params.invocation.toolUseId ?? `${params.invocation.event}:${params.invocation.receivedAt}`;
await runAgentHarnessAfterToolCallHook({
toolName,
toolCallId,
runId: params.registration.runId,
...(params.registration.agentId ? { agentId: params.registration.agentId } : {}),
sessionId: params.registration.sessionId,
...(params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {}),
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: {
registration: NativeHookRelayRegistration;
invocation: NativeHookRelayInvocation;
adapter: NativeHookRelayProviderAdapter;
}): Promise<NativeHookRelayProcessResponse> {
const request: NativeHookRelayPermissionApprovalRequest = {
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 pendingApproval = pendingPermissionApprovals.get(approvalKey);
try {
const decision = await (pendingApproval ??
requestNativeHookRelayPermissionApprovalWithBudget({
registration: params.registration,
approvalKey,
request,
}));
if (decision === "allow") {
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: ${String(error)}`);
}
return params.adapter.renderNoopResponse(params.invocation.event);
}
async function requestNativeHookRelayPermissionApprovalWithBudget(params: {
registration: NativeHookRelayRegistration;
approvalKey: string;
request: NativeHookRelayPermissionApprovalRequest;
}): Promise<NativeHookRelayPermissionApprovalResult> {
if (!consumeNativeHookRelayPermissionBudget(params.registration.relayId)) {
log.warn(
`native hook permission approval rate limit exceeded; deferring: relay=${params.registration.relayId} run=${params.registration.runId}`,
);
return "defer";
}
const approval = nativeHookRelayPermissionApprovalRequester(params.request).finally(() => {
pendingPermissionApprovals.delete(params.approvalKey);
});
pendingPermissionApprovals.set(params.approvalKey, approval);
return approval;
}
function nativeHookRelayPermissionApprovalKey(params: {
registration: NativeHookRelayRegistration;
request: NativeHookRelayPermissionApprovalRequest;
}): string {
return [
params.registration.relayId,
params.registration.runId,
params.request.toolCallId ?? permissionRequestFallbackKey(params.request),
].join(":");
}
function permissionRequestFallbackKey(request: NativeHookRelayPermissionApprovalRequest): string {
const command = readOptionalString(request.toolInput.command);
if (command) {
return `${request.toolName}:command:${truncateText(command, 240)}`;
}
const keys = Object.keys(request.toolInput).toSorted().join(",");
return `${request.toolName}:keys:${truncateText(keys, 240)}`;
}
function consumeNativeHookRelayPermissionBudget(relayId: string, now = Date.now()): boolean {
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 removeNativeHookRelayPermissionState(relayId: string): void {
permissionApprovalWindows.delete(relayId);
for (const key of pendingPermissionApprovals.keys()) {
if (key.startsWith(`${relayId}:`)) {
pendingPermissionApprovals.delete(key);
}
}
}
function normalizeNativeHookInvocation(params: {
registration: NativeHookRelayRegistration;
event: NativeHookRelayEvent;
rawPayload: JsonValue;
}): NativeHookRelayInvocation {
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: new Date().toISOString(),
};
}
function getNativeHookRelayProviderAdapter(
provider: NativeHookRelayProvider,
): NativeHookRelayProviderAdapter {
return nativeHookRelayProviderAdapters[provider];
}
function normalizeCodexHookMetadata(rawPayload: JsonValue): NativeHookRelayInvocationMetadata {
const payload = isJsonObject(rawPayload) ? rawPayload : {};
const metadata: NativeHookRelayInvocationMetadata = {};
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 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: JsonValue): Record<string, unknown> {
const payload = isJsonObject(rawPayload) ? rawPayload : {};
const toolInput = payload.tool_input;
if (isJsonObject(toolInput)) {
return toolInput;
}
if (toolInput === undefined) {
return {};
}
return { value: toolInput };
}
function readCodexToolResponse(rawPayload: JsonValue): unknown {
const payload = isJsonObject(rawPayload) ? rawPayload : {};
return payload.tool_response;
}
function normalizeNativeHookToolName(toolName: string | undefined): string {
return normalizeToolName(toolName ?? "tool");
}
async function requestNativeHookRelayPermissionApproval(
request: NativeHookRelayPermissionApprovalRequest,
): Promise<NativeHookRelayPermissionApprovalResult> {
const timeoutMs = DEFAULT_PERMISSION_TIMEOUT_MS;
const requestResult: {
id?: string;
decision?: string | null;
} = await callGatewayTool(
"plugin.approval.request",
{ timeoutMs: timeoutMs + 10_000 },
{
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,
agentId: request.agentId,
sessionKey: request.sessionKey,
timeoutMs,
twoPhase: true,
},
{ expectFinal: false },
);
const approvalId = requestResult?.id;
if (!approvalId) {
return "defer";
}
let decision: string | null | undefined;
if (Object.prototype.hasOwnProperty.call(requestResult ?? {}, "decision")) {
decision = requestResult.decision;
} else {
const waitResult = await waitForNativeHookRelayApprovalDecision({
approvalId,
signal: request.signal,
timeoutMs,
});
decision = waitResult?.decision;
}
if (
decision === PluginApprovalResolutions.ALLOW_ONCE ||
decision === PluginApprovalResolutions.ALLOW_ALWAYS
) {
return "allow";
}
if (decision === PluginApprovalResolutions.DENY) {
return "deny";
}
return "defer";
}
async function waitForNativeHookRelayApprovalDecision(params: {
approvalId: string;
signal?: AbortSignal;
timeoutMs: number;
}): Promise<{ id?: string; decision?: string | null } | undefined> {
const waitPromise: Promise<{ id?: string; decision?: string | null } | undefined> =
callGatewayTool(
"plugin.approval.waitDecision",
{ timeoutMs: params.timeoutMs + 10_000 },
{ id: params.approvalId },
);
if (!params.signal) {
return waitPromise;
}
let onAbort: (() => void) | undefined;
const abortPromise = new Promise<never>((_, reject) => {
if (params.signal!.aborted) {
reject(params.signal!.reason);
return;
}
onAbort = () => reject(params.signal!.reason);
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: NativeHookRelayPermissionApprovalRequest,
): string {
const lines = [
`Tool: ${sanitizeApprovalText(request.toolName)}`,
request.cwd ? `Cwd: ${sanitizeApprovalText(request.cwd)}` : undefined,
request.model ? `Model: ${sanitizeApprovalText(request.model)}` : undefined,
formatToolInputPreview(request.toolInput),
].filter((line): line is string => Boolean(line));
return lines.join("\n");
}
function formatToolInputPreview(toolInput: Record<string, unknown>): string | undefined {
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 undefined;
}
const shownKeys = keys.slice(0, 12).join(", ");
const omitted = keys.length > 12 ? ` (${keys.length - 12} omitted)` : "";
return `Input keys: ${shownKeys}${omitted}`;
}
function sanitizeApprovalText(value: string): string {
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: number): boolean {
return (
(codePoint >= 0 && codePoint <= 8) ||
codePoint === 11 ||
codePoint === 12 ||
(codePoint >= 14 && codePoint <= 31) ||
(codePoint >= 127 && codePoint <= 159) ||
(codePoint >= 0x202a && codePoint <= 0x202e) ||
(codePoint >= 0x2066 && codePoint <= 0x2069)
);
}
function nativeHookRelayProviderDisplayName(provider: NativeHookRelayProvider): string {
if (provider === "codex") {
return "Codex";
}
return provider;
}
function truncateText(value: string, maxLength: number): string {
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
}
function resolveOpenClawCliExecutable(): string {
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: readonly NativeHookRelayEvent[] | undefined,
): readonly NativeHookRelayEvent[] {
if (!events?.length) {
return NATIVE_HOOK_RELAY_EVENTS;
}
return [...new Set(events)];
}
function normalizePositiveInteger(value: number | undefined, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) && value > 0
? Math.floor(value)
: fallback;
}
function shellQuoteArgs(args: readonly string[]): string {
return args.map((arg) => shellQuoteArg(arg, process.platform)).join(" ");
}
function shellQuoteArg(value: string, platform: NodeJS.Platform): string {
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
return value;
}
if (platform === "win32") {
return `"${value.replaceAll('"', '\\"')}"`;
}
return `'${value.replaceAll("'", "'\\''")}'`;
}
function readNativeHookRelayProvider(value: unknown): NativeHookRelayProvider {
if (value === "codex") {
return value;
}
throw new Error("unsupported native hook relay provider");
}
function readNativeHookRelayEvent(value: unknown): NativeHookRelayEvent {
if (value === "pre_tool_use" || value === "post_tool_use" || value === "permission_request") {
return value;
}
throw new Error("unsupported native hook relay event");
}
function readNonEmptyString(value: unknown, name: string): string {
if (typeof value === "string" && value.trim()) {
return value.trim();
}
throw new Error(`native hook relay ${name} is required`);
}
function readOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function isJsonValue(value: unknown): value is JsonValue {
const stack: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
let nodes = 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 || typeof current.value === "string") {
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 item of current.value) {
stack.push({ value: item, depth: current.depth + 1 });
}
continue;
}
if (!isJsonObject(current.value)) {
return false;
}
try {
for (const item of Object.values(current.value)) {
stack.push({ value: item, depth: current.depth + 1 });
}
} catch {
return false;
}
}
return true;
}
function isJsonObject(value: unknown): value is Record<string, unknown> {
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;
}
}
export const __testing = {
clearNativeHookRelaysForTests(): void {
relays.clear();
invocations.length = 0;
pendingPermissionApprovals.clear();
permissionApprovalWindows.clear();
nativeHookRelayPermissionApprovalRequester = requestNativeHookRelayPermissionApproval;
},
getNativeHookRelayInvocationsForTests(): NativeHookRelayInvocation[] {
return [...invocations];
},
getNativeHookRelayRegistrationForTests(relayId: string): NativeHookRelayRegistration | undefined {
return relays.get(relayId);
},
formatPermissionApprovalDescriptionForTests(
request: NativeHookRelayPermissionApprovalRequest,
): string {
return formatPermissionApprovalDescription(request);
},
setNativeHookRelayPermissionApprovalRequesterForTests(
requester: NativeHookRelayPermissionApprovalRequester,
): void {
nativeHookRelayPermissionApprovalRequester = requester;
},
} as const;
¤ Dauer der Verarbeitung: 0.1 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|
2026-05-26
|
|
|
|
|