@openclaw/codex 2026.6.11 → 2026.7.1-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-server-policy-BPTiVNsW.js → app-server-policy-C968Kgin.js} +5 -5
- package/dist/{attempt-notifications-BYnndyFl.js → attempt-notifications-BGsEIIDI.js} +29 -40
- package/dist/{command-handlers-DEPTJYk0.js → command-handlers-Dv-pzAmg.js} +278 -237
- package/dist/command-rpc-eLM_mtC7.js +784 -0
- package/dist/compact-4mVBriKT.js +603 -0
- package/dist/{computer-use-ClrJwoFp.js → computer-use-Bmaz333N.js} +3 -3
- package/dist/{config-CszD0vP3.js → config-CYEDnLJ2.js} +44 -18
- package/dist/conversation-binding-data-BHRpYbX3.js +121 -0
- package/dist/doctor-contract-api.js +590 -22
- package/dist/harness.js +29 -10
- package/dist/index.js +1134 -35
- package/dist/{media-understanding-provider-ZUQHN7gT.js → media-understanding-provider-BHOu9-Gg.js} +13 -8
- package/dist/media-understanding-provider.js +1 -1
- package/dist/{models-CAmfBWRl.js → models-DDmO1zwd.js} +2 -2
- package/dist/notification-correlation-Bo7KB3ks.js +35 -0
- package/dist/{plugin-app-cache-key-C9ILt7bl.js → plugin-app-cache-key-BrhVdeEf.js} +13 -5
- package/dist/{protocol-dh-ETiNd.js → protocol-2POPqAY4.js} +2 -1
- package/dist/{protocol-validators-B19q5BIX.js → protocol-validators-dZQ-UTOa.js} +205 -56
- package/dist/provider-capabilities-DzI8fgEu.js +6172 -0
- package/dist/provider-catalog.js +4 -2
- package/dist/{provider-B-OHpbD3.js → provider-zjPfx5Fs.js} +94 -23
- package/dist/provider.js +2 -2
- package/dist/{request-Dcd452Nk.js → request-BcJyl8KL.js} +2 -2
- package/dist/{run-attempt-CXxtFC9d.js → run-attempt-DNSNk_G8.js} +1250 -2992
- package/dist/{sandbox-guard-3tnjhjFb.js → sandbox-guard-DA2TQfZW.js} +5 -3
- package/dist/session-binding-C1ZXdP-x.js +627 -0
- package/dist/session-binding-meta-B7aEMU7g.js +6 -0
- package/dist/shared-client-4ICy3U6d.js +1853 -0
- package/dist/{side-question-C-DhgJOd.js → side-question-CMOpEhvY.js} +134 -142
- package/dist/{thread-lifecycle-DtT3-ehU.js → thread-lifecycle-qWE88Dn2.js} +803 -481
- package/dist/{web-search-provider.runtime-CkwxrXQj.js → web-search-provider.runtime-BSlriav6.js} +3 -3
- package/npm-shrinkwrap.json +34 -34
- package/openclaw.plugin.json +23 -3
- package/package.json +9 -6
- package/dist/client-CLrtWgrD.js +0 -729
- package/dist/client-factory-Bm6HsGob.js +0 -18
- package/dist/compact-aQJ0ZKhe.js +0 -278
- package/dist/conversation-binding-t0XMLz-j.js +0 -1557
- package/dist/provider-capabilities-D7hnTO76.js +0 -3201
- package/dist/rate-limit-cache-C7qmZ0Jh.js +0 -26
- package/dist/session-binding-HOuPt-E0.js +0 -338
- package/dist/shared-client-Dtx5i7Ez.js +0 -1013
|
@@ -1,3201 +0,0 @@
|
|
|
1
|
-
import { n as isJsonObject } from "./protocol-dh-ETiNd.js";
|
|
2
|
-
import { C as isForcedPrivateQaCodexRuntime, S as filterCodexDynamicTools, _ as resolveCodexWebSearchPlan, w as normalizeCodexDynamicToolName, x as sanitizeInlineImageDataUrl, y as invalidInlineImageText } from "./thread-lifecycle-DtT3-ehU.js";
|
|
3
|
-
import { c as isTrustedCodexModelBackedOpenAIProvider, u as readCodexPluginConfig } from "./config-CszD0vP3.js";
|
|
4
|
-
import { a as formatCodexDisplayText } from "./app-server-policy-BPTiVNsW.js";
|
|
5
|
-
import { o as releaseLeasedSharedCodexAppServerClient } from "./shared-client-Dtx5i7Ez.js";
|
|
6
|
-
import { i as resolveCodexNativeExecutionPolicy } from "./sandbox-guard-3tnjhjFb.js";
|
|
7
|
-
import { addTimerTimeoutGraceMs, finiteSecondsToTimerSafeMilliseconds, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
8
|
-
import { asOptionalRecord, isRecord, normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
11
|
-
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
12
|
-
import { HEARTBEAT_RESPONSE_TOOL_NAME, buildAgentHookContextChannelFields, buildEmbeddedAttemptToolRunContext, callGatewayTool, consumeAdjustedParamsForToolCall, consumePreExecutionBlockedToolCall, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractMessagingToolSend, extractMessagingToolSendResult, extractToolResultMediaArtifact, filterProviderNormalizableTools, filterToolResultMediaUrls, finalizeToolTerminalPresentation, formatApprovalDisplayPath, getChannelAgentToolMeta, getPluginToolMeta, hasNativeHookRelayInvocation, invokeNativeHookRelay, isMessagingTool, isMessagingToolSendAction, isReplaySafeToolCall, isSubagentSessionKey, isToolResultError, isToolWrappedWithBeforeToolCallHook, normalizeAgentRuntimeTools, normalizeHeartbeatToolResponse, projectRuntimeToolInputSchema, registerNativeHookRelay, resolveAttemptSpawnWorkspaceDir, resolveModelAuthMode, resolveNativeHookRelayDeferredToolApproval, runAgentHarnessAfterToolCallHook, runBeforeToolCallHook, sanitizeToolResult, setBeforeToolCallDiagnosticsEnabled, supportsModelTools, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
13
|
-
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
|
|
14
|
-
import { isToolAllowed } from "openclaw/plugin-sdk/sandbox";
|
|
15
|
-
import { buildExecAutoReviewInputForShellCommand, reviewExecRequestWithConfiguredModel } from "openclaw/plugin-sdk/agent-harness-exec-review-runtime";
|
|
16
|
-
/** Default idle timeout while waiting for app-server turn completion. */
|
|
17
|
-
const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 6e4;
|
|
18
|
-
/** Short guard after apparent assistant completion. */
|
|
19
|
-
const CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 1e4;
|
|
20
|
-
const CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
21
|
-
/** Guard after reasoning/commentary progress when no tool handoff occurred. */
|
|
22
|
-
const CODEX_POST_REASONING_REPLY_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
23
|
-
/** Long terminal idle watch for app-server turns that never send completion. */
|
|
24
|
-
const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
25
|
-
function resolvePositiveIntegerTimeoutMs(value, fallbackMs) {
|
|
26
|
-
return resolveTimerTimeoutMs(value, resolveTimerTimeoutMs(fallbackMs, 1));
|
|
27
|
-
}
|
|
28
|
-
/** Runs startup work with abort and timeout handling plus optional cleanup. */
|
|
29
|
-
async function withCodexStartupTimeout(params) {
|
|
30
|
-
if (params.signal.aborted) throw new Error("codex app-server startup aborted");
|
|
31
|
-
let timeout;
|
|
32
|
-
let abortCleanup;
|
|
33
|
-
let timeoutError;
|
|
34
|
-
let timeoutCleanup;
|
|
35
|
-
try {
|
|
36
|
-
return await Promise.race([params.operation(), new Promise((_, reject) => {
|
|
37
|
-
const rejectOnce = (error) => {
|
|
38
|
-
if (timeout) {
|
|
39
|
-
clearTimeout(timeout);
|
|
40
|
-
timeout = void 0;
|
|
41
|
-
}
|
|
42
|
-
reject(error);
|
|
43
|
-
};
|
|
44
|
-
timeout = setTimeout(() => {
|
|
45
|
-
timeoutError = /* @__PURE__ */ new Error("codex app-server startup timed out");
|
|
46
|
-
timeoutCleanup = Promise.resolve(params.onTimeout?.()).then(() => void 0, () => void 0);
|
|
47
|
-
timeoutCleanup.finally(() => {
|
|
48
|
-
rejectOnce(timeoutError);
|
|
49
|
-
});
|
|
50
|
-
}, params.timeoutMs);
|
|
51
|
-
const abortListener = () => rejectOnce(/* @__PURE__ */ new Error("codex app-server startup aborted"));
|
|
52
|
-
params.signal.addEventListener("abort", abortListener, { once: true });
|
|
53
|
-
abortCleanup = () => params.signal.removeEventListener("abort", abortListener);
|
|
54
|
-
})]);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (timeoutError) {
|
|
57
|
-
await timeoutCleanup;
|
|
58
|
-
throw timeoutError;
|
|
59
|
-
}
|
|
60
|
-
throw error;
|
|
61
|
-
} finally {
|
|
62
|
-
if (timeout) clearTimeout(timeout);
|
|
63
|
-
abortCleanup?.();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Resolves startup timeout while honoring the configured floor. */
|
|
67
|
-
function resolveCodexStartupTimeoutMs(params) {
|
|
68
|
-
const timeoutFloorMs = resolvePositiveIntegerTimeoutMs(params.timeoutFloorMs, 100);
|
|
69
|
-
const timeoutMs = resolvePositiveIntegerTimeoutMs(params.timeoutMs, timeoutFloorMs);
|
|
70
|
-
return Math.max(timeoutFloorMs, timeoutMs);
|
|
71
|
-
}
|
|
72
|
-
/** Resolves the completion-idle timeout for an active turn. */
|
|
73
|
-
function resolveCodexTurnCompletionIdleTimeoutMs(value) {
|
|
74
|
-
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS);
|
|
75
|
-
}
|
|
76
|
-
/** Resolves the short assistant-completion release timeout. */
|
|
77
|
-
function resolveCodexTurnAssistantCompletionIdleTimeoutMs(value) {
|
|
78
|
-
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS);
|
|
79
|
-
}
|
|
80
|
-
/** Resolves the conservative post-tool raw assistant guard timeout. */
|
|
81
|
-
function resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(value, fallbackMs) {
|
|
82
|
-
return resolvePositiveIntegerTimeoutMs(value, Math.max(resolvePositiveIntegerTimeoutMs(void 0, fallbackMs), CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS));
|
|
83
|
-
}
|
|
84
|
-
/** Resolves the long terminal turn idle timeout. */
|
|
85
|
-
function resolveCodexTurnTerminalIdleTimeoutMs(value) {
|
|
86
|
-
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS);
|
|
87
|
-
}
|
|
88
|
-
/** Adds gateway grace time to a caller timeout without overflowing invalid values. */
|
|
89
|
-
function resolveCodexGatewayTimeoutWithGraceMs(timeoutMs, graceMs = 1e4) {
|
|
90
|
-
const timeout = resolvePositiveIntegerTimeoutMs(timeoutMs, 1);
|
|
91
|
-
return addTimerTimeoutGraceMs(timeout, resolveTimerTimeoutMs(graceMs, 0, 0)) ?? timeout;
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region extensions/codex/src/app-server/plugin-approval-roundtrip.ts
|
|
95
|
-
/**
|
|
96
|
-
* Routes Codex app-server plugin approval prompts through OpenClaw's gateway
|
|
97
|
-
* approval tool and maps gateway decisions back to Codex outcomes.
|
|
98
|
-
*/
|
|
99
|
-
const DEFAULT_CODEX_APPROVAL_TIMEOUT_MS = 12e4;
|
|
100
|
-
const MAX_PLUGIN_APPROVAL_TITLE_LENGTH = 80;
|
|
101
|
-
const MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH = 256;
|
|
102
|
-
/** Starts a two-phase plugin approval request through the OpenClaw gateway. */
|
|
103
|
-
async function requestPluginApproval(params) {
|
|
104
|
-
const timeoutMs = DEFAULT_CODEX_APPROVAL_TIMEOUT_MS;
|
|
105
|
-
return callGatewayTool("plugin.approval.request", { timeoutMs: resolveCodexGatewayTimeoutWithGraceMs(timeoutMs) }, {
|
|
106
|
-
pluginId: "openclaw-codex-app-server",
|
|
107
|
-
title: truncateForGateway(params.title, MAX_PLUGIN_APPROVAL_TITLE_LENGTH),
|
|
108
|
-
description: truncateForGateway(params.description, MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH),
|
|
109
|
-
severity: params.severity,
|
|
110
|
-
toolName: params.toolName,
|
|
111
|
-
toolCallId: params.toolCallId,
|
|
112
|
-
agentId: params.paramsForRun.agentId,
|
|
113
|
-
sessionKey: params.paramsForRun.sessionKey,
|
|
114
|
-
turnSourceChannel: params.paramsForRun.messageChannel ?? params.paramsForRun.messageProvider,
|
|
115
|
-
turnSourceTo: params.paramsForRun.currentChannelId,
|
|
116
|
-
turnSourceAccountId: params.paramsForRun.agentAccountId,
|
|
117
|
-
turnSourceThreadId: params.paramsForRun.currentThreadTs,
|
|
118
|
-
timeoutMs,
|
|
119
|
-
twoPhase: true,
|
|
120
|
-
...params.allowedDecisions ? { allowedDecisions: params.allowedDecisions } : {}
|
|
121
|
-
}, { expectFinal: false });
|
|
122
|
-
}
|
|
123
|
-
/** Detects the gateway's explicit null-decision marker for unavailable approvals. */
|
|
124
|
-
function approvalRequestExplicitlyUnavailable(result) {
|
|
125
|
-
if (result === null || result === void 0 || typeof result !== "object") return false;
|
|
126
|
-
let descriptor;
|
|
127
|
-
try {
|
|
128
|
-
descriptor = Object.getOwnPropertyDescriptor(result, "decision");
|
|
129
|
-
} catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return descriptor !== void 0 && "value" in descriptor && descriptor.value === null;
|
|
133
|
-
}
|
|
134
|
-
/** Waits for the gateway's final approval decision, respecting turn aborts. */
|
|
135
|
-
async function waitForPluginApprovalDecision(params) {
|
|
136
|
-
const waitPromise = callGatewayTool("plugin.approval.waitDecision", { timeoutMs: resolveCodexGatewayTimeoutWithGraceMs(DEFAULT_CODEX_APPROVAL_TIMEOUT_MS) }, { id: params.approvalId });
|
|
137
|
-
if (!params.signal) return (await waitPromise)?.decision;
|
|
138
|
-
let onAbort;
|
|
139
|
-
const abortPromise = new Promise((_, reject) => {
|
|
140
|
-
if (params.signal.aborted) {
|
|
141
|
-
reject(toLintErrorObject(params.signal.reason, "Non-Error rejection"));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
onAbort = () => reject(toLintErrorObject(params.signal.reason, "Non-Error rejection"));
|
|
145
|
-
params.signal.addEventListener("abort", onAbort, { once: true });
|
|
146
|
-
});
|
|
147
|
-
try {
|
|
148
|
-
return (await Promise.race([waitPromise, abortPromise]))?.decision;
|
|
149
|
-
} finally {
|
|
150
|
-
if (onAbort) params.signal.removeEventListener("abort", onAbort);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
/** Converts a gateway exec approval decision into the app-server approval outcome enum. */
|
|
154
|
-
function mapExecDecisionToOutcome(decision) {
|
|
155
|
-
if (decision === "allow-once") return "approved-once";
|
|
156
|
-
if (decision === "allow-always") return "approved-session";
|
|
157
|
-
if (decision === null || decision === void 0) return "unavailable";
|
|
158
|
-
return "denied";
|
|
159
|
-
}
|
|
160
|
-
function truncateForGateway(value, maxLength) {
|
|
161
|
-
return value.length <= maxLength ? value : `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
162
|
-
}
|
|
163
|
-
function toLintErrorObject(value, fallbackMessage) {
|
|
164
|
-
if (value instanceof Error) return value;
|
|
165
|
-
if (typeof value === "string") return new Error(value);
|
|
166
|
-
const error = new Error(fallbackMessage, { cause: value });
|
|
167
|
-
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
|
|
168
|
-
return error;
|
|
169
|
-
}
|
|
170
|
-
//#endregion
|
|
171
|
-
//#region extensions/codex/src/app-server/approval-bridge.ts
|
|
172
|
-
/**
|
|
173
|
-
* Bridges Codex app-server approval requests into OpenClaw policy hooks and
|
|
174
|
-
* plugin approval UX.
|
|
175
|
-
*/
|
|
176
|
-
const PERMISSION_DESCRIPTION_MAX_LENGTH = 700;
|
|
177
|
-
const PERMISSION_SAMPLE_LIMIT = 2;
|
|
178
|
-
const PERMISSION_VALUE_MAX_LENGTH = 48;
|
|
179
|
-
const COMMAND_PREVIEW_WITH_DETAILS_MAX_LENGTH = 80;
|
|
180
|
-
const APPROVAL_PREVIEW_SCAN_MAX_LENGTH = 4096;
|
|
181
|
-
const APPROVAL_PREVIEW_OMITTED = "[preview truncated or unsafe content omitted]";
|
|
182
|
-
const ANSI_OSC_SEQUENCE_RE$1 = new RegExp(String.raw`(?:\u001b]|\u009d)[^\u001b\u009c\u0007]*(?:\u0007|\u001b\\|\u009c)`, "g");
|
|
183
|
-
const ANSI_CONTROL_SEQUENCE_RE$1 = new RegExp(String.raw`(?:\u001b\[[0-?]*[ -/]*[@-~]|\u009b[0-?]*[ -/]*[@-~]|\u001b[@-Z\\-_])`, "g");
|
|
184
|
-
const CONTROL_CHARACTER_RE$1 = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]+`, "g");
|
|
185
|
-
const INVISIBLE_FORMATTING_CONTROL_RE$1 = new RegExp(String.raw`[\u00ad\u034f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff\ufe00-\ufe0f\u{e0100}-\u{e01ef}]`, "gu");
|
|
186
|
-
const DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE$1 = new RegExp(String.raw`(?:\u001b\][^\u001b\u009c\u0007]*|\u009d[^\u001b\u009c\u0007]*|\u001b\[[0-?]*[ -/]*|\u009b[0-?]*[ -/]*|\u001b)$`);
|
|
187
|
-
/**
|
|
188
|
-
* Handles one app-server approval request for the active thread/turn, returning
|
|
189
|
-
* the app-server response payload when the request belongs to this run.
|
|
190
|
-
*/
|
|
191
|
-
async function handleCodexAppServerApprovalRequest(params) {
|
|
192
|
-
const requestParams = isJsonObject(params.requestParams) ? params.requestParams : void 0;
|
|
193
|
-
if (!matchesCurrentTurn(requestParams, params.threadId, params.turnId)) return;
|
|
194
|
-
if (!isSupportedAppServerApprovalMethod(params.method)) return unsupportedApprovalResponse();
|
|
195
|
-
const context = buildApprovalContext({
|
|
196
|
-
method: params.method,
|
|
197
|
-
requestParams,
|
|
198
|
-
paramsForRun: params.paramsForRun
|
|
199
|
-
});
|
|
200
|
-
try {
|
|
201
|
-
const policyOutcome = await runOpenClawToolPolicyForApprovalRequest({
|
|
202
|
-
method: params.method,
|
|
203
|
-
requestParams,
|
|
204
|
-
paramsForRun: params.paramsForRun,
|
|
205
|
-
context,
|
|
206
|
-
nativeHookRelay: params.nativeHookRelay,
|
|
207
|
-
signal: params.signal
|
|
208
|
-
});
|
|
209
|
-
if (policyOutcome?.outcome === "denied") {
|
|
210
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
211
|
-
phase: "resolved",
|
|
212
|
-
kind: context.kind,
|
|
213
|
-
status: "denied",
|
|
214
|
-
title: context.title,
|
|
215
|
-
...context.eventDetails,
|
|
216
|
-
...approvalEventScope(params.method, "denied"),
|
|
217
|
-
message: policyOutcome.reason
|
|
218
|
-
});
|
|
219
|
-
return buildApprovalResponse(params.method, context.requestParams, "denied");
|
|
220
|
-
}
|
|
221
|
-
if (policyOutcome?.outcome === "approved-once" || policyOutcome?.outcome === "approved-session") {
|
|
222
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
223
|
-
phase: "resolved",
|
|
224
|
-
kind: context.kind,
|
|
225
|
-
status: "approved",
|
|
226
|
-
title: context.title,
|
|
227
|
-
...context.eventDetails,
|
|
228
|
-
...approvalEventScope(params.method, policyOutcome.outcome),
|
|
229
|
-
message: approvalResolutionMessage(policyOutcome.outcome)
|
|
230
|
-
});
|
|
231
|
-
return buildApprovalResponse(params.method, context.requestParams, policyOutcome.outcome);
|
|
232
|
-
}
|
|
233
|
-
if (params.autoApprove === true) {
|
|
234
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
235
|
-
phase: "resolved",
|
|
236
|
-
kind: context.kind,
|
|
237
|
-
status: "approved",
|
|
238
|
-
title: context.title,
|
|
239
|
-
...context.eventDetails,
|
|
240
|
-
...approvalEventScope(params.method, "approved-session"),
|
|
241
|
-
message: "Codex app-server approval auto-approved by runtime policy."
|
|
242
|
-
});
|
|
243
|
-
return buildApprovalResponse(params.method, context.requestParams, "approved-session");
|
|
244
|
-
}
|
|
245
|
-
const autoReviewOutcome = await runInternalExecAutoReviewForApprovalRequest({
|
|
246
|
-
enabled: params.internalExecAutoReview === true && params.execPolicy?.mode === "auto",
|
|
247
|
-
method: params.method,
|
|
248
|
-
requestParams,
|
|
249
|
-
paramsForRun: params.paramsForRun,
|
|
250
|
-
context,
|
|
251
|
-
agentId: params.execReviewerAgentId,
|
|
252
|
-
signal: params.signal
|
|
253
|
-
});
|
|
254
|
-
if (autoReviewOutcome?.outcome === "approved-once") {
|
|
255
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
256
|
-
phase: "resolved",
|
|
257
|
-
kind: context.kind,
|
|
258
|
-
status: "approved",
|
|
259
|
-
title: context.title,
|
|
260
|
-
...context.eventDetails,
|
|
261
|
-
...approvalEventScope(params.method, autoReviewOutcome.outcome),
|
|
262
|
-
message: autoReviewOutcome.reason
|
|
263
|
-
});
|
|
264
|
-
return buildApprovalResponse(params.method, context.requestParams, autoReviewOutcome.outcome);
|
|
265
|
-
}
|
|
266
|
-
const requestResult = await requestPluginApproval({
|
|
267
|
-
paramsForRun: params.paramsForRun,
|
|
268
|
-
title: context.title,
|
|
269
|
-
description: context.description,
|
|
270
|
-
severity: context.severity,
|
|
271
|
-
toolName: context.toolName,
|
|
272
|
-
toolCallId: context.itemId
|
|
273
|
-
});
|
|
274
|
-
const approvalId = requestResult?.id;
|
|
275
|
-
if (!approvalId) {
|
|
276
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
277
|
-
phase: "resolved",
|
|
278
|
-
kind: context.kind,
|
|
279
|
-
status: "unavailable",
|
|
280
|
-
title: context.title,
|
|
281
|
-
...context.eventDetails,
|
|
282
|
-
...approvalEventScope(params.method, "denied"),
|
|
283
|
-
message: "Codex app-server approval route unavailable."
|
|
284
|
-
});
|
|
285
|
-
return buildApprovalResponse(params.method, context.requestParams, "denied");
|
|
286
|
-
}
|
|
287
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
288
|
-
phase: "requested",
|
|
289
|
-
kind: context.kind,
|
|
290
|
-
status: "pending",
|
|
291
|
-
title: context.title,
|
|
292
|
-
approvalId,
|
|
293
|
-
approvalSlug: approvalId,
|
|
294
|
-
...context.eventDetails,
|
|
295
|
-
message: "Codex app-server approval requested."
|
|
296
|
-
});
|
|
297
|
-
const outcome = mapExecDecisionToOutcome(approvalRequestExplicitlyUnavailable(requestResult) ? null : await waitForPluginApprovalDecision({
|
|
298
|
-
approvalId,
|
|
299
|
-
signal: params.signal
|
|
300
|
-
}));
|
|
301
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
302
|
-
phase: "resolved",
|
|
303
|
-
kind: context.kind,
|
|
304
|
-
status: outcome === "denied" ? "denied" : outcome === "unavailable" ? "unavailable" : outcome === "cancelled" ? "failed" : "approved",
|
|
305
|
-
title: context.title,
|
|
306
|
-
approvalId,
|
|
307
|
-
approvalSlug: approvalId,
|
|
308
|
-
...context.eventDetails,
|
|
309
|
-
...approvalEventScope(params.method, outcome),
|
|
310
|
-
message: approvalResolutionMessage(outcome)
|
|
311
|
-
});
|
|
312
|
-
return buildApprovalResponse(params.method, context.requestParams, outcome);
|
|
313
|
-
} catch (error) {
|
|
314
|
-
const cancelled = params.signal?.aborted === true;
|
|
315
|
-
emitApprovalEvent(params.paramsForRun, {
|
|
316
|
-
phase: "resolved",
|
|
317
|
-
kind: context.kind,
|
|
318
|
-
status: cancelled ? "failed" : "unavailable",
|
|
319
|
-
title: context.title,
|
|
320
|
-
...context.eventDetails,
|
|
321
|
-
...approvalEventScope(params.method, cancelled ? "cancelled" : "denied"),
|
|
322
|
-
message: cancelled ? "Codex app-server approval cancelled because the run stopped." : `Codex app-server approval route failed: ${formatCodexDisplayText(formatErrorMessage$1(error))}`
|
|
323
|
-
});
|
|
324
|
-
return buildApprovalResponse(params.method, context.requestParams, cancelled ? "cancelled" : "denied");
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
/** Converts an OpenClaw approval outcome into the app-server method response. */
|
|
328
|
-
function buildApprovalResponse(method, requestParams, outcome) {
|
|
329
|
-
if (method === "item/commandExecution/requestApproval") return { decision: commandApprovalDecision(requestParams, outcome) };
|
|
330
|
-
if (method === "item/fileChange/requestApproval") return { decision: fileChangeApprovalDecision(outcome) };
|
|
331
|
-
if (method === "item/permissions/requestApproval") {
|
|
332
|
-
if (outcome === "approved-session" || outcome === "approved-once") return {
|
|
333
|
-
permissions: requestedPermissions(requestParams),
|
|
334
|
-
scope: outcome === "approved-session" ? "session" : "turn"
|
|
335
|
-
};
|
|
336
|
-
return {
|
|
337
|
-
permissions: {},
|
|
338
|
-
scope: "turn"
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
return unsupportedApprovalResponse();
|
|
342
|
-
}
|
|
343
|
-
function matchesCurrentTurn(requestParams, threadId, turnId) {
|
|
344
|
-
if (!requestParams) return false;
|
|
345
|
-
const requestThreadId = readString$1(requestParams, "threadId") ?? readString$1(requestParams, "conversationId");
|
|
346
|
-
const requestTurnId = readString$1(requestParams, "turnId");
|
|
347
|
-
return requestThreadId === threadId && requestTurnId === turnId;
|
|
348
|
-
}
|
|
349
|
-
function buildApprovalContext(params) {
|
|
350
|
-
const itemId = readString$1(params.requestParams, "itemId") ?? readString$1(params.requestParams, "callId") ?? readString$1(params.requestParams, "approvalId");
|
|
351
|
-
const commandDetailLines = params.method === "item/commandExecution/requestApproval" ? describeCommandApprovalDetails(params.requestParams) : [];
|
|
352
|
-
const commandPreview = sanitizeApprovalPreview(readDisplayCommandPreview(params.requestParams), commandDetailLines.length > 0 ? COMMAND_PREVIEW_WITH_DETAILS_MAX_LENGTH : 180);
|
|
353
|
-
const reasonPreview = sanitizeApprovalPreview(readStringPreview(params.requestParams, "reason"), 180);
|
|
354
|
-
const command = commandPreview.text;
|
|
355
|
-
const reason = reasonPreview.text;
|
|
356
|
-
const kind = approvalKindForMethod(params.method);
|
|
357
|
-
const permissionLines = params.method === "item/permissions/requestApproval" ? describeRequestedPermissions(params.requestParams) : [];
|
|
358
|
-
const title = kind === "exec" ? "Codex app-server command approval" : params.method === "item/permissions/requestApproval" ? "Codex app-server permission approval" : kind === "plugin" ? "Codex app-server file approval" : "Codex app-server approval";
|
|
359
|
-
const subject = permissionLines[0] ?? (command ? `Command: ${formatApprovalPreviewSubject(command, commandPreview.omitted)}` : commandPreview.omitted ? `Command: ${APPROVAL_PREVIEW_OMITTED}` : reason ? `Reason: ${formatApprovalPreviewSubject(reason, reasonPreview.omitted)}` : reasonPreview.omitted ? `Reason: ${APPROVAL_PREVIEW_OMITTED}` : `Request method: ${params.method}`);
|
|
360
|
-
return {
|
|
361
|
-
kind,
|
|
362
|
-
title,
|
|
363
|
-
description: permissionLines.length > 0 ? joinDescriptionLinesWithinLimit(permissionLines, PERMISSION_DESCRIPTION_MAX_LENGTH) : [
|
|
364
|
-
subject,
|
|
365
|
-
...commandDetailLines,
|
|
366
|
-
params.paramsForRun.sessionKey && `Session: ${params.paramsForRun.sessionKey}`
|
|
367
|
-
].filter(Boolean).join("\n"),
|
|
368
|
-
severity: kind === "exec" ? "warning" : "info",
|
|
369
|
-
toolName: kind === "exec" ? "codex_command_approval" : params.method === "item/permissions/requestApproval" ? "codex_permission_approval" : "codex_file_approval",
|
|
370
|
-
itemId,
|
|
371
|
-
requestParams: params.requestParams,
|
|
372
|
-
eventDetails: {
|
|
373
|
-
...itemId ? { itemId } : {},
|
|
374
|
-
...command ? { command } : {},
|
|
375
|
-
...commandPreview.omitted ? { commandPreviewOmitted: true } : {},
|
|
376
|
-
...reason ? { reason } : {},
|
|
377
|
-
...reasonPreview.omitted ? { reasonPreviewOmitted: true } : {}
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
async function runInternalExecAutoReviewForApprovalRequest(params) {
|
|
382
|
-
if (!params.enabled || params.method !== "item/commandExecution/requestApproval") return;
|
|
383
|
-
if (hasCommandApprovalCapabilityAmendments(params.requestParams)) return;
|
|
384
|
-
const input = await buildAppServerExecAutoReviewInput({
|
|
385
|
-
requestParams: params.requestParams,
|
|
386
|
-
paramsForRun: params.paramsForRun
|
|
387
|
-
});
|
|
388
|
-
if (!input) return;
|
|
389
|
-
const reviewerConfig = resolveExecReviewerConfig(params.paramsForRun, params.agentId);
|
|
390
|
-
if (!canUseInternalExecAutoReviewReviewer(reviewerConfig, params.paramsForRun.config, process.env, params.paramsForRun.agentDir)) return;
|
|
391
|
-
const decision = await waitForInternalExecAutoReviewDecision({
|
|
392
|
-
signal: params.signal,
|
|
393
|
-
promise: reviewExecRequestWithConfiguredModel({
|
|
394
|
-
cfg: params.paramsForRun.config,
|
|
395
|
-
agentId: params.agentId ?? params.paramsForRun.agentId,
|
|
396
|
-
reviewer: reviewerConfig,
|
|
397
|
-
input
|
|
398
|
-
})
|
|
399
|
-
});
|
|
400
|
-
if (decision.decision !== "allow-once") return;
|
|
401
|
-
return {
|
|
402
|
-
outcome: "approved-once",
|
|
403
|
-
reason: `Codex app-server command approval granted by OpenClaw exec auto-reviewer: ${formatCodexDisplayText(decision.rationale)}`
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
async function waitForInternalExecAutoReviewDecision(params) {
|
|
407
|
-
if (!params.signal) return params.promise;
|
|
408
|
-
if (params.signal.aborted) throw toCodexAppServerApprovalCancellationError(params.signal.reason);
|
|
409
|
-
let onAbort;
|
|
410
|
-
const abortPromise = new Promise((_, reject) => {
|
|
411
|
-
onAbort = () => reject(toCodexAppServerApprovalCancellationError(params.signal?.reason));
|
|
412
|
-
params.signal?.addEventListener("abort", onAbort, { once: true });
|
|
413
|
-
});
|
|
414
|
-
try {
|
|
415
|
-
return await Promise.race([params.promise, abortPromise]);
|
|
416
|
-
} finally {
|
|
417
|
-
if (onAbort) params.signal.removeEventListener("abort", onAbort);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
function toCodexAppServerApprovalCancellationError(reason) {
|
|
421
|
-
if (reason instanceof Error) return reason;
|
|
422
|
-
return new Error(typeof reason === "string" && reason.trim() ? reason : "Codex app-server approval cancelled.");
|
|
423
|
-
}
|
|
424
|
-
async function buildAppServerExecAutoReviewInput(params) {
|
|
425
|
-
const command = readString$1(params.requestParams, "command");
|
|
426
|
-
if (!command) return;
|
|
427
|
-
return buildExecAutoReviewInputForShellCommand({
|
|
428
|
-
command,
|
|
429
|
-
cwd: readString$1(params.requestParams, "cwd") ?? params.paramsForRun.workspaceDir ?? null,
|
|
430
|
-
host: "codex-app-server",
|
|
431
|
-
agent: {
|
|
432
|
-
id: params.paramsForRun.agentId ?? null,
|
|
433
|
-
sessionKey: params.paramsForRun.sessionKey ?? null
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
function hasCommandApprovalCapabilityAmendments(requestParams) {
|
|
438
|
-
return hasNonEmptyJsonObject(requestParams?.additionalPermissions) || hasNonEmptyJsonObject(requestParams?.networkApprovalContext) || hasNonEmptyJsonObject(requestParams?.proposedExecpolicyAmendment) || hasNonEmptyArray(requestParams?.proposedExecpolicyAmendment) || hasNonEmptyArray(requestParams?.proposedNetworkPolicyAmendments) || findAvailableCommandAmendmentDecision(requestParams) !== void 0 || commandAcceptDecisionUnavailable(requestParams);
|
|
439
|
-
}
|
|
440
|
-
function commandAcceptDecisionUnavailable(requestParams) {
|
|
441
|
-
const available = requestParams?.availableDecisions;
|
|
442
|
-
return Array.isArray(available) && !available.includes("accept");
|
|
443
|
-
}
|
|
444
|
-
function hasNonEmptyJsonObject(value) {
|
|
445
|
-
return isJsonObject(value) && Object.keys(value).length > 0;
|
|
446
|
-
}
|
|
447
|
-
function hasNonEmptyArray(value) {
|
|
448
|
-
return Array.isArray(value) && value.length > 0;
|
|
449
|
-
}
|
|
450
|
-
function resolveExecReviewerConfig(params, agentId) {
|
|
451
|
-
const configRoot = readUnknownRecord(params.config);
|
|
452
|
-
const globalExec = readUnknownRecord(readUnknownRecord(configRoot?.tools)?.exec);
|
|
453
|
-
return readUnknownRecord(resolveAgentExecConfig(configRoot, agentId ?? params.agentId)?.reviewer) ?? readUnknownRecord(globalExec?.reviewer);
|
|
454
|
-
}
|
|
455
|
-
function canUseInternalExecAutoReviewReviewer(reviewerConfig, config, env, agentDir) {
|
|
456
|
-
const model = readExecReviewerModelRef(reviewerConfig);
|
|
457
|
-
const slashIndex = model?.indexOf("/") ?? -1;
|
|
458
|
-
if (!model || slashIndex <= 0) return false;
|
|
459
|
-
if (configuredAgentModelAliasMatches(config, model)) return false;
|
|
460
|
-
if (model.slice(0, slashIndex).trim().toLowerCase() !== "openai") return false;
|
|
461
|
-
return isTrustedCodexModelBackedOpenAIProvider({
|
|
462
|
-
config,
|
|
463
|
-
env,
|
|
464
|
-
agentDir,
|
|
465
|
-
model: model.slice(slashIndex + 1).trim()
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
function readExecReviewerModelRef(reviewerConfig) {
|
|
469
|
-
const model = reviewerConfig?.model;
|
|
470
|
-
if (typeof model === "string") return model.trim() || void 0;
|
|
471
|
-
const primary = readUnknownRecord(model)?.primary;
|
|
472
|
-
return typeof primary === "string" && primary.trim() ? primary.trim() : void 0;
|
|
473
|
-
}
|
|
474
|
-
function configuredAgentModelAliasMatches(config, modelRef) {
|
|
475
|
-
const normalizedModelRef = normalizeExecReviewerAliasRef(modelRef);
|
|
476
|
-
return agentModelAliasMatches(readUnknownRecord(readUnknownRecord(readUnknownRecord(config)?.agents)?.defaults), normalizedModelRef);
|
|
477
|
-
}
|
|
478
|
-
function agentModelAliasMatches(agentConfig, normalizedModelRef) {
|
|
479
|
-
const models = readUnknownRecord(agentConfig?.models);
|
|
480
|
-
if (!models) return false;
|
|
481
|
-
for (const entry of Object.values(models)) {
|
|
482
|
-
const alias = readUnknownRecord(entry)?.alias;
|
|
483
|
-
if (typeof alias === "string" && normalizeExecReviewerAliasRef(alias) === normalizedModelRef) return true;
|
|
484
|
-
}
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
function normalizeExecReviewerAliasRef(modelRef) {
|
|
488
|
-
const trimmed = modelRef.trim().toLowerCase();
|
|
489
|
-
const slashIndex = trimmed.indexOf("/");
|
|
490
|
-
const authProfileIndex = trimmed.indexOf("@", slashIndex + 1);
|
|
491
|
-
return authProfileIndex > 0 ? trimmed.slice(0, authProfileIndex) : trimmed;
|
|
492
|
-
}
|
|
493
|
-
function resolveAgentExecConfig(configRoot, agentId) {
|
|
494
|
-
const normalizedAgentId = agentId ? normalizeAgentId(agentId) : void 0;
|
|
495
|
-
if (!normalizedAgentId) return;
|
|
496
|
-
const agentList = readUnknownRecord(configRoot?.agents)?.list;
|
|
497
|
-
if (!Array.isArray(agentList)) return;
|
|
498
|
-
for (const entry of agentList) {
|
|
499
|
-
const record = readUnknownRecord(entry);
|
|
500
|
-
if (typeof record?.id !== "string" || normalizeAgentId(record.id) !== normalizedAgentId) continue;
|
|
501
|
-
return readUnknownRecord(readUnknownRecord(record.tools)?.exec);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
function readUnknownRecord(value) {
|
|
505
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
506
|
-
}
|
|
507
|
-
async function runOpenClawToolPolicyForApprovalRequest(params) {
|
|
508
|
-
const policyRequest = buildOpenClawToolPolicyRequest(params.method, params.requestParams);
|
|
509
|
-
if (!policyRequest) return;
|
|
510
|
-
const cwd = readString$1(params.requestParams, "cwd") ?? params.paramsForRun.workspaceDir;
|
|
511
|
-
const nativeRelayOutcome = await runNativeRelayToolPolicyForApprovalRequest({
|
|
512
|
-
method: params.method,
|
|
513
|
-
requestParams: params.requestParams,
|
|
514
|
-
context: params.context,
|
|
515
|
-
policyRequest,
|
|
516
|
-
nativeHookRelay: params.nativeHookRelay,
|
|
517
|
-
cwd,
|
|
518
|
-
signal: params.signal
|
|
519
|
-
});
|
|
520
|
-
if (nativeRelayOutcome?.blocked) return {
|
|
521
|
-
outcome: "denied",
|
|
522
|
-
reason: nativeRelayOutcome.reason
|
|
523
|
-
};
|
|
524
|
-
if (nativeRelayOutcome?.outcome === "approved-once" || nativeRelayOutcome?.outcome === "approved-session") return { outcome: nativeRelayOutcome.outcome };
|
|
525
|
-
if (nativeRelayOutcome?.handled) return { outcome: "no-decision" };
|
|
526
|
-
const hookChannelId = buildAgentHookContextChannelFields({
|
|
527
|
-
sessionKey: params.paramsForRun.sessionKey,
|
|
528
|
-
messageChannel: params.paramsForRun.messageChannel,
|
|
529
|
-
messageProvider: params.paramsForRun.messageProvider,
|
|
530
|
-
currentChannelId: params.paramsForRun.currentChannelId,
|
|
531
|
-
messageTo: params.paramsForRun.messageTo
|
|
532
|
-
}).channelId;
|
|
533
|
-
const outcome = await runBeforeToolCallHook({
|
|
534
|
-
toolName: policyRequest.toolName,
|
|
535
|
-
params: policyRequest.params,
|
|
536
|
-
...params.context.itemId ? { toolCallId: params.context.itemId } : {},
|
|
537
|
-
approvalMode: "request",
|
|
538
|
-
signal: params.signal,
|
|
539
|
-
ctx: {
|
|
540
|
-
...params.paramsForRun.agentId ? { agentId: params.paramsForRun.agentId } : {},
|
|
541
|
-
...params.paramsForRun.config ? { config: params.paramsForRun.config } : {},
|
|
542
|
-
...cwd ? { cwd } : {},
|
|
543
|
-
...params.paramsForRun.sessionKey ? { sessionKey: params.paramsForRun.sessionKey } : {},
|
|
544
|
-
...params.paramsForRun.sessionId ? { sessionId: params.paramsForRun.sessionId } : {},
|
|
545
|
-
...params.paramsForRun.runId ? { runId: params.paramsForRun.runId } : {},
|
|
546
|
-
...hookChannelId ? { channelId: hookChannelId } : {}
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
if (outcome.blocked) return {
|
|
550
|
-
outcome: "denied",
|
|
551
|
-
reason: outcome.reason
|
|
552
|
-
};
|
|
553
|
-
if ("params" in outcome && toolPolicyParamsWereRewritten(policyRequest.params, outcome.params)) return {
|
|
554
|
-
outcome: "denied",
|
|
555
|
-
reason: "OpenClaw tool policy rewrote Codex app-server approval params; refusing original request."
|
|
556
|
-
};
|
|
557
|
-
if (outcome.approvalResolution) return { outcome: "approved-once" };
|
|
558
|
-
}
|
|
559
|
-
async function runNativeRelayToolPolicyForApprovalRequest(params) {
|
|
560
|
-
if (params.method !== "item/commandExecution/requestApproval" || !params.nativeHookRelay?.allowedEvents.includes("pre_tool_use")) return;
|
|
561
|
-
const payload = buildNativeRelayPreToolUsePayload({
|
|
562
|
-
requestParams: params.requestParams,
|
|
563
|
-
policyRequest: params.policyRequest,
|
|
564
|
-
context: params.context,
|
|
565
|
-
cwd: params.cwd
|
|
566
|
-
});
|
|
567
|
-
if (!payload) return;
|
|
568
|
-
if (hasNativeHookRelayInvocation({
|
|
569
|
-
relayId: params.nativeHookRelay.relayId,
|
|
570
|
-
event: "pre_tool_use",
|
|
571
|
-
toolUseId: params.context.itemId
|
|
572
|
-
})) {
|
|
573
|
-
const approvalOutcome = await resolveNativeHookRelayDeferredToolApproval({
|
|
574
|
-
relayId: params.nativeHookRelay.relayId,
|
|
575
|
-
toolUseId: params.context.itemId,
|
|
576
|
-
signal: params.signal
|
|
577
|
-
});
|
|
578
|
-
if (approvalOutcome?.outcome === "denied") return {
|
|
579
|
-
handled: true,
|
|
580
|
-
blocked: true,
|
|
581
|
-
reason: approvalOutcome.reason
|
|
582
|
-
};
|
|
583
|
-
if (approvalOutcome?.outcome === "approved-once") return {
|
|
584
|
-
handled: true,
|
|
585
|
-
outcome: approvalOutcome.outcome
|
|
586
|
-
};
|
|
587
|
-
return { handled: true };
|
|
588
|
-
}
|
|
589
|
-
try {
|
|
590
|
-
const decision = readNativeRelayPreToolUseDecision(await invokeNativeHookRelay({
|
|
591
|
-
provider: "codex",
|
|
592
|
-
relayId: params.nativeHookRelay.relayId,
|
|
593
|
-
generation: params.nativeHookRelay.generation,
|
|
594
|
-
event: "pre_tool_use",
|
|
595
|
-
rawPayload: payload,
|
|
596
|
-
requireGeneration: true
|
|
597
|
-
}));
|
|
598
|
-
if (decision.blocked) return {
|
|
599
|
-
handled: true,
|
|
600
|
-
blocked: true,
|
|
601
|
-
reason: decision.reason
|
|
602
|
-
};
|
|
603
|
-
const approvalOutcome = await resolveNativeHookRelayDeferredToolApproval({
|
|
604
|
-
relayId: params.nativeHookRelay.relayId,
|
|
605
|
-
toolUseId: params.context.itemId,
|
|
606
|
-
signal: params.signal
|
|
607
|
-
});
|
|
608
|
-
if (approvalOutcome?.outcome === "denied") return {
|
|
609
|
-
handled: true,
|
|
610
|
-
blocked: true,
|
|
611
|
-
reason: approvalOutcome.reason
|
|
612
|
-
};
|
|
613
|
-
if (approvalOutcome?.outcome === "approved-once") return {
|
|
614
|
-
handled: true,
|
|
615
|
-
outcome: approvalOutcome.outcome
|
|
616
|
-
};
|
|
617
|
-
return { handled: true };
|
|
618
|
-
} catch (error) {
|
|
619
|
-
return {
|
|
620
|
-
handled: true,
|
|
621
|
-
blocked: true,
|
|
622
|
-
reason: `OpenClaw native hook relay unavailable for Codex app-server approval: ${formatCodexDisplayText(formatErrorMessage$1(error))}`
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
function buildNativeRelayPreToolUsePayload(params) {
|
|
627
|
-
const command = readString$1(params.policyRequest.params, "command");
|
|
628
|
-
if (!command) return;
|
|
629
|
-
const turnId = readString$1(params.requestParams, "turnId");
|
|
630
|
-
return {
|
|
631
|
-
hook_event_name: "PreToolUse",
|
|
632
|
-
openclaw_approval_mode: "report",
|
|
633
|
-
tool_name: "exec_command",
|
|
634
|
-
...params.context.itemId ? { tool_use_id: params.context.itemId } : {},
|
|
635
|
-
...params.cwd ? { cwd: params.cwd } : {},
|
|
636
|
-
...turnId ? { turn_id: turnId } : {},
|
|
637
|
-
tool_input: {
|
|
638
|
-
...params.policyRequest.params,
|
|
639
|
-
command,
|
|
640
|
-
cmd: command
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
function readNativeRelayPreToolUseDecision(response) {
|
|
645
|
-
if (!response || response.exitCode !== 0) return {
|
|
646
|
-
blocked: true,
|
|
647
|
-
reason: sanitizeRelayDecisionReason(response?.stderr) || sanitizeRelayDecisionReason(response?.stdout) || "OpenClaw native hook relay failed for Codex app-server approval."
|
|
648
|
-
};
|
|
649
|
-
const stdout = response.stdout?.trim();
|
|
650
|
-
if (!stdout) return { blocked: false };
|
|
651
|
-
const parsed = parseRelayJsonResponse(stdout);
|
|
652
|
-
const output = isJsonObject(parsed?.hookSpecificOutput) ? parsed.hookSpecificOutput : void 0;
|
|
653
|
-
if (output?.permissionDecision === "deny") return {
|
|
654
|
-
blocked: true,
|
|
655
|
-
reason: readString$1(output, "permissionDecisionReason") || "OpenClaw native hook policy denied Codex app-server approval."
|
|
656
|
-
};
|
|
657
|
-
return {
|
|
658
|
-
blocked: true,
|
|
659
|
-
reason: output ? "OpenClaw native hook relay returned a non-deny Codex app-server approval decision." : "OpenClaw native hook relay returned an unreadable Codex app-server approval result."
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
function parseRelayJsonResponse(text) {
|
|
663
|
-
try {
|
|
664
|
-
const parsed = JSON.parse(text);
|
|
665
|
-
return isJsonObject(parsed) ? parsed : void 0;
|
|
666
|
-
} catch {
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
function sanitizeRelayDecisionReason(value) {
|
|
671
|
-
return sanitizeApprovalPreview(value ? {
|
|
672
|
-
value,
|
|
673
|
-
clipped: false
|
|
674
|
-
} : void 0, 240).text;
|
|
675
|
-
}
|
|
676
|
-
function buildOpenClawToolPolicyRequest(method, requestParams) {
|
|
677
|
-
if (method === "item/commandExecution/requestApproval") {
|
|
678
|
-
const command = readPolicyCommand(requestParams);
|
|
679
|
-
return {
|
|
680
|
-
toolName: "exec",
|
|
681
|
-
params: {
|
|
682
|
-
...command ? { command } : {},
|
|
683
|
-
...readString$1(requestParams, "cwd") ? { cwd: readString$1(requestParams, "cwd") } : {},
|
|
684
|
-
approval: requestParams ?? {}
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
if (method === "item/fileChange/requestApproval") return {
|
|
689
|
-
toolName: "apply_patch",
|
|
690
|
-
params: requestParams ?? {}
|
|
691
|
-
};
|
|
692
|
-
if (method === "item/permissions/requestApproval") return {
|
|
693
|
-
toolName: "codex_permission_approval",
|
|
694
|
-
params: requestParams ?? {}
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
function toolPolicyParamsWereRewritten(original, candidate) {
|
|
698
|
-
if (candidate === original) return false;
|
|
699
|
-
const originalText = stableJsonText(original);
|
|
700
|
-
const candidateText = stableJsonText(candidate);
|
|
701
|
-
return !candidateText || candidateText !== originalText;
|
|
702
|
-
}
|
|
703
|
-
function stableJsonText(value) {
|
|
704
|
-
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
705
|
-
if (Array.isArray(value)) {
|
|
706
|
-
const items = value.map((item) => stableJsonText(item));
|
|
707
|
-
return items.every((item) => item !== void 0) ? `[${items.join(",")}]` : void 0;
|
|
708
|
-
}
|
|
709
|
-
if (isPlainRecord(value)) {
|
|
710
|
-
const entries = Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, item]) => {
|
|
711
|
-
const text = stableJsonText(item);
|
|
712
|
-
return text === void 0 ? void 0 : `${JSON.stringify(key)}:${text}`;
|
|
713
|
-
});
|
|
714
|
-
return entries.every((entry) => entry !== void 0) ? `{${entries.join(",")}}` : void 0;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
function isPlainRecord(value) {
|
|
718
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
719
|
-
}
|
|
720
|
-
function commandApprovalDecision(requestParams, outcome) {
|
|
721
|
-
if (outcome === "cancelled") return commandRejectionDecision(requestParams, "cancel");
|
|
722
|
-
if (outcome === "denied" || outcome === "unavailable") return commandRejectionDecision(requestParams, "decline");
|
|
723
|
-
if (outcome === "approved-session") {
|
|
724
|
-
if (hasAvailableDecision(requestParams, "acceptForSession")) return "acceptForSession";
|
|
725
|
-
const amendmentDecision = findAvailableCommandAmendmentDecision(requestParams);
|
|
726
|
-
if (amendmentDecision) return amendmentDecision;
|
|
727
|
-
}
|
|
728
|
-
return hasAvailableDecision(requestParams, "accept") ? "accept" : commandRejectionDecision(requestParams, "decline");
|
|
729
|
-
}
|
|
730
|
-
function fileChangeApprovalDecision(outcome) {
|
|
731
|
-
if (outcome === "cancelled") return "cancel";
|
|
732
|
-
if (outcome === "denied" || outcome === "unavailable") return "decline";
|
|
733
|
-
return outcome === "approved-session" ? "acceptForSession" : "accept";
|
|
734
|
-
}
|
|
735
|
-
function requestedPermissions(requestParams) {
|
|
736
|
-
const permissions = isJsonObject(requestParams?.permissions) ? requestParams.permissions : {};
|
|
737
|
-
const granted = {};
|
|
738
|
-
if (isJsonObject(permissions.network)) granted.network = permissions.network;
|
|
739
|
-
if (isJsonObject(permissions.fileSystem)) granted.fileSystem = permissions.fileSystem;
|
|
740
|
-
return granted;
|
|
741
|
-
}
|
|
742
|
-
function unsupportedApprovalResponse() {
|
|
743
|
-
return {
|
|
744
|
-
decision: "decline",
|
|
745
|
-
reason: "OpenClaw codex app-server bridge does not grant native approvals yet."
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
function describeRequestedPermissions(requestParams) {
|
|
749
|
-
return describePermissionProfile(requestedPermissions(requestParams), "Permissions");
|
|
750
|
-
}
|
|
751
|
-
function describeCommandApprovalDetails(requestParams) {
|
|
752
|
-
const lines = [];
|
|
753
|
-
const additionalPermissions = isJsonObject(requestParams?.additionalPermissions) ? requestParams.additionalPermissions : void 0;
|
|
754
|
-
if (additionalPermissions) lines.push(...describePermissionProfile(additionalPermissions, "Additional permissions"));
|
|
755
|
-
const execpolicySummary = summarizeStringArray(requestParams?.proposedExecpolicyAmendment, "Proposed exec policy", sanitizePermissionScalar);
|
|
756
|
-
if (execpolicySummary) lines.push(execpolicySummary);
|
|
757
|
-
const networkAmendmentSummary = summarizeNetworkPolicyAmendments(requestParams?.proposedNetworkPolicyAmendments);
|
|
758
|
-
if (networkAmendmentSummary) lines.push(networkAmendmentSummary);
|
|
759
|
-
return lines;
|
|
760
|
-
}
|
|
761
|
-
function describePermissionProfile(permissions, label) {
|
|
762
|
-
const lines = [];
|
|
763
|
-
const kinds = [];
|
|
764
|
-
const risks = /* @__PURE__ */ new Set();
|
|
765
|
-
if (isJsonObject(permissions.network)) kinds.push("network");
|
|
766
|
-
if (isJsonObject(permissions.fileSystem)) kinds.push("fileSystem");
|
|
767
|
-
if (kinds.length > 0) lines.push(`${label}: ${kinds.join(", ")}`);
|
|
768
|
-
let networkSummary;
|
|
769
|
-
if (isJsonObject(permissions.network)) {
|
|
770
|
-
const summaries = [summarizeNetworkEnabledPermission(permissions.network, risks), summarizePermissionRecord(permissions.network, risks, [{
|
|
771
|
-
key: "allowHosts",
|
|
772
|
-
label: "allowHosts",
|
|
773
|
-
sanitize: sanitizePermissionHostValue,
|
|
774
|
-
risksFor: permissionHostRisks
|
|
775
|
-
}])].filter((summary) => Boolean(summary));
|
|
776
|
-
networkSummary = summaries.length > 0 ? summaries.join("; ") : void 0;
|
|
777
|
-
}
|
|
778
|
-
let fileSystemSummary;
|
|
779
|
-
if (isJsonObject(permissions.fileSystem)) {
|
|
780
|
-
const summaries = [summarizePermissionRecord(permissions.fileSystem, risks, [
|
|
781
|
-
{
|
|
782
|
-
key: "read",
|
|
783
|
-
label: "read",
|
|
784
|
-
sanitize: sanitizePermissionPathValue,
|
|
785
|
-
risksFor: permissionPathRisks
|
|
786
|
-
},
|
|
787
|
-
{
|
|
788
|
-
key: "write",
|
|
789
|
-
label: "write",
|
|
790
|
-
sanitize: sanitizePermissionPathValue,
|
|
791
|
-
risksFor: permissionPathRisks
|
|
792
|
-
},
|
|
793
|
-
{
|
|
794
|
-
key: "roots",
|
|
795
|
-
label: "roots",
|
|
796
|
-
sanitize: sanitizePermissionPathValue,
|
|
797
|
-
risksFor: permissionPathRisks
|
|
798
|
-
},
|
|
799
|
-
{
|
|
800
|
-
key: "readPaths",
|
|
801
|
-
label: "readPaths",
|
|
802
|
-
sanitize: sanitizePermissionPathValue,
|
|
803
|
-
risksFor: permissionPathRisks
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
key: "writePaths",
|
|
807
|
-
label: "writePaths",
|
|
808
|
-
sanitize: sanitizePermissionPathValue,
|
|
809
|
-
risksFor: permissionPathRisks
|
|
810
|
-
}
|
|
811
|
-
]), summarizeFileSystemEntries(permissions.fileSystem, risks)].filter((summary) => Boolean(summary));
|
|
812
|
-
fileSystemSummary = summaries.length > 0 ? summaries.join("; ") : void 0;
|
|
813
|
-
}
|
|
814
|
-
if (risks.size > 0) lines.push(`High-risk targets: ${[...risks].join(", ")}`);
|
|
815
|
-
if (networkSummary) lines.push(`Network ${networkSummary}`);
|
|
816
|
-
if (fileSystemSummary) lines.push(`File system ${fileSystemSummary}`);
|
|
817
|
-
return lines;
|
|
818
|
-
}
|
|
819
|
-
function summarizeNetworkEnabledPermission(permission, risks) {
|
|
820
|
-
const enabled = permission.enabled;
|
|
821
|
-
if (typeof enabled !== "boolean") return;
|
|
822
|
-
if (enabled) risks.add("network access");
|
|
823
|
-
return `enabled: ${enabled}`;
|
|
824
|
-
}
|
|
825
|
-
function summarizeFileSystemEntries(permission, risks) {
|
|
826
|
-
const entries = permission.entries;
|
|
827
|
-
if (!Array.isArray(entries)) return;
|
|
828
|
-
const samples = [];
|
|
829
|
-
let count = 0;
|
|
830
|
-
for (const entry of entries) {
|
|
831
|
-
const item = isJsonObject(entry) ? entry : void 0;
|
|
832
|
-
const path = typeof item?.path === "string" ? item.path.trim() : "";
|
|
833
|
-
const access = typeof item?.access === "string" ? item.access.trim() : "";
|
|
834
|
-
if (!path || !access) continue;
|
|
835
|
-
count += 1;
|
|
836
|
-
if (access !== "none") for (const risk of permissionPathRisks(path)) risks.add(risk);
|
|
837
|
-
if (samples.length < PERMISSION_SAMPLE_LIMIT) samples.push(`${sanitizePermissionScalar(access)} ${sanitizePermissionPathValue(path)}`);
|
|
838
|
-
}
|
|
839
|
-
if (count === 0) return;
|
|
840
|
-
const remaining = count - samples.length;
|
|
841
|
-
const remainderSuffix = remaining > 0 ? ` (+${remaining} more)` : "";
|
|
842
|
-
return `entries: ${samples.join(", ")}${remainderSuffix}`;
|
|
843
|
-
}
|
|
844
|
-
function summarizePermissionRecord(permission, risks, descriptors) {
|
|
845
|
-
const details = [];
|
|
846
|
-
for (const descriptor of descriptors) {
|
|
847
|
-
const summary = summarizePermissionArray(permission, descriptor, risks);
|
|
848
|
-
if (summary) details.push(summary);
|
|
849
|
-
}
|
|
850
|
-
return details.length > 0 ? details.join("; ") : void 0;
|
|
851
|
-
}
|
|
852
|
-
function summarizePermissionArray(record, descriptor, risks) {
|
|
853
|
-
const values = readStringArray(record, descriptor.key);
|
|
854
|
-
if (values.length === 0) return;
|
|
855
|
-
for (const value of values) for (const risk of descriptor.risksFor(value)) risks.add(risk);
|
|
856
|
-
const sampleValues = values.slice(0, PERMISSION_SAMPLE_LIMIT).map(descriptor.sanitize).filter(Boolean);
|
|
857
|
-
if (sampleValues.length === 0) return `${descriptor.label}: ${values.length}`;
|
|
858
|
-
const remaining = values.length - sampleValues.length;
|
|
859
|
-
const remainderSuffix = remaining > 0 ? ` (+${remaining} more)` : "";
|
|
860
|
-
return `${descriptor.label}: ${sampleValues.join(", ")}${remainderSuffix}`;
|
|
861
|
-
}
|
|
862
|
-
function summarizeStringArray(value, label, sanitize) {
|
|
863
|
-
if (!Array.isArray(value)) return;
|
|
864
|
-
const values = value.filter((entry) => typeof entry === "string").map((entry) => sanitize(entry)).filter(Boolean);
|
|
865
|
-
if (values.length === 0) return;
|
|
866
|
-
const samples = values.slice(0, PERMISSION_SAMPLE_LIMIT);
|
|
867
|
-
const remaining = values.length - samples.length;
|
|
868
|
-
const remainderSuffix = remaining > 0 ? ` (+${remaining} more)` : "";
|
|
869
|
-
return `${label}: ${samples.join(", ")}${remainderSuffix}`;
|
|
870
|
-
}
|
|
871
|
-
function summarizeNetworkPolicyAmendments(value) {
|
|
872
|
-
if (!Array.isArray(value)) return;
|
|
873
|
-
const samples = [];
|
|
874
|
-
let count = 0;
|
|
875
|
-
for (const entry of value) {
|
|
876
|
-
const amendment = isJsonObject(entry) ? entry : void 0;
|
|
877
|
-
const host = typeof amendment?.host === "string" ? amendment.host : "";
|
|
878
|
-
const action = typeof amendment?.action === "string" ? amendment.action : "";
|
|
879
|
-
if (!host || !action) continue;
|
|
880
|
-
count += 1;
|
|
881
|
-
if (samples.length < PERMISSION_SAMPLE_LIMIT) samples.push(`${sanitizePermissionScalar(action)} ${sanitizePermissionHostValue(host)}`);
|
|
882
|
-
}
|
|
883
|
-
if (count === 0) return;
|
|
884
|
-
const remaining = count - samples.length;
|
|
885
|
-
const remainderSuffix = remaining > 0 ? ` (+${remaining} more)` : "";
|
|
886
|
-
return `Proposed network policy: ${samples.join(", ")}${remainderSuffix}`;
|
|
887
|
-
}
|
|
888
|
-
function readStringArray(record, key) {
|
|
889
|
-
return normalizeTrimmedStringList(record[key]);
|
|
890
|
-
}
|
|
891
|
-
function sanitizePermissionHostValue(value) {
|
|
892
|
-
const withoutScheme = sanitizePermissionScalar(value).toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
893
|
-
const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? withoutScheme;
|
|
894
|
-
return truncate(authority.includes("@") ? authority.slice(authority.lastIndexOf("@") + 1) : authority, PERMISSION_VALUE_MAX_LENGTH);
|
|
895
|
-
}
|
|
896
|
-
function sanitizePermissionPathValue(value) {
|
|
897
|
-
return truncate(formatApprovalDisplayPath(sanitizePermissionScalar(value)), PERMISSION_VALUE_MAX_LENGTH);
|
|
898
|
-
}
|
|
899
|
-
function sanitizePermissionScalar(value) {
|
|
900
|
-
return sanitizeVisibleScalar(value);
|
|
901
|
-
}
|
|
902
|
-
function permissionHostRisks(value) {
|
|
903
|
-
const normalized = value.trim().toLowerCase();
|
|
904
|
-
const risks = [];
|
|
905
|
-
if (normalized.includes("*")) {
|
|
906
|
-
risks.push("wildcard hosts");
|
|
907
|
-
if (isPrivateNetworkHostPattern(normalized)) risks.push("private-network wildcards");
|
|
908
|
-
}
|
|
909
|
-
return risks;
|
|
910
|
-
}
|
|
911
|
-
function permissionPathRisks(value) {
|
|
912
|
-
const normalized = sanitizePermissionScalar(value);
|
|
913
|
-
const risks = [];
|
|
914
|
-
if (normalized === "/" || normalized === "\\" || /^[A-Za-z]:[\\/]*$/.test(normalized)) risks.push("filesystem root");
|
|
915
|
-
return risks;
|
|
916
|
-
}
|
|
917
|
-
function isPrivateNetworkHostPattern(value) {
|
|
918
|
-
const wildcardStripped = value.toLowerCase().replace(/^\*\./, "");
|
|
919
|
-
if (wildcardStripped === "localhost" || wildcardStripped === "local" || wildcardStripped === "internal" || wildcardStripped === "lan" || wildcardStripped === "home" || wildcardStripped === "corp" || wildcardStripped === "private" || wildcardStripped.endsWith(".local") || wildcardStripped.endsWith(".internal") || wildcardStripped.endsWith(".lan") || wildcardStripped.endsWith(".home") || wildcardStripped.endsWith(".corp") || wildcardStripped.endsWith(".private")) return true;
|
|
920
|
-
if (wildcardStripped.startsWith("10.") || wildcardStripped.startsWith("127.") || wildcardStripped.startsWith("192.168.") || wildcardStripped.startsWith("169.254.")) return true;
|
|
921
|
-
return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
|
|
922
|
-
}
|
|
923
|
-
function hasAvailableDecision(requestParams, decision) {
|
|
924
|
-
const available = requestParams?.availableDecisions;
|
|
925
|
-
if (!Array.isArray(available)) return true;
|
|
926
|
-
return available.includes(decision);
|
|
927
|
-
}
|
|
928
|
-
function findAvailableCommandAmendmentDecision(requestParams) {
|
|
929
|
-
const available = requestParams?.availableDecisions;
|
|
930
|
-
if (!Array.isArray(available)) return;
|
|
931
|
-
return available.find((entry) => isJsonObject(entry) && (isJsonObject(entry.acceptWithExecpolicyAmendment) || isJsonObject(entry.applyNetworkPolicyAmendment)));
|
|
932
|
-
}
|
|
933
|
-
function commandRejectionDecision(requestParams, preferred) {
|
|
934
|
-
const available = requestParams?.availableDecisions;
|
|
935
|
-
if (!Array.isArray(available)) return preferred;
|
|
936
|
-
if (available.includes(preferred)) return preferred;
|
|
937
|
-
const alternate = preferred === "decline" ? "cancel" : "decline";
|
|
938
|
-
if (available.includes(alternate)) return alternate;
|
|
939
|
-
return preferred;
|
|
940
|
-
}
|
|
941
|
-
function approvalResolutionMessage(outcome) {
|
|
942
|
-
if (outcome === "approved-session") return "Codex app-server approval granted for the session.";
|
|
943
|
-
if (outcome === "approved-once") return "Codex app-server approval granted for this turn.";
|
|
944
|
-
if (outcome === "cancelled") return "Codex app-server approval cancelled.";
|
|
945
|
-
if (outcome === "unavailable") return "Codex app-server approval unavailable.";
|
|
946
|
-
return "Codex app-server approval denied.";
|
|
947
|
-
}
|
|
948
|
-
function approvalScopeForOutcome(outcome) {
|
|
949
|
-
return outcome === "approved-session" ? "session" : "turn";
|
|
950
|
-
}
|
|
951
|
-
function approvalEventScope(method, outcome) {
|
|
952
|
-
return method === "item/permissions/requestApproval" ? { scope: approvalScopeForOutcome(outcome) } : {};
|
|
953
|
-
}
|
|
954
|
-
function approvalKindForMethod(method) {
|
|
955
|
-
if (method.includes("commandExecution") || method.includes("execCommand")) return "exec";
|
|
956
|
-
if (method.includes("fileChange") || method.includes("Patch") || method.includes("permissions")) return "plugin";
|
|
957
|
-
return "unknown";
|
|
958
|
-
}
|
|
959
|
-
function isSupportedAppServerApprovalMethod(method) {
|
|
960
|
-
return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval";
|
|
961
|
-
}
|
|
962
|
-
function emitApprovalEvent(params, data) {
|
|
963
|
-
params.onAgentEvent?.({
|
|
964
|
-
stream: "approval",
|
|
965
|
-
data
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
function readDisplayCommandPreview(record) {
|
|
969
|
-
const actionCommand = readCommandActionsPreview(record);
|
|
970
|
-
if (actionCommand) return actionCommand;
|
|
971
|
-
return readCommandPreview(record);
|
|
972
|
-
}
|
|
973
|
-
function readPolicyCommand(record) {
|
|
974
|
-
const command = record?.command;
|
|
975
|
-
if (typeof command === "string") return command;
|
|
976
|
-
if (Array.isArray(command) && command.every((part) => typeof part === "string")) return command.join(" ");
|
|
977
|
-
const actionCommands = readCommandActions(record);
|
|
978
|
-
if (actionCommands.length > 0) return actionCommands.join(" && ");
|
|
979
|
-
}
|
|
980
|
-
function readCommandActions(record) {
|
|
981
|
-
const actions = record?.commandActions;
|
|
982
|
-
if (!Array.isArray(actions)) return [];
|
|
983
|
-
return actions.map((action) => isJsonObject(action) ? readString$1(action, "command") : void 0).filter((command) => Boolean(command));
|
|
984
|
-
}
|
|
985
|
-
function readCommandActionsPreview(record) {
|
|
986
|
-
let source;
|
|
987
|
-
for (const command of readCommandActions(record)) {
|
|
988
|
-
source = appendPreviewPart(source, command, " && ");
|
|
989
|
-
if (source.clipped) break;
|
|
990
|
-
}
|
|
991
|
-
return source;
|
|
992
|
-
}
|
|
993
|
-
function readCommandPreview(record) {
|
|
994
|
-
const command = record?.command;
|
|
995
|
-
if (typeof command === "string") return previewSource(command);
|
|
996
|
-
if (!Array.isArray(command)) return;
|
|
997
|
-
let source;
|
|
998
|
-
for (const part of command) {
|
|
999
|
-
if (typeof part !== "string") return;
|
|
1000
|
-
source = appendPreviewPart(source, part, " ");
|
|
1001
|
-
if (source.clipped) break;
|
|
1002
|
-
}
|
|
1003
|
-
return source;
|
|
1004
|
-
}
|
|
1005
|
-
function readStringPreview(record, key) {
|
|
1006
|
-
const value = readString$1(record, key);
|
|
1007
|
-
return value === void 0 ? void 0 : previewSource(value);
|
|
1008
|
-
}
|
|
1009
|
-
function readString$1(record, key) {
|
|
1010
|
-
const value = record?.[key];
|
|
1011
|
-
return typeof value === "string" ? value : void 0;
|
|
1012
|
-
}
|
|
1013
|
-
function truncate(value, maxLength) {
|
|
1014
|
-
return value.length <= maxLength ? value : `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
1015
|
-
}
|
|
1016
|
-
function previewSource(value) {
|
|
1017
|
-
return {
|
|
1018
|
-
value: value.slice(0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
|
|
1019
|
-
clipped: value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
function appendPreviewPart(source, part, separator) {
|
|
1023
|
-
const value = `${source?.value ? `${source.value}${separator}` : ""}${part}`;
|
|
1024
|
-
const clipped = source?.clipped === true || value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH;
|
|
1025
|
-
return {
|
|
1026
|
-
value: value.slice(0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
|
|
1027
|
-
clipped
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
function sanitizeApprovalPreview(source, maxLength) {
|
|
1031
|
-
if (!source || !source.value) return { omitted: false };
|
|
1032
|
-
const sanitized = sanitizeVisibleScalar(source.value.replace(DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE$1, ""));
|
|
1033
|
-
if (!sanitized) return { omitted: true };
|
|
1034
|
-
return {
|
|
1035
|
-
text: formatCodexDisplayText(truncate(sanitized, maxLength)),
|
|
1036
|
-
omitted: source.clipped
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
function sanitizeVisibleScalar(value) {
|
|
1040
|
-
return value.replace(ANSI_OSC_SEQUENCE_RE$1, "").replace(ANSI_CONTROL_SEQUENCE_RE$1, "").replace(INVISIBLE_FORMATTING_CONTROL_RE$1, " ").replace(CONTROL_CHARACTER_RE$1, " ").replace(/\s+/g, " ").trim();
|
|
1041
|
-
}
|
|
1042
|
-
function formatApprovalPreviewSubject(text, omitted) {
|
|
1043
|
-
return omitted ? `${text} ${APPROVAL_PREVIEW_OMITTED}` : text;
|
|
1044
|
-
}
|
|
1045
|
-
function joinDescriptionLinesWithinLimit(lines, maxLength) {
|
|
1046
|
-
let description = "";
|
|
1047
|
-
for (const line of lines) {
|
|
1048
|
-
const prefix = description ? "\n" : "";
|
|
1049
|
-
const next = `${description}${prefix}${line}`;
|
|
1050
|
-
if (next.length <= maxLength) {
|
|
1051
|
-
description = next;
|
|
1052
|
-
continue;
|
|
1053
|
-
}
|
|
1054
|
-
const remaining = maxLength - description.length - prefix.length;
|
|
1055
|
-
if (remaining < 3) break;
|
|
1056
|
-
description += `${prefix}${truncate(line, remaining)}`;
|
|
1057
|
-
break;
|
|
1058
|
-
}
|
|
1059
|
-
return description;
|
|
1060
|
-
}
|
|
1061
|
-
function formatErrorMessage$1(error) {
|
|
1062
|
-
return error instanceof Error ? error.message : String(error);
|
|
1063
|
-
}
|
|
1064
|
-
//#endregion
|
|
1065
|
-
//#region extensions/codex/src/app-server/vision-tools.ts
|
|
1066
|
-
/**
|
|
1067
|
-
* Filters Codex dynamic tools for turns that already contain image inputs so
|
|
1068
|
-
* models with native vision do not get redundant image-inspection tools.
|
|
1069
|
-
*/
|
|
1070
|
-
/** Removes the image tool when the model can directly consume inbound images. */
|
|
1071
|
-
function filterToolsForVisionInputs(tools, params) {
|
|
1072
|
-
if (!params.modelHasVision || !params.hasInboundImages) return tools;
|
|
1073
|
-
return tools.filter((tool) => tool.name !== "image");
|
|
1074
|
-
}
|
|
1075
|
-
//#endregion
|
|
1076
|
-
//#region extensions/codex/src/app-server/dynamic-tool-build.ts
|
|
1077
|
-
/**
|
|
1078
|
-
* Builds the Codex app-server dynamic tool list for one turn, including
|
|
1079
|
-
* OpenClaw-owned tools, Codex native-tool fallback rules, sandbox shell shims,
|
|
1080
|
-
* and provider allowlist normalization.
|
|
1081
|
-
*/
|
|
1082
|
-
const CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS = [
|
|
1083
|
-
"exec",
|
|
1084
|
-
"process",
|
|
1085
|
-
"read",
|
|
1086
|
-
"write",
|
|
1087
|
-
"edit",
|
|
1088
|
-
"apply_patch"
|
|
1089
|
-
];
|
|
1090
|
-
const CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW = new Set(["read", "write"]);
|
|
1091
|
-
const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
|
|
1092
|
-
const CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME = "node_process";
|
|
1093
|
-
const CODEX_NODE_EXEC_HIDDEN_PARAMETER_NAMES = new Set([
|
|
1094
|
-
"host",
|
|
1095
|
-
"security",
|
|
1096
|
-
"ask",
|
|
1097
|
-
"node"
|
|
1098
|
-
]);
|
|
1099
|
-
/** Splits sandbox and run session keys so tool calls can bind to both scopes when needed. */
|
|
1100
|
-
function resolveOpenClawCodingToolsSessionKeys(params, sandboxSessionKey) {
|
|
1101
|
-
return {
|
|
1102
|
-
sessionKey: sandboxSessionKey,
|
|
1103
|
-
runSessionKey: params.sessionKey && params.sessionKey !== sandboxSessionKey ? params.sessionKey : void 0
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
/** Returns the canonical channel used for Codex message routing and receipts. */
|
|
1107
|
-
function resolveCodexMessageToolProvider(params) {
|
|
1108
|
-
return params.messageChannel ?? params.messageProvider;
|
|
1109
|
-
}
|
|
1110
|
-
/** Resolves the channel id that hook events should target for this Codex app-server turn. */
|
|
1111
|
-
function resolveCodexAppServerHookChannelId(params, sandboxSessionKey) {
|
|
1112
|
-
return buildAgentHookContextChannelFields({
|
|
1113
|
-
sessionKey: sandboxSessionKey,
|
|
1114
|
-
messageChannel: params.messageChannel,
|
|
1115
|
-
messageProvider: params.messageProvider,
|
|
1116
|
-
currentChannelId: params.currentChannelId,
|
|
1117
|
-
messageTo: params.messageTo
|
|
1118
|
-
}).channelId;
|
|
1119
|
-
}
|
|
1120
|
-
const CODEX_DYNAMIC_TOOL_BUILD_WARN_TOTAL_MS = 1e3;
|
|
1121
|
-
const CODEX_DYNAMIC_TOOL_BUILD_WARN_STAGE_MS = 500;
|
|
1122
|
-
/** Creates cheap optional timing instrumentation for the dynamic-tool hot path. */
|
|
1123
|
-
function createCodexDynamicToolBuildStageTracker(options = {}) {
|
|
1124
|
-
if (!options.enabled) return {
|
|
1125
|
-
mark() {},
|
|
1126
|
-
snapshot() {
|
|
1127
|
-
return {
|
|
1128
|
-
totalMs: 0,
|
|
1129
|
-
stages: []
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
};
|
|
1133
|
-
const startedAt = Date.now();
|
|
1134
|
-
let previousAt = startedAt;
|
|
1135
|
-
const stages = [];
|
|
1136
|
-
const toMs = (value) => Math.max(0, Math.round(value));
|
|
1137
|
-
return {
|
|
1138
|
-
mark(name) {
|
|
1139
|
-
const currentAt = Date.now();
|
|
1140
|
-
stages.push({
|
|
1141
|
-
name,
|
|
1142
|
-
durationMs: toMs(currentAt - previousAt),
|
|
1143
|
-
elapsedMs: toMs(currentAt - startedAt)
|
|
1144
|
-
});
|
|
1145
|
-
previousAt = currentAt;
|
|
1146
|
-
},
|
|
1147
|
-
snapshot() {
|
|
1148
|
-
return {
|
|
1149
|
-
totalMs: toMs(Date.now() - startedAt),
|
|
1150
|
-
stages: stages.slice()
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
/** Returns true when dynamic-tool construction is slow enough to warrant a warning log. */
|
|
1156
|
-
function shouldWarnCodexDynamicToolBuildStageSummary(summary) {
|
|
1157
|
-
return summary.totalMs >= CODEX_DYNAMIC_TOOL_BUILD_WARN_TOTAL_MS || summary.stages.some((stage) => stage.durationMs >= CODEX_DYNAMIC_TOOL_BUILD_WARN_STAGE_MS);
|
|
1158
|
-
}
|
|
1159
|
-
/** Formats per-stage timings into the compact form used by Codex app-server logs. */
|
|
1160
|
-
function formatCodexDynamicToolBuildStageSummary(summary) {
|
|
1161
|
-
return summary.stages.length > 0 ? summary.stages.map((stage) => `${stage.name}:${stage.durationMs}ms@${stage.elapsedMs}ms`).join(",") : "none";
|
|
1162
|
-
}
|
|
1163
|
-
/** Builds, filters, and normalizes Codex-compatible runtime tools for a single turn. */
|
|
1164
|
-
async function buildDynamicTools(input) {
|
|
1165
|
-
const { params } = input;
|
|
1166
|
-
const messagePolicyParams = input.ignoreDisableMessageTool ? {
|
|
1167
|
-
...params,
|
|
1168
|
-
disableMessageTool: false
|
|
1169
|
-
} : params;
|
|
1170
|
-
if (params.disableTools) {
|
|
1171
|
-
input.onWebSearchPolicyResolved?.(false);
|
|
1172
|
-
return [];
|
|
1173
|
-
}
|
|
1174
|
-
if (!supportsModelTools(params.model)) {
|
|
1175
|
-
input.onPersistentWebSearchPolicyResolved?.(false);
|
|
1176
|
-
input.onWebSearchPolicyResolved?.(false);
|
|
1177
|
-
return [];
|
|
1178
|
-
}
|
|
1179
|
-
const toolBuildStages = createCodexDynamicToolBuildStageTracker({ enabled: input.profilerEnabled });
|
|
1180
|
-
const modelHasVision = params.model.input?.includes("image") ?? false;
|
|
1181
|
-
const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, input.sessionAgentId);
|
|
1182
|
-
const agentHarness = await import("openclaw/plugin-sdk/agent-harness");
|
|
1183
|
-
const createOpenClawCodingTools = agentHarness.createOpenClawCodingTools;
|
|
1184
|
-
toolBuildStages.mark("load-agent-harness-tools");
|
|
1185
|
-
const sessionKeys = resolveOpenClawCodingToolsSessionKeys(params, input.sandboxSessionKey);
|
|
1186
|
-
const nativeExecutionPolicy = resolveCodexNativeExecutionPolicyForDynamicTools(input);
|
|
1187
|
-
const allTools = createOpenClawCodingTools({
|
|
1188
|
-
agentId: input.sessionAgentId,
|
|
1189
|
-
...buildEmbeddedAttemptToolRunContext(params),
|
|
1190
|
-
exec: {
|
|
1191
|
-
...params.execOverrides,
|
|
1192
|
-
...resolveNodeExecToolOverrides(nativeExecutionPolicy),
|
|
1193
|
-
config: params.config,
|
|
1194
|
-
elevated: params.bashElevated
|
|
1195
|
-
},
|
|
1196
|
-
sandbox: input.sandbox,
|
|
1197
|
-
messageProvider: resolveCodexMessageToolProvider(params),
|
|
1198
|
-
toolPolicyMessageProvider: params.messageProvider ?? params.messageChannel,
|
|
1199
|
-
agentAccountId: params.agentAccountId,
|
|
1200
|
-
messageTo: params.messageTo,
|
|
1201
|
-
messageThreadId: params.messageThreadId,
|
|
1202
|
-
groupId: params.groupId,
|
|
1203
|
-
groupChannel: params.groupChannel,
|
|
1204
|
-
groupSpace: params.groupSpace,
|
|
1205
|
-
spawnedBy: params.spawnedBy,
|
|
1206
|
-
senderId: params.senderId,
|
|
1207
|
-
senderName: params.senderName,
|
|
1208
|
-
senderUsername: params.senderUsername,
|
|
1209
|
-
senderE164: params.senderE164,
|
|
1210
|
-
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
|
|
1211
|
-
...sessionKeys,
|
|
1212
|
-
sessionId: params.sessionId,
|
|
1213
|
-
runId: params.runId,
|
|
1214
|
-
approvalReviewerDeviceId: params.approvalReviewerDeviceId,
|
|
1215
|
-
agentDir,
|
|
1216
|
-
cwd: input.effectiveCwd ?? input.effectiveWorkspace,
|
|
1217
|
-
workspaceDir: input.effectiveWorkspace,
|
|
1218
|
-
spawnWorkspaceDir: input.effectiveCwd && input.effectiveCwd !== input.effectiveWorkspace ? input.resolvedWorkspace : resolveAttemptSpawnWorkspaceDir({
|
|
1219
|
-
sandbox: input.sandbox,
|
|
1220
|
-
resolvedWorkspace: input.resolvedWorkspace
|
|
1221
|
-
}),
|
|
1222
|
-
config: params.config,
|
|
1223
|
-
authProfileStore: params.toolAuthProfileStore ?? params.authProfileStore,
|
|
1224
|
-
abortSignal: input.runAbortController.signal,
|
|
1225
|
-
emitBeforeToolCallDiagnostics: false,
|
|
1226
|
-
modelProvider: params.model.provider,
|
|
1227
|
-
modelId: params.modelId,
|
|
1228
|
-
modelCompat: params.model.compat && typeof params.model.compat === "object" ? params.model.compat : void 0,
|
|
1229
|
-
modelApi: params.model.api,
|
|
1230
|
-
modelContextWindowTokens: params.model.contextWindow,
|
|
1231
|
-
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, params.toolAuthProfileStore ?? params.authProfileStore, { workspaceDir: input.effectiveWorkspace }),
|
|
1232
|
-
suppressManagedWebSearch: false,
|
|
1233
|
-
currentChannelId: params.currentChannelId,
|
|
1234
|
-
currentMessagingTarget: params.currentMessagingTarget,
|
|
1235
|
-
hookChannelId: resolveCodexAppServerHookChannelId(params, input.sandboxSessionKey),
|
|
1236
|
-
currentThreadTs: params.currentThreadTs,
|
|
1237
|
-
currentMessageId: params.currentMessageId,
|
|
1238
|
-
replyToMode: params.replyToMode,
|
|
1239
|
-
hasRepliedRef: params.hasRepliedRef,
|
|
1240
|
-
modelHasVision,
|
|
1241
|
-
requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
|
|
1242
|
-
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
|
1243
|
-
disableMessageTool: input.ignoreDisableMessageTool ? false : params.disableMessageTool,
|
|
1244
|
-
forceMessageTool: shouldForceMessageTool(messagePolicyParams),
|
|
1245
|
-
enableHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
|
|
1246
|
-
forceHeartbeatTool: params.trigger === "heartbeat" || input.forceHeartbeatTool === true,
|
|
1247
|
-
onYield: (message) => {
|
|
1248
|
-
input.onYieldDetected();
|
|
1249
|
-
input.onCodexAppServerEvent?.({
|
|
1250
|
-
stream: "codex_app_server.tool",
|
|
1251
|
-
data: {
|
|
1252
|
-
name: "sessions_yield",
|
|
1253
|
-
message
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
},
|
|
1257
|
-
recordToolPrepStage: (name) => {
|
|
1258
|
-
toolBuildStages.mark(name);
|
|
1259
|
-
},
|
|
1260
|
-
onToolOutcome: params.onToolOutcome,
|
|
1261
|
-
allocateToolOutcomeOrdinal: params.allocateToolOutcomeOrdinal
|
|
1262
|
-
});
|
|
1263
|
-
toolBuildStages.mark("create-openclaw-coding-tools");
|
|
1264
|
-
const preNormalizationDiagnostics = [];
|
|
1265
|
-
const readableAllToolProjection = filterProviderNormalizableTools(allTools);
|
|
1266
|
-
preNormalizationDiagnostics.push(...readableAllToolProjection.diagnostics);
|
|
1267
|
-
const webSearchPlan = resolveCodexWebSearchPlan({
|
|
1268
|
-
config: params.config,
|
|
1269
|
-
disableTools: params.disableTools,
|
|
1270
|
-
nativeToolSurfaceEnabled: input.nativeToolSurfaceEnabled,
|
|
1271
|
-
nativeProviderWebSearchSupport: input.nativeProviderWebSearchSupport
|
|
1272
|
-
});
|
|
1273
|
-
const readableAllTools = [...readableAllToolProjection.tools];
|
|
1274
|
-
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(addSandboxShellDynamicToolsIfAvailable(isCodexMemoryFlushRun(params) ? filterCodexMemoryFlushDynamicTools(readableAllTools) : filterCodexDynamicTools(readableAllTools, input.pluginConfig), readableAllTools, input), readableAllTools, input, nativeExecutionPolicy);
|
|
1275
|
-
toolBuildStages.mark("codex-filtering");
|
|
1276
|
-
const visionFilteredTools = filterToolsForVisionInputs(codexFilteredTools, {
|
|
1277
|
-
modelHasVision,
|
|
1278
|
-
hasInboundImages: (params.images?.length ?? 0) > 0
|
|
1279
|
-
});
|
|
1280
|
-
toolBuildStages.mark("vision-filtering");
|
|
1281
|
-
const webSearchPresent = visionFilteredTools.some((tool) => tool.name === "web_search");
|
|
1282
|
-
const webSearchPolicy = agentHarness.resolveWebSearchToolPolicy({
|
|
1283
|
-
config: params.config,
|
|
1284
|
-
modelProvider: params.model.provider,
|
|
1285
|
-
modelId: params.modelId,
|
|
1286
|
-
agentId: input.sessionAgentId,
|
|
1287
|
-
sessionKey: input.sandboxSessionKey,
|
|
1288
|
-
sandboxToolPolicy: input.sandbox?.tools,
|
|
1289
|
-
messageProvider: resolveCodexMessageToolProvider(params),
|
|
1290
|
-
agentAccountId: params.agentAccountId,
|
|
1291
|
-
groupId: params.groupId,
|
|
1292
|
-
groupChannel: params.groupChannel,
|
|
1293
|
-
groupSpace: params.groupSpace,
|
|
1294
|
-
spawnedBy: params.spawnedBy,
|
|
1295
|
-
senderId: params.senderId,
|
|
1296
|
-
senderName: params.senderName,
|
|
1297
|
-
senderUsername: params.senderUsername,
|
|
1298
|
-
senderE164: params.senderE164
|
|
1299
|
-
});
|
|
1300
|
-
const transientWebSearchRestriction = !webSearchPolicy.allowed && webSearchPolicy.persistentAllowed || isCodexMemoryFlushRun(params);
|
|
1301
|
-
const persistentCodexWebSearchSurface = params.config?.tools?.web?.search?.enabled !== false && !(input.pluginConfig.codexDynamicToolsExclude ?? []).some((name) => normalizeCodexDynamicToolName(name) === "web_search");
|
|
1302
|
-
input.onPersistentWebSearchPolicyResolved?.(webSearchPresent || persistentCodexWebSearchSurface && transientWebSearchRestriction && webSearchPolicy.persistentAllowed);
|
|
1303
|
-
const filteredTools = filterCodexDynamicToolsForAllowlist(visionFilteredTools, includeForcedCodexDynamicToolAllow(params.toolsAllow, messagePolicyParams));
|
|
1304
|
-
toolBuildStages.mark("allowlist-filter");
|
|
1305
|
-
const normalizedTools = normalizeAgentRuntimeTools({
|
|
1306
|
-
runtimePlan: input.ignoreRuntimePlan ? void 0 : params.runtimePlan,
|
|
1307
|
-
tools: filteredTools,
|
|
1308
|
-
provider: params.provider,
|
|
1309
|
-
config: params.config,
|
|
1310
|
-
workspaceDir: input.effectiveWorkspace,
|
|
1311
|
-
env: process.env,
|
|
1312
|
-
modelId: params.modelId,
|
|
1313
|
-
modelApi: params.model.api,
|
|
1314
|
-
model: params.model,
|
|
1315
|
-
onPreNormalizationSchemaDiagnostics: (diagnostics) => preNormalizationDiagnostics.push(...diagnostics)
|
|
1316
|
-
});
|
|
1317
|
-
toolBuildStages.mark("runtime-normalization");
|
|
1318
|
-
input.onWebSearchPolicyResolved?.(normalizedTools.some((tool) => tool.name === "web_search"));
|
|
1319
|
-
const exposedTools = webSearchPlan.suppressManagedWebSearch ? normalizedTools.filter((tool) => tool.name !== "web_search") : normalizedTools;
|
|
1320
|
-
if (preNormalizationDiagnostics.length > 0) embeddedAgentLog.warn(`codex app-server quarantined ${preNormalizationDiagnostics.length} unsupported runtime tool schema${preNormalizationDiagnostics.length === 1 ? "" : "s"} before dynamic tool registration`, {
|
|
1321
|
-
runId: params.runId,
|
|
1322
|
-
sessionId: params.sessionId,
|
|
1323
|
-
diagnostics: preNormalizationDiagnostics.map((diagnostic) => ({
|
|
1324
|
-
index: diagnostic.toolIndex,
|
|
1325
|
-
tool: diagnostic.toolName,
|
|
1326
|
-
violations: diagnostic.violations.slice(0, 12),
|
|
1327
|
-
violationCount: diagnostic.violations.length
|
|
1328
|
-
}))
|
|
1329
|
-
});
|
|
1330
|
-
const summary = toolBuildStages.snapshot();
|
|
1331
|
-
if (shouldWarnCodexDynamicToolBuildStageSummary(summary)) {
|
|
1332
|
-
const phase = input.forceHeartbeatTool ? "registered-tools" : "runtime-tools";
|
|
1333
|
-
embeddedAgentLog.warn(`codex app-server dynamic tool build timings runId=${params.runId} sessionId=${params.sessionId} phase=${phase} totalMs=${summary.totalMs} stages=${formatCodexDynamicToolBuildStageSummary(summary)}`, {
|
|
1334
|
-
runId: params.runId,
|
|
1335
|
-
sessionId: params.sessionId,
|
|
1336
|
-
phase,
|
|
1337
|
-
totalMs: summary.totalMs,
|
|
1338
|
-
stages: summary.stages,
|
|
1339
|
-
allToolCount: readableAllTools.length,
|
|
1340
|
-
codexFilteredToolCount: codexFilteredTools.length,
|
|
1341
|
-
visionFilteredToolCount: visionFilteredTools.length,
|
|
1342
|
-
filteredToolCount: filteredTools.length,
|
|
1343
|
-
normalizedToolCount: exposedTools.length,
|
|
1344
|
-
forceHeartbeatTool: input.forceHeartbeatTool === true,
|
|
1345
|
-
ignoreRuntimePlan: input.ignoreRuntimePlan === true,
|
|
1346
|
-
nativeToolSurfaceEnabled: input.nativeToolSurfaceEnabled === true
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
return exposedTools;
|
|
1350
|
-
}
|
|
1351
|
-
/** Preserves delivery-critical tools when a narrow allowlist would otherwise hide them. */
|
|
1352
|
-
function includeForcedCodexDynamicToolAllow(toolsAllow, params) {
|
|
1353
|
-
if (toolsAllow === void 0 || hasWildcardCodexToolsAllow(toolsAllow)) return toolsAllow;
|
|
1354
|
-
const forcedToolNames = shouldForceMessageTool(params) ? ["message"] : [];
|
|
1355
|
-
if (forcedToolNames.length === 0) return toolsAllow;
|
|
1356
|
-
if (toolsAllow.length === 0) return forcedToolNames;
|
|
1357
|
-
const normalized = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)));
|
|
1358
|
-
const missingToolNames = forcedToolNames.filter((toolName) => !normalized.has(normalizeCodexDynamicToolName(toolName)));
|
|
1359
|
-
return missingToolNames.length === 0 ? toolsAllow : [...toolsAllow, ...missingToolNames];
|
|
1360
|
-
}
|
|
1361
|
-
/** Decides whether Codex native code mode can own shell/file tools for this turn. */
|
|
1362
|
-
function shouldEnableCodexAppServerNativeToolSurface(params, sandbox, options = {}) {
|
|
1363
|
-
if (isCodexMemoryFlushRun(params)) return false;
|
|
1364
|
-
const toolsAllow = includeForcedCodexDynamicToolAllow(params.toolsAllow, params);
|
|
1365
|
-
if (toolsAllow === void 0) return canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
|
|
1366
|
-
return hasWildcardCodexToolsAllow(toolsAllow) && canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options);
|
|
1367
|
-
}
|
|
1368
|
-
/** Returns true when OpenClaw policy requires the Node-owned exec/process tools instead. */
|
|
1369
|
-
function isCodexNativeExecutionBlockedByNodeExecHost(params, options = {}) {
|
|
1370
|
-
return !resolveCodexNativeExecutionPolicy({
|
|
1371
|
-
config: params.config,
|
|
1372
|
-
sessionKey: resolveCodexRuntimePolicySessionKey(params, options.runtimeSessionKey),
|
|
1373
|
-
sessionId: params.sessionId,
|
|
1374
|
-
agentId: options.agentId,
|
|
1375
|
-
execOverrides: params.execOverrides,
|
|
1376
|
-
sandboxAvailable: options.sandbox?.enabled,
|
|
1377
|
-
readRuntimeSessionEntry: true
|
|
1378
|
-
}).nativeToolSurfaceAllowed;
|
|
1379
|
-
}
|
|
1380
|
-
function resolveCodexRuntimePolicySessionKey(params, runtimeSessionKey) {
|
|
1381
|
-
return runtimeSessionKey?.trim() || params.sandboxSessionKey?.trim() || params.sessionKey?.trim() || params.sessionId;
|
|
1382
|
-
}
|
|
1383
|
-
function canCodexAppServerNativeToolSurfaceHonorSandbox(sandbox, options = {}) {
|
|
1384
|
-
if (!sandbox?.enabled) return true;
|
|
1385
|
-
if (options.sandboxExecServerEnabled === true && sandbox.backend && canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox)) return true;
|
|
1386
|
-
return false;
|
|
1387
|
-
}
|
|
1388
|
-
function canSandboxToolPolicyExposeCodexNativeToolSurface(sandbox) {
|
|
1389
|
-
return CODEX_NATIVE_SANDBOX_TOOL_REQUIREMENTS.every((toolName) => isToolAllowed(sandbox.tools, toolName));
|
|
1390
|
-
}
|
|
1391
|
-
function isCodexMemoryFlushRun(params) {
|
|
1392
|
-
return params?.trigger === "memory" && Boolean(params.memoryFlushWritePath?.trim());
|
|
1393
|
-
}
|
|
1394
|
-
function filterCodexMemoryFlushDynamicTools(tools) {
|
|
1395
|
-
return tools.filter((tool) => CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW.has(normalizeCodexDynamicToolName(tool.name)));
|
|
1396
|
-
}
|
|
1397
|
-
/** Requires a Codex sandbox environment only when native tools must run inside OpenClaw sandboxing. */
|
|
1398
|
-
function shouldRequireCodexSandboxExecServerEnvironment(params) {
|
|
1399
|
-
return Boolean(params.sandbox?.enabled && params.nativeToolSurfaceEnabled && params.sandboxExecServerEnabled);
|
|
1400
|
-
}
|
|
1401
|
-
/** Selects the sandbox exec-server environment passed through the Codex app-server protocol. */
|
|
1402
|
-
function resolveCodexSandboxEnvironmentSelection(environment, nativeToolSurfaceEnabled) {
|
|
1403
|
-
return environment && nativeToolSurfaceEnabled ? [environment] : void 0;
|
|
1404
|
-
}
|
|
1405
|
-
/** Chooses the cwd visible to Codex native execution after sandbox exec-server setup. */
|
|
1406
|
-
function resolveCodexAppServerExecutionCwd(params) {
|
|
1407
|
-
return mapCodexAppServerRemoteWorkspacePath({
|
|
1408
|
-
value: params.environment && params.nativeToolSurfaceEnabled ? params.environment.cwd : params.effectiveCwd,
|
|
1409
|
-
localWorkspaceRoot: params.localWorkspaceRoot,
|
|
1410
|
-
remoteWorkspaceRoot: params.remoteWorkspaceRoot
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
|
-
/** Projects a local OpenClaw workspace cwd into the remote Codex app-server workspace root. */
|
|
1414
|
-
function mapCodexAppServerRemoteWorkspacePath(params) {
|
|
1415
|
-
if (!params.remoteWorkspaceRoot) return params.value;
|
|
1416
|
-
const localRoot = normalizeRemoteWorkspaceMatchPath(params.localWorkspaceRoot);
|
|
1417
|
-
const remoteRoot = normalizeRemoteWorkspaceMatchPath(params.remoteWorkspaceRoot);
|
|
1418
|
-
const normalizedValue = normalizeRemoteWorkspaceMatchPath(params.value);
|
|
1419
|
-
if (!localRoot || !remoteRoot) throw new Error("Codex remoteWorkspaceRoot requires non-empty workspace roots.");
|
|
1420
|
-
if (normalizedValue === localRoot) return remoteRoot;
|
|
1421
|
-
const prefix = `${localRoot}/`;
|
|
1422
|
-
if (!normalizedValue.startsWith(prefix)) throw new Error(`Codex remoteWorkspaceRoot is configured but cwd ${params.value} is outside OpenClaw workspace root ${params.localWorkspaceRoot}; refusing to send a gateway-local cwd to the remote Codex app-server.`);
|
|
1423
|
-
return joinRemoteWorkspacePath(remoteRoot, normalizedValue.slice(prefix.length));
|
|
1424
|
-
}
|
|
1425
|
-
function normalizeRemoteWorkspaceMatchPath(value) {
|
|
1426
|
-
return trimTrailingPathSeparator(value.replace(/\\/gu, "/"));
|
|
1427
|
-
}
|
|
1428
|
-
function trimTrailingPathSeparator(value) {
|
|
1429
|
-
return value.length > 1 ? value.replace(/[\\/]+$/u, "") : value;
|
|
1430
|
-
}
|
|
1431
|
-
function joinRemoteWorkspacePath(remoteRoot, suffix) {
|
|
1432
|
-
return remoteRoot === "/" ? `/${suffix}` : `${remoteRoot}/${suffix}`;
|
|
1433
|
-
}
|
|
1434
|
-
/** Converts OpenClaw sandbox networking into Codex's external-sandbox policy shape. */
|
|
1435
|
-
function resolveCodexExternalSandboxPolicyForOpenClawSandbox(sandbox) {
|
|
1436
|
-
return {
|
|
1437
|
-
type: "externalSandbox",
|
|
1438
|
-
networkAccess: codexNetworkAccessForOpenClawSandbox(sandbox) ? "enabled" : "restricted"
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
|
-
function codexNetworkAccessForOpenClawSandbox(sandbox) {
|
|
1442
|
-
if (sandbox?.backendId !== "docker") return true;
|
|
1443
|
-
const network = sandbox?.docker?.network?.trim().toLowerCase();
|
|
1444
|
-
return Boolean(network && network !== "none");
|
|
1445
|
-
}
|
|
1446
|
-
/** Returns a Codex config copy with app-server Codex plugin loading disabled for thread tools. */
|
|
1447
|
-
function disableCodexPluginThreadConfig(pluginConfig) {
|
|
1448
|
-
const config = readCodexPluginConfig(pluginConfig);
|
|
1449
|
-
return {
|
|
1450
|
-
...config,
|
|
1451
|
-
codexPlugins: {
|
|
1452
|
-
...config.codexPlugins,
|
|
1453
|
-
enabled: false
|
|
1454
|
-
}
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
/** Adds sandbox_exec/process aliases when native Code Mode cannot directly honor the sandbox. */
|
|
1458
|
-
function addSandboxShellDynamicToolsIfAvailable(filteredTools, allTools, input) {
|
|
1459
|
-
if (!shouldExposeSandboxExecDynamicTool(input) || isSandboxShellDynamicToolExcluded(input.pluginConfig)) return filteredTools;
|
|
1460
|
-
const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
|
|
1461
|
-
const processTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "process");
|
|
1462
|
-
if (!execTool || !processTool) return filteredTools;
|
|
1463
|
-
const sandboxExecTool = {
|
|
1464
|
-
...execTool,
|
|
1465
|
-
name: "sandbox_exec",
|
|
1466
|
-
description: "Run a shell command through OpenClaw's configured sandbox backend for this session. Use when OpenClaw sandboxing is active or when a command must execute in the sandbox backend, such as an SSH-backed sandbox or Docker container-path bind layout. Use Codex's native shell only when no OpenClaw sandbox is active and native Code Mode is available.",
|
|
1467
|
-
execute: async (toolCallId, args, signal, onUpdate) => {
|
|
1468
|
-
const result = await execTool.execute(toolCallId, args, signal, onUpdate);
|
|
1469
|
-
return {
|
|
1470
|
-
...result,
|
|
1471
|
-
content: result.content.map((item) => item.type === "text" ? Object.assign({}, item, { text: item.text.replace("Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.", "Use sandbox_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.") }) : item)
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
|
-
};
|
|
1475
|
-
const sandboxProcessTool = {
|
|
1476
|
-
...processTool,
|
|
1477
|
-
name: "sandbox_process",
|
|
1478
|
-
description: "Manage sandbox_exec sessions that were started through OpenClaw's configured sandbox backend for this session: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for sandbox_exec follow-up; use Codex's native shell session handling only when no OpenClaw sandbox is active and native Code Mode is available."
|
|
1479
|
-
};
|
|
1480
|
-
return [
|
|
1481
|
-
...filteredTools,
|
|
1482
|
-
sandboxExecTool,
|
|
1483
|
-
sandboxProcessTool
|
|
1484
|
-
];
|
|
1485
|
-
}
|
|
1486
|
-
function shouldExposeSandboxExecDynamicTool(input) {
|
|
1487
|
-
if (isCodexMemoryFlushRun(input.params)) return false;
|
|
1488
|
-
if (isCodexNativeExecutionBlockedByNodeExecHost(input.params, {
|
|
1489
|
-
agentId: input.sessionAgentId,
|
|
1490
|
-
runtimeSessionKey: input.sandboxSessionKey,
|
|
1491
|
-
sandbox: input.sandbox
|
|
1492
|
-
})) return false;
|
|
1493
|
-
const backendId = input.sandbox?.enabled ? input.sandbox.backendId.trim().toLowerCase() : "";
|
|
1494
|
-
return Boolean(backendId && input.nativeToolSurfaceEnabled === false);
|
|
1495
|
-
}
|
|
1496
|
-
function isCodexDynamicToolExcluded(config, names) {
|
|
1497
|
-
const normalizedNames = new Set(names.map((name) => normalizeCodexDynamicToolName(name)));
|
|
1498
|
-
return (config.codexDynamicToolsExclude ?? []).some((name) => {
|
|
1499
|
-
const normalized = normalizeCodexDynamicToolName(name);
|
|
1500
|
-
return normalizedNames.has(normalized);
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
function isSandboxShellDynamicToolExcluded(config) {
|
|
1504
|
-
return isCodexDynamicToolExcluded(config, [
|
|
1505
|
-
"exec",
|
|
1506
|
-
"sandbox_exec",
|
|
1507
|
-
"process",
|
|
1508
|
-
"sandbox_process"
|
|
1509
|
-
]);
|
|
1510
|
-
}
|
|
1511
|
-
function addNodeShellDynamicToolsIfNeeded(filteredTools, allTools, input, nodePolicy) {
|
|
1512
|
-
if (isCodexMemoryFlushRun(input.params)) return filteredTools;
|
|
1513
|
-
if (nodePolicy.effectiveExecHost !== "node") return filteredTools;
|
|
1514
|
-
const execTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "exec");
|
|
1515
|
-
const processTool = allTools.find((tool) => normalizeCodexDynamicToolName(tool.name) === "process");
|
|
1516
|
-
if (!execTool || !processTool) return filteredTools;
|
|
1517
|
-
const toolsToAppend = [];
|
|
1518
|
-
if (!isCodexDynamicToolExcluded(input.pluginConfig, ["exec", CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME]) && !filteredTools.some((tool) => normalizeCodexDynamicToolName(tool.name) === CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME)) toolsToAppend.push(createNodeExecDynamicTool(execTool, nodePolicy.node));
|
|
1519
|
-
if (!isCodexDynamicToolExcluded(input.pluginConfig, ["process", CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME]) && !filteredTools.some((tool) => normalizeCodexDynamicToolName(tool.name) === CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME)) toolsToAppend.push(createNodeProcessDynamicTool(processTool));
|
|
1520
|
-
return toolsToAppend.length > 0 ? [...filteredTools, ...toolsToAppend] : filteredTools;
|
|
1521
|
-
}
|
|
1522
|
-
function createNodeExecDynamicTool(execTool, configuredNode) {
|
|
1523
|
-
return {
|
|
1524
|
-
...execTool,
|
|
1525
|
-
name: CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME,
|
|
1526
|
-
description: "Run a shell command on the OpenClaw configured remote node for this session. This tool always uses OpenClaw host=node internally and follows the existing node exec approval and allowlist policy. Use node_process for follow-up on backgrounded node_exec sessions. Use Codex's native shell for local app-server work.",
|
|
1527
|
-
parameters: hideNodeExecDynamicToolParameters(execTool.parameters),
|
|
1528
|
-
execute: async (toolCallId, args, signal, onUpdate) => {
|
|
1529
|
-
const result = await execTool.execute(toolCallId, pinNodeExecDynamicToolArgs(args, configuredNode), signal, onUpdate);
|
|
1530
|
-
return {
|
|
1531
|
-
...result,
|
|
1532
|
-
content: result.content.map((item) => item.type === "text" ? Object.assign({}, item, { text: item.text.replace("Use process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.", "Use node_process (list/poll/log/write/send-keys/submit/paste/kill/clear/remove) for follow-up.") }) : item)
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
function createNodeProcessDynamicTool(processTool) {
|
|
1538
|
-
return {
|
|
1539
|
-
...processTool,
|
|
1540
|
-
name: CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME,
|
|
1541
|
-
description: "Manage node_exec sessions that were started on the OpenClaw configured remote node for this session: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use only for node_exec follow-up; use Codex's native shell session handling for local app-server work."
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
function pinNodeExecDynamicToolArgs(args, configuredNode) {
|
|
1545
|
-
const { host: _host, security: _security, ask: _ask, node: _node, ...rest } = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
1546
|
-
const node = configuredNode?.trim();
|
|
1547
|
-
return {
|
|
1548
|
-
...rest,
|
|
1549
|
-
host: "node",
|
|
1550
|
-
...node ? { node } : {}
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
function hideNodeExecDynamicToolParameters(parameters) {
|
|
1554
|
-
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
|
|
1555
|
-
const schema = parameters;
|
|
1556
|
-
const rawProperties = schema.properties;
|
|
1557
|
-
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) return parameters;
|
|
1558
|
-
const nextProperties = Object.fromEntries(Object.entries(rawProperties).filter(([name]) => !CODEX_NODE_EXEC_HIDDEN_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name))));
|
|
1559
|
-
const rawRequired = schema.required;
|
|
1560
|
-
const nextRequired = Array.isArray(rawRequired) ? rawRequired.filter((name) => typeof name !== "string" || !CODEX_NODE_EXEC_HIDDEN_PARAMETER_NAMES.has(normalizeCodexDynamicToolName(name))) : rawRequired;
|
|
1561
|
-
return {
|
|
1562
|
-
...schema,
|
|
1563
|
-
properties: nextProperties,
|
|
1564
|
-
...Array.isArray(rawRequired) ? { required: nextRequired } : {}
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
function resolveCodexNativeExecutionPolicyForDynamicTools(input) {
|
|
1568
|
-
return resolveCodexNativeExecutionPolicy({
|
|
1569
|
-
config: input.params.config,
|
|
1570
|
-
sessionKey: resolveCodexRuntimePolicySessionKey(input.params, input.sandboxSessionKey),
|
|
1571
|
-
sessionId: input.params.sessionId,
|
|
1572
|
-
agentId: input.sessionAgentId,
|
|
1573
|
-
execOverrides: input.params.execOverrides,
|
|
1574
|
-
sandboxAvailable: input.sandbox?.enabled,
|
|
1575
|
-
readRuntimeSessionEntry: true
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
function resolveNodeExecToolOverrides(policy) {
|
|
1579
|
-
if (policy.effectiveExecHost !== "node") return;
|
|
1580
|
-
const node = policy.node?.trim();
|
|
1581
|
-
return {
|
|
1582
|
-
host: "node",
|
|
1583
|
-
...node ? { node } : {}
|
|
1584
|
-
};
|
|
1585
|
-
}
|
|
1586
|
-
/** Applies a normalized tool allowlist while preserving shell aliases for exec/process. */
|
|
1587
|
-
function filterCodexDynamicToolsForAllowlist(tools, toolsAllow) {
|
|
1588
|
-
if (!toolsAllow) return tools;
|
|
1589
|
-
if (toolsAllow.length === 0) return [];
|
|
1590
|
-
if (hasWildcardCodexToolsAllow(toolsAllow)) return tools;
|
|
1591
|
-
const allowSet = new Set(toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean));
|
|
1592
|
-
return tools.filter((tool) => {
|
|
1593
|
-
const normalized = normalizeCodexDynamicToolName(tool.name);
|
|
1594
|
-
return allowSet.has(normalized) || normalized === "sandbox_exec" && allowSet.has("exec") || normalized === "sandbox_process" && (allowSet.has("exec") || allowSet.has("process")) || normalized === CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME && allowSet.has("exec") || normalized === CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME && (allowSet.has("exec") || allowSet.has("process"));
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
/** Detects the wildcard allowlist marker after Codex tool-name normalization. */
|
|
1598
|
-
function hasWildcardCodexToolsAllow(toolsAllow) {
|
|
1599
|
-
return toolsAllow.some((name) => normalizeCodexDynamicToolName(name) === "*");
|
|
1600
|
-
}
|
|
1601
|
-
/** Forces message delivery through the message tool when the source channel requires it. */
|
|
1602
|
-
function shouldForceMessageTool(params) {
|
|
1603
|
-
return params.disableMessageTool !== true && params.sourceReplyDeliveryMode === "message_tool_only";
|
|
1604
|
-
}
|
|
1605
|
-
//#endregion
|
|
1606
|
-
//#region extensions/codex/src/app-server/dynamic-tool-diagnostics.ts
|
|
1607
|
-
/**
|
|
1608
|
-
* Trusted diagnostics emitted around Codex dynamic tool execution lifecycle.
|
|
1609
|
-
*/
|
|
1610
|
-
/** Emits a start event for one Codex dynamic tool call. */
|
|
1611
|
-
function emitDynamicToolStartedDiagnostic(params) {
|
|
1612
|
-
emitTrustedDiagnosticEvent({
|
|
1613
|
-
type: "tool.execution.started",
|
|
1614
|
-
runId: params.runId,
|
|
1615
|
-
sessionId: params.sessionId,
|
|
1616
|
-
sessionKey: params.sessionKey,
|
|
1617
|
-
toolName: params.call.tool,
|
|
1618
|
-
toolCallId: params.call.callId
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
/** Emits an error event for one Codex dynamic tool call. */
|
|
1622
|
-
function emitDynamicToolErrorDiagnostic(params) {
|
|
1623
|
-
emitTrustedDiagnosticEvent({
|
|
1624
|
-
type: "tool.execution.error",
|
|
1625
|
-
runId: params.runId,
|
|
1626
|
-
sessionId: params.sessionId,
|
|
1627
|
-
sessionKey: params.sessionKey,
|
|
1628
|
-
toolName: params.call.tool,
|
|
1629
|
-
toolCallId: params.call.callId,
|
|
1630
|
-
durationMs: params.durationMs,
|
|
1631
|
-
errorCategory: "codex_dynamic_tool_error"
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
/** Emits the terminal event matching a dynamic tool response's diagnostic type. */
|
|
1635
|
-
function emitDynamicToolTerminalDiagnostic(params) {
|
|
1636
|
-
const terminalType = params.response.diagnosticTerminalType ?? (params.response.success ? "completed" : "error");
|
|
1637
|
-
if (terminalType === "completed") {
|
|
1638
|
-
emitTrustedDiagnosticEvent({
|
|
1639
|
-
type: "tool.execution.completed",
|
|
1640
|
-
runId: params.runId,
|
|
1641
|
-
sessionId: params.sessionId,
|
|
1642
|
-
sessionKey: params.sessionKey,
|
|
1643
|
-
toolName: params.call.tool,
|
|
1644
|
-
toolCallId: params.call.callId,
|
|
1645
|
-
durationMs: params.durationMs
|
|
1646
|
-
});
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
if (terminalType === "blocked") {
|
|
1650
|
-
emitTrustedDiagnosticEvent({
|
|
1651
|
-
type: "tool.execution.blocked",
|
|
1652
|
-
runId: params.runId,
|
|
1653
|
-
sessionId: params.sessionId,
|
|
1654
|
-
sessionKey: params.sessionKey,
|
|
1655
|
-
toolName: params.call.tool,
|
|
1656
|
-
toolCallId: params.call.callId,
|
|
1657
|
-
deniedReason: "plugin-before-tool-call",
|
|
1658
|
-
reason: "Tool call blocked"
|
|
1659
|
-
});
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
emitDynamicToolErrorDiagnostic(params);
|
|
1663
|
-
}
|
|
1664
|
-
//#endregion
|
|
1665
|
-
//#region extensions/codex/src/app-server/dynamic-tools.ts
|
|
1666
|
-
function applyCurrentMessageProvider(toolName, args, currentProvider) {
|
|
1667
|
-
const hasProvider = typeof args.provider === "string" && args.provider.trim().length > 0 ? true : typeof args.channel === "string" && args.channel.trim().length > 0;
|
|
1668
|
-
const provider = currentProvider?.trim();
|
|
1669
|
-
if (toolName !== "message" || hasProvider || !provider) return args;
|
|
1670
|
-
return {
|
|
1671
|
-
...args,
|
|
1672
|
-
provider
|
|
1673
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
/** Namespace attached to OpenClaw-owned dynamic tools exposed to Codex. */
|
|
1676
|
-
const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
|
|
1677
|
-
const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = new Set(["sessions_yield"]);
|
|
1678
|
-
const DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS = 16e3;
|
|
1679
|
-
/**
|
|
1680
|
-
* Creates dynamic tool specs and a call handler that executes OpenClaw tools,
|
|
1681
|
-
* applies hooks/middleware, and records delivery/media telemetry.
|
|
1682
|
-
*/
|
|
1683
|
-
function createCodexDynamicToolBridge(params) {
|
|
1684
|
-
const toolResultHookContext = toToolResultHookContext(params.hookContext);
|
|
1685
|
-
const toolResultMaxChars = resolveCodexDynamicToolResultMaxChars(params.hookContext);
|
|
1686
|
-
const availableProjection = projectCodexDynamicTools(params.tools);
|
|
1687
|
-
const registeredProjection = params.registeredTools ? projectCodexDynamicTools(params.registeredTools) : availableProjection;
|
|
1688
|
-
const wrappedAvailableProjection = wrapProjectedCodexDynamicTools(availableProjection.tools, params.hookContext);
|
|
1689
|
-
const availableTools = wrappedAvailableProjection.tools;
|
|
1690
|
-
const quarantinedAvailableToolNames = new Set([...availableProjection.quarantinedTools, ...wrappedAvailableProjection.quarantinedTools].map((tool) => tool.tool));
|
|
1691
|
-
const registeredSpecTools = (params.registeredTools ? registeredProjection.tools : availableTools).filter((entry) => !quarantinedAvailableToolNames.has(entry.name));
|
|
1692
|
-
const toolMap = new Map(availableTools.map((entry) => [entry.name, entry]));
|
|
1693
|
-
const registeredToolNames = new Set(registeredSpecTools.map((entry) => entry.name));
|
|
1694
|
-
const quarantinedTools = dedupeQuarantinedDynamicTools([
|
|
1695
|
-
...availableProjection.quarantinedTools,
|
|
1696
|
-
...registeredProjection.quarantinedTools,
|
|
1697
|
-
...wrappedAvailableProjection.quarantinedTools
|
|
1698
|
-
]);
|
|
1699
|
-
warnQuarantinedDynamicTools(quarantinedTools);
|
|
1700
|
-
emitQuarantinedDynamicToolDiagnostics(quarantinedTools, params.hookContext);
|
|
1701
|
-
const telemetry = {
|
|
1702
|
-
didSendViaMessagingTool: false,
|
|
1703
|
-
messagingToolSentTexts: [],
|
|
1704
|
-
messagingToolSentMediaUrls: [],
|
|
1705
|
-
messagingToolSentTargets: [],
|
|
1706
|
-
messagingToolSourceReplyPayloads: [],
|
|
1707
|
-
toolMediaUrls: [],
|
|
1708
|
-
toolAudioAsVoice: false,
|
|
1709
|
-
quarantinedTools
|
|
1710
|
-
};
|
|
1711
|
-
const middlewareRunner = createAgentToolResultMiddlewareRunner({
|
|
1712
|
-
runtime: "codex",
|
|
1713
|
-
...toolResultHookContext
|
|
1714
|
-
});
|
|
1715
|
-
const isReplaySafeToolInstance = (tool) => {
|
|
1716
|
-
const pluginMeta = getPluginToolMeta(tool);
|
|
1717
|
-
if (pluginMeta) return pluginMeta.replaySafe === true;
|
|
1718
|
-
return getChannelAgentToolMeta(tool) === void 0;
|
|
1719
|
-
};
|
|
1720
|
-
const legacyExtensionRunner = createCodexAppServerToolResultExtensionRunner(toolResultHookContext);
|
|
1721
|
-
const directToolNames = new Set([...ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES, ...params.directToolNames ?? []]);
|
|
1722
|
-
return {
|
|
1723
|
-
availableSpecs: createCodexDynamicToolSpecs({
|
|
1724
|
-
entries: availableTools,
|
|
1725
|
-
loading: params.loading ?? "searchable",
|
|
1726
|
-
directToolNames
|
|
1727
|
-
}),
|
|
1728
|
-
specs: createCodexDynamicToolSpecs({
|
|
1729
|
-
entries: registeredSpecTools,
|
|
1730
|
-
loading: params.loading ?? "searchable",
|
|
1731
|
-
directToolNames
|
|
1732
|
-
}),
|
|
1733
|
-
telemetry,
|
|
1734
|
-
handleToolCall: async (call, options) => {
|
|
1735
|
-
const toolEntry = toolMap.get(call.tool);
|
|
1736
|
-
if (!toolEntry) {
|
|
1737
|
-
const message = registeredToolNames.has(call.tool) ? `OpenClaw tool is not available for this turn: ${call.tool}` : `Unknown OpenClaw tool: ${call.tool}`;
|
|
1738
|
-
finalizeToolTerminalPresentation({
|
|
1739
|
-
toolCallId: call.callId,
|
|
1740
|
-
runId: toolResultHookContext.runId,
|
|
1741
|
-
result: failedToolResult(message),
|
|
1742
|
-
isError: true,
|
|
1743
|
-
observer: params.hookContext?.onToolOutcome,
|
|
1744
|
-
toolName: call.tool,
|
|
1745
|
-
toolCallOrdinal: options?.toolCallOrdinal
|
|
1746
|
-
});
|
|
1747
|
-
notifyAgentToolResult(options?.onAgentToolResult, call.tool, failedToolResult(message), true);
|
|
1748
|
-
if (registeredToolNames.has(call.tool)) return {
|
|
1749
|
-
contentItems: [{
|
|
1750
|
-
type: "inputText",
|
|
1751
|
-
text: message
|
|
1752
|
-
}],
|
|
1753
|
-
success: false
|
|
1754
|
-
};
|
|
1755
|
-
return {
|
|
1756
|
-
contentItems: [{
|
|
1757
|
-
type: "inputText",
|
|
1758
|
-
text: message
|
|
1759
|
-
}],
|
|
1760
|
-
success: false
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
const { tool, name: toolName } = toolEntry;
|
|
1764
|
-
const args = jsonObjectToRecord(call.arguments);
|
|
1765
|
-
const startedAt = Date.now();
|
|
1766
|
-
const signal = composeAbortSignals(params.signal, options?.signal);
|
|
1767
|
-
let didStartExecution = false;
|
|
1768
|
-
let executionPrevented = false;
|
|
1769
|
-
let executedArgs = structuredClone(args);
|
|
1770
|
-
try {
|
|
1771
|
-
const preparedArgs = tool.prepareArguments ? tool.prepareArguments(args) : args;
|
|
1772
|
-
const telemetryArgs = isRecord(preparedArgs) ? preparedArgs : args;
|
|
1773
|
-
executedArgs = structuredClone(telemetryArgs);
|
|
1774
|
-
const messagingContext = {
|
|
1775
|
-
config: params.hookContext?.config,
|
|
1776
|
-
currentChannelId: params.hookContext?.currentChannelId,
|
|
1777
|
-
currentMessagingTarget: params.hookContext?.currentMessagingTarget,
|
|
1778
|
-
currentThreadId: params.hookContext?.currentThreadId,
|
|
1779
|
-
replyToMode: params.hookContext?.replyToMode,
|
|
1780
|
-
hasRepliedRef: params.hookContext?.hasRepliedRef ? { value: params.hookContext.hasRepliedRef.value } : void 0
|
|
1781
|
-
};
|
|
1782
|
-
didStartExecution = true;
|
|
1783
|
-
const rawResult = await tool.execute(call.callId, preparedArgs, signal);
|
|
1784
|
-
const adjustedExecutedArgs = consumeAdjustedParamsForToolCall(call.callId, toolResultHookContext.runId);
|
|
1785
|
-
if (isRecord(adjustedExecutedArgs)) executedArgs = structuredClone(adjustedExecutedArgs);
|
|
1786
|
-
executionPrevented = consumePreExecutionBlockedToolCall(call.callId, toolResultHookContext.runId);
|
|
1787
|
-
const telemetryRawResult = sanitizeToolResult(rawResult);
|
|
1788
|
-
const rawIsError = isCodexToolResultError(rawResult);
|
|
1789
|
-
const middlewareResult = await middlewareRunner.applyToolResultMiddleware({
|
|
1790
|
-
threadId: call.threadId,
|
|
1791
|
-
turnId: call.turnId,
|
|
1792
|
-
toolCallId: call.callId,
|
|
1793
|
-
toolName,
|
|
1794
|
-
args: structuredClone(executedArgs),
|
|
1795
|
-
isError: rawIsError,
|
|
1796
|
-
result: rawResult
|
|
1797
|
-
});
|
|
1798
|
-
const result = await legacyExtensionRunner.applyToolResultExtensions({
|
|
1799
|
-
threadId: call.threadId,
|
|
1800
|
-
turnId: call.turnId,
|
|
1801
|
-
toolCallId: call.callId,
|
|
1802
|
-
toolName,
|
|
1803
|
-
args: structuredClone(executedArgs),
|
|
1804
|
-
result: middlewareResult
|
|
1805
|
-
});
|
|
1806
|
-
const resultIsError = rawIsError || isCodexToolResultError(result);
|
|
1807
|
-
notifyAgentToolResult(options?.onAgentToolResult, toolName, result, resultIsError);
|
|
1808
|
-
runAgentHarnessAfterToolCallHook({
|
|
1809
|
-
toolName,
|
|
1810
|
-
toolCallId: call.callId,
|
|
1811
|
-
runId: toolResultHookContext.runId,
|
|
1812
|
-
agentId: toolResultHookContext.agentId,
|
|
1813
|
-
sessionId: toolResultHookContext.sessionId,
|
|
1814
|
-
sessionKey: toolResultHookContext.sessionKey,
|
|
1815
|
-
channelId: toolResultHookContext.channelId,
|
|
1816
|
-
startArgs: executedArgs,
|
|
1817
|
-
result,
|
|
1818
|
-
startedAt
|
|
1819
|
-
});
|
|
1820
|
-
finalizeToolTerminalPresentation({
|
|
1821
|
-
toolCallId: call.callId,
|
|
1822
|
-
runId: toolResultHookContext.runId,
|
|
1823
|
-
result,
|
|
1824
|
-
isError: resultIsError,
|
|
1825
|
-
observer: params.hookContext?.onToolOutcome,
|
|
1826
|
-
toolName,
|
|
1827
|
-
toolCallOrdinal: options?.toolCallOrdinal
|
|
1828
|
-
});
|
|
1829
|
-
const messagingTelemetryArgs = applyCurrentMessageProvider(toolName, executedArgs, params.hookContext?.currentChannelProvider);
|
|
1830
|
-
const messagingTarget = isMessagingTool(toolName) && isMessagingToolSendAction(toolName, executedArgs) ? extractMessagingToolSend(toolName, messagingTelemetryArgs, messagingContext) : void 0;
|
|
1831
|
-
const confirmedMessagingTarget = !rawIsError && messagingTarget ? extractMessagingToolSendResult(messagingTarget, telemetryRawResult) : messagingTarget;
|
|
1832
|
-
collectToolTelemetry({
|
|
1833
|
-
toolName,
|
|
1834
|
-
args: executedArgs,
|
|
1835
|
-
result,
|
|
1836
|
-
mediaTrustResult: telemetryRawResult,
|
|
1837
|
-
telemetry,
|
|
1838
|
-
isError: resultIsError,
|
|
1839
|
-
messagingTarget: confirmedMessagingTarget
|
|
1840
|
-
});
|
|
1841
|
-
const terminalType = inferToolResultDiagnosticTerminalType(result, resultIsError);
|
|
1842
|
-
const response = withDiagnosticTerminalType({
|
|
1843
|
-
contentItems: convertToolContents(result.content, toolResultMaxChars),
|
|
1844
|
-
success: !resultIsError
|
|
1845
|
-
}, terminalType);
|
|
1846
|
-
withDynamicToolTermination(response, rawResult.terminate === true || result.terminate === true || isToolResultYield(rawResult) || isToolResultYield(result));
|
|
1847
|
-
const asyncStarted = isAsyncStartedToolResult(rawResult) || isAsyncStartedToolResult(result);
|
|
1848
|
-
withDynamicToolAsyncStarted(response, asyncStarted);
|
|
1849
|
-
return withSideEffectEvidence(response, !(executionPrevented || !asyncStarted && isReplaySafeToolInstance(toolEntry.tool) && isReplaySafeToolCall(toolName, executedArgs)));
|
|
1850
|
-
} catch (error) {
|
|
1851
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1852
|
-
const adjustedExecutedArgs = consumeAdjustedParamsForToolCall(call.callId, toolResultHookContext.runId);
|
|
1853
|
-
if (isRecord(adjustedExecutedArgs)) executedArgs = structuredClone(adjustedExecutedArgs);
|
|
1854
|
-
executionPrevented = executionPrevented || consumePreExecutionBlockedToolCall(call.callId, toolResultHookContext.runId);
|
|
1855
|
-
const failedResult = failedToolResult(errorMessage);
|
|
1856
|
-
finalizeToolTerminalPresentation({
|
|
1857
|
-
toolCallId: call.callId,
|
|
1858
|
-
runId: toolResultHookContext.runId,
|
|
1859
|
-
result: failedResult,
|
|
1860
|
-
isError: true,
|
|
1861
|
-
observer: params.hookContext?.onToolOutcome,
|
|
1862
|
-
toolName,
|
|
1863
|
-
toolCallOrdinal: options?.toolCallOrdinal
|
|
1864
|
-
});
|
|
1865
|
-
notifyAgentToolResult(options?.onAgentToolResult, toolName, failedResult, true);
|
|
1866
|
-
collectToolTelemetry({
|
|
1867
|
-
toolName,
|
|
1868
|
-
args: executedArgs,
|
|
1869
|
-
result: void 0,
|
|
1870
|
-
telemetry,
|
|
1871
|
-
isError: true
|
|
1872
|
-
});
|
|
1873
|
-
runAgentHarnessAfterToolCallHook({
|
|
1874
|
-
toolName,
|
|
1875
|
-
toolCallId: call.callId,
|
|
1876
|
-
runId: toolResultHookContext.runId,
|
|
1877
|
-
agentId: toolResultHookContext.agentId,
|
|
1878
|
-
sessionId: toolResultHookContext.sessionId,
|
|
1879
|
-
sessionKey: toolResultHookContext.sessionKey,
|
|
1880
|
-
channelId: toolResultHookContext.channelId,
|
|
1881
|
-
startArgs: executedArgs,
|
|
1882
|
-
error: errorMessage,
|
|
1883
|
-
startedAt
|
|
1884
|
-
});
|
|
1885
|
-
const replaySafe = !didStartExecution || executionPrevented || isReplaySafeToolInstance(toolEntry.tool) && isReplaySafeToolCall(toolName, executedArgs);
|
|
1886
|
-
return withSideEffectEvidence(withDiagnosticTerminalType({
|
|
1887
|
-
contentItems: [{
|
|
1888
|
-
type: "inputText",
|
|
1889
|
-
text: errorMessage
|
|
1890
|
-
}],
|
|
1891
|
-
success: false
|
|
1892
|
-
}, "error"), didStartExecution && !replaySafe);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
};
|
|
1896
|
-
}
|
|
1897
|
-
function notifyAgentToolResult(observer, toolName, result, isError) {
|
|
1898
|
-
try {
|
|
1899
|
-
observer?.({
|
|
1900
|
-
toolName,
|
|
1901
|
-
result: sanitizeToolResult(result),
|
|
1902
|
-
isError
|
|
1903
|
-
});
|
|
1904
|
-
} catch (error) {
|
|
1905
|
-
embeddedAgentLog.warn(`onAgentToolResult handler failed: tool=${toolName} error=${String(error)}`);
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
function failedToolResult(message) {
|
|
1909
|
-
return {
|
|
1910
|
-
content: [{
|
|
1911
|
-
type: "text",
|
|
1912
|
-
text: message
|
|
1913
|
-
}],
|
|
1914
|
-
details: {
|
|
1915
|
-
status: "failed",
|
|
1916
|
-
error: message
|
|
1917
|
-
}
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
function wrapProjectedCodexDynamicTools(tools, hookContext) {
|
|
1921
|
-
const wrappedTools = [];
|
|
1922
|
-
const quarantinedTools = [];
|
|
1923
|
-
for (const entry of tools) try {
|
|
1924
|
-
if (isToolWrappedWithBeforeToolCallHook(entry.tool)) {
|
|
1925
|
-
setBeforeToolCallDiagnosticsEnabled(entry.tool, false);
|
|
1926
|
-
wrappedTools.push(entry);
|
|
1927
|
-
continue;
|
|
1928
|
-
}
|
|
1929
|
-
wrappedTools.push({
|
|
1930
|
-
...entry,
|
|
1931
|
-
tool: wrapToolWithBeforeToolCallHook(entry.tool, hookContext, { emitDiagnostics: false })
|
|
1932
|
-
});
|
|
1933
|
-
} catch {
|
|
1934
|
-
quarantinedTools.push({
|
|
1935
|
-
tool: entry.name,
|
|
1936
|
-
violations: [`${entry.name} could not be wrapped for before-tool-call hooks`]
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
return {
|
|
1940
|
-
tools: wrappedTools,
|
|
1941
|
-
quarantinedTools
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
function createCodexDynamicToolSpecs(params) {
|
|
1945
|
-
const specs = [];
|
|
1946
|
-
const namespaceTools = [];
|
|
1947
|
-
for (const entry of params.entries) {
|
|
1948
|
-
const functionSpec = createCodexDynamicToolFunctionSpec({ entry });
|
|
1949
|
-
if (params.loading === "direct" || params.directToolNames.has(entry.name)) {
|
|
1950
|
-
specs.push(functionSpec);
|
|
1951
|
-
continue;
|
|
1952
|
-
}
|
|
1953
|
-
namespaceTools.push({
|
|
1954
|
-
...functionSpec,
|
|
1955
|
-
deferLoading: true
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
if (namespaceTools.length > 0) specs.push({
|
|
1959
|
-
type: "namespace",
|
|
1960
|
-
name: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
|
|
1961
|
-
description: "",
|
|
1962
|
-
tools: namespaceTools
|
|
1963
|
-
});
|
|
1964
|
-
return specs;
|
|
1965
|
-
}
|
|
1966
|
-
function createCodexDynamicToolFunctionSpec(params) {
|
|
1967
|
-
return {
|
|
1968
|
-
type: "function",
|
|
1969
|
-
name: params.entry.name,
|
|
1970
|
-
description: params.entry.description,
|
|
1971
|
-
inputSchema: params.entry.inputSchema
|
|
1972
|
-
};
|
|
1973
|
-
}
|
|
1974
|
-
function projectCodexDynamicTools(tools) {
|
|
1975
|
-
const projectedTools = [];
|
|
1976
|
-
const quarantinedTools = [];
|
|
1977
|
-
let length;
|
|
1978
|
-
try {
|
|
1979
|
-
length = tools.length;
|
|
1980
|
-
} catch {
|
|
1981
|
-
return {
|
|
1982
|
-
tools: [],
|
|
1983
|
-
quarantinedTools: [{
|
|
1984
|
-
tool: "tool[0]",
|
|
1985
|
-
violations: ["tool[0] is unreadable"]
|
|
1986
|
-
}]
|
|
1987
|
-
};
|
|
1988
|
-
}
|
|
1989
|
-
for (let toolIndex = 0; toolIndex < length; toolIndex += 1) {
|
|
1990
|
-
let tool;
|
|
1991
|
-
try {
|
|
1992
|
-
tool = tools[toolIndex];
|
|
1993
|
-
} catch {
|
|
1994
|
-
quarantinedTools.push({
|
|
1995
|
-
tool: `tool[${toolIndex}]`,
|
|
1996
|
-
violations: [`tool[${toolIndex}] is unreadable`]
|
|
1997
|
-
});
|
|
1998
|
-
continue;
|
|
1999
|
-
}
|
|
2000
|
-
const descriptor = readCodexDynamicToolDescriptor(tool, toolIndex);
|
|
2001
|
-
if (!descriptor.ok) {
|
|
2002
|
-
quarantinedTools.push(descriptor.diagnostic);
|
|
2003
|
-
continue;
|
|
2004
|
-
}
|
|
2005
|
-
const projection = projectRuntimeToolInputSchema(descriptor.parameters, `${descriptor.name}.inputSchema`);
|
|
2006
|
-
if (projection.violations.length > 0) {
|
|
2007
|
-
quarantinedTools.push({
|
|
2008
|
-
tool: descriptor.name,
|
|
2009
|
-
violations: projection.violations
|
|
2010
|
-
});
|
|
2011
|
-
continue;
|
|
2012
|
-
}
|
|
2013
|
-
projectedTools.push({
|
|
2014
|
-
tool,
|
|
2015
|
-
name: descriptor.name,
|
|
2016
|
-
description: descriptor.description,
|
|
2017
|
-
inputSchema: projection.schema
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
return {
|
|
2021
|
-
tools: projectedTools,
|
|
2022
|
-
quarantinedTools
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
|
-
function readCodexDynamicToolDescriptor(tool, toolIndex) {
|
|
2026
|
-
const fallbackName = `tool[${toolIndex}]`;
|
|
2027
|
-
let name;
|
|
2028
|
-
try {
|
|
2029
|
-
const rawName = tool.name;
|
|
2030
|
-
if (typeof rawName !== "string" || !rawName) return {
|
|
2031
|
-
ok: false,
|
|
2032
|
-
diagnostic: {
|
|
2033
|
-
tool: fallbackName,
|
|
2034
|
-
violations: [`${fallbackName}.name must be a non-empty string`]
|
|
2035
|
-
}
|
|
2036
|
-
};
|
|
2037
|
-
name = rawName;
|
|
2038
|
-
} catch {
|
|
2039
|
-
return {
|
|
2040
|
-
ok: false,
|
|
2041
|
-
diagnostic: {
|
|
2042
|
-
tool: fallbackName,
|
|
2043
|
-
violations: [`${fallbackName}.name is unreadable`]
|
|
2044
|
-
}
|
|
2045
|
-
};
|
|
2046
|
-
}
|
|
2047
|
-
let description;
|
|
2048
|
-
try {
|
|
2049
|
-
description = typeof tool.description === "string" ? tool.description : "";
|
|
2050
|
-
} catch {
|
|
2051
|
-
return {
|
|
2052
|
-
ok: false,
|
|
2053
|
-
diagnostic: {
|
|
2054
|
-
tool: name,
|
|
2055
|
-
violations: [`${name}.description is unreadable`]
|
|
2056
|
-
}
|
|
2057
|
-
};
|
|
2058
|
-
}
|
|
2059
|
-
let parameters;
|
|
2060
|
-
try {
|
|
2061
|
-
parameters = tool.parameters;
|
|
2062
|
-
} catch {
|
|
2063
|
-
return {
|
|
2064
|
-
ok: false,
|
|
2065
|
-
diagnostic: {
|
|
2066
|
-
tool: name,
|
|
2067
|
-
violations: [`${name}.inputSchema is unreadable`]
|
|
2068
|
-
}
|
|
2069
|
-
};
|
|
2070
|
-
}
|
|
2071
|
-
return {
|
|
2072
|
-
ok: true,
|
|
2073
|
-
name,
|
|
2074
|
-
description,
|
|
2075
|
-
parameters
|
|
2076
|
-
};
|
|
2077
|
-
}
|
|
2078
|
-
function warnQuarantinedDynamicTools(tools) {
|
|
2079
|
-
if (tools.length === 0) return;
|
|
2080
|
-
const unique = /* @__PURE__ */ new Map();
|
|
2081
|
-
for (const tool of tools) unique.set(tool.tool, tool.violations);
|
|
2082
|
-
embeddedAgentLog.warn(`codex app-server quarantined ${unique.size} dynamic ${unique.size === 1 ? "tool" : "tools"} with unsupported input schemas: ${[...unique.keys()].join(", ")}`, { tools: [...unique.entries()].map(([tool, violations]) => ({
|
|
2083
|
-
tool,
|
|
2084
|
-
violations
|
|
2085
|
-
})) });
|
|
2086
|
-
}
|
|
2087
|
-
function emitQuarantinedDynamicToolDiagnostics(tools, ctx) {
|
|
2088
|
-
for (const tool of tools) emitTrustedDiagnosticEvent({
|
|
2089
|
-
type: "tool.execution.blocked",
|
|
2090
|
-
runId: ctx?.runId,
|
|
2091
|
-
sessionId: ctx?.sessionId,
|
|
2092
|
-
sessionKey: ctx?.sessionKey,
|
|
2093
|
-
toolName: tool.tool,
|
|
2094
|
-
deniedReason: "unsupported_tool_schema",
|
|
2095
|
-
reason: tool.violations.join(", ")
|
|
2096
|
-
});
|
|
2097
|
-
}
|
|
2098
|
-
function dedupeQuarantinedDynamicTools(tools) {
|
|
2099
|
-
return [...new Map(tools.map((tool) => [tool.tool, {
|
|
2100
|
-
tool: tool.tool,
|
|
2101
|
-
violations: tool.violations
|
|
2102
|
-
}])).values()];
|
|
2103
|
-
}
|
|
2104
|
-
function toToolResultHookContext(ctx) {
|
|
2105
|
-
const { agentId, sessionId, sessionKey, runId, channelId } = ctx ?? {};
|
|
2106
|
-
return {
|
|
2107
|
-
...agentId && { agentId },
|
|
2108
|
-
...sessionId && { sessionId },
|
|
2109
|
-
...sessionKey && { sessionKey },
|
|
2110
|
-
...runId && { runId },
|
|
2111
|
-
...channelId && { channelId }
|
|
2112
|
-
};
|
|
2113
|
-
}
|
|
2114
|
-
function resolveCodexDynamicToolResultMaxChars(ctx) {
|
|
2115
|
-
return resolveAgentContextLimitValue({
|
|
2116
|
-
config: ctx?.config,
|
|
2117
|
-
agentId: ctx?.agentId,
|
|
2118
|
-
key: "toolResultMaxChars"
|
|
2119
|
-
}) ?? DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS;
|
|
2120
|
-
}
|
|
2121
|
-
function resolveAgentContextLimitValue(params) {
|
|
2122
|
-
const agents = asOptionalRecord(params.config?.agents);
|
|
2123
|
-
const defaultValue = readPositiveInteger(asOptionalRecord(asOptionalRecord(agents?.defaults)?.contextLimits)?.[params.key]);
|
|
2124
|
-
if (!params.agentId) return defaultValue;
|
|
2125
|
-
const list = agents?.list;
|
|
2126
|
-
if (!Array.isArray(list)) return defaultValue;
|
|
2127
|
-
const normalizedAgentId = normalizeAgentId(params.agentId);
|
|
2128
|
-
return readPositiveInteger(asOptionalRecord(asOptionalRecord(list.find((entry) => {
|
|
2129
|
-
const entryId = asOptionalRecord(entry)?.id;
|
|
2130
|
-
return typeof entryId === "string" && normalizeAgentId(entryId) === normalizedAgentId;
|
|
2131
|
-
}))?.contextLimits)?.[params.key]) ?? defaultValue;
|
|
2132
|
-
}
|
|
2133
|
-
function composeAbortSignals(...signals) {
|
|
2134
|
-
const activeSignals = signals.filter((signal) => Boolean(signal));
|
|
2135
|
-
if (activeSignals.length === 0) return new AbortController().signal;
|
|
2136
|
-
if (activeSignals.length === 1) return activeSignals[0];
|
|
2137
|
-
return AbortSignal.any(activeSignals);
|
|
2138
|
-
}
|
|
2139
|
-
function collectToolTelemetry(params) {
|
|
2140
|
-
if (params.isError) return;
|
|
2141
|
-
if (!params.isError && params.toolName === "cron" && isCronAddAction(params.args)) params.telemetry.successfulCronAdds = (params.telemetry.successfulCronAdds ?? 0) + 1;
|
|
2142
|
-
if (!params.isError && params.toolName === HEARTBEAT_RESPONSE_TOOL_NAME) {
|
|
2143
|
-
const response = normalizeHeartbeatToolResponse(params.result?.details);
|
|
2144
|
-
if (response) params.telemetry.heartbeatToolResponse = response;
|
|
2145
|
-
}
|
|
2146
|
-
if (!params.isError && params.result) {
|
|
2147
|
-
const media = extractToolResultMediaArtifact(params.result);
|
|
2148
|
-
if (media) {
|
|
2149
|
-
const mediaUrls = filterToolResultMediaUrls(params.toolName, media.mediaUrls, params.mediaTrustResult ?? params.result);
|
|
2150
|
-
const seen = new Set(params.telemetry.toolMediaUrls);
|
|
2151
|
-
for (const mediaUrl of mediaUrls) if (!seen.has(mediaUrl)) {
|
|
2152
|
-
seen.add(mediaUrl);
|
|
2153
|
-
params.telemetry.toolMediaUrls.push(mediaUrl);
|
|
2154
|
-
}
|
|
2155
|
-
if (media.audioAsVoice) params.telemetry.toolAudioAsVoice = true;
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
if (!isMessagingTool(params.toolName) || !isMessagingToolSendAction(params.toolName, params.args)) return;
|
|
2159
|
-
params.telemetry.didSendViaMessagingTool = true;
|
|
2160
|
-
const sourceReplyPayload = extractInternalSourceReplyPayload(params.result?.details);
|
|
2161
|
-
if (sourceReplyPayload) {
|
|
2162
|
-
params.telemetry.messagingToolSourceReplyPayloads.push(sourceReplyPayload);
|
|
2163
|
-
return;
|
|
2164
|
-
}
|
|
2165
|
-
const text = readFirstString$1(params.args, [
|
|
2166
|
-
"text",
|
|
2167
|
-
"message",
|
|
2168
|
-
"body",
|
|
2169
|
-
"content"
|
|
2170
|
-
]);
|
|
2171
|
-
if (text) params.telemetry.messagingToolSentTexts.push(text);
|
|
2172
|
-
const mediaUrls = collectMediaUrls(params.args);
|
|
2173
|
-
params.telemetry.messagingToolSentMediaUrls.push(...mediaUrls);
|
|
2174
|
-
params.telemetry.messagingToolSentTargets.push({
|
|
2175
|
-
...params.messagingTarget ?? {
|
|
2176
|
-
tool: params.toolName,
|
|
2177
|
-
provider: readFirstString$1(params.args, ["provider", "channel"]) ?? params.toolName,
|
|
2178
|
-
accountId: readFirstString$1(params.args, ["accountId", "account_id"]),
|
|
2179
|
-
to: readFirstString$1(params.args, [
|
|
2180
|
-
"to",
|
|
2181
|
-
"target",
|
|
2182
|
-
"recipient"
|
|
2183
|
-
]),
|
|
2184
|
-
threadId: readFirstString$1(params.args, [
|
|
2185
|
-
"threadId",
|
|
2186
|
-
"thread_id",
|
|
2187
|
-
"messageThreadId"
|
|
2188
|
-
])
|
|
2189
|
-
},
|
|
2190
|
-
...text ? { text } : {},
|
|
2191
|
-
...mediaUrls.length > 0 ? { mediaUrls } : {}
|
|
2192
|
-
});
|
|
2193
|
-
}
|
|
2194
|
-
function extractInternalSourceReplyPayload(details) {
|
|
2195
|
-
if (!isRecord(details) || details.sourceReplySink !== "internal-ui") return;
|
|
2196
|
-
const rawPayload = details.sourceReply;
|
|
2197
|
-
if (!isRecord(rawPayload)) return;
|
|
2198
|
-
const text = readFirstString$1(rawPayload, ["text", "message"]);
|
|
2199
|
-
const mediaUrls = collectMediaUrls(rawPayload);
|
|
2200
|
-
const mediaUrl = typeof rawPayload.mediaUrl === "string" && rawPayload.mediaUrl.trim() ? rawPayload.mediaUrl.trim() : mediaUrls[0];
|
|
2201
|
-
const payload = {
|
|
2202
|
-
...text ? { text } : {},
|
|
2203
|
-
...mediaUrl ? { mediaUrl } : {},
|
|
2204
|
-
...mediaUrls.length > 0 ? { mediaUrls } : {},
|
|
2205
|
-
...rawPayload.audioAsVoice === true ? { audioAsVoice: true } : {},
|
|
2206
|
-
...isRecord(rawPayload.presentation) ? { presentation: rawPayload.presentation } : {},
|
|
2207
|
-
...isRecord(rawPayload.interactive) ? { interactive: rawPayload.interactive } : {},
|
|
2208
|
-
...isRecord(rawPayload.channelData) ? { channelData: rawPayload.channelData } : {},
|
|
2209
|
-
...typeof details.idempotencyKey === "string" && details.idempotencyKey.trim() ? { idempotencyKey: details.idempotencyKey.trim() } : {}
|
|
2210
|
-
};
|
|
2211
|
-
return text || mediaUrls.length > 0 || payload.presentation || payload.interactive ? payload : void 0;
|
|
2212
|
-
}
|
|
2213
|
-
function readPositiveInteger(value) {
|
|
2214
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
|
|
2215
|
-
return Math.floor(value);
|
|
2216
|
-
}
|
|
2217
|
-
function isCodexToolResultError(result) {
|
|
2218
|
-
if (isToolResultError(result)) return true;
|
|
2219
|
-
const details = result.details;
|
|
2220
|
-
if (!isRecord(details)) return false;
|
|
2221
|
-
if (details.ok === true || details.success === true) return false;
|
|
2222
|
-
if (details.timedOut === true) return true;
|
|
2223
|
-
if (typeof details.exitCode === "number" && details.exitCode !== 0) return true;
|
|
2224
|
-
if (typeof details.status !== "string") return false;
|
|
2225
|
-
const status = details.status.trim().toLowerCase();
|
|
2226
|
-
return status !== "" && status !== "0" && status !== "ok" && status !== "success" && status !== "completed" && status !== "recorded" && status !== "pending" && status !== "started" && status !== "running" && status !== "yielded";
|
|
2227
|
-
}
|
|
2228
|
-
function isToolResultYield(result) {
|
|
2229
|
-
const details = result.details;
|
|
2230
|
-
if (!isRecord(details) || typeof details.status !== "string") return false;
|
|
2231
|
-
return details.status.trim().toLowerCase() === "yielded";
|
|
2232
|
-
}
|
|
2233
|
-
function isAsyncStartedToolResult(result) {
|
|
2234
|
-
const details = result.details;
|
|
2235
|
-
return isRecord(details) && details.async === true && details.status === "started";
|
|
2236
|
-
}
|
|
2237
|
-
function inferToolResultDiagnosticTerminalType(result, isError) {
|
|
2238
|
-
const details = result.details;
|
|
2239
|
-
if (isRecord(details) && typeof details.status === "string") {
|
|
2240
|
-
if (details.status.trim().toLowerCase() === "blocked") return "blocked";
|
|
2241
|
-
}
|
|
2242
|
-
return isError ? "error" : "completed";
|
|
2243
|
-
}
|
|
2244
|
-
function withDiagnosticTerminalType(response, terminalType) {
|
|
2245
|
-
Object.defineProperty(response, "diagnosticTerminalType", {
|
|
2246
|
-
configurable: true,
|
|
2247
|
-
enumerable: false,
|
|
2248
|
-
value: terminalType
|
|
2249
|
-
});
|
|
2250
|
-
return response;
|
|
2251
|
-
}
|
|
2252
|
-
function withSideEffectEvidence(response, sideEffectEvidence) {
|
|
2253
|
-
if (!sideEffectEvidence) return response;
|
|
2254
|
-
Object.defineProperty(response, "sideEffectEvidence", {
|
|
2255
|
-
configurable: true,
|
|
2256
|
-
enumerable: false,
|
|
2257
|
-
value: true
|
|
2258
|
-
});
|
|
2259
|
-
return response;
|
|
2260
|
-
}
|
|
2261
|
-
function withDynamicToolTermination(response, terminate) {
|
|
2262
|
-
if (!terminate) return response;
|
|
2263
|
-
Object.defineProperty(response, "terminate", {
|
|
2264
|
-
configurable: true,
|
|
2265
|
-
enumerable: false,
|
|
2266
|
-
value: true
|
|
2267
|
-
});
|
|
2268
|
-
return response;
|
|
2269
|
-
}
|
|
2270
|
-
function withDynamicToolAsyncStarted(response, asyncStarted) {
|
|
2271
|
-
if (!asyncStarted) return response;
|
|
2272
|
-
Object.defineProperty(response, "asyncStarted", {
|
|
2273
|
-
configurable: true,
|
|
2274
|
-
enumerable: false,
|
|
2275
|
-
value: true
|
|
2276
|
-
});
|
|
2277
|
-
return response;
|
|
2278
|
-
}
|
|
2279
|
-
function normalizeToolResultMaxChars(maxChars) {
|
|
2280
|
-
return typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS;
|
|
2281
|
-
}
|
|
2282
|
-
function convertToolContents(content, toolResultMaxChars = DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS) {
|
|
2283
|
-
const maxChars = normalizeToolResultMaxChars(toolResultMaxChars);
|
|
2284
|
-
const totalTextChars = content.reduce((total, item) => total + (item.type === "text" ? item.text.length : 0), 0);
|
|
2285
|
-
if (totalTextChars <= maxChars) return content.flatMap(convertToolContent);
|
|
2286
|
-
const noticeText = `...(OpenClaw truncated dynamic tool result: original ${totalTextChars} chars, showing ${maxChars}; rerun with narrower args.)`;
|
|
2287
|
-
const notice = `\n${noticeText}`;
|
|
2288
|
-
let remainingTextBudget = Math.max(0, maxChars - notice.length);
|
|
2289
|
-
let appendedNotice = false;
|
|
2290
|
-
const output = [];
|
|
2291
|
-
for (const item of content) {
|
|
2292
|
-
if (item.type !== "text") {
|
|
2293
|
-
output.push(...convertToolContent(item));
|
|
2294
|
-
continue;
|
|
2295
|
-
}
|
|
2296
|
-
if (appendedNotice) continue;
|
|
2297
|
-
if (notice.length >= maxChars) {
|
|
2298
|
-
output.push({
|
|
2299
|
-
type: "inputText",
|
|
2300
|
-
text: noticeText.slice(0, maxChars)
|
|
2301
|
-
});
|
|
2302
|
-
appendedNotice = true;
|
|
2303
|
-
continue;
|
|
2304
|
-
}
|
|
2305
|
-
const sliceLength = Math.min(item.text.length, remainingTextBudget);
|
|
2306
|
-
remainingTextBudget -= sliceLength;
|
|
2307
|
-
const shouldAppendNotice = remainingTextBudget <= 0;
|
|
2308
|
-
const text = item.text.slice(0, sliceLength);
|
|
2309
|
-
if (shouldAppendNotice) {
|
|
2310
|
-
output.push({
|
|
2311
|
-
type: "inputText",
|
|
2312
|
-
text: `${text.trimEnd()}${notice}`.slice(0, maxChars)
|
|
2313
|
-
});
|
|
2314
|
-
appendedNotice = true;
|
|
2315
|
-
} else if (text.length > 0) output.push({
|
|
2316
|
-
type: "inputText",
|
|
2317
|
-
text
|
|
2318
|
-
});
|
|
2319
|
-
}
|
|
2320
|
-
if (!appendedNotice) output.push({
|
|
2321
|
-
type: "inputText",
|
|
2322
|
-
text: noticeText.slice(0, maxChars)
|
|
2323
|
-
});
|
|
2324
|
-
return output;
|
|
2325
|
-
}
|
|
2326
|
-
function convertToolContent(content) {
|
|
2327
|
-
if (content.type === "text") return [{
|
|
2328
|
-
type: "inputText",
|
|
2329
|
-
text: content.text
|
|
2330
|
-
}];
|
|
2331
|
-
const imageUrl = sanitizeInlineImageDataUrl(`data:${content.mimeType};base64,${content.data}`);
|
|
2332
|
-
if (!imageUrl) return [{
|
|
2333
|
-
type: "inputText",
|
|
2334
|
-
text: invalidInlineImageText("codex dynamic tool")
|
|
2335
|
-
}];
|
|
2336
|
-
return [{
|
|
2337
|
-
type: "inputImage",
|
|
2338
|
-
imageUrl
|
|
2339
|
-
}];
|
|
2340
|
-
}
|
|
2341
|
-
function jsonObjectToRecord(value) {
|
|
2342
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2343
|
-
return value;
|
|
2344
|
-
}
|
|
2345
|
-
function readFirstString$1(record, keys) {
|
|
2346
|
-
for (const key of keys) {
|
|
2347
|
-
const value = record[key];
|
|
2348
|
-
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2349
|
-
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
function collectMediaUrls(record) {
|
|
2353
|
-
const urls = [];
|
|
2354
|
-
const pushMediaUrl = (value) => {
|
|
2355
|
-
if (typeof value === "string" && value.trim()) urls.push(value.trim());
|
|
2356
|
-
};
|
|
2357
|
-
const pushAttachment = (value) => {
|
|
2358
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2359
|
-
const attachment = value;
|
|
2360
|
-
for (const key of [
|
|
2361
|
-
"media",
|
|
2362
|
-
"mediaUrl",
|
|
2363
|
-
"path",
|
|
2364
|
-
"filePath",
|
|
2365
|
-
"fileUrl",
|
|
2366
|
-
"url"
|
|
2367
|
-
]) pushMediaUrl(attachment[key]);
|
|
2368
|
-
};
|
|
2369
|
-
for (const key of [
|
|
2370
|
-
"media",
|
|
2371
|
-
"mediaUrl",
|
|
2372
|
-
"media_url",
|
|
2373
|
-
"path",
|
|
2374
|
-
"filePath",
|
|
2375
|
-
"fileUrl",
|
|
2376
|
-
"imageUrl",
|
|
2377
|
-
"image_url"
|
|
2378
|
-
]) {
|
|
2379
|
-
const value = record[key];
|
|
2380
|
-
pushMediaUrl(value);
|
|
2381
|
-
}
|
|
2382
|
-
for (const key of [
|
|
2383
|
-
"mediaUrls",
|
|
2384
|
-
"media_urls",
|
|
2385
|
-
"imageUrls",
|
|
2386
|
-
"image_urls"
|
|
2387
|
-
]) {
|
|
2388
|
-
const value = record[key];
|
|
2389
|
-
if (!Array.isArray(value)) continue;
|
|
2390
|
-
for (const entry of value) pushMediaUrl(entry);
|
|
2391
|
-
}
|
|
2392
|
-
const attachments = record.attachments;
|
|
2393
|
-
if (Array.isArray(attachments)) for (const attachment of attachments) pushAttachment(attachment);
|
|
2394
|
-
return urls;
|
|
2395
|
-
}
|
|
2396
|
-
function isCronAddAction(args) {
|
|
2397
|
-
const action = args.action;
|
|
2398
|
-
return typeof action === "string" && action.trim().toLowerCase() === "add";
|
|
2399
|
-
}
|
|
2400
|
-
//#endregion
|
|
2401
|
-
//#region extensions/codex/src/app-server/elicitation-bridge.ts
|
|
2402
|
-
const MCP_TOOL_APPROVAL_KIND = "mcp_tool_call";
|
|
2403
|
-
const MCP_TOOL_APPROVAL_KIND_KEY = "codex_approval_kind";
|
|
2404
|
-
const MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY = "connector_name";
|
|
2405
|
-
const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY = "tool_title";
|
|
2406
|
-
const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY = "tool_description";
|
|
2407
|
-
const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY = "tool_params_display";
|
|
2408
|
-
const MCP_TOOL_APPROVAL_SOURCE_KEY = "source";
|
|
2409
|
-
const MCP_TOOL_APPROVAL_CONNECTOR_SOURCE = "connector";
|
|
2410
|
-
const CODEX_APPS_SERVER_NAME = "codex_apps";
|
|
2411
|
-
const COMPUTER_USE_APPROVAL_TITLE = "Computer Use approval";
|
|
2412
|
-
const EMPTY_OBJECT_SCHEMA = {
|
|
2413
|
-
type: "object",
|
|
2414
|
-
properties: {}
|
|
2415
|
-
};
|
|
2416
|
-
const PLUGIN_APP_ID_META_KEYS = [
|
|
2417
|
-
"app_id",
|
|
2418
|
-
"appId",
|
|
2419
|
-
"codex_app_id",
|
|
2420
|
-
"codexAppId"
|
|
2421
|
-
];
|
|
2422
|
-
const PLUGIN_CONNECTOR_ID_META_KEYS = ["connector_id", "connectorId"];
|
|
2423
|
-
const PLUGIN_NAME_META_KEYS = [
|
|
2424
|
-
"plugin_name",
|
|
2425
|
-
"pluginName",
|
|
2426
|
-
"codex_plugin_name",
|
|
2427
|
-
"codexPluginName"
|
|
2428
|
-
];
|
|
2429
|
-
const PLUGIN_CONFIG_KEY_META_KEYS = [
|
|
2430
|
-
"config_key",
|
|
2431
|
-
"configKey",
|
|
2432
|
-
"codex_config_key"
|
|
2433
|
-
];
|
|
2434
|
-
const PLUGIN_MARKETPLACE_NAME_META_KEYS = [
|
|
2435
|
-
"marketplace_name",
|
|
2436
|
-
"marketplaceName",
|
|
2437
|
-
"codex_marketplace_name",
|
|
2438
|
-
"codexMarketplaceName"
|
|
2439
|
-
];
|
|
2440
|
-
const MAX_DISPLAY_PARAM_ENTRIES = 8;
|
|
2441
|
-
const MAX_DISPLAY_PARAM_VALUE_LENGTH = 120;
|
|
2442
|
-
const MAX_DISPLAY_VALUE_ARRAY_ITEMS = 8;
|
|
2443
|
-
const MAX_DISPLAY_VALUE_OBJECT_KEYS = 8;
|
|
2444
|
-
const MAX_DISPLAY_VALUE_DEPTH = 3;
|
|
2445
|
-
const DISPLAY_TEXT_SCAN_MAX_LENGTH = 4096;
|
|
2446
|
-
const ANSI_OSC_SEQUENCE_RE = new RegExp(String.raw`(?:\u001b]|\u009d)[^\u001b\u009c\u0007]*(?:\u0007|\u001b\\|\u009c)`, "g");
|
|
2447
|
-
const ANSI_CONTROL_SEQUENCE_RE = new RegExp(String.raw`(?:\u001b\[[0-?]*[ -/]*[@-~]|\u009b[0-?]*[ -/]*[@-~]|\u001b[@-Z\\-_])`, "g");
|
|
2448
|
-
const CONTROL_CHARACTER_RE = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]+`, "g");
|
|
2449
|
-
const INVISIBLE_FORMATTING_CONTROL_RE = new RegExp(String.raw`[\u00ad\u034f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff\ufe00-\ufe0f\u{e0100}-\u{e01ef}]`, "gu");
|
|
2450
|
-
const DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE = new RegExp(String.raw`(?:\u001b\][^\u001b\u009c\u0007]*|\u009d[^\u001b\u009c\u0007]*|\u001b\[[0-?]*[ -/]*|\u009b[0-?]*[ -/]*|\u001b)$`);
|
|
2451
|
-
async function handleCodexAppServerElicitationRequest(params) {
|
|
2452
|
-
const requestParams = isJsonObject(params.requestParams) ? params.requestParams : void 0;
|
|
2453
|
-
if (!requestParams) return;
|
|
2454
|
-
if (!matchesCurrentThread(requestParams, params.threadId)) return;
|
|
2455
|
-
if (turnIdMismatches(requestParams, params.turnId)) return;
|
|
2456
|
-
const pluginResolution = resolvePluginElicitation({
|
|
2457
|
-
requestParams,
|
|
2458
|
-
pluginAppPolicyContext: params.pluginAppPolicyContext
|
|
2459
|
-
});
|
|
2460
|
-
if (pluginResolution.kind !== "not_plugin") {
|
|
2461
|
-
if (pluginResolution.kind === "decline") {
|
|
2462
|
-
logPluginElicitationDecline(pluginResolution.reason, requestParams);
|
|
2463
|
-
return declineElicitationResponse();
|
|
2464
|
-
}
|
|
2465
|
-
if (!hasExactTurnId(requestParams, params.turnId)) {
|
|
2466
|
-
logPluginElicitationDecline("missing_active_turn", requestParams);
|
|
2467
|
-
return declineElicitationResponse();
|
|
2468
|
-
}
|
|
2469
|
-
return await buildPluginPolicyElicitationResponse({
|
|
2470
|
-
entry: pluginResolution.entry,
|
|
2471
|
-
requestParams,
|
|
2472
|
-
paramsForRun: params.paramsForRun,
|
|
2473
|
-
signal: params.signal
|
|
2474
|
-
});
|
|
2475
|
-
}
|
|
2476
|
-
const approvalPrompt = readComputerUseApprovalElicitation(requestParams, params.computerUseMcpServerName) ?? readBridgeableApprovalElicitation(requestParams);
|
|
2477
|
-
if (!approvalPrompt) return;
|
|
2478
|
-
return buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
|
|
2479
|
-
paramsForRun: params.paramsForRun,
|
|
2480
|
-
title: approvalPrompt.title,
|
|
2481
|
-
description: approvalPrompt.description,
|
|
2482
|
-
allowedDecisions: approvalPrompt.allowedDecisions,
|
|
2483
|
-
signal: params.signal
|
|
2484
|
-
}));
|
|
2485
|
-
}
|
|
2486
|
-
function matchesCurrentThread(requestParams, threadId) {
|
|
2487
|
-
if (!requestParams) return false;
|
|
2488
|
-
return readString(requestParams, "threadId") === threadId;
|
|
2489
|
-
}
|
|
2490
|
-
function turnIdMismatches(requestParams, turnId) {
|
|
2491
|
-
const rawTurnId = requestParams?.turnId;
|
|
2492
|
-
return rawTurnId !== null && rawTurnId !== void 0 && rawTurnId !== turnId;
|
|
2493
|
-
}
|
|
2494
|
-
function hasExactTurnId(requestParams, turnId) {
|
|
2495
|
-
return requestParams?.turnId === turnId;
|
|
2496
|
-
}
|
|
2497
|
-
function resolvePluginElicitation(params) {
|
|
2498
|
-
const requestParams = params.requestParams;
|
|
2499
|
-
if (!requestParams) return { kind: "not_plugin" };
|
|
2500
|
-
const meta = isJsonObject(requestParams["_meta"]) ? requestParams["_meta"] : {};
|
|
2501
|
-
const context = params.pluginAppPolicyContext;
|
|
2502
|
-
const entries = context ? Object.values(context.apps) : [];
|
|
2503
|
-
const appId = readFirstString(meta, PLUGIN_APP_ID_META_KEYS) ?? readFirstString(requestParams, PLUGIN_APP_ID_META_KEYS);
|
|
2504
|
-
const connectorId = readFirstString(meta, PLUGIN_CONNECTOR_ID_META_KEYS);
|
|
2505
|
-
const isCodexConnectorApproval = isCodexConnectorApprovalElicitation(requestParams, meta);
|
|
2506
|
-
if (isCodexConnectorApproval && appId && connectorId && appId !== connectorId) return {
|
|
2507
|
-
kind: "decline",
|
|
2508
|
-
reason: "app_id_connector_id_mismatch"
|
|
2509
|
-
};
|
|
2510
|
-
if (appId) {
|
|
2511
|
-
if (!context) return {
|
|
2512
|
-
kind: "decline",
|
|
2513
|
-
reason: "missing_policy_context"
|
|
2514
|
-
};
|
|
2515
|
-
const entry = context.apps[appId];
|
|
2516
|
-
return uniquePluginMatch(entry ? [entry] : [], "app_id");
|
|
2517
|
-
}
|
|
2518
|
-
if (isCodexConnectorApproval && connectorId) {
|
|
2519
|
-
if (!context) return {
|
|
2520
|
-
kind: "decline",
|
|
2521
|
-
reason: "missing_policy_context"
|
|
2522
|
-
};
|
|
2523
|
-
const entry = context.apps[connectorId];
|
|
2524
|
-
return uniquePluginMatch(entry ? [entry] : [], "connector_id");
|
|
2525
|
-
}
|
|
2526
|
-
const serverName = readString(requestParams, "serverName");
|
|
2527
|
-
if (serverName && context) {
|
|
2528
|
-
const matches = entries.filter((entry) => entry.mcpServerNames.includes(serverName));
|
|
2529
|
-
if (matches.length > 0) return uniquePluginMatch(matches, "server_name");
|
|
2530
|
-
}
|
|
2531
|
-
const metadataResolution = resolvePluginStableMetadataMatch({
|
|
2532
|
-
meta,
|
|
2533
|
-
requestParams,
|
|
2534
|
-
entries,
|
|
2535
|
-
context
|
|
2536
|
-
});
|
|
2537
|
-
if (metadataResolution.kind !== "not_plugin") return metadataResolution;
|
|
2538
|
-
if (context && hasDisplayNameOnlyPluginMatch(meta, entries)) return {
|
|
2539
|
-
kind: "decline",
|
|
2540
|
-
reason: "display_name_only"
|
|
2541
|
-
};
|
|
2542
|
-
return { kind: "not_plugin" };
|
|
2543
|
-
}
|
|
2544
|
-
function isCodexConnectorApprovalElicitation(requestParams, meta) {
|
|
2545
|
-
return readString(requestParams, "serverName") === CODEX_APPS_SERVER_NAME && readString(meta, MCP_TOOL_APPROVAL_KIND_KEY) === MCP_TOOL_APPROVAL_KIND && readString(meta, MCP_TOOL_APPROVAL_SOURCE_KEY) === MCP_TOOL_APPROVAL_CONNECTOR_SOURCE;
|
|
2546
|
-
}
|
|
2547
|
-
function resolvePluginStableMetadataMatch(params) {
|
|
2548
|
-
const pluginName = readFirstString(params.meta, PLUGIN_NAME_META_KEYS) ?? readFirstString(params.requestParams, PLUGIN_NAME_META_KEYS);
|
|
2549
|
-
const configKey = readFirstString(params.meta, PLUGIN_CONFIG_KEY_META_KEYS) ?? readFirstString(params.requestParams, PLUGIN_CONFIG_KEY_META_KEYS);
|
|
2550
|
-
const marketplaceName = readFirstString(params.meta, PLUGIN_MARKETPLACE_NAME_META_KEYS) ?? readFirstString(params.requestParams, PLUGIN_MARKETPLACE_NAME_META_KEYS);
|
|
2551
|
-
if (!pluginName && !configKey) return { kind: "not_plugin" };
|
|
2552
|
-
if (!params.context) return {
|
|
2553
|
-
kind: "decline",
|
|
2554
|
-
reason: "missing_policy_context"
|
|
2555
|
-
};
|
|
2556
|
-
return uniquePluginMatch(params.entries.filter((entry) => {
|
|
2557
|
-
if (marketplaceName && entry.marketplaceName !== marketplaceName) return false;
|
|
2558
|
-
if (pluginName && entry.pluginName !== pluginName) return false;
|
|
2559
|
-
if (configKey && entry.configKey !== configKey) return false;
|
|
2560
|
-
return true;
|
|
2561
|
-
}), "metadata");
|
|
2562
|
-
}
|
|
2563
|
-
function uniquePluginMatch(matches, source) {
|
|
2564
|
-
if (matches.length === 1 && matches[0]) return {
|
|
2565
|
-
kind: "matched",
|
|
2566
|
-
entry: matches[0]
|
|
2567
|
-
};
|
|
2568
|
-
return {
|
|
2569
|
-
kind: "decline",
|
|
2570
|
-
reason: matches.length === 0 ? `${source}_not_enabled` : `${source}_ambiguous`
|
|
2571
|
-
};
|
|
2572
|
-
}
|
|
2573
|
-
function hasDisplayNameOnlyPluginMatch(meta, entries) {
|
|
2574
|
-
const connectorName = readString(meta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY);
|
|
2575
|
-
if (!connectorName) return false;
|
|
2576
|
-
const normalized = normalizePluginIdentityText(connectorName);
|
|
2577
|
-
return entries.some((entry) => normalizePluginIdentityText(entry.pluginName) === normalized || normalizePluginIdentityText(entry.configKey) === normalized);
|
|
2578
|
-
}
|
|
2579
|
-
function normalizePluginIdentityText(value) {
|
|
2580
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
2581
|
-
}
|
|
2582
|
-
async function buildPluginPolicyElicitationResponse(params) {
|
|
2583
|
-
const mode = resolvePluginDestructiveApprovalMode(params.entry);
|
|
2584
|
-
if (mode === "deny") {
|
|
2585
|
-
logPluginElicitationDecline("destructive_actions_disabled", params.requestParams);
|
|
2586
|
-
return declineElicitationResponse();
|
|
2587
|
-
}
|
|
2588
|
-
const approvalPrompt = readPluginApprovalElicitation(params.entry, params.requestParams);
|
|
2589
|
-
if (!approvalPrompt) {
|
|
2590
|
-
logPluginElicitationDecline("unsupported_schema", params.requestParams);
|
|
2591
|
-
return declineElicitationResponse();
|
|
2592
|
-
}
|
|
2593
|
-
const response = buildElicitationResponse(approvalPrompt, "approved-once");
|
|
2594
|
-
if (isJsonObject(response) && response.action === "accept") {
|
|
2595
|
-
if (mode === "allow") return response;
|
|
2596
|
-
return buildElicitationResponse(approvalPrompt, await requestPluginApprovalOutcome({
|
|
2597
|
-
paramsForRun: params.paramsForRun,
|
|
2598
|
-
title: approvalPrompt.title,
|
|
2599
|
-
description: approvalPrompt.description,
|
|
2600
|
-
allowedDecisions: approvalPrompt.allowedDecisions,
|
|
2601
|
-
signal: params.signal
|
|
2602
|
-
}));
|
|
2603
|
-
}
|
|
2604
|
-
logPluginElicitationDecline("unmappable_schema", params.requestParams);
|
|
2605
|
-
return declineElicitationResponse();
|
|
2606
|
-
}
|
|
2607
|
-
function resolvePluginDestructiveApprovalMode(entry) {
|
|
2608
|
-
return entry.destructiveApprovalMode ?? (entry.allowDestructiveActions ? "allow" : "deny");
|
|
2609
|
-
}
|
|
2610
|
-
function readPluginApprovalElicitation(entry, requestParams) {
|
|
2611
|
-
if (readString(requestParams, "mode") !== "form" || !isJsonObject(requestParams.requestedSchema)) return;
|
|
2612
|
-
const requestedSchema = requestParams.requestedSchema;
|
|
2613
|
-
if (readString(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
|
|
2614
|
-
const meta = isJsonObject(requestParams["_meta"]) ? requestParams["_meta"] : {};
|
|
2615
|
-
const title = sanitizeDisplayText(readString(requestParams, "message") ?? "") || "Codex plugin approval";
|
|
2616
|
-
const descriptionMeta = { ...meta };
|
|
2617
|
-
if (!readString(descriptionMeta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY)) descriptionMeta[MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY] = entry.pluginName;
|
|
2618
|
-
return {
|
|
2619
|
-
title,
|
|
2620
|
-
description: buildApprovalDescription({
|
|
2621
|
-
title,
|
|
2622
|
-
meta: descriptionMeta,
|
|
2623
|
-
requestedSchema,
|
|
2624
|
-
serverName: sanitizeOptionalDisplayText(readString(requestParams, "serverName"))
|
|
2625
|
-
}),
|
|
2626
|
-
requestedSchema,
|
|
2627
|
-
meta,
|
|
2628
|
-
persistHintsMode: "explicit",
|
|
2629
|
-
allowedDecisions: buildApprovalAllowedDecisions(requestedSchema, meta)
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
function buildApprovalAllowedDecisions(requestedSchema, meta) {
|
|
2633
|
-
return canMapPersistentApproval(requestedSchema, meta) ? [
|
|
2634
|
-
"allow-once",
|
|
2635
|
-
"allow-always",
|
|
2636
|
-
"deny"
|
|
2637
|
-
] : ["allow-once", "deny"];
|
|
2638
|
-
}
|
|
2639
|
-
function canMapPersistentApproval(requestedSchema, meta) {
|
|
2640
|
-
const persistHints = readPersistHints(meta, "explicit");
|
|
2641
|
-
if (persistHints.length > 0) return persistHints.includes("always");
|
|
2642
|
-
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
2643
|
-
return Object.entries(properties).some(([name, value]) => {
|
|
2644
|
-
const schema = isJsonObject(value) ? value : void 0;
|
|
2645
|
-
if (!schema) return false;
|
|
2646
|
-
return isPersistField({
|
|
2647
|
-
name,
|
|
2648
|
-
schema,
|
|
2649
|
-
required: false
|
|
2650
|
-
}) && chooseAlwaysPersistOptionValue(readEnumOptions(schema)) !== void 0;
|
|
2651
|
-
});
|
|
2652
|
-
}
|
|
2653
|
-
function declineElicitationResponse() {
|
|
2654
|
-
return {
|
|
2655
|
-
action: "decline",
|
|
2656
|
-
content: null,
|
|
2657
|
-
_meta: null
|
|
2658
|
-
};
|
|
2659
|
-
}
|
|
2660
|
-
function logPluginElicitationDecline(reason, requestParams) {
|
|
2661
|
-
embeddedAgentLog.debug("codex plugin elicitation declined", {
|
|
2662
|
-
reason,
|
|
2663
|
-
serverName: readString(requestParams, "serverName"),
|
|
2664
|
-
mode: readString(requestParams, "mode")
|
|
2665
|
-
});
|
|
2666
|
-
}
|
|
2667
|
-
function readBridgeableApprovalElicitation(requestParams) {
|
|
2668
|
-
if (!requestParams || readString(requestParams, "mode") !== "form" || !isJsonObject(requestParams["_meta"]) || requestParams["_meta"][MCP_TOOL_APPROVAL_KIND_KEY] !== MCP_TOOL_APPROVAL_KIND || !isJsonObject(requestParams.requestedSchema)) return;
|
|
2669
|
-
const requestedSchema = requestParams.requestedSchema;
|
|
2670
|
-
if (readString(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
|
|
2671
|
-
const title = sanitizeDisplayText(readString(requestParams, "message") ?? "") || "Codex MCP tool approval";
|
|
2672
|
-
return {
|
|
2673
|
-
title,
|
|
2674
|
-
description: buildApprovalDescription({
|
|
2675
|
-
title,
|
|
2676
|
-
meta: requestParams["_meta"],
|
|
2677
|
-
requestedSchema,
|
|
2678
|
-
serverName: sanitizeOptionalDisplayText(readString(requestParams, "serverName"))
|
|
2679
|
-
}),
|
|
2680
|
-
requestedSchema,
|
|
2681
|
-
meta: requestParams["_meta"]
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
function readComputerUseApprovalElicitation(requestParams, expectedServerName) {
|
|
2685
|
-
const serverName = readString(requestParams, "serverName");
|
|
2686
|
-
if (!serverName || !expectedServerName || serverName !== expectedServerName || readString(requestParams, "mode") !== "form") return;
|
|
2687
|
-
const requestedSchema = isJsonObject(requestParams?.requestedSchema) ? requestParams.requestedSchema : EMPTY_OBJECT_SCHEMA;
|
|
2688
|
-
if (readString(requestedSchema, "type") !== "object" || !isJsonObject(requestedSchema.properties)) return;
|
|
2689
|
-
const meta = isJsonObject(requestParams?.["_meta"]) ? requestParams["_meta"] : {};
|
|
2690
|
-
const title = sanitizeDisplayText(readString(requestParams, "message") ?? "") || COMPUTER_USE_APPROVAL_TITLE;
|
|
2691
|
-
return {
|
|
2692
|
-
title,
|
|
2693
|
-
description: buildApprovalDescription({
|
|
2694
|
-
title,
|
|
2695
|
-
meta,
|
|
2696
|
-
requestedSchema,
|
|
2697
|
-
serverName: sanitizeOptionalDisplayText(serverName)
|
|
2698
|
-
}),
|
|
2699
|
-
requestedSchema,
|
|
2700
|
-
meta
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
function buildApprovalDescription(params) {
|
|
2704
|
-
const connectorName = sanitizeOptionalDisplayText(readString(params.meta, MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY));
|
|
2705
|
-
const toolTitle = sanitizeOptionalDisplayText(readString(params.meta, MCP_TOOL_APPROVAL_TOOL_TITLE_KEY));
|
|
2706
|
-
const toolDescription = sanitizeOptionalDisplayText(readString(params.meta, MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY));
|
|
2707
|
-
const summaryLines = [
|
|
2708
|
-
connectorName && `App: ${connectorName}`,
|
|
2709
|
-
toolTitle && `Tool: ${toolTitle}`,
|
|
2710
|
-
params.serverName && `MCP server: ${params.serverName}`,
|
|
2711
|
-
toolDescription
|
|
2712
|
-
].filter((line) => Boolean(line));
|
|
2713
|
-
const paramLines = readDisplayParamLines(params.meta);
|
|
2714
|
-
const propertyLines = readPropertyDescriptionLines(params.requestedSchema);
|
|
2715
|
-
return [
|
|
2716
|
-
params.title,
|
|
2717
|
-
summaryLines.join("\n"),
|
|
2718
|
-
paramLines.length > 0 ? ["Parameters:", ...paramLines].join("\n") : "",
|
|
2719
|
-
propertyLines.length > 0 ? ["Fields:", ...propertyLines].join("\n") : ""
|
|
2720
|
-
].filter(Boolean).join("\n\n");
|
|
2721
|
-
}
|
|
2722
|
-
function readPropertyDescriptionLines(requestedSchema) {
|
|
2723
|
-
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
2724
|
-
return Object.entries(properties).map(([name, value]) => {
|
|
2725
|
-
const schema = isJsonObject(value) ? value : void 0;
|
|
2726
|
-
if (!schema) return;
|
|
2727
|
-
const propTitle = sanitizeDisplayText(readString(schema, "title") ?? "") || sanitizeDisplayText(name) || "field";
|
|
2728
|
-
const description = sanitizeOptionalDisplayText(readString(schema, "description"));
|
|
2729
|
-
return description ? `- ${propTitle}: ${description}` : `- ${propTitle}`;
|
|
2730
|
-
}).filter((line) => Boolean(line));
|
|
2731
|
-
}
|
|
2732
|
-
function readDisplayParamLines(meta) {
|
|
2733
|
-
const displayParams = meta[MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY];
|
|
2734
|
-
if (!Array.isArray(displayParams)) return [];
|
|
2735
|
-
const lines = displayParams.slice(0, MAX_DISPLAY_PARAM_ENTRIES).map((entry) => {
|
|
2736
|
-
const param = isJsonObject(entry) ? entry : void 0;
|
|
2737
|
-
if (!param) return;
|
|
2738
|
-
const name = sanitizeOptionalDisplayText(readString(param, "display_name")) ?? sanitizeOptionalDisplayText(readString(param, "name"));
|
|
2739
|
-
if (!name) return;
|
|
2740
|
-
return `- ${name}: ${formatDisplayParamValue(param.value)}`;
|
|
2741
|
-
}).filter((line) => Boolean(line));
|
|
2742
|
-
const remaining = displayParams.length - MAX_DISPLAY_PARAM_ENTRIES;
|
|
2743
|
-
return remaining > 0 ? [...lines, `- Additional parameters: ${remaining} more`] : lines;
|
|
2744
|
-
}
|
|
2745
|
-
function formatDisplayParamValue(value) {
|
|
2746
|
-
return truncateDisplayText(sanitizeDisplayText(typeof value === "string" ? value : formatDisplayJsonValue(value ?? null)), MAX_DISPLAY_PARAM_VALUE_LENGTH);
|
|
2747
|
-
}
|
|
2748
|
-
function formatDisplayJsonValue(value, depth = MAX_DISPLAY_VALUE_DEPTH) {
|
|
2749
|
-
if (value === null) return "null";
|
|
2750
|
-
if (typeof value === "string") return JSON.stringify(truncateDisplayText(sanitizeDisplayText(value), 80));
|
|
2751
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2752
|
-
if (Array.isArray(value)) {
|
|
2753
|
-
if (depth <= 0) return "[truncated]";
|
|
2754
|
-
const parts = [];
|
|
2755
|
-
const limit = Math.min(value.length, MAX_DISPLAY_VALUE_ARRAY_ITEMS);
|
|
2756
|
-
for (let i = 0; i < limit; i += 1) parts.push(formatDisplayJsonValue(value[i] ?? null, depth - 1));
|
|
2757
|
-
if (value.length > MAX_DISPLAY_VALUE_ARRAY_ITEMS) parts.push("...");
|
|
2758
|
-
return `[${parts.join(",")}]`;
|
|
2759
|
-
}
|
|
2760
|
-
if (typeof value === "object") {
|
|
2761
|
-
if (depth <= 0) return "{truncated}";
|
|
2762
|
-
const parts = [];
|
|
2763
|
-
let count = 0;
|
|
2764
|
-
let truncated = false;
|
|
2765
|
-
for (const key in value) {
|
|
2766
|
-
if (!Object.hasOwn(value, key)) continue;
|
|
2767
|
-
if (count >= MAX_DISPLAY_VALUE_OBJECT_KEYS) {
|
|
2768
|
-
truncated = true;
|
|
2769
|
-
break;
|
|
2770
|
-
}
|
|
2771
|
-
const safeKey = truncateDisplayText(sanitizeDisplayText(key), 80);
|
|
2772
|
-
parts.push(`${JSON.stringify(safeKey)}:${formatDisplayJsonValue(value[key] ?? null, depth - 1)}`);
|
|
2773
|
-
count += 1;
|
|
2774
|
-
}
|
|
2775
|
-
if (truncated) parts.push("...");
|
|
2776
|
-
return `{${parts.join(",")}}`;
|
|
2777
|
-
}
|
|
2778
|
-
return "null";
|
|
2779
|
-
}
|
|
2780
|
-
function sanitizeOptionalDisplayText(value) {
|
|
2781
|
-
return (value === void 0 ? "" : sanitizeDisplayText(value)) || void 0;
|
|
2782
|
-
}
|
|
2783
|
-
function sanitizeDisplayText(value) {
|
|
2784
|
-
const scanned = value.slice(0, DISPLAY_TEXT_SCAN_MAX_LENGTH);
|
|
2785
|
-
const clipped = value.length > DISPLAY_TEXT_SCAN_MAX_LENGTH;
|
|
2786
|
-
const sanitized = scanned.replace(ANSI_OSC_SEQUENCE_RE, "").replace(ANSI_CONTROL_SEQUENCE_RE, "").replace(DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE, "").replace(INVISIBLE_FORMATTING_CONTROL_RE, " ").replace(CONTROL_CHARACTER_RE, " ").replace(/\s+/g, " ").trim();
|
|
2787
|
-
const escaped = sanitized ? formatCodexDisplayText(sanitized) : "";
|
|
2788
|
-
return clipped && escaped ? `${escaped}...` : escaped;
|
|
2789
|
-
}
|
|
2790
|
-
function truncateDisplayText(value, maxLength) {
|
|
2791
|
-
return value.length <= maxLength ? value : `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
2792
|
-
}
|
|
2793
|
-
async function requestPluginApprovalOutcome(params) {
|
|
2794
|
-
try {
|
|
2795
|
-
const requestResult = await requestPluginApproval({
|
|
2796
|
-
paramsForRun: params.paramsForRun,
|
|
2797
|
-
title: params.title,
|
|
2798
|
-
description: params.description,
|
|
2799
|
-
severity: "warning",
|
|
2800
|
-
toolName: "codex_mcp_tool_approval",
|
|
2801
|
-
allowedDecisions: params.allowedDecisions
|
|
2802
|
-
});
|
|
2803
|
-
const approvalId = requestResult?.id;
|
|
2804
|
-
if (!approvalId) return "unavailable";
|
|
2805
|
-
return mapExecDecisionToOutcome(approvalRequestExplicitlyUnavailable(requestResult) ? null : await waitForPluginApprovalDecision({
|
|
2806
|
-
approvalId,
|
|
2807
|
-
signal: params.signal
|
|
2808
|
-
}));
|
|
2809
|
-
} catch {
|
|
2810
|
-
return params.signal?.aborted ? "cancelled" : "denied";
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
function buildElicitationResponse(approvalPrompt, outcome) {
|
|
2814
|
-
const { requestedSchema, meta } = approvalPrompt;
|
|
2815
|
-
if (outcome === "cancelled") return {
|
|
2816
|
-
action: "cancel",
|
|
2817
|
-
content: null,
|
|
2818
|
-
_meta: null
|
|
2819
|
-
};
|
|
2820
|
-
if (outcome === "denied" || outcome === "unavailable") return {
|
|
2821
|
-
action: "decline",
|
|
2822
|
-
content: null,
|
|
2823
|
-
_meta: null
|
|
2824
|
-
};
|
|
2825
|
-
const content = buildAcceptedContent(approvalPrompt, outcome);
|
|
2826
|
-
if (!content) {
|
|
2827
|
-
if (hasNoSchemaProperties(requestedSchema)) return {
|
|
2828
|
-
action: "accept",
|
|
2829
|
-
content: null,
|
|
2830
|
-
_meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
|
|
2831
|
-
};
|
|
2832
|
-
embeddedAgentLog.warn("codex MCP approval elicitation approved without a mappable response", {
|
|
2833
|
-
approvalKind: meta[MCP_TOOL_APPROVAL_KIND_KEY],
|
|
2834
|
-
fields: Object.keys(requestedSchema.properties ?? {}),
|
|
2835
|
-
outcome
|
|
2836
|
-
});
|
|
2837
|
-
return {
|
|
2838
|
-
action: "decline",
|
|
2839
|
-
content: null,
|
|
2840
|
-
_meta: null
|
|
2841
|
-
};
|
|
2842
|
-
}
|
|
2843
|
-
return {
|
|
2844
|
-
action: "accept",
|
|
2845
|
-
content,
|
|
2846
|
-
_meta: buildAcceptedMeta(meta, outcome, approvalPrompt.persistHintsMode ?? "legacy")
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
function buildAcceptedContent(approvalPrompt, outcome) {
|
|
2850
|
-
const { requestedSchema, meta } = approvalPrompt;
|
|
2851
|
-
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : void 0;
|
|
2852
|
-
if (!properties) return;
|
|
2853
|
-
const required = Array.isArray(requestedSchema.required) ? new Set(requestedSchema.required.filter((entry) => typeof entry === "string")) : /* @__PURE__ */ new Set();
|
|
2854
|
-
const content = {};
|
|
2855
|
-
let sawApprovalField = false;
|
|
2856
|
-
for (const [name, value] of Object.entries(properties)) {
|
|
2857
|
-
const schema = isJsonObject(value) ? value : void 0;
|
|
2858
|
-
if (!schema) continue;
|
|
2859
|
-
const property = {
|
|
2860
|
-
name,
|
|
2861
|
-
schema,
|
|
2862
|
-
required: required.has(name)
|
|
2863
|
-
};
|
|
2864
|
-
const next = readApprovalFieldValue(property, outcome) ?? readPersistFieldValue(property, meta, outcome, approvalPrompt.persistHintsMode ?? "legacy") ?? readFallbackFieldValue(property, outcome);
|
|
2865
|
-
if (next === void 0) {
|
|
2866
|
-
if (isApprovalField(property)) sawApprovalField = true;
|
|
2867
|
-
if (property.required) return;
|
|
2868
|
-
continue;
|
|
2869
|
-
}
|
|
2870
|
-
if (isApprovalField(property)) sawApprovalField = true;
|
|
2871
|
-
content[name] = next;
|
|
2872
|
-
}
|
|
2873
|
-
return sawApprovalField ? content : void 0;
|
|
2874
|
-
}
|
|
2875
|
-
function readApprovalFieldValue(property, outcome) {
|
|
2876
|
-
if (!isApprovalField(property)) return;
|
|
2877
|
-
if (readString(property.schema, "type") === "boolean") return true;
|
|
2878
|
-
const options = readEnumOptions(property.schema);
|
|
2879
|
-
if (options.length === 0) return;
|
|
2880
|
-
const sessionChoice = options.find((option) => isSessionApprovalOption(option));
|
|
2881
|
-
const acceptChoice = options.find((option) => isPositiveApprovalOption(option));
|
|
2882
|
-
if (outcome === "approved-session") return sessionChoice?.value ?? acceptChoice?.value;
|
|
2883
|
-
return acceptChoice?.value ?? sessionChoice?.value;
|
|
2884
|
-
}
|
|
2885
|
-
function readPersistFieldValue(property, meta, outcome, persistHintsMode) {
|
|
2886
|
-
if (!isPersistField(property) || outcome !== "approved-session") return;
|
|
2887
|
-
const persistHints = readPersistHints(meta, persistHintsMode);
|
|
2888
|
-
const options = readEnumOptions(property.schema);
|
|
2889
|
-
if (options.length === 0) return;
|
|
2890
|
-
const preferred = choosePersistHint(persistHints);
|
|
2891
|
-
if (preferred) return options.find((option) => option.value === preferred || option.label === preferred)?.value;
|
|
2892
|
-
if (persistHintsMode === "explicit") return chooseAlwaysPersistOptionValue(options);
|
|
2893
|
-
}
|
|
2894
|
-
function readDefaultValue(schema) {
|
|
2895
|
-
return schema.default;
|
|
2896
|
-
}
|
|
2897
|
-
function readFallbackFieldValue(property, outcome) {
|
|
2898
|
-
if (outcome === "approved-once" && isPersistField(property)) return;
|
|
2899
|
-
return readDefaultValue(property.schema);
|
|
2900
|
-
}
|
|
2901
|
-
function isApprovalField(property) {
|
|
2902
|
-
const haystack = propertyText(property).toLowerCase();
|
|
2903
|
-
return /\b(approve|approval|allow|accept|decision)\b/.test(haystack);
|
|
2904
|
-
}
|
|
2905
|
-
function isPersistField(property) {
|
|
2906
|
-
const haystack = propertyText(property).toLowerCase();
|
|
2907
|
-
return /\b(persist|session|always|scope)\b/.test(haystack);
|
|
2908
|
-
}
|
|
2909
|
-
function propertyText(property) {
|
|
2910
|
-
return [
|
|
2911
|
-
property.name,
|
|
2912
|
-
readString(property.schema, "title"),
|
|
2913
|
-
readString(property.schema, "description")
|
|
2914
|
-
].filter(Boolean).join(" ");
|
|
2915
|
-
}
|
|
2916
|
-
function readPersistHints(meta, mode = "legacy") {
|
|
2917
|
-
const raw = meta.persist;
|
|
2918
|
-
if (typeof raw === "string") return [raw];
|
|
2919
|
-
if (Array.isArray(raw)) return raw.filter((entry) => typeof entry === "string");
|
|
2920
|
-
return mode === "legacy" ? ["session", "always"] : [];
|
|
2921
|
-
}
|
|
2922
|
-
function buildAcceptedMeta(meta, outcome, persistHintsMode) {
|
|
2923
|
-
if (outcome !== "approved-session") return null;
|
|
2924
|
-
const persist = choosePersistHint(readPersistHints(meta, persistHintsMode));
|
|
2925
|
-
return persist ? { persist } : null;
|
|
2926
|
-
}
|
|
2927
|
-
function choosePersistHint(persistHints) {
|
|
2928
|
-
if (persistHints.includes("always")) return "always";
|
|
2929
|
-
if (persistHints.includes("session")) return "session";
|
|
2930
|
-
}
|
|
2931
|
-
function chooseAlwaysPersistOptionValue(options) {
|
|
2932
|
-
return options.find((option) => optionMatchesPersist(option, "always"))?.value;
|
|
2933
|
-
}
|
|
2934
|
-
function optionMatchesPersist(option, persist) {
|
|
2935
|
-
return option.value.toLowerCase() === persist || option.label.toLowerCase() === persist;
|
|
2936
|
-
}
|
|
2937
|
-
function hasNoSchemaProperties(requestedSchema) {
|
|
2938
|
-
const properties = isJsonObject(requestedSchema.properties) ? requestedSchema.properties : {};
|
|
2939
|
-
return Object.keys(properties).length === 0;
|
|
2940
|
-
}
|
|
2941
|
-
function readEnumOptions(schema) {
|
|
2942
|
-
if (Array.isArray(schema.enum)) {
|
|
2943
|
-
const values = schema.enum.filter((entry) => typeof entry === "string");
|
|
2944
|
-
const labels = Array.isArray(schema.enumNames) ? schema.enumNames.filter((entry) => typeof entry === "string") : [];
|
|
2945
|
-
return values.map((value, index) => ({
|
|
2946
|
-
value,
|
|
2947
|
-
label: labels[index] ?? value
|
|
2948
|
-
}));
|
|
2949
|
-
}
|
|
2950
|
-
if (Array.isArray(schema.oneOf)) return schema.oneOf.map((entry) => {
|
|
2951
|
-
const option = isJsonObject(entry) ? entry : void 0;
|
|
2952
|
-
const value = readString(option, "const");
|
|
2953
|
-
if (!value) return;
|
|
2954
|
-
return {
|
|
2955
|
-
value,
|
|
2956
|
-
label: readString(option, "title") ?? value
|
|
2957
|
-
};
|
|
2958
|
-
}).filter((entry) => Boolean(entry));
|
|
2959
|
-
return [];
|
|
2960
|
-
}
|
|
2961
|
-
function isPositiveApprovalOption(option) {
|
|
2962
|
-
const haystack = `${option.value} ${option.label}`.toLowerCase();
|
|
2963
|
-
return /\b(allow|approve|accept|yes|continue|proceed|true)\b/.test(haystack);
|
|
2964
|
-
}
|
|
2965
|
-
function isSessionApprovalOption(option) {
|
|
2966
|
-
const haystack = `${option.value} ${option.label}`.toLowerCase();
|
|
2967
|
-
return /\b(session|always|persistent)\b/.test(haystack) && /\b(allow|approve|accept)\b/.test(haystack);
|
|
2968
|
-
}
|
|
2969
|
-
function readString(record, key) {
|
|
2970
|
-
const value = record?.[key];
|
|
2971
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
2972
|
-
}
|
|
2973
|
-
function readFirstString(record, keys) {
|
|
2974
|
-
for (const key of keys) {
|
|
2975
|
-
const value = readString(record, key);
|
|
2976
|
-
if (value) return value;
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
//#endregion
|
|
2980
|
-
//#region extensions/codex/src/app-server/native-hook-relay.ts
|
|
2981
|
-
/**
|
|
2982
|
-
* Bridges Codex native hook callbacks into OpenClaw's native hook relay so
|
|
2983
|
-
* app-server tool events can still run OpenClaw policy and diagnostics.
|
|
2984
|
-
*/
|
|
2985
|
-
/** Codex hook events that can be registered through OpenClaw's native relay. */
|
|
2986
|
-
const CODEX_NATIVE_HOOK_RELAY_EVENTS = [
|
|
2987
|
-
"pre_tool_use",
|
|
2988
|
-
"post_tool_use",
|
|
2989
|
-
"permission_request",
|
|
2990
|
-
"before_agent_finalize"
|
|
2991
|
-
];
|
|
2992
|
-
const CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS = CODEX_NATIVE_HOOK_RELAY_EVENTS.filter((event) => event !== "permission_request");
|
|
2993
|
-
const CODEX_NATIVE_HOOK_RELAY_MIN_TTL_MS = 30 * 6e4;
|
|
2994
|
-
/** Extra relay lifetime after the expected turn budget, preventing late hook drops. */
|
|
2995
|
-
const CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS = 5 * 6e4;
|
|
2996
|
-
const CODEX_NATIVE_HOOK_RELAY_COMMAND_MIN_PARENT_MARGIN_MS = 250;
|
|
2997
|
-
const CODEX_NATIVE_HOOK_RELAY_COMMAND_MAX_PARENT_MARGIN_MS = 1e3;
|
|
2998
|
-
const CODEX_NATIVE_HOOK_RELAY_UNREGISTER_GRACE_MS = 1e4;
|
|
2999
|
-
const CODEX_NATIVE_HOOK_RELAY_UNREGISTER_EXTRA_GRACE_MS = 5e3;
|
|
3000
|
-
const pendingCodexNativeHookRelayUnregisters = /* @__PURE__ */ new Set();
|
|
3001
|
-
/** Defers relay unregister so late native hook subprocesses can still resolve. */
|
|
3002
|
-
function scheduleCodexNativeHookRelayUnregister(params) {
|
|
3003
|
-
let pending;
|
|
3004
|
-
const unregister = () => {
|
|
3005
|
-
if (!pending) return;
|
|
3006
|
-
const current = pending;
|
|
3007
|
-
pending = void 0;
|
|
3008
|
-
if (!pendingCodexNativeHookRelayUnregisters.delete(current)) return;
|
|
3009
|
-
params.relay.unregister();
|
|
3010
|
-
};
|
|
3011
|
-
const timeout = setTimeout(unregister, resolveCodexNativeHookRelayUnregisterGraceMs(params.hookTimeoutSec));
|
|
3012
|
-
pending = {
|
|
3013
|
-
timeout,
|
|
3014
|
-
unregister
|
|
3015
|
-
};
|
|
3016
|
-
pendingCodexNativeHookRelayUnregisters.add(pending);
|
|
3017
|
-
timeout.unref();
|
|
3018
|
-
}
|
|
3019
|
-
/** Computes the delayed unregister window from Codex's hook timeout. */
|
|
3020
|
-
function resolveCodexNativeHookRelayUnregisterGraceMs(hookTimeoutSec) {
|
|
3021
|
-
const hookTimeoutMs = typeof hookTimeoutSec === "number" && Number.isFinite(hookTimeoutSec) && hookTimeoutSec > 0 ? finiteSecondsToTimerSafeMilliseconds(Math.ceil(hookTimeoutSec)) ?? 0 : 0;
|
|
3022
|
-
return Math.max(CODEX_NATIVE_HOOK_RELAY_UNREGISTER_GRACE_MS, addTimerTimeoutGraceMs(hookTimeoutMs, CODEX_NATIVE_HOOK_RELAY_UNREGISTER_EXTRA_GRACE_MS) ?? 0);
|
|
3023
|
-
}
|
|
3024
|
-
/** Registers an OpenClaw native hook relay for a Codex app-server turn. */
|
|
3025
|
-
function createCodexNativeHookRelay(params) {
|
|
3026
|
-
if (params.options?.enabled === false) return;
|
|
3027
|
-
return registerNativeHookRelay({
|
|
3028
|
-
provider: "codex",
|
|
3029
|
-
relayId: buildCodexNativeHookRelayId({
|
|
3030
|
-
agentId: params.agentId,
|
|
3031
|
-
sessionId: params.sessionId,
|
|
3032
|
-
sessionKey: params.sessionKey
|
|
3033
|
-
}),
|
|
3034
|
-
...params.generation ? { generation: params.generation } : {},
|
|
3035
|
-
...params.generationMismatchGraceMs ? { generationMismatchGraceMs: params.generationMismatchGraceMs } : {},
|
|
3036
|
-
...params.agentId ? { agentId: params.agentId } : {},
|
|
3037
|
-
sessionId: params.sessionId,
|
|
3038
|
-
...params.sessionKey ? { sessionKey: params.sessionKey } : {},
|
|
3039
|
-
...params.config ? { config: params.config } : {},
|
|
3040
|
-
runId: params.runId,
|
|
3041
|
-
...params.channelId ? { channelId: params.channelId } : {},
|
|
3042
|
-
allowedEvents: params.events,
|
|
3043
|
-
ttlMs: resolveCodexNativeHookRelayTtlMs({
|
|
3044
|
-
explicitTtlMs: params.options?.ttlMs,
|
|
3045
|
-
attemptTimeoutMs: params.attemptTimeoutMs,
|
|
3046
|
-
startupTimeoutMs: params.startupTimeoutMs,
|
|
3047
|
-
turnStartTimeoutMs: params.turnStartTimeoutMs
|
|
3048
|
-
}),
|
|
3049
|
-
signal: params.signal,
|
|
3050
|
-
command: {
|
|
3051
|
-
nice: 10,
|
|
3052
|
-
timeoutMs: params.options?.gatewayTimeoutMs
|
|
3053
|
-
}
|
|
3054
|
-
});
|
|
3055
|
-
}
|
|
3056
|
-
/** Selects the native hook events Codex should install for the current approval mode. */
|
|
3057
|
-
function resolveCodexNativeHookRelayEvents(params) {
|
|
3058
|
-
if (params.configuredEvents?.length) return params.configuredEvents;
|
|
3059
|
-
return params.appServer.approvalPolicy === "never" ? CODEX_NATIVE_HOOK_RELAY_EVENTS : CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS;
|
|
3060
|
-
}
|
|
3061
|
-
/** Derives the native hook relay TTL from the turn budget unless explicitly configured. */
|
|
3062
|
-
function resolveCodexNativeHookRelayTtlMs(params) {
|
|
3063
|
-
if (params.explicitTtlMs !== void 0) return params.explicitTtlMs;
|
|
3064
|
-
const relayBudgetMs = params.attemptTimeoutMs + params.startupTimeoutMs + params.turnStartTimeoutMs + CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS;
|
|
3065
|
-
return Math.max(CODEX_NATIVE_HOOK_RELAY_MIN_TTL_MS, Math.floor(relayBudgetMs));
|
|
3066
|
-
}
|
|
3067
|
-
/** Builds a stable relay id scoped to the agent and session identity. */
|
|
3068
|
-
function buildCodexNativeHookRelayId(params) {
|
|
3069
|
-
const hash = createHash("sha256");
|
|
3070
|
-
hash.update("openclaw:codex:native-hook-relay:v1");
|
|
3071
|
-
hash.update("\0");
|
|
3072
|
-
hash.update(params.agentId?.trim() || "");
|
|
3073
|
-
hash.update("\0");
|
|
3074
|
-
hash.update(params.sessionKey?.trim() || params.sessionId);
|
|
3075
|
-
return `codex-${hash.digest("hex").slice(0, 40)}`;
|
|
3076
|
-
}
|
|
3077
|
-
const CODEX_HOOK_EVENT_BY_NATIVE_EVENT = {
|
|
3078
|
-
pre_tool_use: "PreToolUse",
|
|
3079
|
-
post_tool_use: "PostToolUse",
|
|
3080
|
-
permission_request: "PermissionRequest",
|
|
3081
|
-
before_agent_finalize: "Stop"
|
|
3082
|
-
};
|
|
3083
|
-
const CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT = {
|
|
3084
|
-
pre_tool_use: "pre_tool_use",
|
|
3085
|
-
post_tool_use: "post_tool_use",
|
|
3086
|
-
permission_request: "permission_request",
|
|
3087
|
-
before_agent_finalize: "stop"
|
|
3088
|
-
};
|
|
3089
|
-
const CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS = ["/<session-flags>/config.toml", "<session-flags>/config.toml"];
|
|
3090
|
-
/** Builds the Codex config overlay that installs trusted command hooks for relay events. */
|
|
3091
|
-
function buildCodexNativeHookRelayConfig(params) {
|
|
3092
|
-
const events = params.events?.length ? params.events : CODEX_NATIVE_HOOK_RELAY_EVENTS;
|
|
3093
|
-
const selectedEvents = new Set(events);
|
|
3094
|
-
const config = { "features.hooks": true };
|
|
3095
|
-
const hookState = {};
|
|
3096
|
-
for (const event of CODEX_NATIVE_HOOK_RELAY_EVENTS) {
|
|
3097
|
-
const codexEvent = CODEX_HOOK_EVENT_BY_NATIVE_EVENT[event];
|
|
3098
|
-
const selected = selectedEvents.has(event);
|
|
3099
|
-
const shouldRelay = params.relay.shouldRelayEvent(event);
|
|
3100
|
-
if (!selected || !shouldRelay && !(selected && event === "pre_tool_use" && !shouldRelay)) {
|
|
3101
|
-
if (selected || params.clearOmittedEvents) config[`hooks.${codexEvent}`] = [];
|
|
3102
|
-
if (params.clearOmittedEvents) for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] = { enabled: false };
|
|
3103
|
-
continue;
|
|
3104
|
-
}
|
|
3105
|
-
const timeout = normalizeHookTimeoutSec(params.hookTimeoutSec);
|
|
3106
|
-
const command = params.relay.commandForEvent(event, { timeoutMs: resolveCodexNativeHookRelayCommandTimeoutMs(timeout) });
|
|
3107
|
-
config[`hooks.${codexEvent}`] = [{ hooks: [{
|
|
3108
|
-
type: "command",
|
|
3109
|
-
command,
|
|
3110
|
-
timeout,
|
|
3111
|
-
async: false,
|
|
3112
|
-
statusMessage: "OpenClaw native hook relay"
|
|
3113
|
-
}] }];
|
|
3114
|
-
const state = {
|
|
3115
|
-
enabled: true,
|
|
3116
|
-
trusted_hash: codexCommandHookTrustedHash({
|
|
3117
|
-
event,
|
|
3118
|
-
command,
|
|
3119
|
-
timeout,
|
|
3120
|
-
statusMessage: "OpenClaw native hook relay"
|
|
3121
|
-
})
|
|
3122
|
-
};
|
|
3123
|
-
for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] = state;
|
|
3124
|
-
}
|
|
3125
|
-
config["hooks.state"] = hookState;
|
|
3126
|
-
return config;
|
|
3127
|
-
}
|
|
3128
|
-
/** Builds a Codex config overlay that disables native hooks and clears hook arrays. */
|
|
3129
|
-
function buildCodexNativeHookRelayDisabledConfig() {
|
|
3130
|
-
return {
|
|
3131
|
-
"features.hooks": false,
|
|
3132
|
-
"hooks.PreToolUse": [],
|
|
3133
|
-
"hooks.PostToolUse": [],
|
|
3134
|
-
"hooks.PermissionRequest": [],
|
|
3135
|
-
"hooks.Stop": []
|
|
3136
|
-
};
|
|
3137
|
-
}
|
|
3138
|
-
function normalizeHookTimeoutSec(value) {
|
|
3139
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.ceil(value) : 5;
|
|
3140
|
-
}
|
|
3141
|
-
function resolveCodexNativeHookRelayCommandTimeoutMs(hookTimeoutSec) {
|
|
3142
|
-
const parentTimeoutMs = finiteSecondsToTimerSafeMilliseconds(normalizeHookTimeoutSec(hookTimeoutSec)) ?? 5e3;
|
|
3143
|
-
const parentMarginMs = Math.min(CODEX_NATIVE_HOOK_RELAY_COMMAND_MAX_PARENT_MARGIN_MS, Math.max(CODEX_NATIVE_HOOK_RELAY_COMMAND_MIN_PARENT_MARGIN_MS, Math.floor(parentTimeoutMs / 5)));
|
|
3144
|
-
return Math.max(1, parentTimeoutMs - parentMarginMs);
|
|
3145
|
-
}
|
|
3146
|
-
function codexCommandHookTrustedHash(params) {
|
|
3147
|
-
const identity = {
|
|
3148
|
-
event_name: CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[params.event],
|
|
3149
|
-
hooks: [{
|
|
3150
|
-
async: false,
|
|
3151
|
-
command: params.command,
|
|
3152
|
-
statusMessage: params.statusMessage,
|
|
3153
|
-
timeout: params.timeout,
|
|
3154
|
-
type: "command"
|
|
3155
|
-
}]
|
|
3156
|
-
};
|
|
3157
|
-
return `sha256:${createHash("sha256").update(JSON.stringify(sortJsonValue(identity))).digest("hex")}`;
|
|
3158
|
-
}
|
|
3159
|
-
function sortJsonValue(value) {
|
|
3160
|
-
if (!value || typeof value !== "object") return value;
|
|
3161
|
-
if (Array.isArray(value)) return value.map(sortJsonValue);
|
|
3162
|
-
const sorted = {};
|
|
3163
|
-
for (const key of Object.keys(value).toSorted()) sorted[key] = sortJsonValue(value[key]);
|
|
3164
|
-
return sorted;
|
|
3165
|
-
}
|
|
3166
|
-
//#endregion
|
|
3167
|
-
//#region extensions/codex/src/app-server/provider-capabilities.ts
|
|
3168
|
-
async function readConfiguredProviderWebSearchSupport(params) {
|
|
3169
|
-
return (await params.client.request("modelProvider/capabilities/read", {}, {
|
|
3170
|
-
timeoutMs: params.timeoutMs,
|
|
3171
|
-
signal: params.signal
|
|
3172
|
-
})).webSearch ? "supported" : "unsupported";
|
|
3173
|
-
}
|
|
3174
|
-
async function resolveCodexProviderWebSearchSupportForClient(params) {
|
|
3175
|
-
const modelProviderOverride = params.modelProviderOverride?.trim().toLowerCase();
|
|
3176
|
-
if (modelProviderOverride === "openai") return "supported";
|
|
3177
|
-
if (modelProviderOverride) return "unsupported";
|
|
3178
|
-
try {
|
|
3179
|
-
return await readConfiguredProviderWebSearchSupport(params);
|
|
3180
|
-
} catch {
|
|
3181
|
-
return "unknown";
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
async function resolveCodexProviderWebSearchSupport(params) {
|
|
3185
|
-
let client;
|
|
3186
|
-
try {
|
|
3187
|
-
client = await params.clientFactory(params.appServer.start, params.authProfileId, params.agentDir, params.config, { timeoutMs: params.appServer.requestTimeoutMs });
|
|
3188
|
-
return await resolveCodexProviderWebSearchSupportForClient({
|
|
3189
|
-
client,
|
|
3190
|
-
timeoutMs: params.appServer.requestTimeoutMs,
|
|
3191
|
-
modelProviderOverride: params.modelProviderOverride,
|
|
3192
|
-
signal: params.signal
|
|
3193
|
-
});
|
|
3194
|
-
} catch {
|
|
3195
|
-
return "unknown";
|
|
3196
|
-
} finally {
|
|
3197
|
-
if (client) releaseLeasedSharedCodexAppServerClient(client);
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3200
|
-
//#endregion
|
|
3201
|
-
export { CODEX_POST_REASONING_REPLY_IDLE_TIMEOUT_MS as A, resolveCodexMessageToolProvider as C, shouldWarnCodexDynamicToolBuildStageSummary as D, shouldRequireCodexSandboxExecServerEnvironment as E, resolveCodexTurnTerminalIdleTimeoutMs as F, withCodexStartupTimeout as I, resolveCodexStartupTimeoutMs as M, resolveCodexTurnAssistantCompletionIdleTimeoutMs as N, filterToolsForVisionInputs as O, resolveCodexTurnCompletionIdleTimeoutMs as P, resolveCodexExternalSandboxPolicyForOpenClawSandbox as S, shouldEnableCodexAppServerNativeToolSurface as T, createCodexDynamicToolBuildStageTracker as _, buildCodexNativeHookRelayConfig as a, resolveCodexAppServerExecutionCwd as b, resolveCodexNativeHookRelayEvents as c, handleCodexAppServerElicitationRequest as d, createCodexDynamicToolBridge as f, buildDynamicTools as g, emitDynamicToolTerminalDiagnostic as h, CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS as i, resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs as j, handleCodexAppServerApprovalRequest as k, resolveCodexNativeHookRelayTtlMs as l, emitDynamicToolStartedDiagnostic as m, resolveCodexProviderWebSearchSupportForClient as n, buildCodexNativeHookRelayDisabledConfig as o, emitDynamicToolErrorDiagnostic as p, CODEX_NATIVE_HOOK_RELAY_EVENTS as r, createCodexNativeHookRelay as s, resolveCodexProviderWebSearchSupport as t, scheduleCodexNativeHookRelayUnregister as u, disableCodexPluginThreadConfig as v, resolveCodexSandboxEnvironmentSelection as w, resolveCodexAppServerHookChannelId as x, formatCodexDynamicToolBuildStageSummary as y };
|