@tangle-network/sdk-provider-codex 0.0.0-stage → 0.4.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/index.mjs ADDED
@@ -0,0 +1,1782 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { InteractionKind, ProxySecretBindingSchema, ProxySecretGrantSchema, ProxySecretReceiptSchema, ProxySecretUpdateControlsSchema, ProxySecretsSchema, isProxySecretScopeAttenuation, ownPropertyRecordSchema, proxySecretPresentationsEqual, resolveTangleRouterDefaults } from "@tangle-network/agent-interface";
3
+ import { z } from "zod";
4
+ import { HELD_SPARE_IDLE_MS, createDeferredParser, createHeldSpareRegistry, launchDifferences, resolveRawCapturePath, spawnRpcBridge } from "@tangle-network/sdk-cli-runner";
5
+ import { CODEX_DISABLED_FEATURE_ARGS, CliProviderAdapterBase, InteractionBroker, McpInteractionSupport, NATIVE_SESSION_CONTRACTS, PROMPT_SECTION_SEPARATOR, SEALED_NATIVE_HOME_LAYOUT, assertSealedNativeHome, assertSealedNativeSearchPath, codexDeveloperInstructionArgs, deriveFilePart, forgetSessionScopes, interactionToggleEnabled, isLoopbackEndpoint, isPlainRecord, jsonEventParser, localMcpArgv, localMcpEnv, nativeEffortArgs, partitionImageAttachments, provenStoreBinary, reasoningEvents, recordCliTokenUsage, refuseSecretLaunch, resolveExecutableRealpath, secretMcpScope, secretSessionTurnQueue, servableMcpServer, servableStdioServer, sessionObservedEvents, streamPartBase, textDeltaEvents, toolResultEvents, toolStartEvents, trackFilePart, turnEndReason, untilSettledOrAborted, withTurnInteractions } from "@tangle-network/sdk-provider-cli-base";
6
+ import { existsSync } from "node:fs";
7
+ import path, { join } from "node:path";
8
+ const TURN_KEY_PLACEHOLDER_PREFIX = "tangle-turn-key";
9
+ const MODEL_KEY_PROXY_PATH_PREFIX = "/v1/";
10
+ const LOOPBACK_BASE_URL = /^http:\/\/127\.0\.0\.1:[0-9]+$/;
11
+ function isTurnKeyPlaceholder(value) {
12
+ return value?.startsWith(`${TURN_KEY_PLACEHOLDER_PREFIX}.`) === true;
13
+ }
14
+ function parseModelKeyProxyBinding(raw) {
15
+ if (raw === void 0) return void 0;
16
+ const parsed = JSON.parse(raw);
17
+ const baseUrl = typeof parsed === "object" && parsed !== null && "baseUrl" in parsed ? parsed.baseUrl : void 0;
18
+ if (typeof baseUrl !== "string" || !LOOPBACK_BASE_URL.test(baseUrl)) throw new Error("Model key proxy binding is invalid");
19
+ return {
20
+ baseUrl,
21
+ passThrough: typeof parsed === "object" && parsed !== null && "passThrough" in parsed && parsed.passThrough === true
22
+ };
23
+ }
24
+ const MODEL_KEY_PROXY_HOME_PATH_PREFIX = "/v1h/";
25
+ function modelKeyProxyHomeTag(runtimeHome) {
26
+ return Buffer.from(runtimeHome.replace(/\/+$/, "")).toString("base64url");
27
+ }
28
+ function proxiedModelUrl(proxyBaseUrl, upstream, options) {
29
+ const encoded = Buffer.from(upstream).toString("base64url");
30
+ return options?.runtimeHome ? `${proxyBaseUrl}${MODEL_KEY_PROXY_HOME_PATH_PREFIX}${modelKeyProxyHomeTag(options.runtimeHome)}/${encoded}` : `${proxyBaseUrl}${MODEL_KEY_PROXY_PATH_PREFIX}${encoded}`;
31
+ }
32
+ function baseProxyToken(token) {
33
+ const i = token.indexOf(".");
34
+ return i === -1 ? token : token.slice(0, i);
35
+ }
36
+ const MODEL_PROXY_TOKENS = {
37
+ OPENCODE_MODEL_API_KEY: "proxy-model-token",
38
+ OPENAI_API_KEY: "proxy-openai-token",
39
+ ANTHROPIC_API_KEY: "proxy-anthropic-token",
40
+ OPENROUTER_API_KEY: "proxy-openrouter-token",
41
+ TANGLE_INFERENCE_KEY: "proxy-openai-token",
42
+ ZAI_API_KEY: "proxy-zai-token",
43
+ ZHIPU_API_KEY: "proxy-zai-token"
44
+ };
45
+ const MODEL_PROXY_TOKEN_VALUES = new Set(Object.values(MODEL_PROXY_TOKENS));
46
+ function isManagedModelToken(value) {
47
+ const key = value?.trim();
48
+ return !!key && MODEL_PROXY_TOKEN_VALUES.has(baseProxyToken(key));
49
+ }
50
+ function managedModelProxyUrl(input) {
51
+ if (!isManagedModelToken(input.apiKey) || isTurnKeyPlaceholder(input.apiKey)) return void 0;
52
+ const binding = parseModelKeyProxyBinding(input.binding);
53
+ if (binding?.passThrough !== true) return void 0;
54
+ try {
55
+ const url = new URL(input.upstream);
56
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
57
+ if (url.username || url.password || url.search || url.hash) return void 0;
58
+ } catch {
59
+ return;
60
+ }
61
+ const runtimeHome = input.runtimeHome?.trim();
62
+ return proxiedModelUrl(binding.baseUrl, input.upstream, { ...runtimeHome ? { runtimeHome } : {} });
63
+ }
64
+ new Set([
65
+ ...Object.keys(MODEL_PROXY_TOKENS),
66
+ "TANGLE_API_KEY",
67
+ "GITHUB_TOKEN",
68
+ "GH_TOKEN"
69
+ ]);
70
+ /** True for a slot name, which exists at the proxy and never in a box env. */
71
+ function isDeclaredSecretSlotEnvVar(name) {
72
+ return /^TANGLE_DECLARED_SLOT_\d+$/.test(name);
73
+ }
74
+ //#endregion
75
+ //#region ../runtime-contracts/dist/harness-warm-transport.js
76
+ /** Set only by the Sandbox API after the OpenCode warm-server rollout decision. */
77
+ const AGENT_INTERNAL_OPENCODE_WARM_SERVER_ENV_VAR = "AGENT_INTERNAL_OPENCODE_WARM_SERVER";
78
+ /**
79
+ * Sidecar switch: after a warm claim, start the first session's OpenCode
80
+ * server before that session's first turn. Effective only with
81
+ * AGENT_INTERNAL_OPENCODE_WARM_SERVER=1. Any value other than "1" is off.
82
+ */
83
+ const AGENT_INTERNAL_OPENCODE_PREWARM_ENV_VAR = "AGENT_INTERNAL_OPENCODE_PREWARM";
84
+ /** Persistent Kimi wire process and shared create/resume prewarm gates. */
85
+ const AGENT_INTERNAL_KIMI_WARM_SERVER_ENV_VAR = "AGENT_INTERNAL_KIMI_WARM_SERVER";
86
+ const AGENT_INTERNAL_KIMI_PREWARM_ENV_VAR = "AGENT_INTERNAL_KIMI_PREWARM";
87
+ /**
88
+ * Claude Code and Codex run one process per turn. A harness's warm switch
89
+ * sends its managed-token turns' model requests through the sidecar's key
90
+ * proxy (which reports each turn's first model request) and lets a process
91
+ * started ahead of the turn take the turn: Claude Code launches a managed turn
92
+ * with its prompt on stdin (stream-json); Codex already reads its prompt from
93
+ * stdin. Its prewarm switch starts that process at claim, boot and recover.
94
+ * Both default off; off, every turn calls its upstream directly.
95
+ */
96
+ const AGENT_INTERNAL_CLAUDE_CODE_WARM_SERVER_ENV_VAR = "AGENT_INTERNAL_CLAUDE_CODE_WARM_SERVER";
97
+ const AGENT_INTERNAL_CLAUDE_CODE_PREWARM_ENV_VAR = "AGENT_INTERNAL_CLAUDE_CODE_PREWARM";
98
+ const AGENT_INTERNAL_CODEX_WARM_SERVER_ENV_VAR = "AGENT_INTERNAL_CODEX_WARM_SERVER";
99
+ /**
100
+ * Per harness, the two switches the Sandbox API sets from its warm-server
101
+ * rollout decision: the long-lived process transport, and prewarm, which
102
+ * starts that process before the turn that uses it and is only meaningful
103
+ * with it. A harness joins the rollout by adding one row; the Sandbox API,
104
+ * the orchestrator's provision guard and its recreate carry all read this
105
+ * table. Values are "1" or "0"; anything else is off.
106
+ */
107
+ const HARNESS_WARM_TRANSPORT_ENV = {
108
+ opencode: {
109
+ warmServer: AGENT_INTERNAL_OPENCODE_WARM_SERVER_ENV_VAR,
110
+ prewarm: AGENT_INTERNAL_OPENCODE_PREWARM_ENV_VAR
111
+ },
112
+ "kimi-code": {
113
+ warmServer: AGENT_INTERNAL_KIMI_WARM_SERVER_ENV_VAR,
114
+ prewarm: AGENT_INTERNAL_KIMI_PREWARM_ENV_VAR
115
+ },
116
+ "claude-code": {
117
+ warmServer: AGENT_INTERNAL_CLAUDE_CODE_WARM_SERVER_ENV_VAR,
118
+ prewarm: AGENT_INTERNAL_CLAUDE_CODE_PREWARM_ENV_VAR
119
+ },
120
+ codex: {
121
+ warmServer: AGENT_INTERNAL_CODEX_WARM_SERVER_ENV_VAR,
122
+ prewarm: "AGENT_INTERNAL_CODEX_PREWARM"
123
+ }
124
+ };
125
+ function isReservedProfileUid(uid) {
126
+ return Number.isSafeInteger(uid) && uid >= 2e5;
127
+ }
128
+ //#endregion
129
+ //#region ../runtime-contracts/dist/proxy-secret-materialization.js
130
+ const syntheticHeader = {
131
+ alg: "HS256",
132
+ typ: "JWT"
133
+ };
134
+ const encode = (value) => btoa(value instanceof Uint8Array ? String.fromCharCode(...value) : JSON.stringify(value)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
135
+ function placeholderPrefix(slot, format, expiresAt) {
136
+ return format === "jwt" ? `${encode(syntheticHeader)}.${encode({
137
+ sub: "sandbox-placeholder",
138
+ aud: "proxy-secret-placeholder",
139
+ exp: Math.floor(Date.parse(expiresAt) / 1e3)
140
+ })}.` : `proxy-declared-${slot.slice(21)}-token-`;
141
+ }
142
+ function isSyntheticPlaceholder(material) {
143
+ const prefix = placeholderPrefix(material.slot, material.grant.placeholderFormat, material.issuerExpiresAt);
144
+ return material.placeholder.startsWith(prefix) && /^[A-Za-z0-9_-]{43}$/.test(material.placeholder.slice(prefix.length));
145
+ }
146
+ z.array(z.strictObject({
147
+ binding: ProxySecretBindingSchema,
148
+ grant: ProxySecretGrantSchema,
149
+ placeholder: z.string().min(1),
150
+ slot: z.string().refine(isDeclaredSecretSlotEnvVar),
151
+ issuerExpiresAt: z.iso.datetime()
152
+ })).superRefine((materials, ctx) => {
153
+ const bindings = ProxySecretsSchema.safeParse(materials.map((m) => m.binding));
154
+ const unique = (values) => new Set(values).size === values.length;
155
+ if (!bindings.success || !unique(materials.map((m) => m.slot)) || !unique(materials.map((m) => m.placeholder)) || !unique(materials.map((m) => m.grant.grantId)) || new Set(materials.map((m) => m.grant.epoch)).size > 1 || new Set(materials.map((m) => m.grant.sandboxId)).size > 1 || materials.some((material) => !isSyntheticPlaceholder(material)) || materials.some(({ binding, grant, issuerExpiresAt }) => !isProxySecretScopeAttenuation(binding, grant.scope) || !proxySecretPresentationsEqual(binding.presentation, grant.presentation) || (binding.placeholderFormat ?? "opaque") !== grant.placeholderFormat || Date.parse(grant.expiresAt) > Date.parse(issuerExpiresAt))) ctx.addIssue({
156
+ code: "custom",
157
+ message: "Invalid proxy secret materialization"
158
+ });
159
+ });
160
+ ProxySecretUpdateControlsSchema.extend({
161
+ resolvedSecrets: ownPropertyRecordSchema(z.string().min(1), { maxEntries: Number.MAX_SAFE_INTEGER }).default({}),
162
+ proxySecretGrants: ProxySecretReceiptSchema.options[0].shape.bindings,
163
+ expectedProxySecretGeneration: ProxySecretUpdateControlsSchema.shape.expectedProxySecretGeneration.unwrap()
164
+ }).strict().refine((body) => new Set(body.proxySecretGrants.map((b) => b.envName)).size === body.proxySecretGrants.length);
165
+ //#endregion
166
+ //#region ../runtime-contracts/dist/runtime-environment.js
167
+ const SAFE_ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
168
+ function isSafeEnvVarName(name) {
169
+ if (typeof name !== "string") return false;
170
+ if (name.length === 0 || name.length > 256) return false;
171
+ return SAFE_ENV_VAR_NAME_RE.test(name);
172
+ }
173
+ /** Client proxy URLs first, then bypass names. */
174
+ const EGRESS_PROXY_ENV_NAMES = [
175
+ "HTTPS_PROXY",
176
+ "https_proxy",
177
+ "HTTP_PROXY",
178
+ "http_proxy",
179
+ "NO_PROXY",
180
+ "no_proxy"
181
+ ];
182
+ const CA_TRUST_ENV_NAMES = [
183
+ "NODE_EXTRA_CA_CERTS",
184
+ "SSL_CERT_FILE",
185
+ "CURL_CA_BUNDLE",
186
+ "GIT_SSL_CAINFO",
187
+ "NIX_SSL_CERT_FILE"
188
+ ];
189
+ [...EGRESS_PROXY_ENV_NAMES, ...CA_TRUST_ENV_NAMES];
190
+ const FORBIDDEN_ENV_VAR_NAMES = new Set([
191
+ "LD_PRELOAD",
192
+ "LD_LIBRARY_PATH",
193
+ "LD_AUDIT",
194
+ "DYLD_INSERT_LIBRARIES",
195
+ "DYLD_LIBRARY_PATH",
196
+ "DYLD_FALLBACK_LIBRARY_PATH",
197
+ "NODE_OPTIONS",
198
+ "NODE_PATH",
199
+ "NODE_TLS_REJECT_UNAUTHORIZED",
200
+ "PYTHONPATH",
201
+ "PYTHONSTARTUP",
202
+ "PERL5OPT",
203
+ "PERL5LIB",
204
+ "RUBYOPT",
205
+ "RUBYLIB",
206
+ "PATH",
207
+ "NIX_BIN_PATH",
208
+ "EGRESS_EDGE_POOL_ENABLED",
209
+ "EDGE_BIND_ID",
210
+ "PRIVACY_EGRESS",
211
+ "LOG_REDACT",
212
+ "IFS",
213
+ "BASH_ENV",
214
+ "ENV",
215
+ "SHELL",
216
+ "SHELLOPTS",
217
+ "PROMPT_COMMAND",
218
+ "HOME",
219
+ "XDG_CONFIG_HOME",
220
+ "XDG_DATA_HOME",
221
+ "XDG_CACHE_HOME",
222
+ "XDG_STATE_HOME",
223
+ "XDG_RUNTIME_DIR",
224
+ "HTTPS_PROXY",
225
+ "HTTP_PROXY",
226
+ "ALL_PROXY",
227
+ "NO_PROXY",
228
+ "https_proxy",
229
+ "http_proxy",
230
+ "all_proxy",
231
+ "no_proxy",
232
+ "SSL_CERT_FILE",
233
+ "SSL_CERT_DIR",
234
+ "REQUESTS_CA_BUNDLE",
235
+ "CURL_CA_BUNDLE",
236
+ "NODE_EXTRA_CA_CERTS",
237
+ "HOSTNAME",
238
+ "CONTAINER_ID",
239
+ "AGENT_WORKSPACE_ROOT",
240
+ "WORKSPACE_PATH",
241
+ "STORAGE_PATH",
242
+ "WORKSPACE_FILE_API_ROOT_ONLY",
243
+ "WORKSPACE_ALLOWED_ABSOLUTE_PATHS",
244
+ "TANGLE_PLAN_CALLBACK_ALLOWED_ORIGINS"
245
+ ]);
246
+ const FORBIDDEN_ENV_VAR_PREFIXES = [
247
+ "SIDECAR_",
248
+ "ORCHESTRATOR_",
249
+ "AGENT_INTERNAL_",
250
+ "SANDBOX_DEV_",
251
+ "AGENT_SUBPROCESS_",
252
+ "TEE_ATTESTATION_"
253
+ ];
254
+ const RUNTIME_CALLER_ALLOWED_ENV_NAMES = new Set(["SIDECAR_CAPABILITIES", "SIDECAR_PLATFORM_CONTROL_RUNTIME"]);
255
+ new Set(Object.values(HARNESS_WARM_TRANSPORT_ENV).flatMap(({ warmServer, prewarm }) => [warmServer, prewarm]));
256
+ function isForbiddenEnvVarName(name) {
257
+ if (RUNTIME_CALLER_ALLOWED_ENV_NAMES.has(name)) return false;
258
+ if (FORBIDDEN_ENV_VAR_NAMES.has(name)) return true;
259
+ for (const prefix of FORBIDDEN_ENV_VAR_PREFIXES) if (name.startsWith(prefix)) return true;
260
+ return false;
261
+ }
262
+ /**
263
+ * POSIX names for caller-owned container env. Runtime process.env merges must additionally enforce
264
+ * isForbiddenEnvVarName, including project provisioning and Sidecar config.
265
+ */
266
+ const safeEnvRecordSchema = ownPropertyRecordSchema(z.string(), { maxEntries: Number.MAX_SAFE_INTEGER }).refine((value) => Object.keys(value).every(isSafeEnvVarName), { message: "invalid env var name" });
267
+ /** Names only: the caller must keep a separate source and pass values to resume({ env }). */
268
+ const sandboxEnvRecoverySchema = z.object({ callerResupplies: z.array(z.string().max(256).refine(isSafeEnvVarName, { message: "invalid env var name" })).max(128).refine((keys) => new Set(keys).size === keys.length, { message: "callerResupplies must contain unique keys" }) }).strict();
269
+ z.object({
270
+ envRecovery: sandboxEnvRecoverySchema,
271
+ dryRun: z.boolean().optional()
272
+ }).strict();
273
+ z.object({
274
+ envRecovery: sandboxEnvRecoverySchema,
275
+ eligibleCallerResupplyKeys: z.array(z.string()),
276
+ requiredOnColdResumeKeys: z.array(z.string()),
277
+ automaticCredentialRecovery: z.literal(false),
278
+ dryRun: z.boolean()
279
+ }).strict();
280
+ safeEnvRecordSchema.refine((value) => Object.keys(value).every((key) => !isForbiddenEnvVarName(key) && !key.startsWith("SIDECAR_")), { message: "runtime-owned env var name cannot be set by callers" });
281
+ //#endregion
282
+ //#region src/app-server-protocol.ts
283
+ function grantToPermissionProfile(grant, requested) {
284
+ if (grant === "deny") return {
285
+ permissions: {},
286
+ scope: "turn"
287
+ };
288
+ const permissions = {};
289
+ if (requested.network != null) permissions.network = requested.network;
290
+ if (requested.fileSystem != null) permissions.fileSystem = requested.fileSystem;
291
+ return {
292
+ permissions,
293
+ scope: grant === "allow_once" ? "turn" : "session"
294
+ };
295
+ }
296
+ function normalizeChangeKind(kind) {
297
+ const raw = typeof kind === "string" ? kind : typeof kind === "object" && kind !== null ? kind.type : void 0;
298
+ if (raw === "add" || raw === "update" || raw === "delete") return raw;
299
+ }
300
+ function sandboxModeToPolicy(mode, workspaceRoot) {
301
+ if (mode === "danger-full-access") return { type: "dangerFullAccess" };
302
+ if (mode === "read-only") return {
303
+ type: "readOnly",
304
+ networkAccess: false
305
+ };
306
+ return {
307
+ type: "workspaceWrite",
308
+ writableRoots: [workspaceRoot],
309
+ networkAccess: false,
310
+ excludeTmpdirEnvVar: false,
311
+ excludeSlashTmp: false
312
+ };
313
+ }
314
+ function grantToReviewDecision(grant) {
315
+ if (grant === "deny") return "denied";
316
+ if (grant === "allow_once") return "approved";
317
+ return "approved_for_session";
318
+ }
319
+ function grantToApprovalDecision(grant) {
320
+ if (grant === "deny") return "decline";
321
+ if (grant === "allow_once") return "accept";
322
+ return "acceptForSession";
323
+ }
324
+ //#endregion
325
+ //#region src/codex-app-server-interactions.ts
326
+ /**
327
+ * Approval reverse-calls (v1 + v2) → the unified broker; the resolved grant
328
+ * maps back to codex's decision enum and unblocks the turn. One shape, four
329
+ * wirings — each supplies its own id/tool extractors and v1-vs-v2 mapper.
330
+ */
331
+ function registerBrokeredApprovals(bridge, approve) {
332
+ const onApproval = (method, id, tool, toDecision) => bridge.onRequest(method, async (p) => ({ decision: toDecision(await approve(id(p), tool(p), p)) }));
333
+ const cmd = (p) => (p.command ?? []).join(" ");
334
+ onApproval("execCommandApproval", (p) => p.callId, cmd, grantToReviewDecision);
335
+ onApproval("applyPatchApproval", (p) => p.callId, () => "apply_patch", grantToReviewDecision);
336
+ onApproval("item/commandExecution/requestApproval", (p) => p.callId ?? p.itemId ?? "", cmd, grantToApprovalDecision);
337
+ onApproval("item/fileChange/requestApproval", (p) => p.callId ?? p.itemId ?? "", () => "apply_patch", grantToApprovalDecision);
338
+ bridge.onRequest("item/permissions/requestApproval", async (p) => grantToPermissionProfile(await approve(p.itemId ?? "", `permission escalation${p.reason ? `: ${p.reason}` : ""}`, p), p.permissions ?? {}));
339
+ }
340
+ /**
341
+ * Under approvalPolicy "never" codex should not ask; if it does anyway,
342
+ * deny in the method's own response shape and surface a warning instead
343
+ * of leaving a silent stall in the log. No Permission interaction is
344
+ * raised — the question channel never enabled that capability.
345
+ */
346
+ function registerDeniedApprovals(bridge, onEvent) {
347
+ const denyApproval = (method, response) => bridge.onRequest(method, async () => {
348
+ onEvent?.({
349
+ type: "warning",
350
+ code: "CODEX_UNEXPECTED_APPROVAL",
351
+ message: `codex requested ${method} despite approvalPolicy "never"; denied (question interactions do not route permissions)`
352
+ });
353
+ return response;
354
+ });
355
+ denyApproval("execCommandApproval", { decision: grantToReviewDecision("deny") });
356
+ denyApproval("applyPatchApproval", { decision: grantToReviewDecision("deny") });
357
+ denyApproval("item/commandExecution/requestApproval", { decision: grantToApprovalDecision("deny") });
358
+ denyApproval("item/fileChange/requestApproval", { decision: grantToApprovalDecision("deny") });
359
+ denyApproval("item/permissions/requestApproval", grantToPermissionProfile("deny", {}));
360
+ }
361
+ /**
362
+ * The agent asking the user a clarifying question (codex's request_user_input
363
+ * tool) → the unified broker's question round-trip. The positional answers
364
+ * map back onto codex's per-question-id reply; no answer (timeout / teardown /
365
+ * no live stream) replies with empty slots so the turn unblocks.
366
+ */
367
+ function registerUserInputQuestions(bridge, permissions, sessionId, interactionBinding, questions, onEvent) {
368
+ bridge.onRequest("item/tool/requestUserInput", async (p) => {
369
+ if (!questions) {
370
+ onEvent?.({
371
+ type: "warning",
372
+ code: "CODEX_UNEXPECTED_QUESTION",
373
+ message: "codex requested item/tool/requestUserInput but question interactions were not enabled; answered with no selections"
374
+ });
375
+ const empty = {};
376
+ for (const q of p.questions ?? []) empty[q.id] = { answers: [] };
377
+ return { answers: empty };
378
+ }
379
+ const codexQuestions = p.questions ?? [];
380
+ const answers = await permissions.requestQuestion({
381
+ id: randomUUID(),
382
+ sessionId,
383
+ binding: interactionBinding,
384
+ questions: codexQuestions.map((q) => ({
385
+ question: q.question,
386
+ options: q.options?.map((o) => ({
387
+ label: o.label,
388
+ description: o.description
389
+ }))
390
+ })),
391
+ timeoutMs: typeof p.autoResolutionMs === "number" && p.autoResolutionMs > 0 ? p.autoResolutionMs : void 0,
392
+ emit: onEvent
393
+ });
394
+ const byId = {};
395
+ codexQuestions.forEach((q, index) => {
396
+ byId[q.id] = { answers: answers?.[index] ?? [] };
397
+ });
398
+ return { answers: byId };
399
+ });
400
+ }
401
+ function registerAppServerInteractions(bridge, permissions, sessionId, interactionBinding, interactive, questions, onEvent) {
402
+ const approve = (id, toolName, raw) => permissions.request({
403
+ id: id && id.length > 0 ? id : randomUUID(),
404
+ sessionId,
405
+ binding: interactionBinding,
406
+ toolName,
407
+ input: raw,
408
+ allowlistGrant: "deny",
409
+ emit: onEvent
410
+ });
411
+ if (interactive) registerBrokeredApprovals(bridge, approve);
412
+ else registerDeniedApprovals(bridge, onEvent);
413
+ registerUserInputQuestions(bridge, permissions, sessionId, interactionBinding, questions, onEvent);
414
+ }
415
+ function routesPermissions(config) {
416
+ return config.interactivePermissions === true || interactionToggleEnabled(config.interactions, "permission");
417
+ }
418
+ function routesQuestions(config) {
419
+ return routesPermissions(config) || interactionToggleEnabled(config.interactions, "question");
420
+ }
421
+ function codexRoutesPlans(config) {
422
+ return interactionToggleEnabled(config.interactions, "plan");
423
+ }
424
+ //#endregion
425
+ //#region src/app-server-policy.ts
426
+ function appServerPolicy(config, input = {}) {
427
+ const turnConfig = withTurnInteractions(config, input);
428
+ const interactive = routesPermissions(turnConfig);
429
+ const questions = routesQuestions(turnConfig);
430
+ const plans = codexRoutesPlans(turnConfig);
431
+ return {
432
+ interactive,
433
+ questions,
434
+ plans,
435
+ approvalPolicy: interactive ? config.approvalPolicy ?? "on-request" : "never",
436
+ sandbox: plans ? "read-only" : interactive || config.bypassPermissions === false ? config.sandboxMode ?? "workspace-write" : "danger-full-access"
437
+ };
438
+ }
439
+ function initialThreadId(input) {
440
+ const continuation = input.planContinuation;
441
+ if (!continuation) return input.nativeResume?.nativeSessionId;
442
+ if (continuation.providerState.kind !== "codex") throw new Error(`Codex cannot resume ${continuation.providerState.kind} plan state`);
443
+ return continuation.providerState.threadId;
444
+ }
445
+ //#endregion
446
+ //#region src/app-server-items.ts
447
+ function isTool(item) {
448
+ return item.type === "commandExecution" || item.type === "fileChange";
449
+ }
450
+ /** Native item IDs stay stable across tool, file and reasoning updates. */
451
+ var AppServerItemMapper = class {
452
+ tools = /* @__PURE__ */ new Map();
453
+ reasoning = /* @__PURE__ */ new Map();
454
+ emittedThinking = false;
455
+ constructor(ids, logger) {
456
+ this.ids = ids;
457
+ this.logger = logger;
458
+ }
459
+ partBase(id) {
460
+ return {
461
+ id,
462
+ sessionID: this.ids.sessionId,
463
+ messageID: this.ids.messageId
464
+ };
465
+ }
466
+ filePart(path) {
467
+ const derived = deriveFilePart({
468
+ path,
469
+ messageId: this.ids.messageId
470
+ });
471
+ if (!derived) {
472
+ this.logger?.debug("skipping file part for non-absolute fileChange path", { path });
473
+ return null;
474
+ }
475
+ return {
476
+ ...this.partBase(derived.id),
477
+ type: "file",
478
+ filename: derived.filename,
479
+ ...derived.mediaType ? { mediaType: derived.mediaType } : {},
480
+ url: derived.url
481
+ };
482
+ }
483
+ toolPart(itemId, state) {
484
+ const settled = {
485
+ input: state.input,
486
+ output: state.output,
487
+ time: {
488
+ start: state.startTime,
489
+ end: Date.now()
490
+ }
491
+ };
492
+ const toolState = state.status === "running" ? {
493
+ status: "running",
494
+ input: state.input,
495
+ time: { start: state.startTime }
496
+ } : state.status === "failed" ? {
497
+ ...settled,
498
+ status: "failed",
499
+ error: typeof state.output === "string" ? state.output : void 0
500
+ } : {
501
+ ...settled,
502
+ status: "completed"
503
+ };
504
+ return {
505
+ ...this.partBase(itemId),
506
+ type: "tool",
507
+ callID: itemId,
508
+ tool: state.tool,
509
+ state: toolState
510
+ };
511
+ }
512
+ onItemStarted(params) {
513
+ const item = params.item;
514
+ if (!isTool(item)) return [];
515
+ const state = {
516
+ tool: item.type === "commandExecution" ? "shell" : "apply_patch",
517
+ input: item.type === "commandExecution" ? {
518
+ command: item.command,
519
+ cwd: item.cwd
520
+ } : { changes: item.changes.map((change) => ({
521
+ path: change.path,
522
+ kind: normalizeChangeKind(change.kind) ?? change.kind
523
+ })) },
524
+ startTime: params.startedAtMs ?? Date.now(),
525
+ outputBuffer: "",
526
+ status: "running"
527
+ };
528
+ this.tools.set(item.id, state);
529
+ return [{
530
+ type: "message.part.updated",
531
+ part: this.toolPart(item.id, state)
532
+ }];
533
+ }
534
+ onItemCompleted(params) {
535
+ const item = params.item;
536
+ if (!isTool(item)) {
537
+ if (item.type === "reasoning" && typeof item.id === "string") this.reasoning.delete(item.id);
538
+ return [];
539
+ }
540
+ const state = this.tools.get(item.id);
541
+ if (!state) return [{
542
+ type: "warning",
543
+ code: "UNKNOWN_TOOL_RESULT",
544
+ message: `Received completion for unknown app-server item: ${item.id}`
545
+ }];
546
+ state.status = item.status === "completed" && (item.type !== "commandExecution" || (item.exitCode ?? 0) === 0) ? "completed" : "failed";
547
+ state.output = item.type === "commandExecution" ? item.aggregatedOutput ?? state.outputBuffer : item.changes.map((change) => change.diff).join("\n");
548
+ const events = [{
549
+ type: "message.part.updated",
550
+ part: this.toolPart(item.id, state)
551
+ }];
552
+ if (state.status !== "completed") return events;
553
+ if (item.type === "commandExecution") events.push({
554
+ type: "model-processing",
555
+ phase: "tool-result",
556
+ toolName: state.tool,
557
+ elapsedMs: item.durationMs ?? (params.completedAtMs ?? Date.now()) - state.startTime
558
+ });
559
+ else for (const change of item.changes) {
560
+ const kind = normalizeChangeKind(change.kind);
561
+ if (kind === "delete") continue;
562
+ if (kind === void 0 && change.kind == null) {
563
+ this.logger?.debug("fileChange change without a kind", { path: change.path });
564
+ continue;
565
+ }
566
+ const part = this.filePart(change.path);
567
+ if (part) events.push({
568
+ type: "message.part.updated",
569
+ part
570
+ });
571
+ }
572
+ return events;
573
+ }
574
+ onCommandOutputDelta(params) {
575
+ const state = this.tools.get(params.itemId);
576
+ if (state) state.outputBuffer += params.delta;
577
+ return [];
578
+ }
579
+ onReasoningDelta(params) {
580
+ if (!params.delta) return [];
581
+ const text = (this.reasoning.get(params.itemId) ?? "") + params.delta;
582
+ this.reasoning.set(params.itemId, text);
583
+ const part = {
584
+ ...this.partBase(params.itemId),
585
+ type: "reasoning",
586
+ text
587
+ };
588
+ const events = [];
589
+ if (!this.emittedThinking) {
590
+ this.emittedThinking = true;
591
+ events.push({
592
+ type: "model-processing",
593
+ phase: "thinking"
594
+ });
595
+ }
596
+ events.push({
597
+ type: "message.part.updated",
598
+ part,
599
+ delta: params.delta
600
+ });
601
+ return events;
602
+ }
603
+ toolInvocations() {
604
+ return [...this.tools.values()].map((state) => ({
605
+ toolName: state.tool,
606
+ input: state.input,
607
+ result: state.output,
608
+ ...state.status === "failed" ? { isError: true } : {}
609
+ }));
610
+ }
611
+ };
612
+ //#endregion
613
+ //#region src/codex-app-server-turn.ts
614
+ const CODEX_PLAN_MCP_NAME = "tangle-interaction";
615
+ function durablePlanInstruction(continuation, planMode) {
616
+ if (continuation?.decision.outcome === "approved") return `The user approved durable plan ${continuation.plan.id}. Execute this exact plan now:\n\n${continuation.plan.body}`;
617
+ if (continuation?.decision.outcome === "rejected") return `The user rejected durable plan ${continuation.plan.id}. Revise it using this feedback:\n\n${continuation.decision.feedback}\n\nPrevious plan:\n${continuation.plan.body}\n\nWhen the revision is complete, call request_plan with the full revised plan. Submission ends this turn.`;
618
+ if (planMode) return "Produce a complete implementation plan, then call request_plan with its full body. Submission ends this turn; do not execute it.";
619
+ }
620
+ var CodexAppServerTurn = class {
621
+ activeThreadId;
622
+ submittedPlan;
623
+ planInteractions;
624
+ text = "";
625
+ tokenUsage;
626
+ activeTurnId;
627
+ closeEnd;
628
+ pendingTurnCompletions = [];
629
+ pendingTurnErrors = [];
630
+ planInterruptRequested = false;
631
+ terminalSettled = false;
632
+ resolveTurn;
633
+ rejectTurn;
634
+ turnDone;
635
+ items;
636
+ constructor(turn) {
637
+ this.turn = turn;
638
+ this.turnDone = new Promise((resolve, reject) => {
639
+ this.resolveTurn = resolve;
640
+ this.rejectTurn = reject;
641
+ });
642
+ this.turnDone.catch(() => {});
643
+ this.items = new AppServerItemMapper({
644
+ sessionId: turn.sessionId,
645
+ messageId: turn.messageId
646
+ }, turn.logger);
647
+ }
648
+ planProviderState() {
649
+ if (!this.activeThreadId) throw new Error("Codex thread identity was unavailable during plan submission");
650
+ return {
651
+ kind: "codex",
652
+ version: 1,
653
+ threadId: this.activeThreadId
654
+ };
655
+ }
656
+ subscribe(bridge) {
657
+ const unsubscribers = [
658
+ ...this.subscribeItems(bridge),
659
+ this.subscribeAgentMessage(bridge),
660
+ this.subscribeTokenUsage(bridge),
661
+ bridge.onNotification("turn/completed", (p) => {
662
+ this.handleTurnCompleted(p);
663
+ }),
664
+ bridge.onNotification("error", (p) => this.handleErrorNotification(p)),
665
+ bridge.onClose((reason) => {
666
+ if (this.activeTurnId === void 0 && this.pendingTurnCompletions.length > 0) {
667
+ queueMicrotask(() => this.failFromClose(reason));
668
+ return;
669
+ }
670
+ this.failFromClose(reason);
671
+ })
672
+ ];
673
+ return () => {
674
+ for (const unsubscribe of unsubscribers) unsubscribe();
675
+ };
676
+ }
677
+ async run(bridge, params) {
678
+ const { onEvent, sessionId } = this.turn;
679
+ try {
680
+ onEvent?.({
681
+ type: "status",
682
+ status: "started"
683
+ });
684
+ await this.handshake(bridge, params);
685
+ await this.startTurn(bridge, params);
686
+ await this.turnDone;
687
+ await params.cleanup?.();
688
+ if (this.submittedPlan) {
689
+ if (!this.planInteractions) throw new Error("Codex committed a plan without terminal plan support");
690
+ this.submittedPlan = await this.planInteractions.confirmSubmittedPlan();
691
+ }
692
+ return {
693
+ text: this.text,
694
+ toolInvocations: this.items.toolInvocations(),
695
+ sessionId,
696
+ outcome: this.submittedPlan ? {
697
+ type: "awaiting_plan_decision",
698
+ plan: this.submittedPlan
699
+ } : { type: "completed" },
700
+ ...this.tokenUsage ? { tokenUsage: this.tokenUsage } : {}
701
+ };
702
+ } catch (error) {
703
+ if (this.submittedPlan) try {
704
+ await this.planInteractions?.withdrawSubmittedPlan(`Codex failed to terminalize its planning turn: ${error instanceof Error ? error.message : String(error)}`);
705
+ } catch (withdrawalError) {
706
+ throw new AggregateError([error, withdrawalError], "Codex planning failed and its durable plan could not be withdrawn");
707
+ }
708
+ throw Object.assign(error, { reason: error.reason ?? this.closeEnd ?? turnEndReason({ aborted: this.turn.input.abortSignal?.aborted }) });
709
+ }
710
+ }
711
+ async handshake(bridge, params) {
712
+ const initializeStarted = performance.now();
713
+ await bridge.request("initialize", {
714
+ clientInfo: {
715
+ name: "tangle-sidecar",
716
+ title: null,
717
+ version: "0.1.0"
718
+ },
719
+ capabilities: params.initialize ? { experimentalApi: true } : null
720
+ });
721
+ bridge.notify("initialized");
722
+ await params.initialize?.(bridge);
723
+ const previous = this.activeThreadId;
724
+ const { thread } = await bridge.request(previous ? "thread/resume" : "thread/start", {
725
+ ...previous ? { threadId: previous } : {},
726
+ cwd: params.cwd,
727
+ model: params.model,
728
+ approvalPolicy: params.approvalPolicy,
729
+ sandbox: params.sandbox
730
+ });
731
+ if (previous && thread.id !== previous) throw new Error("Codex resumed a different thread");
732
+ this.activeThreadId = thread.id;
733
+ await this.turn.onNativeSession?.(thread.id);
734
+ this.turn.onEvent?.({
735
+ type: "native.session.observed",
736
+ provider: "codex",
737
+ nativeSessionId: thread.id
738
+ });
739
+ this.turn.logger.observePhase?.("cli_initialize", performance.now() - initializeStarted);
740
+ }
741
+ async startTurn(bridge, params) {
742
+ const { input, plans } = this.turn;
743
+ const turnMessage = [durablePlanInstruction(input.planContinuation, plans), params.promptText].filter((part) => typeof part === "string" && part.length > 0).join(PROMPT_SECTION_SEPARATOR);
744
+ const startedTurn = await bridge.request("turn/start", {
745
+ threadId: this.activeThreadId,
746
+ input: [{
747
+ type: "text",
748
+ text: turnMessage,
749
+ text_elements: []
750
+ }],
751
+ cwd: params.cwd,
752
+ approvalPolicy: params.approvalPolicy,
753
+ sandboxPolicy: sandboxModeToPolicy(params.sandbox, params.cwd),
754
+ model: params.model
755
+ });
756
+ this.activeTurnId = startedTurn.turn.id;
757
+ this.drainPendingTurnEvents();
758
+ }
759
+ completeTurn() {
760
+ if (this.terminalSettled) return;
761
+ this.terminalSettled = true;
762
+ this.resolveTurn?.();
763
+ }
764
+ failTurn(cause) {
765
+ if (this.terminalSettled) return;
766
+ this.terminalSettled = true;
767
+ this.rejectTurn?.(cause instanceof Error ? cause : new Error(String(cause)));
768
+ }
769
+ forward(events) {
770
+ for (const event of events) this.turn.onEvent?.(event);
771
+ }
772
+ subscribeItems(bridge) {
773
+ return [
774
+ bridge.onNotification("item/started", (p) => this.forward(this.items.onItemStarted(p))),
775
+ bridge.onNotification("item/completed", (p) => this.handleItemCompleted(bridge, p)),
776
+ bridge.onNotification("item/commandExecution/outputDelta", (p) => this.forward(this.items.onCommandOutputDelta(p))),
777
+ bridge.onNotification("item/reasoning/textDelta", (p) => this.forward(this.items.onReasoningDelta(p))),
778
+ bridge.onNotification("item/reasoning/summaryTextDelta", (p) => this.forward(this.items.onReasoningDelta(p)))
779
+ ];
780
+ }
781
+ async handleItemCompleted(bridge, lifecycle) {
782
+ this.forward(this.items.onItemCompleted(lifecycle));
783
+ const item = lifecycle.item;
784
+ if (!this.turn.plans || item.type !== "mcpToolCall" || item.server !== "tangle-interaction" || item.tool !== "request_plan") return;
785
+ if (lifecycle.threadId !== this.activeThreadId || lifecycle.turnId !== this.activeTurnId) {
786
+ this.failTurn(/* @__PURE__ */ new Error("Codex completed request_plan for a different thread or turn"));
787
+ return;
788
+ }
789
+ if (item.status !== "completed" || !this.submittedPlan) {
790
+ this.failTurn(/* @__PURE__ */ new Error("Codex request_plan completed without a durable plan receipt"));
791
+ return;
792
+ }
793
+ if (this.planInterruptRequested) return;
794
+ this.planInterruptRequested = true;
795
+ try {
796
+ await bridge.request("turn/interrupt", {
797
+ threadId: this.activeThreadId,
798
+ turnId: this.activeTurnId
799
+ });
800
+ } catch (error) {
801
+ this.failTurn(error);
802
+ }
803
+ }
804
+ subscribeAgentMessage(bridge) {
805
+ const { sessionId, messageId, onEvent } = this.turn;
806
+ return bridge.onNotification("item/agentMessage/delta", (p) => {
807
+ const delta = p.delta ?? p.text ?? "";
808
+ if (!delta) return;
809
+ this.text += delta;
810
+ const part = {
811
+ id: messageId,
812
+ sessionID: sessionId,
813
+ messageID: messageId,
814
+ type: "text",
815
+ text: this.text
816
+ };
817
+ onEvent?.({
818
+ type: "message.part.updated",
819
+ part,
820
+ delta
821
+ });
822
+ });
823
+ }
824
+ subscribeTokenUsage(bridge) {
825
+ return bridge.onNotification("thread/tokenUsage/updated", (p) => {
826
+ const total = p.tokenUsage?.total;
827
+ if (!total) return;
828
+ this.tokenUsage = {
829
+ inputTokens: total.inputTokens ?? 0,
830
+ outputTokens: total.outputTokens ?? 0,
831
+ cacheReadInputTokens: total.cachedInputTokens
832
+ };
833
+ });
834
+ }
835
+ handleTurnCompleted(completed) {
836
+ if (this.activeThreadId === void 0 || this.activeTurnId === void 0) {
837
+ this.pendingTurnCompletions.push(completed);
838
+ return;
839
+ }
840
+ if (completed.threadId !== this.activeThreadId || completed.turn.id !== this.activeTurnId) return;
841
+ if (this.submittedPlan) {
842
+ if (!this.planInterruptRequested || completed.turn.status !== "interrupted") {
843
+ this.failTurn(/* @__PURE__ */ new Error(`Codex planning turn ended as ${completed.turn.status} before its durable plan was interrupted`));
844
+ return;
845
+ }
846
+ this.completeTurn();
847
+ return;
848
+ }
849
+ if (completed.turn.status === "completed") {
850
+ this.completeTurn();
851
+ return;
852
+ }
853
+ this.failTurn(new Error(completed.turn.error?.message ?? `Codex turn ended with status ${completed.turn.status}`));
854
+ }
855
+ reportTurnError(error, warning) {
856
+ if (this.terminalSettled) return;
857
+ this.turn.onEvent?.({
858
+ type: "warning",
859
+ code: "CODEX_TURN_ERROR",
860
+ message: warning
861
+ });
862
+ this.failTurn(error);
863
+ }
864
+ drainPendingTurnEvents() {
865
+ if (this.activeThreadId === void 0 || this.activeTurnId === void 0) return;
866
+ const pendingCompletions = this.pendingTurnCompletions.splice(0);
867
+ for (const completed of pendingCompletions) this.handleTurnCompleted(completed);
868
+ const pendingErrors = this.pendingTurnErrors.splice(0);
869
+ for (const pending of pendingErrors) this.reportTurnError(pending.error, pending.warning);
870
+ }
871
+ handleErrorNotification(p) {
872
+ const e = p;
873
+ const base = e.error?.message ?? "codex turn error";
874
+ const warning = e.error?.additionalDetails ? `${base} (${e.error.additionalDetails})` : base;
875
+ if (e.willRetry === true) {
876
+ this.turn.onEvent?.({
877
+ type: "warning",
878
+ code: "CODEX_TURN_ERROR",
879
+ message: warning
880
+ });
881
+ return;
882
+ }
883
+ if (this.activeTurnId === void 0 && this.pendingTurnCompletions.length > 0) {
884
+ this.pendingTurnErrors.push({
885
+ error: new Error(base),
886
+ warning
887
+ });
888
+ return;
889
+ }
890
+ this.reportTurnError(new Error(base), warning);
891
+ }
892
+ failFromClose(reason) {
893
+ this.drainPendingTurnEvents();
894
+ if (this.terminalSettled) return;
895
+ const error = reason.error ?? /* @__PURE__ */ new Error(`Codex app-server closed before the matching turn completed (${reason.reason}): send the next turn to resume its thread`);
896
+ this.closeEnd = turnEndReason({
897
+ aborted: this.turn.input.abortSignal?.aborted,
898
+ timedOut: reason.reason === "aborted",
899
+ linkLost: this.activeTurnId !== void 0 && reason.reason !== "graceful"
900
+ });
901
+ this.failTurn(Object.assign(error, { reason: this.closeEnd }));
902
+ }
903
+ };
904
+ //#endregion
905
+ //#region src/codex-connection-config.ts
906
+ const DEFAULT_MODEL = "gpt-5-mini";
907
+ function resolveCodexRouterModel(model) {
908
+ if (!model) return DEFAULT_MODEL;
909
+ if (model.startsWith("openai/")) return model.slice(7);
910
+ if (model.startsWith("gpt-")) return model;
911
+ return DEFAULT_MODEL;
912
+ }
913
+ function normalizeExplicitCodexModel(model) {
914
+ return model.startsWith("openai/") ? model.slice(7) : model;
915
+ }
916
+ function resolveCodexAuthMode(config) {
917
+ return config.authMode ?? (config.apiKey ? "api-key" : "oauth");
918
+ }
919
+ const CODEX_CONFIG_PATH = ".codex/config.toml";
920
+ function isCodexConfigPath(filePath) {
921
+ return path.posix.normalize(filePath.replaceAll("\\", "/").replace(/^~\//, "")) === CODEX_CONFIG_PATH;
922
+ }
923
+ function resolveCodexApiKeyBaseUrl(config) {
924
+ return config.baseUrl || (config.apiKey ? "https://api.openai.com/v1" : resolveTangleRouterDefaults().baseUrl);
925
+ }
926
+ function resolveCodexConnection(config, explicitModel) {
927
+ const authMode = resolveCodexAuthMode(config);
928
+ const gatewayDefaults = resolveTangleRouterDefaults();
929
+ const baseUrl = authMode === "api-key" ? resolveCodexApiKeyBaseUrl(config) : void 0;
930
+ const gatewayModel = process.env.CODEX_MODEL || process.env.TANGLE_ROUTER_CODEX_MODEL || gatewayDefaults.model;
931
+ return {
932
+ baseUrl,
933
+ model: explicitModel !== void 0 ? normalizeExplicitCodexModel(explicitModel) : authMode === "api-key" ? resolveCodexRouterModel(gatewayModel) : void 0
934
+ };
935
+ }
936
+ function codexGatewayConfigArgs(baseUrl) {
937
+ if (!baseUrl) return [];
938
+ return Object.entries({
939
+ model_provider: "tangle",
940
+ "model_providers.tangle.name": "Tangle Router",
941
+ "model_providers.tangle.base_url": baseUrl,
942
+ "model_providers.tangle.env_key": "OPENAI_API_KEY",
943
+ "model_providers.tangle.wire_api": "responses"
944
+ }).flatMap(([key, value]) => ["-c", `${key}=${tomlString(value)}`]);
945
+ }
946
+ function tomlString(value) {
947
+ return JSON.stringify(value);
948
+ }
949
+ function codexPlanMcpConfigArgs(options) {
950
+ return [
951
+ "-c",
952
+ `mcp_servers.${options.name}.url=${tomlString(options.url)}`,
953
+ "-c",
954
+ `mcp_servers.${options.name}.bearer_token_env_var=${tomlString(options.tokenEnv)}`,
955
+ "-c",
956
+ `mcp_servers.${options.name}.tools.request_plan.approval_mode="approve"`
957
+ ];
958
+ }
959
+ function routedBaseUrl(config, baseUrl, getAgentEnvironment) {
960
+ if (!baseUrl || !codexWarmSwitchOn()) return baseUrl;
961
+ const host = getAgentEnvironment();
962
+ return managedModelProxyUrl({
963
+ binding: Object.hasOwn(host, "TANGLE_OPENCODE_MODEL_PROXY") ? host["TANGLE_OPENCODE_MODEL_PROXY"] : process.env["TANGLE_OPENCODE_MODEL_PROXY"],
964
+ apiKey: config.apiKey,
965
+ upstream: baseUrl,
966
+ runtimeHome: config.runtimeHome
967
+ }) ?? baseUrl;
968
+ }
969
+ function codexWarmSwitchOn() {
970
+ return process.env[AGENT_INTERNAL_CODEX_WARM_SERVER_ENV_VAR] === "1";
971
+ }
972
+ //#endregion
973
+ //#region src/codex-native-launch.ts
974
+ var CodexNativeLaunchAdapter = class extends CliProviderAdapterBase {
975
+ inheritProcessEnv = false;
976
+ nativeAttachmentKinds() {
977
+ return ["image"];
978
+ }
979
+ nativeConfigArgs(config, connection, prompts, flags) {
980
+ return [
981
+ ...codexDeveloperInstructionArgs(prompts.appendSystemPrompt === void 0 ? prompts.systemPrompt : [prompts.systemPrompt, prompts.appendSystemPrompt].filter((part) => Boolean(part?.trim())).join("\n\n"), flags.concat(config.extraFlags ?? [])),
982
+ ...codexGatewayConfigArgs(routedBaseUrl(config, connection.baseUrl, () => this.agentEnvironmentValues())),
983
+ ...nativeEffortArgs("codex", config.profile?.model?.reasoningEffort),
984
+ ...flags
985
+ ];
986
+ }
987
+ buildExecArgs(params) {
988
+ const args = ["exec"];
989
+ if (params.sessionId) args.push("resume");
990
+ args.push("--json", ...CODEX_DISABLED_FEATURE_ARGS);
991
+ const explicitModel = params.model ?? params.config.model;
992
+ const connection = resolveCodexConnection(params.config, explicitModel);
993
+ if (connection.model) args.push("--model", connection.model);
994
+ if (!params.sessionId) args.push("--cd", params.workspaceRoot);
995
+ args.push("--skip-git-repo-check");
996
+ if (params.config.bypassPermissions !== false) args.push("--dangerously-bypass-approvals-and-sandbox");
997
+ else if (params.config.sandboxMode) args.push("--sandbox", params.config.sandboxMode);
998
+ args.push(...this.nativeConfigArgs(params.config, connection, params, params.config.extraFlags ?? []));
999
+ const { native: nativeImages, message: messageWithRemoteMarkers } = partitionImageAttachments({
1000
+ message: params.message,
1001
+ attachments: params.attachments,
1002
+ qualifies: (attachment) => attachment.path !== void 0
1003
+ });
1004
+ const imageFlags = nativeImages.flatMap((attachment) => ["-i", attachment.path]);
1005
+ args.push(...imageFlags, ...params.sessionId ? [params.sessionId] : [], "-");
1006
+ return {
1007
+ command: this.getCliPath(),
1008
+ args,
1009
+ useStdin: true,
1010
+ stdin: messageWithRemoteMarkers
1011
+ };
1012
+ }
1013
+ buildExecEnv(config) {
1014
+ const env = this.agentEnvironmentValues();
1015
+ if (config.runtimeHome) env.CODEX_HOME = path.join(config.runtimeHome, ".codex");
1016
+ if (resolveCodexAuthMode(config) === "oauth") {
1017
+ env.OPENAI_API_KEY = void 0;
1018
+ env.OPENAI_BASE_URL = void 0;
1019
+ } else {
1020
+ if (config.apiKey) env.OPENAI_API_KEY = config.apiKey;
1021
+ env.OPENAI_BASE_URL = routedBaseUrl(config, resolveCodexConnection(config).baseUrl, () => this.agentEnvironmentValues());
1022
+ }
1023
+ return env;
1024
+ }
1025
+ buildAppServerEnv(profilePlan, workspaceRoot) {
1026
+ return this.applyWritableHomeDefaults(this.mergeProfilePlanEnvironment(this.buildNativeExecutionEnvironment(this.config), profilePlan, this.config.nativeAuthEnv !== void 0 || (this.config.authFiles?.length ?? 0) > 0), workspaceRoot);
1027
+ }
1028
+ getManagedAuthFiles() {
1029
+ if (resolveCodexAuthMode(this.config) !== "oauth" || this.config.authFiles?.some((file) => isCodexConfigPath(file.path))) return [];
1030
+ const model = this.config.model ? normalizeExplicitCodexModel(this.config.model) : void 0;
1031
+ return [{
1032
+ path: CODEX_CONFIG_PATH,
1033
+ content: `${[...model ? [`model = ${tomlString(model)}`] : [], "cli_auth_credentials_store = \"file\""].join("\n")}\n`,
1034
+ mode: 384
1035
+ }];
1036
+ }
1037
+ };
1038
+ //#endregion
1039
+ //#region src/codex-secret-profile.ts
1040
+ var CodexSecretProfileAdapter = class extends CodexNativeLaunchAdapter {
1041
+ supportsSecretProfileRevisions = true;
1042
+ mcpOwner;
1043
+ binary;
1044
+ constructor(options = {}) {
1045
+ super(options);
1046
+ this.mcpOwner = { proxy: options.mcpCredentialProxy };
1047
+ }
1048
+ refuse(reason, path = this.config.runtimeHome) {
1049
+ return refuseSecretLaunch(`Secret Codex ${reason}`, { path });
1050
+ }
1051
+ getCliPath() {
1052
+ return this.binary ?? super.getCliPath();
1053
+ }
1054
+ secretProfileVerificationScope() {
1055
+ return `${this.provider}\0${resolveExecutableRealpath(super.getCliPath(), this.agentEnvironmentValues().PATH) ?? super.getCliPath()}`;
1056
+ }
1057
+ async verifyPreparedSecretProfileRevision(input, plan) {
1058
+ await this.readNativeConfig(input, plan);
1059
+ }
1060
+ async readNativeConfig(input, plan) {
1061
+ const workspace = input.workspaceRoot ?? this.storagePath;
1062
+ const env = this.buildAppServerEnv(plan, workspace);
1063
+ const cwd = env.TANGLE_PROFILE_REVISION_WORKSPACE;
1064
+ const home = this.config.runtimeHome;
1065
+ const identity = this.subprocessIdentity();
1066
+ if (!cwd || !home || env.HOME !== home || env.CODEX_HOME !== `${home}/.codex` || env.TANGLE_SHARED_WORKSPACE !== workspace || !identity || !isReservedProfileUid(identity.uid)) this.refuse("identity or revision is invalid", home);
1067
+ assertSealedNativeHome(home, identity.gid, SEALED_NATIVE_HOME_LAYOUT.codex);
1068
+ assertSealedNativeSearchPath(cwd, [".codex", ".agents"]);
1069
+ const command = await provenStoreBinary(`codex-cli ${NATIVE_SESSION_CONTRACTS.codex.cliVersion}`, {
1070
+ command: super.getCliPath(),
1071
+ cwd,
1072
+ env,
1073
+ inheritEnv: this.inheritProcessEnv,
1074
+ isolatedIdentity: identity,
1075
+ signal: input.abortSignal
1076
+ });
1077
+ this.binary = command;
1078
+ const bridge = spawnRpcBridge({
1079
+ detached: true,
1080
+ inheritEnv: this.inheritProcessEnv,
1081
+ command,
1082
+ args: this.nativeArgs(input, plan),
1083
+ cwd,
1084
+ env,
1085
+ ...identity,
1086
+ isolatedIdentity: this.config.subprocessUser,
1087
+ defaultRequestTimeoutMs: 15e3,
1088
+ signal: input.abortSignal
1089
+ });
1090
+ try {
1091
+ await bridge.request("initialize", {
1092
+ clientInfo: {
1093
+ name: "tangle-profile-prepare",
1094
+ version: "1"
1095
+ },
1096
+ capabilities: { experimentalApi: true }
1097
+ });
1098
+ bridge.notify("initialized");
1099
+ const servers = await this.readNativeControls(bridge, plan, cwd);
1100
+ const referenced = new Set(Object.values(servers).flatMap((server) => [...Object.values(server.env_http_headers ?? {}), ...server.env_vars ?? []]));
1101
+ if (Object.entries(env).some(([name, value]) => value?.includes("{env:TANGLE_PROFILE_SECRET_") && !referenced.has(name))) this.refuse("secret is outside MCP", cwd);
1102
+ return {
1103
+ cwd,
1104
+ env,
1105
+ servers
1106
+ };
1107
+ } finally {
1108
+ await bridge.close();
1109
+ }
1110
+ }
1111
+ async readNativeControls(bridge, plan, cwd, expected) {
1112
+ const { config, layers } = await bridge.request("config/read", {
1113
+ cwd,
1114
+ includeLayers: true
1115
+ });
1116
+ const prompt = plan.spawn.flags.find((flag) => flag.startsWith("model_instructions_file="));
1117
+ if (config.features?.apps !== false || config.features.plugins !== false || prompt && config.model_instructions_file !== JSON.parse(prompt.slice(prompt.indexOf("=") + 1))) this.refuse("native controls differ", cwd);
1118
+ if (!layers || layers.some((layer) => layer.config?.mcp_servers !== void 0 && layer.name?.type !== "sessionFlags")) this.refuse("has ambient MCP configuration", cwd);
1119
+ const servers = config.mcp_servers ?? {};
1120
+ if (expected && (Object.keys(servers).length !== Object.keys(expected).length || Object.entries(servers).some(([name, server]) => {
1121
+ const declared = expected[name];
1122
+ return !declared || server.url !== declared.url || server.command || (server.args?.length ?? 0) > 0 || Object.keys(server.env ?? {}).length > 0 || (server.env_vars?.length ?? 0) > 0 || (server.bearer_token_env_var ?? void 0) !== declared.bearer_token_env_var || JSON.stringify(server.http_headers ?? {}) !== JSON.stringify(declared.http_headers ?? {}) || Object.keys(server.env_http_headers ?? {}).length > 0;
1123
+ }))) this.refuse("native MCP proxy controls differ", cwd);
1124
+ return servers;
1125
+ }
1126
+ nativeArgs(input, plan, mcp) {
1127
+ const prompts = this.resolveProfilePrompts(input, plan);
1128
+ if (this.config.secretProfileRevisionRoot && plan) {
1129
+ const context = `The mutable workspace is ${JSON.stringify(input.workspaceRoot ?? this.storagePath)}. The current directory holds the read-only supplied profile. Use absolute paths in the mutable workspace for edits, builds and git commands.`;
1130
+ prompts.appendSystemPrompt = [prompts.appendSystemPrompt, context].filter(Boolean).join("\n\n");
1131
+ }
1132
+ const connection = resolveCodexConnection(this.config, this.resolveProfileModel(input.model?.model) ?? this.config.model);
1133
+ const flags = (plan?.spawn.flags ?? []).filter((flag, index, all) => mcp === void 0 || !(flag === "-c" ? all[index + 1] : flag)?.startsWith("mcp_servers="));
1134
+ return [
1135
+ "app-server",
1136
+ "--strict-config",
1137
+ ...CODEX_DISABLED_FEATURE_ARGS,
1138
+ ...this.nativeConfigArgs(this.config, connection, prompts, flags),
1139
+ ...mcp === void 0 ? [] : ["-c", `mcp_servers=${inlineToml(mcp)}`]
1140
+ ];
1141
+ }
1142
+ async prepareSecretCodexTurn(input, plan, sessionId) {
1143
+ const native = await this.readNativeConfig(input, plan);
1144
+ const cwd = this.bindSecretProfileSessionView(native.cwd, sessionId);
1145
+ assertSealedNativeSearchPath(cwd, [".codex", ".agents"]);
1146
+ const refs = this.secretProfileReferences(plan) ?? {};
1147
+ const served = [];
1148
+ const inherited = (entries) => Object.fromEntries(entries.map(([name, key]) => [name, native.env[key]]));
1149
+ for (const [name, entry] of Object.entries(native.servers)) {
1150
+ if (entry.enabled === false) continue;
1151
+ const server = entry.url ? servableMcpServer(name, {
1152
+ url: entry.url,
1153
+ headers: {
1154
+ ...entry.http_headers,
1155
+ ...inherited(Object.entries(entry.env_http_headers ?? {}))
1156
+ }
1157
+ }, refs) : servableStdioServer(name, {
1158
+ command: [entry.command, ...entry.args ?? []],
1159
+ environment: {
1160
+ ...entry.env,
1161
+ ...inherited((entry.env_vars ?? []).map((key) => [key, key]))
1162
+ }
1163
+ }, refs);
1164
+ if (typeof server === "string") this.refuse(`cannot serve MCP ${name}: ${server}`, cwd);
1165
+ served.push(server);
1166
+ }
1167
+ const uid = this.config.subprocessUser?.uid;
1168
+ const scope = secretMcpScope(sessionId, uid, this.mcpOwner);
1169
+ const servers = {};
1170
+ if (served.length) {
1171
+ if (!this.mcpOwner.proxy) this.refuse("requires an MCP proxy", cwd);
1172
+ const endpoint = await this.mcpOwner.proxy.endpoint(scope, served.map((server) => server.name));
1173
+ for (const server of served) {
1174
+ const url = endpoint.urls[server.name];
1175
+ if (!url || !isLoopbackEndpoint(url)) this.refuse("MCP endpoint is not loopback", cwd);
1176
+ const declared = native.servers[server.name];
1177
+ servers[server.name] = {
1178
+ url,
1179
+ http_headers: { Authorization: `Bearer ${endpoint.placeholder}` },
1180
+ ...Object.fromEntries(Object.entries(declared ?? {}).filter(([key, value]) => value !== null && [
1181
+ "tool_timeout_sec",
1182
+ "startup_timeout_sec",
1183
+ "enabled_tools",
1184
+ "disabled_tools",
1185
+ "required",
1186
+ "defer_loading"
1187
+ ].includes(key)))
1188
+ };
1189
+ }
1190
+ }
1191
+ const args = this.nativeArgs(input, plan, servers);
1192
+ return {
1193
+ cwd,
1194
+ env: native.env,
1195
+ args,
1196
+ release: served.length ? await this.mcpOwner.proxy?.lease(scope, served) : void 0,
1197
+ validate: (bridge, extra) => this.readNativeControls(bridge, plan, cwd, {
1198
+ ...servers,
1199
+ ...extra
1200
+ })
1201
+ };
1202
+ }
1203
+ async shutdown() {
1204
+ try {
1205
+ if (!this.config.stableIsolatedIdentity) await forgetSessionScopes({ owner: this.mcpOwner });
1206
+ } finally {
1207
+ await super.shutdown();
1208
+ }
1209
+ }
1210
+ };
1211
+ function inlineToml(value) {
1212
+ if (typeof value === "string") return tomlString(value);
1213
+ if (typeof value === "boolean" || Number.isFinite(value)) return String(value);
1214
+ if (Array.isArray(value)) return `[${value.map(inlineToml).join(", ")}]`;
1215
+ if (isPlainRecord(value)) return `{ ${Object.entries(value).map(([key, entry]) => `${tomlString(key)} = ${inlineToml(entry)}`).join(", ")} }`;
1216
+ refuseSecretLaunch("Secret Codex config value is unsupported", { actual: typeof value });
1217
+ }
1218
+ //#endregion
1219
+ //#region src/codex-app-server-adapter.ts
1220
+ const CODEX_PLAN_MCP_TOKEN_ENV = "TANGLE_INTERACTION_TOKEN";
1221
+ function appServerRawCapture(runtimeHome) {
1222
+ return globalThis.process.getuid?.() === 0 && runtimeHome?.trim() ? { rawCapturePath: resolveRawCapturePath(runtimeHome.trim()) } : {};
1223
+ }
1224
+ var CodexAppServerAdapter = class extends CodexSecretProfileAdapter {
1225
+ permissions = new InteractionBroker();
1226
+ getCapabilities() {
1227
+ const { interactive: permission, questions: question, plans: plan } = appServerPolicy(this.config);
1228
+ return {
1229
+ ...super.getCapabilities(),
1230
+ multimodal: !this.config.secretProfileRevisionRoot && !question && !plan,
1231
+ interactions: [
1232
+ ...permission ? [InteractionKind.Permission] : [],
1233
+ ...question ? [InteractionKind.Question] : [],
1234
+ ...plan ? [InteractionKind.Plan] : []
1235
+ ]
1236
+ };
1237
+ }
1238
+ async respondToInteraction(response) {
1239
+ if (this.permissions.respond(response)) return;
1240
+ this.permissions.respondQuestion(response);
1241
+ }
1242
+ async execute(input, services, onEvent) {
1243
+ const nativeResume = input.nativeResume;
1244
+ if (nativeResume) {
1245
+ if (nativeResume.harness !== "codex" || input.planContinuation) throw new Error("Codex native resume requires Codex without plan continuation");
1246
+ await this.seedNativeSessionForResume(input, nativeResume.nativeSessionId);
1247
+ }
1248
+ const { interactive, questions, plans } = appServerPolicy(this.config, input);
1249
+ if (!this.config.secretProfileRevisionRoot && !interactive && !questions && !plans && !input.planContinuation) return super.execute(input, services, onEvent);
1250
+ return this.withExecution(input.abortSignal, (signal) => this.executeViaAppServer({
1251
+ ...input,
1252
+ abortSignal: signal
1253
+ }, services, onEvent));
1254
+ }
1255
+ async executeViaAppServer(input, services, onEvent) {
1256
+ const prepared = await this.prepareAppServerTurn(input, services, onEvent);
1257
+ if (prepared.cached !== void 0) return prepared.cached;
1258
+ const queue = this.config.secretProfileRevisionRoot ? secretSessionTurnQueue(this.secretProfileSessionView(prepared.sessionId).view) : void 0;
1259
+ let bridge;
1260
+ let result;
1261
+ let closing;
1262
+ const close = () => closing ??= this.closeAppServerTurn(prepared, bridge, queue);
1263
+ prepared.run.cleanup = close;
1264
+ try {
1265
+ if (queue) await untilSettledOrAborted(queue.previous, input.abortSignal ?? new AbortController().signal);
1266
+ bridge = await this.launchAppServerTurn(prepared);
1267
+ result = await this.runAppServerTurn(prepared, bridge);
1268
+ } finally {
1269
+ if (!closing) await close();
1270
+ }
1271
+ await services.recorder.markTurnCompleted?.({ result });
1272
+ onEvent?.({
1273
+ type: "status",
1274
+ status: "completed"
1275
+ });
1276
+ return result;
1277
+ }
1278
+ /** Capture session binding, profile/prompt and native callbacks before acquiring resources.
1279
+ * Field construction stays one preparation record; launch/run/cleanup have separate owners. */
1280
+ async prepareAppServerTurn(input, services, onEvent) {
1281
+ this.ensureInitialized();
1282
+ const opened = await this.openTurnSession({
1283
+ ...input,
1284
+ sessionId: input.sessionId ?? input.interactionBinding?.sessionId
1285
+ }, services.recorder, onEvent);
1286
+ if (opened.cached) return { cached: opened.cached };
1287
+ const { sessionId } = opened;
1288
+ const interactionBinding = this.interactionExecutionBinding(input, sessionId);
1289
+ const workspace = input.workspaceRoot ?? this.storagePath;
1290
+ const { profilePlan } = await this.materializeTurnProfile(input, sessionId);
1291
+ const { promptText } = await this.recordTurnPrompt(input, opened, services.recorder, {
1292
+ nativeAttachmentKinds: [],
1293
+ inlineImageBudgetBytes: 0
1294
+ });
1295
+ const connection = resolveCodexConnection(this.config, this.resolveProfileModel(input.model?.model) ?? this.config.model);
1296
+ const policy = appServerPolicy(this.config, input);
1297
+ const run = {
1298
+ promptText,
1299
+ cwd: workspace,
1300
+ model: connection.model,
1301
+ ...policy
1302
+ };
1303
+ const turn = new CodexAppServerTurn({
1304
+ sessionId,
1305
+ messageId: opened.providerMessageIds?.assistantMessageId ?? randomUUID(),
1306
+ input,
1307
+ onEvent,
1308
+ logger: this.logger,
1309
+ plans: policy.plans,
1310
+ onNativeSession: async (providerSessionId) => {
1311
+ await this.getSessionStore().update(opened.sessionId, {
1312
+ providerSessionId,
1313
+ ...this.config.runtimeHome ? { providerSessionHome: this.config.runtimeHome } : {}
1314
+ });
1315
+ }
1316
+ });
1317
+ turn.activeThreadId = initialThreadId(input) ?? opened.providerSessionId;
1318
+ return {
1319
+ releaseMcp: void 0,
1320
+ input,
1321
+ services,
1322
+ onEvent,
1323
+ sessionId,
1324
+ profilePlan,
1325
+ run,
1326
+ policy,
1327
+ turn,
1328
+ interactionBinding
1329
+ };
1330
+ }
1331
+ async launchAppServerTurn(context) {
1332
+ const { input, profilePlan, run } = context;
1333
+ const secret = this.config.secretProfileRevisionRoot && profilePlan ? await this.prepareSecretCodexTurn(input, profilePlan, context.sessionId) : void 0;
1334
+ context.releaseMcp = secret?.release;
1335
+ const env = secret?.env ?? this.buildAppServerEnv(profilePlan, run.cwd);
1336
+ run.cwd = secret?.cwd ?? run.cwd;
1337
+ const args = secret?.args ?? this.nativeArgs(input, profilePlan);
1338
+ const extraMcp = await this.bindPlanMcp(context, args, env);
1339
+ if (secret) run.initialize = async (initialized) => {
1340
+ await secret.validate(initialized, extraMcp);
1341
+ if (existsSync(join(run.cwd, ".codex/skills"))) await initialized.request("skills/extraRoots/set", { extraRoots: [join(run.cwd, ".codex/skills")] });
1342
+ };
1343
+ return this.spawnAppServer(input, {
1344
+ args,
1345
+ cwd: run.cwd,
1346
+ env
1347
+ });
1348
+ }
1349
+ spawnAppServer(input, launch) {
1350
+ const { timeoutMs } = input;
1351
+ const signal = AbortSignal.any([...input.abortSignal ? [input.abortSignal] : [], ...timeoutMs && timeoutMs > 0 ? [AbortSignal.timeout(timeoutMs)] : []]);
1352
+ const started = performance.now();
1353
+ const bridge = spawnRpcBridge({
1354
+ ...launch,
1355
+ detached: true,
1356
+ inheritEnv: this.inheritProcessEnv,
1357
+ command: this.getCliPath(),
1358
+ signal,
1359
+ ...this.subprocessSpawnOptions(),
1360
+ isolatedIdentity: this.config.subprocessUser,
1361
+ ...appServerRawCapture(this.config.runtimeHome)
1362
+ });
1363
+ bridge.process?.once("spawn", () => this.logger.observePhase?.("cli_spawn", performance.now() - started));
1364
+ return bridge;
1365
+ }
1366
+ async runAppServerTurn(context, bridge) {
1367
+ const { turn, run, sessionId, services, onEvent, interactionBinding } = context;
1368
+ registerAppServerInteractions(bridge, this.permissions, sessionId, interactionBinding, context.policy.interactive, context.policy.questions, onEvent);
1369
+ const unsubscribe = turn.subscribe(bridge);
1370
+ try {
1371
+ const result = await turn.run(bridge, run);
1372
+ await services.recorder.appendAssistantParts([{
1373
+ id: randomUUID(),
1374
+ type: "text",
1375
+ text: result.text
1376
+ }]);
1377
+ await this.getMessageStore().append({
1378
+ sessionId,
1379
+ role: "assistant",
1380
+ parts: [{
1381
+ type: "text",
1382
+ text: result.text
1383
+ }]
1384
+ });
1385
+ return result;
1386
+ } finally {
1387
+ unsubscribe();
1388
+ }
1389
+ }
1390
+ async closeAppServerTurn(context, bridge, queue) {
1391
+ try {
1392
+ await context.turn.planInteractions?.stop(context.sessionId);
1393
+ } finally {
1394
+ this.permissions.failSession(context.sessionId);
1395
+ try {
1396
+ await bridge?.close();
1397
+ } finally {
1398
+ await context.releaseMcp?.();
1399
+ }
1400
+ queue?.release();
1401
+ }
1402
+ }
1403
+ async bindPlanMcp(context, args, env) {
1404
+ if (!context.policy.plans) return {};
1405
+ const { turn, sessionId, interactionBinding, services, onEvent } = context;
1406
+ turn.planInteractions = new McpInteractionSupport(CODEX_PLAN_MCP_NAME, [InteractionKind.Plan]);
1407
+ const planMcp = await turn.planInteractions.start(sessionId, interactionBinding, onEvent, {
1408
+ planHost: services.planHost,
1409
+ providerState: () => turn.planProviderState(),
1410
+ onCommitted: (plan) => {
1411
+ turn.submittedPlan = plan;
1412
+ }
1413
+ });
1414
+ env[CODEX_PLAN_MCP_TOKEN_ENV] = planMcp.token;
1415
+ args.push(...codexPlanMcpConfigArgs({
1416
+ name: CODEX_PLAN_MCP_NAME,
1417
+ tokenEnv: CODEX_PLAN_MCP_TOKEN_ENV,
1418
+ url: planMcp.url
1419
+ }));
1420
+ return { [CODEX_PLAN_MCP_NAME]: {
1421
+ url: planMcp.url,
1422
+ bearer_token_env_var: CODEX_PLAN_MCP_TOKEN_ENV
1423
+ } };
1424
+ }
1425
+ };
1426
+ //#endregion
1427
+ //#region src/codex-exec-events.ts
1428
+ function mapCodexEventToStreamEvents(event, state, provider, logger) {
1429
+ switch (event.type) {
1430
+ case "session_started": return mapSessionStartedEvent(event, state);
1431
+ case "thread.started": return mapThreadStartedEvent(event, state, provider);
1432
+ case "message": return mapMessageEvent(event, state, logger);
1433
+ case "item.started": return mapToolItem(event.item, state, false) ?? [{
1434
+ type: "raw",
1435
+ backend: provider,
1436
+ event
1437
+ }];
1438
+ case "item.completed": return mapToolItem(event.item, state, true) ?? mapItemCompletedEvent(event, state, provider, logger);
1439
+ case "turn.completed":
1440
+ recordCliTokenUsage(state, event.usage);
1441
+ return [{
1442
+ type: "raw",
1443
+ backend: provider,
1444
+ event
1445
+ }];
1446
+ case "task_started": return mapTaskStartedEvent(event);
1447
+ case "error": return mapErrorEvent(event);
1448
+ default: return [{
1449
+ type: "raw",
1450
+ backend: provider,
1451
+ event
1452
+ }];
1453
+ }
1454
+ }
1455
+ /**
1456
+ * A tool the agent ran, as `codex exec --json` reports it: an item that
1457
+ * starts and then completes. A completion without a start emits both.
1458
+ * Undefined for an item that is not a tool.
1459
+ */
1460
+ function mapToolItem(item, state, completed) {
1461
+ const call = codexToolCall(item);
1462
+ if (!call) return void 0;
1463
+ const events = state.toolCalls.has(call.id) ? [] : toolStartEvents(state, call);
1464
+ if (!completed) return events;
1465
+ const failed = item.status === "failed" || item.status === "declined" || typeof item.exit_code === "number" && item.exit_code !== 0 || item.error !== void 0 && item.error !== null;
1466
+ return [...events, ...toolResultEvents(state, {
1467
+ id: call.id,
1468
+ content: item.aggregated_output ?? item.result ?? item.error ?? item.status,
1469
+ isError: failed
1470
+ })];
1471
+ }
1472
+ function codexToolCall(item) {
1473
+ const id = item.id;
1474
+ if (typeof id !== "string" || id.length === 0) return void 0;
1475
+ switch (item.type) {
1476
+ case "command_execution": return {
1477
+ id,
1478
+ name: "bash",
1479
+ input: { command: item.command ?? "" }
1480
+ };
1481
+ case "mcp_tool_call":
1482
+ if (!item.server && !item.tool) return void 0;
1483
+ return {
1484
+ id,
1485
+ name: `mcp__${item.server ?? ""}__${item.tool ?? ""}`,
1486
+ input: item.arguments ?? {}
1487
+ };
1488
+ case "web_search": return {
1489
+ id,
1490
+ name: "websearch",
1491
+ input: { query: item.query ?? "" }
1492
+ };
1493
+ default: return;
1494
+ }
1495
+ }
1496
+ function mapItemCompletedEvent(event, state, provider, logger) {
1497
+ if (event.item?.type === "file_change") {
1498
+ const fileEvents = mapFileChangeCompleted(event.item, state, logger);
1499
+ if (fileEvents !== null) return fileEvents;
1500
+ return [{
1501
+ type: "raw",
1502
+ backend: provider,
1503
+ event
1504
+ }];
1505
+ }
1506
+ if (event.item?.type !== "agent_message" || typeof event.item.text !== "string") return [{
1507
+ type: "raw",
1508
+ backend: provider,
1509
+ event
1510
+ }];
1511
+ const nextText = event.item.text;
1512
+ const previousLength = state.textBuffer.length;
1513
+ const delta = nextText.startsWith(state.textBuffer) ? nextText.slice(previousLength) : void 0;
1514
+ state.textBuffer = nextText;
1515
+ state.textPartId ??= randomUUID();
1516
+ return [{
1517
+ type: "message.part.updated",
1518
+ part: {
1519
+ ...streamPartBase(state, state.textPartId),
1520
+ type: "text",
1521
+ text: state.textBuffer
1522
+ },
1523
+ ...delta ? { delta } : {}
1524
+ }];
1525
+ }
1526
+ function mapFileChangeCompleted(item, state, logger) {
1527
+ if (!Array.isArray(item.changes)) {
1528
+ logger.debug("file_change item.completed without a changes array", { itemId: item.id });
1529
+ return null;
1530
+ }
1531
+ if (item.status !== void 0 && item.status !== "completed") return null;
1532
+ const events = [];
1533
+ for (const change of item.changes) {
1534
+ if (typeof change?.path !== "string" || change.path.length === 0) {
1535
+ logger.debug("malformed change in file_change item", { itemId: item.id });
1536
+ continue;
1537
+ }
1538
+ if (change.kind == null) {
1539
+ logger.debug("change without a kind in file_change item", {
1540
+ itemId: item.id,
1541
+ path: change.path
1542
+ });
1543
+ continue;
1544
+ }
1545
+ const tracked = trackFilePart(state, {
1546
+ path: change.path,
1547
+ changeKind: normalizeChangeKind(change.kind) ?? "update"
1548
+ });
1549
+ if (!tracked) continue;
1550
+ events.push(filePartUpdated(state, tracked));
1551
+ }
1552
+ return events.length > 0 ? events : null;
1553
+ }
1554
+ function mapSessionStartedEvent(event, state) {
1555
+ return [...sessionObservedEvents(state, "codex", event.session_id), {
1556
+ type: "status",
1557
+ status: "processing",
1558
+ detail: "Session started"
1559
+ }];
1560
+ }
1561
+ function mapThreadStartedEvent(event, state, provider) {
1562
+ return [...sessionObservedEvents(state, "codex", event.thread_id), {
1563
+ type: "raw",
1564
+ backend: provider,
1565
+ event
1566
+ }];
1567
+ }
1568
+ function mapMessageEvent(event, state, logger) {
1569
+ if (event.role !== "assistant" || !event.message.content) return [];
1570
+ const events = [];
1571
+ for (const block of event.message.content) events.push(...mapContentBlock(block, state, logger));
1572
+ return events;
1573
+ }
1574
+ function mapContentBlock(block, state, logger) {
1575
+ switch (block.type) {
1576
+ case "text": return textDeltaEvents(state, block.text);
1577
+ case "thinking": return reasoningEvents(state, block.thinking, { signalPhase: true });
1578
+ case "tool_use": return toolStartEvents(state, block);
1579
+ case "tool_result": return toolResultEvents(state, {
1580
+ id: block.tool_use_id,
1581
+ content: block.content,
1582
+ isError: Boolean(block.is_error),
1583
+ afterSuccess: (toolCall) => detectFileParts(toolCall, block.content, state, logger)
1584
+ });
1585
+ default: return [];
1586
+ }
1587
+ }
1588
+ function detectFileParts(toolCall, raw, state, logger) {
1589
+ const events = [];
1590
+ if (!toolCall.name.startsWith("mcp__") || !Array.isArray(raw)) return events;
1591
+ for (const block of raw) {
1592
+ if (typeof block !== "object" || block === null || block.type !== "image") continue;
1593
+ const { data, mimeType } = block;
1594
+ if (typeof data !== "string" || typeof mimeType !== "string" || !/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(mimeType)) {
1595
+ logger.debug("malformed image block in MCP tool result", { tool: toolCall.name });
1596
+ continue;
1597
+ }
1598
+ const tracked = trackFilePart(state, {
1599
+ dataUri: `data:${mimeType};base64,${data}`,
1600
+ mediaType: mimeType
1601
+ });
1602
+ if (!tracked) continue;
1603
+ events.push(filePartUpdated(state, tracked));
1604
+ }
1605
+ return events;
1606
+ }
1607
+ function mapTaskStartedEvent(event) {
1608
+ return [{
1609
+ type: "status",
1610
+ status: "processing",
1611
+ detail: `Task ${event.task_type ?? event.task_id} started`
1612
+ }];
1613
+ }
1614
+ function mapErrorEvent(event) {
1615
+ return [{
1616
+ type: "warning",
1617
+ code: event.code ?? "CODEX_ERROR",
1618
+ message: event.message
1619
+ }];
1620
+ }
1621
+ /** The complete emitted part for either native-path or MCP image observations. */
1622
+ function filePartUpdated(state, tracked) {
1623
+ return {
1624
+ type: "message.part.updated",
1625
+ part: {
1626
+ ...streamPartBase(state, tracked.id),
1627
+ type: "file",
1628
+ ...tracked.filename ? { filename: tracked.filename } : {},
1629
+ ...tracked.mediaType ? { mediaType: tracked.mediaType } : {},
1630
+ url: tracked.url
1631
+ }
1632
+ };
1633
+ }
1634
+ //#endregion
1635
+ //#region src/held-spare.ts
1636
+ /**
1637
+ * The Codex process started before the turn that uses it (the shared
1638
+ * mechanics are sdk-cli-runner held-spare.ts).
1639
+ *
1640
+ * `codex exec ... -` finishes its start (config, auth, model provider) and then
1641
+ * reads its prompt from stdin, so a process started with stdin held open does
1642
+ * that work ahead of the turn and takes the prompt the moment it arrives.
1643
+ * Codex requests carry no per-turn headers, so the turn's launch must equal
1644
+ * the spare's exactly: same argv, same environment.
1645
+ */
1646
+ const codexHeldSpares = createHeldSpareRegistry();
1647
+ //#endregion
1648
+ //#region src/adapter.ts
1649
+ /** Type guard for Codex events. */
1650
+ function isCodexEvent(value) {
1651
+ if (typeof value !== "object" || value === null) return false;
1652
+ return typeof value.type === "string";
1653
+ }
1654
+ /** Codex provider adapter. */
1655
+ var CodexProviderAdapter = class extends CodexAppServerAdapter {
1656
+ provider = "codex";
1657
+ profileHarness = "codex";
1658
+ defaultCliPath = "codex";
1659
+ /** Start the process the box's first turn will adopt: `codex exec` launched as an ordinary new-session managed turn. */
1660
+ async prewarmProcess(workspaceRootOrUndefined, launch) {
1661
+ this.ensureInitialized();
1662
+ const refused = { ready: Promise.resolve(false) };
1663
+ const home = this.config.runtimeHome?.trim();
1664
+ if (!home || !codexWarmSwitchOn() || !isManagedModelToken(this.config.apiKey)) return refused;
1665
+ const workspaceRoot = workspaceRootOrUndefined ?? this.storagePath;
1666
+ const existing = codexHeldSpares.spareFor(home);
1667
+ if (existing) return { ready: Promise.resolve(existing.state === "idle") };
1668
+ const input = {
1669
+ message: "",
1670
+ systemPrompt: launch.systemPrompt,
1671
+ workspaceRoot
1672
+ };
1673
+ if (this.resolveExecutionProfileForInput(input) || this.config.runtimeAttachments) return refused;
1674
+ const config = withTurnInteractions(this.config, input);
1675
+ if (routesPermissions(config) || routesQuestions(config) || codexRoutesPlans(config)) return refused;
1676
+ const execArgs = this.buildExecArgs({
1677
+ message: "",
1678
+ config,
1679
+ model: this.resolveProfileModel(void 0),
1680
+ workspaceRoot,
1681
+ systemPrompt: launch.systemPrompt
1682
+ });
1683
+ const env = this.buildTurnEnvironment(config, void 0, workspaceRoot, void 0);
1684
+ const id = randomUUID();
1685
+ const controller = new AbortController();
1686
+ const parser = createDeferredParser();
1687
+ const child = await super.spawnExecutionProcess({
1688
+ input,
1689
+ sessionId: id,
1690
+ config,
1691
+ execArgs,
1692
+ workspaceRoot,
1693
+ env,
1694
+ signal: controller.signal,
1695
+ parser: parser.config,
1696
+ deferRawCapture: true
1697
+ });
1698
+ const spare = {
1699
+ id,
1700
+ home,
1701
+ workspaceRoot,
1702
+ args: execArgs.args,
1703
+ env,
1704
+ process: child,
1705
+ parser,
1706
+ controller,
1707
+ startedAt: performance.now(),
1708
+ state: "idle"
1709
+ };
1710
+ spare.idleTimer = setTimeout(() => {
1711
+ this.logger.info("Held codex spare idle; stopping it", { home });
1712
+ codexHeldSpares.stop(spare);
1713
+ }, HELD_SPARE_IDLE_MS);
1714
+ spare.idleTimer.unref();
1715
+ codexHeldSpares.register(spare);
1716
+ child.exitPromise.then(() => codexHeldSpares.stop(spare), () => codexHeldSpares.stop(spare));
1717
+ this.logger.info("Held codex spare started", { home });
1718
+ return { ready: Promise.resolve(child.pid !== void 0) };
1719
+ }
1720
+ /** A new session takes the spare in its HOME when its own launch is the spare's: the same. */
1721
+ spawnExecutionProcess(request) {
1722
+ const spare = codexHeldSpares.spareFor(this.config.runtimeHome?.trim());
1723
+ if (!spare || spare.state !== "idle") return super.spawnExecutionProcess(request);
1724
+ const differences = launchDifferences(spare, {
1725
+ args: request.execArgs.args,
1726
+ env: request.env
1727
+ });
1728
+ const reason = spare.workspaceRoot !== request.workspaceRoot ? "workspace" : !request.execArgs.useStdin ? "prompt-not-on-stdin" : request.timeoutMs !== void 0 ? "turn has a timeout" : differences.length > 0 ? `launch differs: ${differences.join(",")}` : void 0;
1729
+ const own = () => super.spawnExecutionProcess(request);
1730
+ if (reason) {
1731
+ this.logger.info("Held codex spare not adopted", { reason });
1732
+ codexHeldSpares.stop(spare);
1733
+ return spare.process.exitPromise.then(own, own);
1734
+ }
1735
+ let adopted;
1736
+ try {
1737
+ adopted = codexHeldSpares.adopt(spare, request);
1738
+ } catch (error) {
1739
+ this.logger.info("Held codex spare not adopted", { reason: error instanceof Error ? error.message : String(error) });
1740
+ return spare.process.exitPromise.then(own, own);
1741
+ }
1742
+ this.logger.observePhase?.("cli_ready_wait", 0);
1743
+ this.logger.info("Held codex spare adopted", { heldMs: Math.round(performance.now() - spare.startedAt) });
1744
+ return adopted;
1745
+ }
1746
+ createEventParser(state, onEvent) {
1747
+ return jsonEventParser(this.logger, onEvent, {
1748
+ label: "Codex",
1749
+ accept: isCodexEvent,
1750
+ map: (event) => this.mapEventToStreamEvents(event, state),
1751
+ isReady: (event) => event.type === "thread.started"
1752
+ });
1753
+ }
1754
+ buildMcpAddArgs(name, config) {
1755
+ const args = [
1756
+ "mcp",
1757
+ "add",
1758
+ name
1759
+ ];
1760
+ if (config.type === "http" || config.type === "remote") {
1761
+ args.push("--url", config.url);
1762
+ if (config.headers?.Authorization) args.push("--bearer-token-env-var", "MCP_BEARER_TOKEN");
1763
+ } else {
1764
+ for (const [key, value] of Object.entries(localMcpEnv(config) ?? {})) args.push("--env", `${key}=${value}`);
1765
+ args.push("--", ...localMcpArgv(config));
1766
+ }
1767
+ return args;
1768
+ }
1769
+ buildMcpListArgs() {
1770
+ return ["mcp", "list"];
1771
+ }
1772
+ formatStdinMessage(message) {
1773
+ return message;
1774
+ }
1775
+ mapEventToStreamEvents(event, state) {
1776
+ return mapCodexEventToStreamEvents(event, state, this.provider, this.logger);
1777
+ }
1778
+ };
1779
+ //#endregion
1780
+ export { CodexProviderAdapter, codexHeldSpares, normalizeExplicitCodexModel, resolveCodexApiKeyBaseUrl };
1781
+
1782
+ //# sourceMappingURL=index.mjs.map