@tangle-network/sdk-provider-codex 0.0.0-stage → 0.4.0

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