@openclaw/codex 2026.5.20 → 2026.5.24-beta.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.
Files changed (28) hide show
  1. package/dist/{client-Ab4Fw77C.js → client-1sSy4p3z.js} +25 -16
  2. package/dist/{client-factory-BbesECdh.js → client-factory-Bk6i4FnW.js} +1 -1
  3. package/dist/{command-handlers-Gceg_rBZ.js → command-handlers-CcTABuem.js} +64 -7
  4. package/dist/{compact-BaSDurdH.js → compact-DnTgL6UT.js} +186 -40
  5. package/dist/{computer-use-B4iTog6z.js → computer-use-Hdq1WgTA.js} +2 -2
  6. package/dist/{config-DqIp6oTk.js → config-DDMrwfJl.js} +21 -2
  7. package/dist/dynamic-tools-Bq717oJR.js +497 -0
  8. package/dist/harness.js +15 -5
  9. package/dist/index.js +10 -9
  10. package/dist/media-understanding-provider.js +6 -3
  11. package/dist/{models-C8MdOXGD.js → models-DtGLkqMP.js} +1 -1
  12. package/dist/{node-cli-sessions-T1olB1SH.js → node-cli-sessions-9CAqnIaA.js} +23 -9
  13. package/dist/{command-formatters-BVBnEgyA.js → notification-correlation-qKY_sgga.js} +66 -15
  14. package/dist/provider-catalog.js +26 -41
  15. package/dist/provider.js +4 -4
  16. package/dist/{request-BePR77QD.js → request-D93E78SA.js} +12 -3
  17. package/dist/{run-attempt-CRHVB240.js → run-attempt-CT1N__qp.js} +3828 -1391
  18. package/dist/sandbox-guard-CTnEWuor.js +233 -0
  19. package/dist/{session-binding-DEiYHgJ_.js → session-binding-Bw_mfIW2.js} +4 -2
  20. package/dist/{shared-client-DCxJx5QU.js → shared-client-CFCUGEVs.js} +2 -2
  21. package/dist/{side-question-DlJAUYst.js → side-question-CYFMTA1O.js} +20 -15
  22. package/dist/test-api.js +4 -2
  23. package/dist/{thread-lifecycle-Cgi62SSl.js → thread-lifecycle-DGoaguJh.js} +628 -592
  24. package/dist/{vision-tools-B2wt6ecs.js → vision-tools-DOnxzH2y.js} +7 -3
  25. package/npm-shrinkwrap.json +1934 -0
  26. package/openclaw.plugin.json +29 -0
  27. package/package.json +8 -6
  28. package/dist/plugin-activation-BkQkPLOY.js +0 -452
@@ -1,593 +1,17 @@
1
- import { r as codexSandboxPolicyForTurn, u as resolveCodexPluginsPolicy } from "./config-DqIp6oTk.js";
1
+ import { d as resolveCodexPluginsPolicy, r as codexSandboxPolicyForTurn, t as CODEX_PLUGINS_MARKETPLACE_NAME } from "./config-DDMrwfJl.js";
2
2
  import { n as assertCodexThreadResumeResponse, r as assertCodexThreadStartResponse } from "./protocol-validators-_gKDcd0x.js";
3
3
  import { t as isJsonObject } from "./protocol-oeJQu4rs.js";
4
4
  import { CODEX_GPT5_HEARTBEAT_PROMPT_OVERLAY } from "./prompt-overlay.js";
5
5
  import { isModernCodexModel } from "./provider.js";
6
- import { i as isCodexAppServerConnectionClosedError } from "./client-Ab4Fw77C.js";
7
- import { i as readCodexAppServerBinding, n as isCodexAppServerNativeAuthProfile, o as writeCodexAppServerBinding, t as clearCodexAppServerBinding } from "./session-binding-DEiYHgJ_.js";
8
- import { a as defaultCodexAppInventoryCache, o as serializeCodexAppInventoryError, r as readCodexPluginInventory, t as ensureCodexPluginActivation } from "./plugin-activation-BkQkPLOY.js";
6
+ import { a as isCodexAppServerConnectionClosedError } from "./client-1sSy4p3z.js";
7
+ import { i as readCodexAppServerBinding, n as isCodexAppServerNativeAuthProfile, o as writeCodexAppServerBinding, t as clearCodexAppServerBinding } from "./session-binding-Bw_mfIW2.js";
9
8
  import crypto from "node:crypto";
10
- import { HEARTBEAT_RESPONSE_TOOL_NAME, createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, embeddedAgentLog, extractToolResultMediaArtifact, filterToolResultMediaUrls, isActiveHarnessContextEngine, isMessagingTool, isMessagingToolSendAction, isToolWrappedWithBeforeToolCallHook, normalizeHeartbeatToolResponse, runAgentHarnessAfterToolCallHook, setBeforeToolCallDiagnosticsEnabled, wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
11
- import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
9
+ import { embeddedAgentLog, isActiveHarnessContextEngine } from "openclaw/plugin-sdk/agent-harness-runtime";
12
10
  import { buildCodexUserMcpServersThreadConfigPatch } from "openclaw/plugin-sdk/codex-mcp-projection";
13
11
  import { listRegisteredPluginAgentPromptGuidance } from "openclaw/plugin-sdk/plugin-runtime";
14
12
  import { redactSensitiveFieldValue, redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
15
- //#region extensions/codex/src/app-server/dynamic-tool-profile.ts
16
- const CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES = [
17
- "read",
18
- "write",
19
- "edit",
20
- "apply_patch",
21
- "exec",
22
- "process",
23
- "update_plan",
24
- "tool_call",
25
- "tool_describe",
26
- "tool_search",
27
- "tool_search_code"
28
- ];
29
- const DYNAMIC_TOOL_NAME_ALIASES = {
30
- bash: "exec",
31
- "apply-patch": "apply_patch"
32
- };
33
- function normalizeCodexDynamicToolName(name) {
34
- const normalized = name.trim().toLowerCase();
35
- return DYNAMIC_TOOL_NAME_ALIASES[normalized] ?? normalized;
36
- }
37
- function isForcedPrivateQaCodexRuntime(env = process.env) {
38
- return env.OPENCLAW_BUILD_PRIVATE_QA === "1" && env.OPENCLAW_QA_FORCE_RUNTIME?.trim().toLowerCase() === "codex";
39
- }
40
- function resolveCodexDynamicToolsLoading(config, env = process.env) {
41
- return isForcedPrivateQaCodexRuntime(env) ? "direct" : config.codexDynamicToolsLoading ?? "searchable";
42
- }
43
- function filterCodexDynamicTools(tools, config, env = process.env) {
44
- const excludes = /* @__PURE__ */ new Set();
45
- if (!isForcedPrivateQaCodexRuntime(env)) for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) excludes.add(name);
46
- for (const name of config.codexDynamicToolsExclude ?? []) {
47
- const trimmed = normalizeCodexDynamicToolName(name);
48
- if (trimmed) excludes.add(trimmed);
49
- }
50
- return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(normalizeCodexDynamicToolName(tool.name)));
51
- }
52
- //#endregion
53
- //#region extensions/codex/src/app-server/image-payload-sanitizer.ts
54
- const DATA_URL_PREFIX = "data:";
55
- const IMAGE_OMITTED_TEXT = "omitted image payload: invalid inline image data";
56
- const IMAGE_SIGNATURES = [
57
- {
58
- mime: "image/png",
59
- matches: (buffer) => buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10
60
- },
61
- {
62
- mime: "image/jpeg",
63
- matches: (buffer) => buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255
64
- },
65
- {
66
- mime: "image/webp",
67
- matches: (buffer) => buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP"
68
- },
69
- {
70
- mime: "image/gif",
71
- matches: (buffer) => buffer.length >= 6 && (buffer.subarray(0, 6).toString("ascii") === "GIF87a" || buffer.subarray(0, 6).toString("ascii") === "GIF89a")
72
- }
73
- ];
74
- function startsWithDataUrl(value) {
75
- return value.slice(0, 5).toLowerCase() === DATA_URL_PREFIX;
76
- }
77
- function canonicalizeBase64(base64) {
78
- let cleaned = "";
79
- let padding = 0;
80
- let sawPadding = false;
81
- for (let i = 0; i < base64.length; i += 1) {
82
- const code = base64.charCodeAt(i);
83
- if (code <= 32) continue;
84
- if (code === 61) {
85
- padding += 1;
86
- if (padding > 2) return;
87
- sawPadding = true;
88
- cleaned += "=";
89
- continue;
90
- }
91
- if (sawPadding || !(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return;
92
- cleaned += base64[i];
93
- }
94
- if (!cleaned || cleaned.length % 4 !== 0) return;
95
- return cleaned;
96
- }
97
- function sniffImageMime(buffer) {
98
- return IMAGE_SIGNATURES.find((signature) => signature.matches(buffer))?.mime;
99
- }
100
- function parseImageDataUrl(value) {
101
- if (!startsWithDataUrl(value)) return {
102
- metadata: [],
103
- payload: value
104
- };
105
- const commaIndex = value.indexOf(",");
106
- if (commaIndex < 0) return;
107
- return {
108
- metadata: value.slice(5, commaIndex).split(";").map((part) => part.trim()),
109
- payload: value.slice(commaIndex + 1)
110
- };
111
- }
112
- function metadataAllowsImageBase64(metadata) {
113
- const [mimeType, ...options] = metadata;
114
- return mimeType !== void 0 && mimeType.toLowerCase().startsWith("image/") && options.some((part) => part.toLowerCase() === "base64");
115
- }
116
- function sanitizeInlineImageDataUrl(imageUrl) {
117
- const parsed = parseImageDataUrl(imageUrl);
118
- if (!parsed) return;
119
- if (parsed.metadata.length === 0) return imageUrl;
120
- if (!metadataAllowsImageBase64(parsed.metadata)) return;
121
- const canonicalPayload = canonicalizeBase64(parsed.payload);
122
- if (!canonicalPayload) return;
123
- const sniffedMimeType = sniffImageMime(Buffer.from(canonicalPayload, "base64"));
124
- if (!sniffedMimeType) return;
125
- return `data:${sniffedMimeType};base64,${canonicalPayload}`;
126
- }
127
- function invalidInlineImageText(label) {
128
- return `[${label}] ${IMAGE_OMITTED_TEXT}`;
129
- }
130
- function isRecord$1(value) {
131
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
132
- }
133
- function sanitizeImageContentRecord(record, label) {
134
- if (record.type === "image" && typeof record.data === "string") {
135
- const mimeType = typeof record.mimeType === "string" ? record.mimeType : "image/png";
136
- const imageUrl = sanitizeInlineImageDataUrl(`data:${mimeType};base64,${record.data}`);
137
- if (!imageUrl) return {
138
- type: "text",
139
- text: invalidInlineImageText(label)
140
- };
141
- const commaIndex = imageUrl.indexOf(",");
142
- const mime = imageUrl.slice(5, commaIndex).split(";")[0] ?? mimeType;
143
- return {
144
- ...record,
145
- mimeType: mime,
146
- data: imageUrl.slice(commaIndex + 1)
147
- };
148
- }
149
- if (record.type === "inputImage" && typeof record.imageUrl === "string") {
150
- const imageUrl = sanitizeInlineImageDataUrl(record.imageUrl);
151
- return imageUrl ? {
152
- ...record,
153
- imageUrl
154
- } : {
155
- type: "inputText",
156
- text: invalidInlineImageText(label)
157
- };
158
- }
159
- if (record.type === "input_image" && typeof record.image_url === "string") {
160
- const imageUrl = sanitizeInlineImageDataUrl(record.image_url);
161
- return imageUrl ? {
162
- ...record,
163
- image_url: imageUrl
164
- } : {
165
- type: "input_text",
166
- text: invalidInlineImageText(label)
167
- };
168
- }
169
- }
170
- function sanitizeCodexHistoryImagePayloads(value, label) {
171
- if (Array.isArray(value)) return value.map((entry) => sanitizeCodexHistoryImagePayloads(entry, label));
172
- if (!isRecord$1(value)) return value;
173
- const imageRecord = sanitizeImageContentRecord(value, label);
174
- if (imageRecord) return imageRecord;
175
- const next = {};
176
- for (const [key, child] of Object.entries(value)) next[key] = sanitizeCodexHistoryImagePayloads(child, label);
177
- return next;
178
- }
179
- //#endregion
180
- //#region extensions/codex/src/app-server/dynamic-tools.ts
181
- const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
182
- const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = new Set(["sessions_yield"]);
183
- const DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS = 16e3;
184
- function createCodexDynamicToolBridge(params) {
185
- const toolResultHookContext = toToolResultHookContext(params.hookContext);
186
- const toolResultMaxChars = resolveCodexDynamicToolResultMaxChars(params.hookContext);
187
- const tools = params.tools.map((tool) => {
188
- if (isToolWrappedWithBeforeToolCallHook(tool)) {
189
- setBeforeToolCallDiagnosticsEnabled(tool, false);
190
- return tool;
191
- }
192
- return wrapToolWithBeforeToolCallHook(tool, params.hookContext, { emitDiagnostics: false });
193
- });
194
- const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
195
- const telemetry = {
196
- didSendViaMessagingTool: false,
197
- messagingToolSentTexts: [],
198
- messagingToolSentMediaUrls: [],
199
- messagingToolSentTargets: [],
200
- messagingToolSourceReplyPayloads: [],
201
- toolMediaUrls: [],
202
- toolAudioAsVoice: false
203
- };
204
- const middlewareRunner = createAgentToolResultMiddlewareRunner({
205
- runtime: "codex",
206
- ...toolResultHookContext
207
- });
208
- const legacyExtensionRunner = createCodexAppServerToolResultExtensionRunner(toolResultHookContext);
209
- const directToolNames = new Set([...ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES, ...params.directToolNames ?? []]);
210
- return {
211
- specs: tools.map((tool) => createCodexDynamicToolSpec({
212
- tool,
213
- loading: params.loading ?? "searchable",
214
- directToolNames
215
- })),
216
- telemetry,
217
- handleToolCall: async (call, options) => {
218
- const tool = toolMap.get(call.tool);
219
- if (!tool) return {
220
- contentItems: [{
221
- type: "inputText",
222
- text: `Unknown OpenClaw tool: ${call.tool}`
223
- }],
224
- success: false
225
- };
226
- const args = jsonObjectToRecord(call.arguments);
227
- const startedAt = Date.now();
228
- const signal = composeAbortSignals(params.signal, options?.signal);
229
- try {
230
- const preparedArgs = tool.prepareArguments ? tool.prepareArguments(args) : args;
231
- const rawResult = await tool.execute(call.callId, preparedArgs, signal);
232
- const rawIsError = isToolResultError(rawResult);
233
- const middlewareResult = await middlewareRunner.applyToolResultMiddleware({
234
- threadId: call.threadId,
235
- turnId: call.turnId,
236
- toolCallId: call.callId,
237
- toolName: tool.name,
238
- args,
239
- isError: rawIsError,
240
- result: rawResult
241
- });
242
- const result = await legacyExtensionRunner.applyToolResultExtensions({
243
- threadId: call.threadId,
244
- turnId: call.turnId,
245
- toolCallId: call.callId,
246
- toolName: tool.name,
247
- args,
248
- result: middlewareResult
249
- });
250
- const resultIsError = rawIsError || isToolResultError(result);
251
- collectToolTelemetry({
252
- toolName: tool.name,
253
- args,
254
- result,
255
- mediaTrustResult: rawResult,
256
- telemetry,
257
- isError: resultIsError
258
- });
259
- runAgentHarnessAfterToolCallHook({
260
- toolName: tool.name,
261
- toolCallId: call.callId,
262
- runId: toolResultHookContext.runId,
263
- agentId: toolResultHookContext.agentId,
264
- sessionId: toolResultHookContext.sessionId,
265
- sessionKey: toolResultHookContext.sessionKey,
266
- channelId: toolResultHookContext.channelId,
267
- startArgs: args,
268
- result,
269
- startedAt
270
- });
271
- return withDiagnosticTerminalType({
272
- contentItems: convertToolContents(result.content, toolResultMaxChars),
273
- success: !resultIsError
274
- }, inferToolResultDiagnosticTerminalType(result, resultIsError));
275
- } catch (error) {
276
- collectToolTelemetry({
277
- toolName: tool.name,
278
- args,
279
- result: void 0,
280
- telemetry,
281
- isError: true
282
- });
283
- runAgentHarnessAfterToolCallHook({
284
- toolName: tool.name,
285
- toolCallId: call.callId,
286
- runId: toolResultHookContext.runId,
287
- agentId: toolResultHookContext.agentId,
288
- sessionId: toolResultHookContext.sessionId,
289
- sessionKey: toolResultHookContext.sessionKey,
290
- channelId: toolResultHookContext.channelId,
291
- startArgs: args,
292
- error: error instanceof Error ? error.message : String(error),
293
- startedAt
294
- });
295
- return withDiagnosticTerminalType({
296
- contentItems: [{
297
- type: "inputText",
298
- text: error instanceof Error ? error.message : String(error)
299
- }],
300
- success: false
301
- }, "error");
302
- }
303
- }
304
- };
305
- }
306
- function createCodexDynamicToolSpec(params) {
307
- const base = {
308
- name: params.tool.name,
309
- description: params.tool.description,
310
- inputSchema: toJsonValue(params.tool.parameters)
311
- };
312
- if (params.loading === "direct" || params.directToolNames.has(params.tool.name)) return base;
313
- return {
314
- ...base,
315
- namespace: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
316
- deferLoading: true
317
- };
318
- }
319
- function toToolResultHookContext(ctx) {
320
- const { agentId, sessionId, sessionKey, runId, channelId } = ctx ?? {};
321
- return {
322
- ...agentId && { agentId },
323
- ...sessionId && { sessionId },
324
- ...sessionKey && { sessionKey },
325
- ...runId && { runId },
326
- ...channelId && { channelId }
327
- };
328
- }
329
- function resolveCodexDynamicToolResultMaxChars(ctx) {
330
- return resolveAgentContextLimitValue({
331
- config: ctx?.config,
332
- agentId: ctx?.agentId,
333
- key: "toolResultMaxChars"
334
- }) ?? DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS;
335
- }
336
- function resolveAgentContextLimitValue(params) {
337
- const agents = readRecord(params.config?.agents);
338
- const defaultValue = readPositiveInteger(readRecord(readRecord(agents?.defaults)?.contextLimits)?.[params.key]);
339
- if (!params.agentId) return defaultValue;
340
- const list = agents?.list;
341
- if (!Array.isArray(list)) return defaultValue;
342
- const normalizedAgentId = normalizeAgentId(params.agentId);
343
- return readPositiveInteger(readRecord(readRecord(list.find((entry) => {
344
- const entryId = readRecord(entry)?.id;
345
- return typeof entryId === "string" && normalizeAgentId(entryId) === normalizedAgentId;
346
- }))?.contextLimits)?.[params.key]) ?? defaultValue;
347
- }
348
- function composeAbortSignals(...signals) {
349
- const activeSignals = signals.filter((signal) => Boolean(signal));
350
- if (activeSignals.length === 0) return new AbortController().signal;
351
- if (activeSignals.length === 1) return activeSignals[0];
352
- return AbortSignal.any(activeSignals);
353
- }
354
- function collectToolTelemetry(params) {
355
- if (params.isError) return;
356
- if (!params.isError && params.toolName === "cron" && isCronAddAction(params.args)) params.telemetry.successfulCronAdds = (params.telemetry.successfulCronAdds ?? 0) + 1;
357
- if (!params.isError && params.toolName === HEARTBEAT_RESPONSE_TOOL_NAME) {
358
- const response = normalizeHeartbeatToolResponse(params.result?.details);
359
- if (response) params.telemetry.heartbeatToolResponse = response;
360
- }
361
- if (!params.isError && params.result) {
362
- const media = extractToolResultMediaArtifact(params.result);
363
- if (media) {
364
- const mediaUrls = filterToolResultMediaUrls(params.toolName, media.mediaUrls, params.mediaTrustResult ?? params.result);
365
- const seen = new Set(params.telemetry.toolMediaUrls);
366
- for (const mediaUrl of mediaUrls) if (!seen.has(mediaUrl)) {
367
- seen.add(mediaUrl);
368
- params.telemetry.toolMediaUrls.push(mediaUrl);
369
- }
370
- if (media.audioAsVoice) params.telemetry.toolAudioAsVoice = true;
371
- }
372
- }
373
- if (!isMessagingTool(params.toolName) || !isMessagingToolSendAction(params.toolName, params.args)) return;
374
- params.telemetry.didSendViaMessagingTool = true;
375
- const sourceReplyPayload = extractInternalSourceReplyPayload(params.result?.details);
376
- if (sourceReplyPayload) {
377
- params.telemetry.messagingToolSourceReplyPayloads.push(sourceReplyPayload);
378
- return;
379
- }
380
- const text = readFirstString(params.args, [
381
- "text",
382
- "message",
383
- "body",
384
- "content"
385
- ]);
386
- if (text) params.telemetry.messagingToolSentTexts.push(text);
387
- const mediaUrls = collectMediaUrls(params.args);
388
- params.telemetry.messagingToolSentMediaUrls.push(...mediaUrls);
389
- params.telemetry.messagingToolSentTargets.push({
390
- tool: params.toolName,
391
- provider: readFirstString(params.args, ["provider", "channel"]) ?? params.toolName,
392
- accountId: readFirstString(params.args, ["accountId", "account_id"]),
393
- to: readFirstString(params.args, [
394
- "to",
395
- "target",
396
- "recipient"
397
- ]),
398
- threadId: readFirstString(params.args, [
399
- "threadId",
400
- "thread_id",
401
- "messageThreadId"
402
- ]),
403
- ...text ? { text } : {},
404
- ...mediaUrls.length > 0 ? { mediaUrls } : {}
405
- });
406
- }
407
- function extractInternalSourceReplyPayload(details) {
408
- if (!isRecord(details) || details.sourceReplySink !== "internal-ui") return;
409
- const rawPayload = details.sourceReply;
410
- if (!isRecord(rawPayload)) return;
411
- const text = readFirstString(rawPayload, ["text", "message"]);
412
- const mediaUrls = collectMediaUrls(rawPayload);
413
- const mediaUrl = typeof rawPayload.mediaUrl === "string" && rawPayload.mediaUrl.trim() ? rawPayload.mediaUrl.trim() : mediaUrls[0];
414
- const payload = {
415
- ...text ? { text } : {},
416
- ...mediaUrl ? { mediaUrl } : {},
417
- ...mediaUrls.length > 0 ? { mediaUrls } : {},
418
- ...rawPayload.audioAsVoice === true ? { audioAsVoice: true } : {},
419
- ...isRecord(rawPayload.presentation) ? { presentation: rawPayload.presentation } : {},
420
- ...isRecord(rawPayload.interactive) ? { interactive: rawPayload.interactive } : {},
421
- ...isRecord(rawPayload.channelData) ? { channelData: rawPayload.channelData } : {},
422
- ...typeof details.idempotencyKey === "string" && details.idempotencyKey.trim() ? { idempotencyKey: details.idempotencyKey.trim() } : {}
423
- };
424
- return text || mediaUrls.length > 0 || payload.presentation || payload.interactive ? payload : void 0;
425
- }
426
- function isRecord(value) {
427
- return value !== null && typeof value === "object" && !Array.isArray(value);
428
- }
429
- function readRecord(value) {
430
- return isRecord(value) ? value : void 0;
431
- }
432
- function readPositiveInteger(value) {
433
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
434
- return Math.floor(value);
435
- }
436
- function isToolResultError(result) {
437
- const details = result.details;
438
- if (!isRecord(details)) return false;
439
- if (details.timedOut === true) return true;
440
- if (typeof details.exitCode === "number" && details.exitCode !== 0) return true;
441
- if (typeof details.status !== "string") return false;
442
- const status = details.status.trim().toLowerCase();
443
- return status !== "" && status !== "0" && status !== "ok" && status !== "success" && status !== "completed" && status !== "recorded" && status !== "running";
444
- }
445
- function inferToolResultDiagnosticTerminalType(result, isError) {
446
- const details = result.details;
447
- if (isRecord(details) && typeof details.status === "string") {
448
- if (details.status.trim().toLowerCase() === "blocked") return "blocked";
449
- }
450
- return isError ? "error" : "completed";
451
- }
452
- function withDiagnosticTerminalType(response, terminalType) {
453
- Object.defineProperty(response, "diagnosticTerminalType", {
454
- configurable: true,
455
- enumerable: false,
456
- value: terminalType
457
- });
458
- return response;
459
- }
460
- function normalizeToolResultMaxChars(maxChars) {
461
- return typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS;
462
- }
463
- function convertToolContents(content, toolResultMaxChars = DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS) {
464
- const maxChars = normalizeToolResultMaxChars(toolResultMaxChars);
465
- const totalTextChars = content.reduce((total, item) => total + (item.type === "text" ? item.text.length : 0), 0);
466
- if (totalTextChars <= maxChars) return content.flatMap(convertToolContent);
467
- const noticeText = `...(OpenClaw truncated dynamic tool result: original ${totalTextChars} chars, showing ${maxChars}; rerun with narrower args.)`;
468
- const notice = `\n${noticeText}`;
469
- let remainingTextBudget = Math.max(0, maxChars - notice.length);
470
- let appendedNotice = false;
471
- const output = [];
472
- for (const item of content) {
473
- if (item.type !== "text") {
474
- output.push(...convertToolContent(item));
475
- continue;
476
- }
477
- if (appendedNotice) continue;
478
- if (notice.length >= maxChars) {
479
- output.push({
480
- type: "inputText",
481
- text: noticeText.slice(0, maxChars)
482
- });
483
- appendedNotice = true;
484
- continue;
485
- }
486
- const sliceLength = Math.min(item.text.length, remainingTextBudget);
487
- remainingTextBudget -= sliceLength;
488
- const shouldAppendNotice = remainingTextBudget <= 0;
489
- const text = item.text.slice(0, sliceLength);
490
- if (shouldAppendNotice) {
491
- output.push({
492
- type: "inputText",
493
- text: `${text.trimEnd()}${notice}`.slice(0, maxChars)
494
- });
495
- appendedNotice = true;
496
- } else if (text.length > 0) output.push({
497
- type: "inputText",
498
- text
499
- });
500
- }
501
- if (!appendedNotice) output.push({
502
- type: "inputText",
503
- text: noticeText.slice(0, maxChars)
504
- });
505
- return output;
506
- }
507
- function convertToolContent(content) {
508
- if (content.type === "text") return [{
509
- type: "inputText",
510
- text: content.text
511
- }];
512
- const imageUrl = sanitizeInlineImageDataUrl(`data:${content.mimeType};base64,${content.data}`);
513
- if (!imageUrl) return [{
514
- type: "inputText",
515
- text: invalidInlineImageText("codex dynamic tool")
516
- }];
517
- return [{
518
- type: "inputImage",
519
- imageUrl
520
- }];
521
- }
522
- function toJsonValue(value) {
523
- try {
524
- const text = JSON.stringify(value);
525
- if (!text) return {};
526
- return JSON.parse(text);
527
- } catch {
528
- return {};
529
- }
530
- }
531
- function jsonObjectToRecord(value) {
532
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
533
- return value;
534
- }
535
- function readFirstString(record, keys) {
536
- for (const key of keys) {
537
- const value = record[key];
538
- if (typeof value === "string" && value.trim()) return value.trim();
539
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
540
- }
541
- }
542
- function collectMediaUrls(record) {
543
- const urls = [];
544
- const pushMediaUrl = (value) => {
545
- if (typeof value === "string" && value.trim()) urls.push(value.trim());
546
- };
547
- const pushAttachment = (value) => {
548
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
549
- const attachment = value;
550
- for (const key of [
551
- "media",
552
- "mediaUrl",
553
- "path",
554
- "filePath",
555
- "fileUrl",
556
- "url"
557
- ]) pushMediaUrl(attachment[key]);
558
- };
559
- for (const key of [
560
- "media",
561
- "mediaUrl",
562
- "media_url",
563
- "path",
564
- "filePath",
565
- "fileUrl",
566
- "imageUrl",
567
- "image_url"
568
- ]) {
569
- const value = record[key];
570
- pushMediaUrl(value);
571
- }
572
- for (const key of [
573
- "mediaUrls",
574
- "media_urls",
575
- "imageUrls",
576
- "image_urls"
577
- ]) {
578
- const value = record[key];
579
- if (!Array.isArray(value)) continue;
580
- for (const entry of value) pushMediaUrl(entry);
581
- }
582
- const attachments = record.attachments;
583
- if (Array.isArray(attachments)) for (const attachment of attachments) pushAttachment(attachment);
584
- return urls;
585
- }
586
- function isCronAddAction(args) {
587
- const action = args.action;
588
- return typeof action === "string" && action.trim().toLowerCase() === "add";
589
- }
590
- //#endregion
13
+ import "node:fs/promises";
14
+ import "node:path";
591
15
  //#region extensions/codex/src/app-server/context-engine-projection.ts
592
16
  const CONTEXT_HEADER = "OpenClaw assembled context for this turn:";
593
17
  const CONTEXT_OPEN = "<conversation_context>";
@@ -599,7 +23,7 @@ const MAX_RENDERED_CONTEXT_CHARS = 1e6;
599
23
  const DEFAULT_TEXT_PART_CHARS = 6e3;
600
24
  const MAX_TEXT_PART_CHARS = 128e3;
601
25
  const APPROX_RENDERED_CHARS_PER_TOKEN = 4;
602
- const DEFAULT_PROJECTION_RESERVE_TOKENS = 2e4;
26
+ const DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS = 2e4;
603
27
  const MIN_PROMPT_BUDGET_RATIO = .5;
604
28
  const MIN_PROMPT_BUDGET_TOKENS = 8e3;
605
29
  /**
@@ -643,11 +67,11 @@ function resolveCodexContextEngineProjectionReserveTokens(params) {
643
67
  const compaction = asRecord(asRecord(asRecord(params.config)?.agents)?.defaults)?.compaction;
644
68
  const configuredReserveTokens = toNonNegativeInt(asRecord(compaction)?.reserveTokens);
645
69
  const configuredReserveTokensFloor = toNonNegativeInt(asRecord(compaction)?.reserveTokensFloor);
646
- if (configuredReserveTokens !== void 0) return Math.max(configuredReserveTokens, configuredReserveTokensFloor ?? DEFAULT_PROJECTION_RESERVE_TOKENS);
70
+ if (configuredReserveTokens !== void 0) return Math.max(configuredReserveTokens, configuredReserveTokensFloor ?? 2e4);
647
71
  if (configuredReserveTokensFloor !== void 0) return configuredReserveTokensFloor;
648
72
  }
649
73
  function resolveProjectionPromptBudgetTokens(params) {
650
- const requestedReserveTokens = typeof params.reserveTokens === "number" && Number.isFinite(params.reserveTokens) && params.reserveTokens >= 0 ? Math.floor(params.reserveTokens) : DEFAULT_PROJECTION_RESERVE_TOKENS;
74
+ const requestedReserveTokens = typeof params.reserveTokens === "number" && Number.isFinite(params.reserveTokens) && params.reserveTokens >= 0 ? Math.floor(params.reserveTokens) : DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS;
651
75
  const minPromptBudget = Math.min(MIN_PROMPT_BUDGET_TOKENS, Math.max(1, Math.floor(params.contextTokenBudget * MIN_PROMPT_BUDGET_RATIO)));
652
76
  const effectiveReserveTokens = Math.min(requestedReserveTokens, Math.max(0, params.contextTokenBudget - minPromptBudget));
653
77
  return Math.max(1, params.contextTokenBudget - effectiveReserveTokens);
@@ -800,6 +224,579 @@ function truncateOlderContext(text, maxChars) {
800
224
  return `${marker}${text.slice(text.length - tailChars).trimStart()}`;
801
225
  }
802
226
  //#endregion
227
+ //#region extensions/codex/src/app-server/image-payload-sanitizer.ts
228
+ const DATA_URL_PREFIX = "data:";
229
+ const IMAGE_OMITTED_TEXT = "omitted image payload: invalid inline image data";
230
+ const IMAGE_SIGNATURES = [
231
+ {
232
+ mime: "image/png",
233
+ matches: (buffer) => buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10
234
+ },
235
+ {
236
+ mime: "image/jpeg",
237
+ matches: (buffer) => buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255
238
+ },
239
+ {
240
+ mime: "image/webp",
241
+ matches: (buffer) => buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP"
242
+ },
243
+ {
244
+ mime: "image/gif",
245
+ matches: (buffer) => buffer.length >= 6 && (buffer.subarray(0, 6).toString("ascii") === "GIF87a" || buffer.subarray(0, 6).toString("ascii") === "GIF89a")
246
+ }
247
+ ];
248
+ function startsWithDataUrl(value) {
249
+ return value.slice(0, 5).toLowerCase() === DATA_URL_PREFIX;
250
+ }
251
+ function canonicalizeBase64(base64) {
252
+ let cleaned = "";
253
+ let padding = 0;
254
+ let sawPadding = false;
255
+ for (let i = 0; i < base64.length; i += 1) {
256
+ const code = base64.charCodeAt(i);
257
+ if (code <= 32) continue;
258
+ if (code === 61) {
259
+ padding += 1;
260
+ if (padding > 2) return;
261
+ sawPadding = true;
262
+ cleaned += "=";
263
+ continue;
264
+ }
265
+ if (sawPadding || !(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return;
266
+ cleaned += base64[i];
267
+ }
268
+ if (!cleaned || cleaned.length % 4 !== 0) return;
269
+ return cleaned;
270
+ }
271
+ function sniffImageMime(buffer) {
272
+ return IMAGE_SIGNATURES.find((signature) => signature.matches(buffer))?.mime;
273
+ }
274
+ function parseImageDataUrl(value) {
275
+ if (!startsWithDataUrl(value)) return {
276
+ metadata: [],
277
+ payload: value
278
+ };
279
+ const commaIndex = value.indexOf(",");
280
+ if (commaIndex < 0) return;
281
+ return {
282
+ metadata: value.slice(5, commaIndex).split(";").map((part) => part.trim()),
283
+ payload: value.slice(commaIndex + 1)
284
+ };
285
+ }
286
+ function metadataAllowsImageBase64(metadata) {
287
+ const [mimeType, ...options] = metadata;
288
+ return mimeType !== void 0 && mimeType.toLowerCase().startsWith("image/") && options.some((part) => part.toLowerCase() === "base64");
289
+ }
290
+ function sanitizeInlineImageDataUrl(imageUrl) {
291
+ const parsed = parseImageDataUrl(imageUrl);
292
+ if (!parsed) return;
293
+ if (parsed.metadata.length === 0) return imageUrl;
294
+ if (!metadataAllowsImageBase64(parsed.metadata)) return;
295
+ const canonicalPayload = canonicalizeBase64(parsed.payload);
296
+ if (!canonicalPayload) return;
297
+ const sniffedMimeType = sniffImageMime(Buffer.from(canonicalPayload, "base64"));
298
+ if (!sniffedMimeType) return;
299
+ return `data:${sniffedMimeType};base64,${canonicalPayload}`;
300
+ }
301
+ function invalidInlineImageText(label) {
302
+ return `[${label}] ${IMAGE_OMITTED_TEXT}`;
303
+ }
304
+ function isRecord$1(value) {
305
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
306
+ }
307
+ function sanitizeImageContentRecord(record, label) {
308
+ if (record.type === "image" && typeof record.data === "string") {
309
+ const mimeType = typeof record.mimeType === "string" ? record.mimeType : "image/png";
310
+ const imageUrl = sanitizeInlineImageDataUrl(`data:${mimeType};base64,${record.data}`);
311
+ if (!imageUrl) return {
312
+ type: "text",
313
+ text: invalidInlineImageText(label)
314
+ };
315
+ const commaIndex = imageUrl.indexOf(",");
316
+ const mime = imageUrl.slice(5, commaIndex).split(";")[0] ?? mimeType;
317
+ return {
318
+ ...record,
319
+ mimeType: mime,
320
+ data: imageUrl.slice(commaIndex + 1)
321
+ };
322
+ }
323
+ if (record.type === "inputImage" && typeof record.imageUrl === "string") {
324
+ const imageUrl = sanitizeInlineImageDataUrl(record.imageUrl);
325
+ return imageUrl ? {
326
+ ...record,
327
+ imageUrl
328
+ } : {
329
+ type: "inputText",
330
+ text: invalidInlineImageText(label)
331
+ };
332
+ }
333
+ if (record.type === "input_image" && typeof record.image_url === "string") {
334
+ const imageUrl = sanitizeInlineImageDataUrl(record.image_url);
335
+ return imageUrl ? {
336
+ ...record,
337
+ image_url: imageUrl
338
+ } : {
339
+ type: "input_text",
340
+ text: invalidInlineImageText(label)
341
+ };
342
+ }
343
+ }
344
+ function sanitizeCodexHistoryImagePayloads(value, label) {
345
+ if (Array.isArray(value)) return value.map((entry) => sanitizeCodexHistoryImagePayloads(entry, label));
346
+ if (!isRecord$1(value)) return value;
347
+ const imageRecord = sanitizeImageContentRecord(value, label);
348
+ if (imageRecord) return imageRecord;
349
+ const next = {};
350
+ for (const [key, child] of Object.entries(value)) next[key] = sanitizeCodexHistoryImagePayloads(child, label);
351
+ return next;
352
+ }
353
+ const MAX_SERIALIZED_ERROR_MESSAGE_LENGTH = 500;
354
+ var CodexAppInventoryCache = class {
355
+ constructor(options = {}) {
356
+ this.entries = /* @__PURE__ */ new Map();
357
+ this.inFlight = /* @__PURE__ */ new Map();
358
+ this.refreshTokens = /* @__PURE__ */ new Map();
359
+ this.diagnostics = /* @__PURE__ */ new Map();
360
+ this.revision = 0;
361
+ this.ttlMs = options.ttlMs ?? 36e5;
362
+ }
363
+ read(params) {
364
+ const nowMs = params.nowMs ?? Date.now();
365
+ const entry = this.entries.get(params.key);
366
+ if (!entry) {
367
+ const refreshScheduled = params.suppressRefresh ? false : this.scheduleRefresh(params);
368
+ return {
369
+ state: "missing",
370
+ key: params.key,
371
+ revision: this.revision,
372
+ refreshScheduled,
373
+ ...this.diagnostics.get(params.key) ? { diagnostic: this.diagnostics.get(params.key) } : {}
374
+ };
375
+ }
376
+ const state = entry.invalidated || entry.expiresAtMs <= nowMs ? "stale" : "fresh";
377
+ const refreshScheduled = state === "fresh" && !params.forceRefetch ? false : this.scheduleRefresh(params);
378
+ return {
379
+ state,
380
+ key: params.key,
381
+ revision: entry.revision,
382
+ snapshot: stripEntryState(entry),
383
+ refreshScheduled,
384
+ ...entry.lastError ? { diagnostic: entry.lastError } : {}
385
+ };
386
+ }
387
+ refreshNow(params) {
388
+ return this.refresh(params);
389
+ }
390
+ invalidate(key, reason, nowMs = Date.now()) {
391
+ this.revision += 1;
392
+ const diagnostic = {
393
+ message: reason,
394
+ atMs: nowMs
395
+ };
396
+ const entry = this.entries.get(key);
397
+ if (entry) {
398
+ entry.invalidated = true;
399
+ entry.lastError = diagnostic;
400
+ entry.revision = this.revision;
401
+ } else this.diagnostics.set(key, diagnostic);
402
+ return this.revision;
403
+ }
404
+ clear() {
405
+ this.entries.clear();
406
+ this.inFlight.clear();
407
+ this.refreshTokens.clear();
408
+ this.diagnostics.clear();
409
+ this.revision = 0;
410
+ }
411
+ getRevision() {
412
+ return this.revision;
413
+ }
414
+ scheduleRefresh(params) {
415
+ if (this.inFlight.has(params.key) && !params.forceRefetch) return true;
416
+ const promise = this.refresh(params);
417
+ this.inFlight.set(params.key, promise);
418
+ promise.catch(() => void 0);
419
+ return true;
420
+ }
421
+ async refresh(params) {
422
+ const existing = this.inFlight.get(params.key);
423
+ if (existing && !params.forceRefetch) return existing;
424
+ const refreshToken = (this.refreshTokens.get(params.key) ?? 0) + 1;
425
+ this.refreshTokens.set(params.key, refreshToken);
426
+ const promise = this.refreshUncoalesced(params, refreshToken);
427
+ this.inFlight.set(params.key, promise);
428
+ try {
429
+ return await promise;
430
+ } finally {
431
+ if (this.inFlight.get(params.key) === promise) this.inFlight.delete(params.key);
432
+ }
433
+ }
434
+ async refreshUncoalesced(params, refreshToken) {
435
+ const nowMs = params.nowMs ?? Date.now();
436
+ try {
437
+ const apps = await listAllApps(params.request, params.forceRefetch ?? false);
438
+ this.revision += 1;
439
+ const snapshot = {
440
+ key: params.key,
441
+ apps,
442
+ fetchedAtMs: nowMs,
443
+ expiresAtMs: nowMs + this.ttlMs,
444
+ revision: this.revision
445
+ };
446
+ if (this.refreshTokens.get(params.key) === refreshToken) {
447
+ this.entries.set(params.key, {
448
+ ...snapshot,
449
+ invalidated: false
450
+ });
451
+ this.diagnostics.delete(params.key);
452
+ }
453
+ return snapshot;
454
+ } catch (error) {
455
+ const diagnostic = {
456
+ message: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
457
+ atMs: nowMs
458
+ };
459
+ this.diagnostics.set(params.key, diagnostic);
460
+ const entry = this.entries.get(params.key);
461
+ if (entry) entry.lastError = diagnostic;
462
+ embeddedAgentLog.warn("codex app inventory refresh failed", {
463
+ forceRefetch: params.forceRefetch === true,
464
+ keyFingerprint: fingerprintInventoryCacheKey(params.key),
465
+ error: serializeCodexAppInventoryError(error)
466
+ });
467
+ throw error;
468
+ }
469
+ }
470
+ };
471
+ function serializeCodexAppInventoryError(error) {
472
+ const record = isRecord(error) ? error : void 0;
473
+ const data = record && "data" in record ? redactErrorData(record.data) : void 0;
474
+ return {
475
+ name: error instanceof Error ? error.name : typeof record?.name === "string" ? record.name : void 0,
476
+ message: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)),
477
+ ...typeof record?.code === "number" ? { code: record.code } : {},
478
+ ...data !== void 0 ? { data } : {}
479
+ };
480
+ }
481
+ const defaultCodexAppInventoryCache = new CodexAppInventoryCache();
482
+ function buildCodexAppInventoryCacheKey(input) {
483
+ return JSON.stringify({
484
+ codexHome: input.codexHome ?? null,
485
+ endpoint: input.endpoint ?? null,
486
+ authProfileId: input.authProfileId ?? null,
487
+ accountId: input.accountId ?? null,
488
+ envApiKeyFingerprint: input.envApiKeyFingerprint ?? null,
489
+ appServerVersion: input.appServerVersion ?? null
490
+ });
491
+ }
492
+ async function listAllApps(request, forceRefetch) {
493
+ const apps = [];
494
+ let cursor;
495
+ do {
496
+ const response = await request("app/list", {
497
+ cursor,
498
+ limit: 100,
499
+ forceRefetch
500
+ });
501
+ apps.push(...response.data);
502
+ cursor = response.nextCursor;
503
+ } while (cursor);
504
+ return apps;
505
+ }
506
+ function stripEntryState(entry) {
507
+ const { invalidated: _invalidated, ...snapshot } = entry;
508
+ return snapshot;
509
+ }
510
+ function fingerprintInventoryCacheKey(key) {
511
+ let hash = 0;
512
+ for (let index = 0; index < key.length; index += 1) hash = hash * 31 + key.charCodeAt(index) >>> 0;
513
+ return hash.toString(16).padStart(8, "0");
514
+ }
515
+ function isRecord(value) {
516
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
517
+ }
518
+ function redactErrorData(value, depth = 0) {
519
+ if (value === void 0) return;
520
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
521
+ if (depth > 6) return "[truncated]";
522
+ if (Array.isArray(value)) return value.map((entry) => redactErrorData(entry, depth + 1) ?? null);
523
+ if (isRecord(value)) {
524
+ const redacted = {};
525
+ for (const [key, entry] of Object.entries(value)) redacted[key] = isSensitiveErrorDataKey(key) ? "<redacted>" : redactErrorData(entry, depth + 1) ?? null;
526
+ return redacted;
527
+ }
528
+ if (typeof value === "string" && value.length > 500) return `${value.slice(0, 500)}...`;
529
+ if (typeof value === "string") return value;
530
+ if (typeof value === "bigint") return value.toString();
531
+ if (typeof value === "symbol") return value.description ? `Symbol(${value.description})` : "Symbol()";
532
+ if (typeof value === "function") return value.name ? `[function ${value.name}]` : "[function]";
533
+ return "[unserializable]";
534
+ }
535
+ function sanitizeErrorMessage(message) {
536
+ const htmlStart = message.search(/<html[\s>]/i);
537
+ const redacted = (htmlStart >= 0 ? `${message.slice(0, htmlStart).trimEnd()} [HTML response body omitted]` : message).replace(/([?&][^=\s"'<>]*(?:api[_-]?key|authorization|cookie|credential|password|secret|token|tk)[^=\s"'<>]*=)[^&\s"'<>]+/gi, "$1<redacted>");
538
+ return redacted.length > MAX_SERIALIZED_ERROR_MESSAGE_LENGTH ? `${redacted.slice(0, MAX_SERIALIZED_ERROR_MESSAGE_LENGTH)}...` : redacted;
539
+ }
540
+ function isSensitiveErrorDataKey(key) {
541
+ return /api[_-]?key|authorization|cookie|credential|password|secret|token/i.test(key);
542
+ }
543
+ //#endregion
544
+ //#region extensions/codex/src/app-server/plugin-inventory.ts
545
+ async function readCodexPluginInventory(params) {
546
+ const policy = params.policy ?? resolveCodexPluginsPolicy(params.pluginConfig);
547
+ if (!policy.enabled) return {
548
+ policy,
549
+ records: [],
550
+ diagnostics: [{
551
+ code: "disabled",
552
+ message: "Native Codex plugin support is disabled."
553
+ }]
554
+ };
555
+ const appInventory = readCachedAppInventory(params);
556
+ const marketplaceEntry = (await params.request("plugin/list", { cwds: [] })).marketplaces.find((marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME);
557
+ if (!marketplaceEntry) return {
558
+ policy,
559
+ records: [],
560
+ diagnostics: policy.pluginPolicies.filter((pluginPolicy) => pluginPolicy.enabled).map((pluginPolicy) => ({
561
+ code: "marketplace_missing",
562
+ plugin: pluginPolicy,
563
+ message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.`
564
+ })),
565
+ ...appInventory ? { appInventory } : {}
566
+ };
567
+ const marketplace = marketplaceRef(marketplaceEntry);
568
+ const diagnostics = [];
569
+ const records = [];
570
+ if (appInventory?.state === "missing") diagnostics.push({
571
+ code: "app_inventory_missing",
572
+ message: "Cached Codex app inventory is missing; plugin apps are excluded for this setup."
573
+ });
574
+ else if (appInventory?.state === "stale") diagnostics.push({
575
+ code: "app_inventory_stale",
576
+ message: "Cached Codex app inventory is stale; using stale app readiness and refreshing."
577
+ });
578
+ for (const pluginPolicy of policy.pluginPolicies) {
579
+ if (!pluginPolicy.enabled) continue;
580
+ const summary = findPluginSummary(marketplaceEntry, pluginPolicy.pluginName);
581
+ if (!summary) {
582
+ diagnostics.push({
583
+ code: "plugin_missing",
584
+ plugin: pluginPolicy,
585
+ message: `${pluginPolicy.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.`
586
+ });
587
+ continue;
588
+ }
589
+ const detail = await readPluginDetail(params, marketplace, pluginPolicy, diagnostics);
590
+ const ownedAppIds = detail?.apps.map((app) => app.id).filter(Boolean).toSorted() ?? [];
591
+ const appOwnership = resolveAppOwnership({
592
+ detail,
593
+ appInventory,
594
+ summary
595
+ });
596
+ if (appOwnership === "ambiguous") diagnostics.push({
597
+ code: "app_ownership_ambiguous",
598
+ plugin: pluginPolicy,
599
+ message: `${pluginPolicy.pluginName} has only display-name app matches; apps are not exposed until ownership is stable.`
600
+ });
601
+ if (summary.installed && !summary.enabled) diagnostics.push({
602
+ code: "plugin_disabled",
603
+ plugin: pluginPolicy,
604
+ message: `${pluginPolicy.pluginName} is installed in Codex but disabled.`
605
+ });
606
+ const apps = resolveOwnedApps({
607
+ pluginPolicy,
608
+ detail,
609
+ appInventory
610
+ });
611
+ records.push({
612
+ policy: pluginPolicy,
613
+ summary,
614
+ ...detail ? { detail } : {},
615
+ activationRequired: !summary.installed || !summary.enabled,
616
+ authRequired: apps.some((app) => app.needsAuth || !app.accessible),
617
+ appOwnership,
618
+ ownedAppIds,
619
+ apps
620
+ });
621
+ }
622
+ return {
623
+ policy,
624
+ marketplace,
625
+ records,
626
+ diagnostics,
627
+ ...appInventory ? { appInventory } : {}
628
+ };
629
+ }
630
+ function findOpenAiCuratedPluginSummary(listed, pluginName) {
631
+ const marketplaceEntry = listed.marketplaces.find((marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME);
632
+ if (!marketplaceEntry) return;
633
+ const summary = findPluginSummary(marketplaceEntry, pluginName);
634
+ return summary ? {
635
+ marketplace: marketplaceRef(marketplaceEntry),
636
+ summary
637
+ } : void 0;
638
+ }
639
+ function pluginReadParams(marketplace, pluginName) {
640
+ return {
641
+ ...marketplace.path ? { marketplacePath: marketplace.path } : {},
642
+ ...marketplace.remoteMarketplaceName ? { remoteMarketplaceName: marketplace.remoteMarketplaceName } : {},
643
+ pluginName
644
+ };
645
+ }
646
+ function readCachedAppInventory(params) {
647
+ if (!params.appCache || !params.appCacheKey) return;
648
+ const request = async (method, requestParams) => await params.request(method, requestParams);
649
+ return params.appCache.read({
650
+ key: params.appCacheKey,
651
+ request,
652
+ nowMs: params.nowMs,
653
+ suppressRefresh: params.suppressAppInventoryRefresh
654
+ });
655
+ }
656
+ async function readPluginDetail(params, marketplace, pluginPolicy, diagnostics) {
657
+ if (params.readPluginDetails === false) return;
658
+ try {
659
+ return (await params.request("plugin/read", pluginReadParams(marketplace, pluginPolicy.pluginName))).plugin;
660
+ } catch (error) {
661
+ diagnostics.push({
662
+ code: "plugin_detail_unavailable",
663
+ plugin: pluginPolicy,
664
+ message: `${pluginPolicy.pluginName} detail unavailable: ${error instanceof Error ? error.message : String(error)}`
665
+ });
666
+ return;
667
+ }
668
+ }
669
+ function resolveAppOwnership(params) {
670
+ if (params.detail && params.detail.apps.length > 0) return "proven";
671
+ return (params.appInventory?.snapshot?.apps ?? []).filter((app) => app.pluginDisplayNames.some((displayName) => displayName === params.summary.name)).length > 0 ? "ambiguous" : "none";
672
+ }
673
+ function resolveOwnedApps(params) {
674
+ const detailApps = params.detail?.apps ?? [];
675
+ if (detailApps.length === 0) return [];
676
+ if (params.appInventory?.state === "missing") {
677
+ embeddedAgentLog.warn("codex plugin inventory missing app inventory for detail apps", {
678
+ configKey: params.pluginPolicy.configKey,
679
+ pluginName: params.pluginPolicy.pluginName,
680
+ appIds: detailApps.map((app) => app.id).toSorted()
681
+ });
682
+ return [];
683
+ }
684
+ const appInfoById = new Map((params.appInventory?.snapshot?.apps ?? []).map((app) => [app.id, app]));
685
+ return detailApps.map((app) => {
686
+ const info = appInfoById.get(app.id);
687
+ if (!info) return {
688
+ id: app.id,
689
+ name: app.name,
690
+ accessible: false,
691
+ enabled: false,
692
+ needsAuth: true
693
+ };
694
+ return {
695
+ id: app.id,
696
+ name: app.name,
697
+ accessible: info.isAccessible,
698
+ enabled: info.isEnabled,
699
+ needsAuth: app.needsAuth || !info.isAccessible
700
+ };
701
+ }).toSorted((left, right) => left.id.localeCompare(right.id));
702
+ }
703
+ function findPluginSummary(marketplace, pluginName) {
704
+ return marketplace.plugins.find((plugin) => plugin.name === pluginName || plugin.id === pluginName || plugin.id === `${pluginName}@${marketplace.name}` || pluginNameFromPluginId(plugin.id, marketplace.name) === pluginName);
705
+ }
706
+ function pluginNameFromPluginId(pluginId, marketplaceName) {
707
+ const trimmed = pluginId.trim();
708
+ if (!trimmed) return;
709
+ const marketplaceSuffix = `@${marketplaceName}`;
710
+ return (trimmed.endsWith(marketplaceSuffix) ? trimmed.slice(0, -marketplaceSuffix.length) : trimmed).split("/").at(-1)?.trim() || void 0;
711
+ }
712
+ function marketplaceRef(marketplace) {
713
+ return {
714
+ name: CODEX_PLUGINS_MARKETPLACE_NAME,
715
+ ...marketplace.path ? { path: marketplace.path } : {},
716
+ ...!marketplace.path ? { remoteMarketplaceName: marketplace.name } : {}
717
+ };
718
+ }
719
+ //#endregion
720
+ //#region extensions/codex/src/app-server/plugin-activation.ts
721
+ async function ensureCodexPluginActivation(params) {
722
+ if (params.identity.marketplaceName !== "openai-curated") return activationFailure(params.identity, "marketplace_missing", { message: "Only " + CODEX_PLUGINS_MARKETPLACE_NAME + " plugins can be activated." });
723
+ const listed = await params.request("plugin/list", { cwds: [] });
724
+ const resolved = findOpenAiCuratedPluginSummary(listed, params.identity.pluginName);
725
+ if (!resolved) {
726
+ if (!listed.marketplaces.some((marketplace) => marketplace.name === "openai-curated")) return activationFailure(params.identity, "marketplace_missing", { message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.` });
727
+ return activationFailure(params.identity, "plugin_missing", { message: `${params.identity.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.` });
728
+ }
729
+ if (resolved.summary.installed && resolved.summary.enabled && !params.installEvenIfActive) return {
730
+ identity: params.identity,
731
+ ok: true,
732
+ reason: "already_active",
733
+ installAttempted: false,
734
+ marketplace: resolved.marketplace,
735
+ diagnostics: []
736
+ };
737
+ const installResponse = await params.request("plugin/install", pluginReadParams(resolved.marketplace, params.identity.pluginName));
738
+ const refreshDiagnostics = [];
739
+ let refreshFailed = false;
740
+ try {
741
+ const refreshResult = await refreshCodexPluginRuntimeState({
742
+ request: params.request,
743
+ appCache: params.appCache,
744
+ appCacheKey: params.appCacheKey
745
+ });
746
+ refreshDiagnostics.push(...refreshResult.diagnostics);
747
+ } catch (error) {
748
+ refreshFailed = true;
749
+ refreshDiagnostics.push({ message: `Codex plugin runtime refresh failed after install: ${error instanceof Error ? error.message : String(error)}` });
750
+ }
751
+ const authRequired = installResponse.appsNeedingAuth.length > 0;
752
+ return {
753
+ identity: params.identity,
754
+ ok: !authRequired && !refreshFailed,
755
+ reason: refreshFailed ? "refresh_failed" : authRequired ? "auth_required" : resolved.summary.installed && resolved.summary.enabled ? "already_active" : "installed",
756
+ installAttempted: true,
757
+ marketplace: resolved.marketplace,
758
+ installResponse,
759
+ diagnostics: [...refreshDiagnostics, ...installResponse.appsNeedingAuth.map((app) => ({ message: `${app.name} requires app authentication before plugin tools are exposed.` }))]
760
+ };
761
+ }
762
+ async function refreshCodexPluginRuntimeState(params) {
763
+ const diagnostics = [];
764
+ await params.request("plugin/list", { cwds: [] });
765
+ await params.request("skills/list", {
766
+ cwds: [],
767
+ forceReload: true
768
+ });
769
+ try {
770
+ await params.request("hooks/list", { cwds: [] });
771
+ } catch (error) {
772
+ diagnostics.push({ message: `Codex hooks refresh skipped: ${error instanceof Error ? error.message : String(error)}` });
773
+ }
774
+ await params.request("config/mcpServer/reload", void 0);
775
+ if (params.appCache && params.appCacheKey) {
776
+ params.appCache.invalidate(params.appCacheKey, "Codex plugin activation changed app inventory");
777
+ const request = async (method, requestParams) => await params.request(method, requestParams);
778
+ try {
779
+ await params.appCache.refreshNow({
780
+ key: params.appCacheKey,
781
+ request,
782
+ forceRefetch: true
783
+ });
784
+ } catch (error) {
785
+ diagnostics.push({ message: `Codex app inventory refresh skipped: ${error instanceof Error ? error.message : String(error)}` });
786
+ }
787
+ }
788
+ return { diagnostics };
789
+ }
790
+ function activationFailure(identity, reason, diagnostic) {
791
+ return {
792
+ identity,
793
+ ok: false,
794
+ reason,
795
+ installAttempted: false,
796
+ diagnostics: [diagnostic]
797
+ };
798
+ }
799
+ //#endregion
803
800
  //#region extensions/codex/src/app-server/plugin-thread-config.ts
804
801
  const CODEX_PLUGIN_THREAD_CONFIG_INPUT_FINGERPRINT_VERSION = 1;
805
802
  const CODEX_PLUGIN_THREAD_CONFIG_FINGERPRINT_VERSION = 1;
@@ -1091,6 +1088,7 @@ async function startOrResumeThread(params) {
1091
1088
  const contextEngineBinding = buildContextEngineBinding(params.params, params.contextEngineProjection);
1092
1089
  const userMcpServersConfigPatch = params.userMcpServersEnabled === false ? void 0 : buildCodexUserMcpServersThreadConfigPatch(params.params.config, { agentId: params.agentId ?? params.params.agentId });
1093
1090
  const userMcpServersFingerprint = fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
1091
+ const environmentSelectionFingerprint = fingerprintEnvironmentSelection(params.environmentSelection);
1094
1092
  let binding = await readCodexAppServerBinding(params.params.sessionFile, {
1095
1093
  authProfileStore: params.params.authProfileStore,
1096
1094
  agentDir: params.params.agentDir,
@@ -1127,6 +1125,11 @@ async function startOrResumeThread(params) {
1127
1125
  await clearCodexAppServerBinding(params.params.sessionFile);
1128
1126
  binding = void 0;
1129
1127
  }
1128
+ if (binding?.threadId && binding.environmentSelectionFingerprint !== environmentSelectionFingerprint) {
1129
+ embeddedAgentLog.debug("codex app-server environment selection changed; starting a new thread", { threadId: binding.threadId });
1130
+ await clearCodexAppServerBinding(params.params.sessionFile);
1131
+ binding = void 0;
1132
+ }
1130
1133
  if (binding?.threadId && params.mcpServersFingerprintEvaluated === true && binding.mcpServersFingerprint !== params.mcpServersFingerprint) {
1131
1134
  embeddedAgentLog.debug("codex app-server MCP config changed; starting a new thread", { threadId: binding.threadId });
1132
1135
  await clearCodexAppServerBinding(params.params.sessionFile);
@@ -1208,6 +1211,7 @@ async function startOrResumeThread(params) {
1208
1211
  pluginAppsInputFingerprint: binding.pluginAppsInputFingerprint,
1209
1212
  pluginAppPolicyContext: binding.pluginAppPolicyContext,
1210
1213
  contextEngine: contextEngineBinding,
1214
+ environmentSelectionFingerprint,
1211
1215
  createdAt: binding.createdAt
1212
1216
  }, {
1213
1217
  authProfileStore: params.params.authProfileStore,
@@ -1237,6 +1241,7 @@ async function startOrResumeThread(params) {
1237
1241
  pluginAppsInputFingerprint: binding.pluginAppsInputFingerprint,
1238
1242
  pluginAppPolicyContext: binding.pluginAppPolicyContext,
1239
1243
  contextEngine: contextEngineBinding,
1244
+ environmentSelectionFingerprint,
1240
1245
  lifecycle: { action: "resumed" }
1241
1246
  };
1242
1247
  } catch (error) {
@@ -1253,7 +1258,8 @@ async function startOrResumeThread(params) {
1253
1258
  developerInstructions: params.developerInstructions,
1254
1259
  config,
1255
1260
  nativeCodeModeEnabled: params.nativeCodeModeEnabled,
1256
- nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled
1261
+ nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
1262
+ environmentSelection: params.environmentSelection
1257
1263
  })));
1258
1264
  const modelProvider = resolveCodexAppServerModelProvider({
1259
1265
  provider: params.params.provider,
@@ -1278,6 +1284,7 @@ async function startOrResumeThread(params) {
1278
1284
  pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
1279
1285
  pluginAppPolicyContext: pluginThreadConfig?.policyContext,
1280
1286
  contextEngine: contextEngineBinding,
1287
+ environmentSelectionFingerprint,
1281
1288
  createdAt
1282
1289
  }, {
1283
1290
  authProfileStore: params.params.authProfileStore,
@@ -1309,6 +1316,7 @@ async function startOrResumeThread(params) {
1309
1316
  pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
1310
1317
  pluginAppPolicyContext: pluginThreadConfig?.policyContext,
1311
1318
  contextEngine: contextEngineBinding,
1319
+ environmentSelectionFingerprint,
1312
1320
  createdAt,
1313
1321
  updatedAt: createdAt,
1314
1322
  lifecycle: {
@@ -1398,7 +1406,7 @@ function buildThreadStartParams(params, options) {
1398
1406
  nativeCodeModeEnabled: options.nativeCodeModeEnabled,
1399
1407
  nativeCodeModeOnlyEnabled: options.nativeCodeModeOnlyEnabled
1400
1408
  }),
1401
- ...options.nativeCodeModeEnabled === false ? { environments: [] } : {},
1409
+ ...resolveCodexThreadEnvironmentSelection(options),
1402
1410
  developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params, { dynamicTools: options.dynamicTools }),
1403
1411
  dynamicTools: options.dynamicTools,
1404
1412
  experimentalRawEvents: true,
@@ -1460,9 +1468,18 @@ function buildTurnStartParams(params, options) {
1460
1468
  model: params.modelId,
1461
1469
  ...options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {},
1462
1470
  effort: resolveReasoningEffort(params.thinkLevel, params.modelId),
1463
- collaborationMode: buildTurnCollaborationMode(params, { heartbeatCollaborationInstructions: options.heartbeatCollaborationInstructions })
1471
+ ...options.environmentSelection ? { environments: options.environmentSelection } : {},
1472
+ collaborationMode: buildTurnCollaborationMode(params, {
1473
+ turnScopedDeveloperInstructions: options.turnScopedDeveloperInstructions,
1474
+ heartbeatCollaborationInstructions: options.heartbeatCollaborationInstructions
1475
+ })
1464
1476
  };
1465
1477
  }
1478
+ function resolveCodexThreadEnvironmentSelection(options) {
1479
+ if (options.nativeCodeModeEnabled === false) return { environments: [] };
1480
+ if (options.environmentSelection) return { environments: options.environmentSelection };
1481
+ return {};
1482
+ }
1466
1483
  function buildTurnCollaborationMode(params, options = {}) {
1467
1484
  return {
1468
1485
  mode: "default",
@@ -1474,10 +1491,26 @@ function buildTurnCollaborationMode(params, options = {}) {
1474
1491
  };
1475
1492
  }
1476
1493
  function buildTurnScopedCollaborationInstructions(params, options = {}) {
1477
- if (params.trigger === "cron") return buildCronCollaborationInstructions();
1478
- if (params.trigger === "heartbeat") return joinPresentSections(buildHeartbeatCollaborationInstructions(), options.heartbeatCollaborationInstructions);
1494
+ if (params.trigger === "cron") return joinPresentSections(buildCronCollaborationInstructions(), options.turnScopedDeveloperInstructions);
1495
+ if (params.trigger === "heartbeat") return joinPresentSections(buildHeartbeatCollaborationInstructions(), options.turnScopedDeveloperInstructions, options.heartbeatCollaborationInstructions);
1496
+ if (options.turnScopedDeveloperInstructions?.trim()) return joinPresentSections(buildDefaultCollaborationInstructions(), options.turnScopedDeveloperInstructions);
1479
1497
  return null;
1480
1498
  }
1499
+ function buildDefaultCollaborationInstructions() {
1500
+ return [
1501
+ "# Collaboration Mode: Default",
1502
+ "",
1503
+ "You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.",
1504
+ "",
1505
+ "Your active mode changes only when new developer instructions with a different `<collaboration_mode>...</collaboration_mode>` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan.",
1506
+ "",
1507
+ "## request_user_input availability",
1508
+ "",
1509
+ "Use the `request_user_input` tool only when it is listed in the available tools for this turn.",
1510
+ "",
1511
+ "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message."
1512
+ ].join("\n");
1513
+ }
1481
1514
  function buildCronCollaborationInstructions() {
1482
1515
  return [
1483
1516
  "This is an OpenClaw cron automation turn. Apply these instructions only to this scheduled job; ordinary chat turns should stay in Codex Default mode.",
@@ -1508,11 +1541,14 @@ function fingerprintDynamicTools(dynamicTools) {
1508
1541
  function fingerprintUserMcpServersConfigPatch(configPatch) {
1509
1542
  return configPatch ? JSON.stringify(stabilizeJsonValue(configPatch)) : void 0;
1510
1543
  }
1544
+ function fingerprintEnvironmentSelection(environments) {
1545
+ return environments ? JSON.stringify(environments.map(stabilizeJsonValue)) : void 0;
1546
+ }
1511
1547
  function fingerprintDynamicToolSpec(tool) {
1512
1548
  if (!isJsonObject(tool)) return stabilizeJsonValue(tool);
1513
1549
  const stable = {};
1514
1550
  for (const [key, child] of Object.entries(tool).toSorted(([left], [right]) => left.localeCompare(right))) {
1515
- if (key === "description") continue;
1551
+ if (key === "description" || key === "deferLoading" || key === "namespace") continue;
1516
1552
  stable[key] = stabilizeJsonValue(child);
1517
1553
  }
1518
1554
  return stable;
@@ -1589,4 +1625,4 @@ function resolveReasoningEffort(thinkLevel, modelId) {
1589
1625
  return null;
1590
1626
  }
1591
1627
  //#endregion
1592
- export { isForcedPrivateQaCodexRuntime as C, filterCodexDynamicTools as S, resolveCodexDynamicToolsLoading as T, projectContextEngineAssemblyForCodex as _, buildThreadResumeParams as a, createCodexDynamicToolBridge as b, codexDynamicToolsFingerprint as c, resolveReasoningEffort as d, startOrResumeThread as f, shouldBuildCodexPluginThreadConfig as g, mergeCodexThreadConfigs as h, buildDeveloperInstructions as i, isContextEngineBindingCompatible as l, buildCodexPluginThreadConfigInputFingerprint as m, buildCodexRuntimeThreadConfig as n, buildThreadStartParams as o, buildCodexPluginThreadConfig as p, buildContextEngineBinding as r, buildTurnStartParams as s, areCodexDynamicToolFingerprintsCompatible as t, resolveCodexAppServerModelProvider as u, resolveCodexContextEngineProjectionMaxChars as v, normalizeCodexDynamicToolName as w, sanitizeCodexHistoryImagePayloads as x, resolveCodexContextEngineProjectionReserveTokens as y };
1628
+ export { sanitizeCodexHistoryImagePayloads as C, resolveCodexContextEngineProjectionMaxChars as D, projectContextEngineAssemblyForCodex as E, resolveCodexContextEngineProjectionReserveTokens as O, invalidInlineImageText as S, DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS as T, shouldBuildCodexPluginThreadConfig as _, buildThreadResumeParams as a, buildCodexAppInventoryCacheKey as b, buildTurnStartParams as c, resolveCodexAppServerModelProvider as d, resolveReasoningEffort as f, mergeCodexThreadConfigs as g, buildCodexPluginThreadConfigInputFingerprint as h, buildDeveloperInstructions as i, codexDynamicToolsFingerprint as l, buildCodexPluginThreadConfig as m, buildCodexRuntimeThreadConfig as n, buildThreadStartParams as o, startOrResumeThread as p, buildContextEngineBinding as r, buildTurnCollaborationMode as s, areCodexDynamicToolFingerprintsCompatible as t, isContextEngineBindingCompatible as u, ensureCodexPluginActivation as v, sanitizeInlineImageDataUrl as w, defaultCodexAppInventoryCache as x, pluginReadParams as y };