@ai-sdk/harness-codex 1.0.135 → 1.0.137

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.js CHANGED
@@ -1,459 +1,330 @@
1
- // src/codex-harness.ts
2
- import { createHash as createHash2 } from "crypto";
3
- import path from "path";
4
- import {
5
- commonTool,
6
- HarnessCapabilityUnsupportedError,
7
- harnessV1DiagnosticFromBridgeFrame,
8
- harnessStateDirectoryPath,
9
- harnessSessionDataDirectoryPath
10
- } from "@ai-sdk/harness";
11
- import {
12
- applyCredentialForwarding,
13
- classifyDiskLog,
14
- createBridgeToken,
15
- resolveSandboxCredentialEnvironment,
16
- createBridgeErrorHandler,
17
- createBridgeStartupError,
18
- experimental_createBridgeUserMessageSubmitter,
19
- drainBridgeProcessStream,
20
- forwardBridgeProcessStream,
21
- getRestrictedSandboxSession,
22
- markBridgeStarting,
23
- resolveSandboxHomeDir,
24
- SandboxChannel,
25
- shellQuote,
26
- warnCredentialBrokeringUnavailable,
27
- waitForBridgeReady,
28
- withBridgeToken,
29
- writeSkills
30
- } from "@ai-sdk/harness/utils";
31
- import {
32
- tool
33
- } from "@ai-sdk/provider-utils";
1
+ import { createHash } from "node:crypto";
2
+ import path, { dirname, join, resolve } from "node:path";
3
+ import { HarnessCapabilityUnsupportedError, commonTool, harnessSessionDataDirectoryPath, harnessStateDirectoryPath, harnessV1BridgeInboundCommandSchemas, harnessV1BridgeOutboundMessageSchema, harnessV1BridgeStartBaseSchema, harnessV1DiagnosticFromBridgeFrame } from "@ai-sdk/harness";
4
+ import { SandboxChannel, applyCredentialForwarding, classifyDiskLog, createBridgeErrorHandler, createBridgeStartupError, createBridgeToken, createCredentialRequestTransformation, createReadBridgeAsset, drainBridgeProcessStream, experimental_createBridgeUserMessageSubmitter, forwardBridgeProcessStream, getAiGatewayAuthFromEnv, getJwtExpiresAt, getRestrictedSandboxSession, isAccessTokenExpiringSoon, isHarnessAuthenticationEnvironment, isLinux, isMacOS, isWindows, markBridgeStarting, readLinuxSecretServicePassword, readMacOSKeychainPassword, readWindowsCredentialManagerPassword, refreshOAuthAccessToken, resolveSandboxCredentialEnvironment, resolveSandboxHomeDir, shellQuote, shouldResolveNativeSubscription, waitForBridgeReady, warnCredentialBrokeringUnavailable, withBridgeToken, writeSkills } from "@ai-sdk/harness/utils";
5
+ import { isRecord, safeParseJSON, tool } from "@ai-sdk/provider-utils";
34
6
  import { WebSocket } from "ws";
35
- import { z as z2 } from "zod/v4";
36
-
37
- // src/codex-bootstrap.ts
38
- import { createReadBridgeAsset } from "@ai-sdk/harness/utils";
39
- var readBridgeAsset = createReadBridgeAsset({
40
- "package.json": new URL("./bridge/package.json", import.meta.url),
41
- "pnpm-lock.yaml": new URL("./bridge/pnpm-lock.yaml", import.meta.url),
42
- "index.mjs": new URL("./bridge/index.mjs", import.meta.url)
7
+ import { z } from "zod/v4";
8
+ import { spawn } from "node:child_process";
9
+ import { chmod, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ //#region src/codex-bootstrap.ts
12
+ const readBridgeAsset = createReadBridgeAsset({
13
+ "package.json": new URL("./bridge/package.json", import.meta.url),
14
+ "pnpm-lock.yaml": new URL("./bridge/pnpm-lock.yaml", import.meta.url),
15
+ "index.mjs": new URL("./bridge/index.mjs", import.meta.url)
43
16
  });
44
- var CODEX_BOOTSTRAP_DIR = ".harness-bootstrap/codex";
45
- var cachedBootstrap;
17
+ const CODEX_BOOTSTRAP_DIR = ".harness-bootstrap/codex";
18
+ let cachedBootstrap;
46
19
  async function getCodexBootstrap() {
47
- if (cachedBootstrap != null) return cachedBootstrap;
48
- const [pkg, lock, bridge] = await Promise.all([
49
- readBridgeAsset("package.json"),
50
- readBridgeAsset("pnpm-lock.yaml"),
51
- readBridgeAsset("index.mjs")
52
- ]);
53
- cachedBootstrap = {
54
- harnessId: "codex",
55
- bootstrapDir: CODEX_BOOTSTRAP_DIR,
56
- files: [
57
- { path: `${CODEX_BOOTSTRAP_DIR}/package.json`, content: pkg },
58
- { path: `${CODEX_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
59
- { path: `${CODEX_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge }
60
- ],
61
- commands: [
62
- {
63
- command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
64
- }
65
- ]
66
- };
67
- return cachedBootstrap;
20
+ if (cachedBootstrap != null) return cachedBootstrap;
21
+ const [pkg, lock, bridge] = await Promise.all([
22
+ readBridgeAsset("package.json"),
23
+ readBridgeAsset("pnpm-lock.yaml"),
24
+ readBridgeAsset("index.mjs")
25
+ ]);
26
+ cachedBootstrap = {
27
+ harnessId: "codex",
28
+ bootstrapDir: CODEX_BOOTSTRAP_DIR,
29
+ files: [
30
+ {
31
+ path: `${CODEX_BOOTSTRAP_DIR}/package.json`,
32
+ content: pkg
33
+ },
34
+ {
35
+ path: `${CODEX_BOOTSTRAP_DIR}/pnpm-lock.yaml`,
36
+ content: lock
37
+ },
38
+ {
39
+ path: `${CODEX_BOOTSTRAP_DIR}/bridge.mjs`,
40
+ content: bridge
41
+ }
42
+ ],
43
+ commands: [{ command: "pnpm install --frozen-lockfile --store-dir .pnpm-store" }]
44
+ };
45
+ return cachedBootstrap;
68
46
  }
69
-
70
- // src/codex-auth.ts
71
- import {
72
- createCredentialRequestTransformation,
73
- getAiGatewayAuthFromEnv,
74
- isHarnessAuthenticationEnvironment
75
- } from "@ai-sdk/harness/utils";
76
- var DEFAULT_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1";
77
- var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
78
- var CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES = [
79
- "AI_GATEWAY_API_KEY",
80
- "CODEX_API_KEY"
81
- ];
82
- function createCodexRequestTransformations({
83
- env: environment,
84
- sandboxEnv: sandboxEnvironment,
85
- auth: authenticationMode
86
- }) {
87
- if (!environment.CODEX_API_KEY || !sandboxEnvironment.CODEX_API_KEY) {
88
- return [];
89
- }
90
- return [
91
- createCredentialRequestTransformation({
92
- matchUrl: environment.OPENAI_BASE_URL ?? (authenticationMode === "ai-gateway" ? DEFAULT_AI_GATEWAY_BASE_URL : DEFAULT_OPENAI_BASE_URL),
93
- matchHeaders: {
94
- Authorization: `Bearer ${sandboxEnvironment.CODEX_API_KEY}`
95
- },
96
- transformHeaders: {
97
- Authorization: `Bearer ${environment.CODEX_API_KEY}`
98
- }
99
- })
100
- ];
47
+ //#endregion
48
+ //#region src/codex-auth.ts
49
+ const DEFAULT_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1";
50
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
51
+ const CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES = ["AI_GATEWAY_API_KEY", "CODEX_API_KEY"];
52
+ function createCodexRequestTransformations({ env: environment, sandboxEnv: sandboxEnvironment, auth: authenticationMode }) {
53
+ if (!environment.CODEX_API_KEY || !sandboxEnvironment.CODEX_API_KEY) return [];
54
+ return [createCredentialRequestTransformation({
55
+ matchUrl: environment.OPENAI_BASE_URL ?? (authenticationMode === "ai-gateway" ? DEFAULT_AI_GATEWAY_BASE_URL : "https://api.openai.com/v1"),
56
+ matchHeaders: { Authorization: `Bearer ${sandboxEnvironment.CODEX_API_KEY}` },
57
+ transformHeaders: { Authorization: `Bearer ${environment.CODEX_API_KEY}` }
58
+ })];
101
59
  }
60
+ /**
61
+ * Resolve the environment-variable blob the codex bridge needs. Precedence:
62
+ *
63
+ * 1. An explicit authentication mode pins the selected route.
64
+ * 2. Auto-detect from the host process env: gateway first
65
+ * (`AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN`), then `CODEX_API_KEY` /
66
+ * `OPENAI_API_KEY`.
67
+ */
102
68
  function resolveCodexEnv(auth, processEnv = process.env) {
103
- const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
104
- const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
105
- if (auth === "direct") {
106
- return pickOpenAI({ processEnv: authenticationEnvironment });
107
- }
108
- const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
109
- env: authenticationEnvironment
110
- });
111
- if (auth === "ai-gateway" || gatewayAuthFromEnv.apiKey) {
112
- return pickGateway({ gatewayAuthFromEnv });
113
- }
114
- return pickOpenAI({ processEnv: authenticationEnvironment });
69
+ const authenticationEnvironment = isHarnessAuthenticationEnvironment(auth) ? auth : processEnv;
70
+ if (auth === "direct") return pickOpenAI({ processEnv: authenticationEnvironment });
71
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: authenticationEnvironment });
72
+ if (auth === "ai-gateway" || gatewayAuthFromEnv.apiKey) return pickGateway({ gatewayAuthFromEnv });
73
+ return pickOpenAI({ processEnv: authenticationEnvironment });
115
74
  }
116
75
  function resolveCodexAuthenticationMode(auth, processEnv = process.env) {
117
- if (isHarnessAuthenticationEnvironment(auth)) {
118
- return getAiGatewayAuthFromEnv({ env: auth }).apiKey ? "ai-gateway" : "direct";
119
- }
120
- if (auth === "direct") {
121
- return "direct";
122
- }
123
- if (auth === "ai-gateway") {
124
- return "ai-gateway";
125
- }
126
- return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey ? "ai-gateway" : "direct";
76
+ if (isHarnessAuthenticationEnvironment(auth)) return getAiGatewayAuthFromEnv({ env: auth }).apiKey ? "ai-gateway" : "direct";
77
+ if (auth === "direct") return "direct";
78
+ if (auth === "ai-gateway") return "ai-gateway";
79
+ return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey ? "ai-gateway" : "direct";
127
80
  }
128
- function pickOpenAI({
129
- processEnv
130
- }) {
131
- const env = {};
132
- const apiKey = processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
133
- if (apiKey) env.CODEX_API_KEY = apiKey;
134
- const baseUrl = processEnv.OPENAI_BASE_URL;
135
- if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
136
- const organization = processEnv.OPENAI_ORGANIZATION;
137
- if (organization) env.OPENAI_ORGANIZATION = organization;
138
- const project = processEnv.OPENAI_PROJECT;
139
- if (project) env.OPENAI_PROJECT = project;
140
- return env;
81
+ function pickOpenAI({ processEnv }) {
82
+ const env = {};
83
+ const apiKey = processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
84
+ if (apiKey) env.CODEX_API_KEY = apiKey;
85
+ const baseUrl = processEnv.OPENAI_BASE_URL;
86
+ if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
87
+ const organization = processEnv.OPENAI_ORGANIZATION;
88
+ if (organization) env.OPENAI_ORGANIZATION = organization;
89
+ const project = processEnv.OPENAI_PROJECT;
90
+ if (project) env.OPENAI_PROJECT = project;
91
+ return env;
141
92
  }
142
- function pickGateway({
143
- gatewayAuthFromEnv
144
- }) {
145
- const apiKey = gatewayAuthFromEnv.apiKey;
146
- const baseUrl = toCodexGatewayBaseUrl(gatewayAuthFromEnv.baseUrl);
147
- const env = {};
148
- if (apiKey) {
149
- env.AI_GATEWAY_API_KEY = apiKey;
150
- env.CODEX_API_KEY = apiKey;
151
- }
152
- env.AI_GATEWAY_BASE_URL = baseUrl;
153
- env.OPENAI_BASE_URL = baseUrl;
154
- return env;
93
+ function pickGateway({ gatewayAuthFromEnv }) {
94
+ const apiKey = gatewayAuthFromEnv.apiKey;
95
+ const baseUrl = toCodexGatewayBaseUrl(gatewayAuthFromEnv.baseUrl);
96
+ const env = {};
97
+ if (apiKey) {
98
+ env.AI_GATEWAY_API_KEY = apiKey;
99
+ env.CODEX_API_KEY = apiKey;
100
+ }
101
+ env.AI_GATEWAY_BASE_URL = baseUrl;
102
+ env.OPENAI_BASE_URL = baseUrl;
103
+ return env;
155
104
  }
156
105
  function toCodexGatewayBaseUrl(baseUrl) {
157
- const trimmed = baseUrl.replace(/\/+$/, "");
158
- return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
106
+ const trimmed = baseUrl.replace(/\/+$/, "");
107
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
159
108
  }
160
-
161
- // src/codex-subscription.ts
162
- import { spawn } from "child_process";
163
- import { createHash } from "crypto";
164
- import {
165
- chmod,
166
- mkdir,
167
- readFile,
168
- realpath,
169
- rename,
170
- writeFile
171
- } from "fs/promises";
172
- import { homedir } from "os";
173
- import { dirname, join, resolve } from "path";
174
- import {
175
- getJwtExpiresAt,
176
- isAccessTokenExpiringSoon,
177
- isHarnessAuthenticationEnvironment as isHarnessAuthenticationEnvironment2,
178
- isLinux,
179
- isMacOS,
180
- isWindows,
181
- readLinuxSecretServicePassword,
182
- readMacOSKeychainPassword,
183
- readWindowsCredentialManagerPassword,
184
- refreshOAuthAccessToken,
185
- shouldResolveNativeSubscription
186
- } from "@ai-sdk/harness/utils";
187
- import { isRecord, safeParseJSON } from "@ai-sdk/provider-utils";
188
- var CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
189
- var OPENAI_TOKEN_URL = "https://auth.openai.com/oauth/token";
190
- var OPENAI_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
191
- function createCodexSubscriptionRequestTransformations({
192
- requestHeaders,
193
- ...sources
194
- }) {
195
- const transformations = createCodexRequestTransformations(sources);
196
- if (requestHeaders == null) return transformations;
197
- return transformations.map((transformation) => ({
198
- ...transformation,
199
- transform: {
200
- ...transformation.transform,
201
- headers: {
202
- ...transformation.transform.headers,
203
- ...requestHeaders
204
- }
205
- }
206
- }));
109
+ //#endregion
110
+ //#region src/codex-subscription.ts
111
+ const CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
112
+ const OPENAI_TOKEN_URL = "https://auth.openai.com/oauth/token";
113
+ const OPENAI_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
114
+ function createCodexSubscriptionRequestTransformations({ requestHeaders, ...sources }) {
115
+ const transformations = createCodexRequestTransformations(sources);
116
+ if (requestHeaders == null) return transformations;
117
+ return transformations.map((transformation) => ({
118
+ ...transformation,
119
+ transform: {
120
+ ...transformation.transform,
121
+ headers: {
122
+ ...transformation.transform.headers,
123
+ ...requestHeaders
124
+ }
125
+ }
126
+ }));
207
127
  }
208
- async function resolveCodexAuthentication({
209
- auth,
210
- processEnv = process.env,
211
- authCredentialsStoreMode,
212
- readSubscription = readCodexSubscription
213
- }) {
214
- const environment = resolveCodexEnv(auth, processEnv);
215
- if (isHarnessAuthenticationEnvironment2(auth)) {
216
- return { environment };
217
- }
218
- if (!shouldResolveNativeSubscription({
219
- auth,
220
- env: environment,
221
- hasDirectCredential: environment.CODEX_API_KEY != null
222
- })) {
223
- return { environment };
224
- }
225
- return await readSubscription({ authCredentialsStoreMode }) ?? { environment };
128
+ async function resolveCodexAuthentication({ auth, processEnv = process.env, authCredentialsStoreMode, readSubscription = readCodexSubscription }) {
129
+ const environment = resolveCodexEnv(auth, processEnv);
130
+ if (isHarnessAuthenticationEnvironment(auth)) return { environment };
131
+ if (!shouldResolveNativeSubscription({
132
+ auth,
133
+ env: environment,
134
+ hasDirectCredential: environment.CODEX_API_KEY != null
135
+ })) return { environment };
136
+ return await readSubscription({ authCredentialsStoreMode }) ?? { environment };
226
137
  }
227
- async function readCodexSubscription({
228
- env = process.env,
229
- homeDirectory = homedir(),
230
- authCredentialsStoreMode,
231
- platform = process.platform,
232
- keyring,
233
- fetch
234
- } = {}) {
235
- const codexHome = resolve(env.CODEX_HOME ?? join(homeDirectory, ".codex"));
236
- const authPath = join(codexHome, "auth.json");
237
- const storageMode = toCodexAuthCredentialsStoreMode(authCredentialsStoreMode) ?? await readCodexAuthCredentialsStoreMode({ codexHome }) ?? "file";
238
- const stored = await readCodexAuthStore({
239
- codexHome,
240
- authPath,
241
- storageMode,
242
- keyring: keyring ?? createCodexKeyring({ platform })
243
- });
244
- if (stored == null) return void 0;
245
- const credential = await toCodexCredential(stored.value);
246
- if (credential == null) return void 0;
247
- let accessToken = credential.accessToken;
248
- if (isAccessTokenExpiringSoon({ expiresAt: credential.expiresAt })) {
249
- const refreshed = await refreshOAuthAccessToken({
250
- tokenUrl: OPENAI_TOKEN_URL,
251
- clientId: OPENAI_CLIENT_ID,
252
- refreshToken: credential.refreshToken,
253
- requestFormat: "json",
254
- ...fetch == null ? {} : { fetch }
255
- });
256
- accessToken = refreshed.accessToken;
257
- await stored.write({
258
- ...stored.value,
259
- last_refresh: (/* @__PURE__ */ new Date()).toISOString(),
260
- tokens: {
261
- ...stored.value.tokens,
262
- access_token: refreshed.accessToken,
263
- refresh_token: refreshed.refreshToken ?? credential.refreshToken
264
- }
265
- });
266
- }
267
- return {
268
- environment: {
269
- CODEX_API_KEY: accessToken,
270
- OPENAI_BASE_URL: CHATGPT_CODEX_BASE_URL
271
- },
272
- ...credential.accountId == null ? {} : {
273
- requestHeaders: {
274
- "ChatGPT-Account-ID": credential.accountId
275
- }
276
- }
277
- };
138
+ async function readCodexSubscription({ env = process.env, homeDirectory = homedir(), authCredentialsStoreMode, platform = process.platform, keyring, fetch } = {}) {
139
+ const codexHome = resolve(env.CODEX_HOME ?? join(homeDirectory, ".codex"));
140
+ const stored = await readCodexAuthStore({
141
+ codexHome,
142
+ authPath: join(codexHome, "auth.json"),
143
+ storageMode: toCodexAuthCredentialsStoreMode(authCredentialsStoreMode) ?? await readCodexAuthCredentialsStoreMode({ codexHome }) ?? "file",
144
+ keyring: keyring ?? createCodexKeyring({ platform })
145
+ });
146
+ if (stored == null) return void 0;
147
+ const credential = await toCodexCredential(stored.value);
148
+ if (credential == null) return void 0;
149
+ let accessToken = credential.accessToken;
150
+ if (isAccessTokenExpiringSoon({ expiresAt: credential.expiresAt })) {
151
+ const refreshed = await refreshOAuthAccessToken({
152
+ tokenUrl: OPENAI_TOKEN_URL,
153
+ clientId: OPENAI_CLIENT_ID,
154
+ refreshToken: credential.refreshToken,
155
+ requestFormat: "json",
156
+ ...fetch == null ? {} : { fetch }
157
+ });
158
+ accessToken = refreshed.accessToken;
159
+ await stored.write({
160
+ ...stored.value,
161
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString(),
162
+ tokens: {
163
+ ...stored.value.tokens,
164
+ access_token: refreshed.accessToken,
165
+ refresh_token: refreshed.refreshToken ?? credential.refreshToken
166
+ }
167
+ });
168
+ }
169
+ return {
170
+ environment: {
171
+ CODEX_API_KEY: accessToken,
172
+ OPENAI_BASE_URL: CHATGPT_CODEX_BASE_URL
173
+ },
174
+ ...credential.accountId == null ? {} : { requestHeaders: { "ChatGPT-Account-ID": credential.accountId } }
175
+ };
278
176
  }
279
- async function readCodexAuthStore({
280
- codexHome,
281
- authPath,
282
- storageMode,
283
- keyring
284
- }) {
285
- if (storageMode === "ephemeral") return void 0;
286
- const readFileStore = async () => {
287
- const value = await readCodexAuthFile(authPath);
288
- return value == null ? void 0 : {
289
- value,
290
- write: (updated) => writeCodexAuthFile({ authPath, value: updated })
291
- };
292
- };
293
- const readKeyringStore = async () => {
294
- if (keyring == null) return void 0;
295
- const canonicalHome = await realpath(codexHome).catch(() => codexHome);
296
- const account = `cli|${createHash("sha256").update(canonicalHome).digest("hex").slice(0, 16)}`;
297
- const text = await keyring.read({ service: "Codex Auth", account });
298
- if (text == null) return void 0;
299
- const value = await parseCodexAuthFile(text);
300
- return value == null ? void 0 : {
301
- value,
302
- write: (updated) => keyring.write({
303
- service: "Codex Auth",
304
- account,
305
- value: JSON.stringify(updated)
306
- })
307
- };
308
- };
309
- if (storageMode === "file") return readFileStore();
310
- if (storageMode === "keyring") return readKeyringStore();
311
- return await readKeyringStore() ?? readFileStore();
177
+ async function readCodexAuthStore({ codexHome, authPath, storageMode, keyring }) {
178
+ if (storageMode === "ephemeral") return void 0;
179
+ const readFileStore = async () => {
180
+ const value = await readCodexAuthFile(authPath);
181
+ return value == null ? void 0 : {
182
+ value,
183
+ write: (updated) => writeCodexAuthFile({
184
+ authPath,
185
+ value: updated
186
+ })
187
+ };
188
+ };
189
+ const readKeyringStore = async () => {
190
+ if (keyring == null) return void 0;
191
+ const canonicalHome = await realpath(codexHome).catch(() => codexHome);
192
+ const account = `cli|${createHash("sha256").update(canonicalHome).digest("hex").slice(0, 16)}`;
193
+ const text = await keyring.read({
194
+ service: "Codex Auth",
195
+ account
196
+ });
197
+ if (text == null) return void 0;
198
+ const value = await parseCodexAuthFile(text);
199
+ return value == null ? void 0 : {
200
+ value,
201
+ write: (updated) => keyring.write({
202
+ service: "Codex Auth",
203
+ account,
204
+ value: JSON.stringify(updated)
205
+ })
206
+ };
207
+ };
208
+ if (storageMode === "file") return readFileStore();
209
+ if (storageMode === "keyring") return readKeyringStore();
210
+ return await readKeyringStore() ?? readFileStore();
312
211
  }
313
- async function readCodexAuthCredentialsStoreMode({
314
- codexHome
315
- }) {
316
- const text = await readFile(join(codexHome, "config.toml"), "utf8").catch(
317
- () => void 0
318
- );
319
- if (text == null) return void 0;
320
- for (const line of text.split(/\r?\n/)) {
321
- if (line.trimStart().startsWith("[")) return void 0;
322
- const match = /^\s*cli_auth_credentials_store\s*=\s*["'](file|keyring|auto|ephemeral)["']\s*(?:#.*)?$/.exec(
323
- line
324
- );
325
- if (match != null) {
326
- return toCodexAuthCredentialsStoreMode(match[1]);
327
- }
328
- }
212
+ async function readCodexAuthCredentialsStoreMode({ codexHome }) {
213
+ const text = await readFile(join(codexHome, "config.toml"), "utf8").catch(() => void 0);
214
+ if (text == null) return void 0;
215
+ for (const line of text.split(/\r?\n/)) {
216
+ if (line.trimStart().startsWith("[")) return void 0;
217
+ const match = /^\s*cli_auth_credentials_store\s*=\s*["'](file|keyring|auto|ephemeral)["']\s*(?:#.*)?$/.exec(line);
218
+ if (match != null) return toCodexAuthCredentialsStoreMode(match[1]);
219
+ }
329
220
  }
330
221
  function toCodexAuthCredentialsStoreMode(value) {
331
- return value === "file" || value === "keyring" || value === "auto" || value === "ephemeral" ? value : void 0;
222
+ return value === "file" || value === "keyring" || value === "auto" || value === "ephemeral" ? value : void 0;
332
223
  }
333
224
  async function readCodexAuthFile(authPath) {
334
- const text = await readFile(authPath, "utf8").catch(() => void 0);
335
- return text == null ? void 0 : parseCodexAuthFile(text);
225
+ const text = await readFile(authPath, "utf8").catch(() => void 0);
226
+ return text == null ? void 0 : parseCodexAuthFile(text);
336
227
  }
337
228
  async function parseCodexAuthFile(text) {
338
- const parsed = await safeParseJSON({ text });
339
- return parsed.success && isRecord(parsed.value) ? parsed.value : void 0;
229
+ const parsed = await safeParseJSON({ text });
230
+ return parsed.success && isRecord(parsed.value) ? parsed.value : void 0;
340
231
  }
341
232
  async function toCodexCredential(value) {
342
- if (value.auth_mode !== "chatgpt" || !isRecord(value.tokens)) {
343
- return void 0;
344
- }
345
- const accessToken = value.tokens.access_token;
346
- const refreshToken = value.tokens.refresh_token;
347
- if (typeof accessToken !== "string" || typeof refreshToken !== "string") {
348
- return void 0;
349
- }
350
- const expiresAt = await getJwtExpiresAt({ token: accessToken });
351
- if (expiresAt == null) return void 0;
352
- const accountId = value.tokens.account_id;
353
- return {
354
- accessToken,
355
- refreshToken,
356
- expiresAt,
357
- ...typeof accountId === "string" ? { accountId } : {}
358
- };
233
+ if (value.auth_mode !== "chatgpt" || !isRecord(value.tokens)) return;
234
+ const accessToken = value.tokens.access_token;
235
+ const refreshToken = value.tokens.refresh_token;
236
+ if (typeof accessToken !== "string" || typeof refreshToken !== "string") return;
237
+ const expiresAt = await getJwtExpiresAt({ token: accessToken });
238
+ if (expiresAt == null) return void 0;
239
+ const accountId = value.tokens.account_id;
240
+ return {
241
+ accessToken,
242
+ refreshToken,
243
+ expiresAt,
244
+ ...typeof accountId === "string" ? { accountId } : {}
245
+ };
359
246
  }
360
- async function writeCodexAuthFile({
361
- authPath,
362
- value
363
- }) {
364
- await mkdir(dirname(authPath), { recursive: true });
365
- const temporaryPath = `${authPath}.${process.pid}.tmp`;
366
- await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
367
- `, {
368
- mode: 384
369
- });
370
- await chmod(temporaryPath, 384);
371
- await rename(temporaryPath, authPath);
247
+ async function writeCodexAuthFile({ authPath, value }) {
248
+ await mkdir(dirname(authPath), { recursive: true });
249
+ const temporaryPath = `${authPath}.${process.pid}.tmp`;
250
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
251
+ await chmod(temporaryPath, 384);
252
+ await rename(temporaryPath, authPath);
372
253
  }
373
- function createCodexKeyring({
374
- platform
375
- }) {
376
- if (isMacOS(platform)) {
377
- return {
378
- async read({ service, account }) {
379
- return readMacOSKeychainPassword({ service, account });
380
- },
381
- async write({ service, account, value }) {
382
- const command = `add-generic-password -U -s ${shellQuoteForSecurity(service)} -a ${shellQuoteForSecurity(account)} -X ${Buffer.from(value, "utf8").toString("hex")}
383
- `;
384
- await runCommandWithInput({
385
- command: "/usr/bin/security",
386
- args: ["-i"],
387
- input: command
388
- });
389
- }
390
- };
391
- }
392
- if (isLinux(platform)) {
393
- return {
394
- async read({ service, account }) {
395
- return readLinuxSecretServicePassword({
396
- attributes: {
397
- service,
398
- username: account,
399
- target: "default"
400
- }
401
- });
402
- },
403
- async write({ service, account, value }) {
404
- await runCommandWithInput({
405
- command: "secret-tool",
406
- args: [
407
- "store",
408
- "--label",
409
- `${account}@${service}:default`,
410
- "service",
411
- service,
412
- "username",
413
- account,
414
- "target",
415
- "default",
416
- "application",
417
- "rust-keyring"
418
- ],
419
- input: value
420
- });
421
- }
422
- };
423
- }
424
- if (isWindows(platform)) {
425
- return createWindowsCodexKeyring();
426
- }
254
+ function createCodexKeyring({ platform }) {
255
+ if (isMacOS(platform)) return {
256
+ async read({ service, account }) {
257
+ return readMacOSKeychainPassword({
258
+ service,
259
+ account
260
+ });
261
+ },
262
+ async write({ service, account, value }) {
263
+ await runCommandWithInput({
264
+ command: "/usr/bin/security",
265
+ args: ["-i"],
266
+ input: `add-generic-password -U -s ${shellQuoteForSecurity(service)} -a ${shellQuoteForSecurity(account)} -X ${Buffer.from(value, "utf8").toString("hex")}\n`
267
+ });
268
+ }
269
+ };
270
+ if (isLinux(platform)) return {
271
+ async read({ service, account }) {
272
+ return readLinuxSecretServicePassword({ attributes: {
273
+ service,
274
+ username: account,
275
+ target: "default"
276
+ } });
277
+ },
278
+ async write({ service, account, value }) {
279
+ await runCommandWithInput({
280
+ command: "secret-tool",
281
+ args: [
282
+ "store",
283
+ "--label",
284
+ `${account}@${service}:default`,
285
+ "service",
286
+ service,
287
+ "username",
288
+ account,
289
+ "target",
290
+ "default",
291
+ "application",
292
+ "rust-keyring"
293
+ ],
294
+ input: value
295
+ });
296
+ }
297
+ };
298
+ if (isWindows(platform)) return createWindowsCodexKeyring();
427
299
  }
428
300
  function shellQuoteForSecurity(value) {
429
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
301
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
430
302
  }
431
- async function runCommandWithInput({
432
- command,
433
- args,
434
- input,
435
- env
436
- }) {
437
- await new Promise((resolvePromise, reject) => {
438
- const child = spawn(command, args, {
439
- stdio: ["pipe", "ignore", "pipe"],
440
- ...env == null ? {} : { env }
441
- });
442
- let stderr = "";
443
- child.stderr.setEncoding("utf8");
444
- child.stderr.on("data", (chunk) => {
445
- stderr += chunk;
446
- });
447
- child.once("error", reject);
448
- child.once("close", (code) => {
449
- if (code === 0) resolvePromise();
450
- else reject(new Error(stderr || `${command} exited with code ${code}.`));
451
- });
452
- child.stdin.end(input);
453
- });
303
+ async function runCommandWithInput({ command, args, input, env }) {
304
+ await new Promise((resolvePromise, reject) => {
305
+ const child = spawn(command, args, {
306
+ stdio: [
307
+ "pipe",
308
+ "ignore",
309
+ "pipe"
310
+ ],
311
+ ...env == null ? {} : { env }
312
+ });
313
+ let stderr = "";
314
+ child.stderr.setEncoding("utf8");
315
+ child.stderr.on("data", (chunk) => {
316
+ stderr += chunk;
317
+ });
318
+ child.once("error", reject);
319
+ child.once("close", (code) => {
320
+ if (code === 0) resolvePromise();
321
+ else reject(new Error(stderr || `${command} exited with code ${code}.`));
322
+ });
323
+ child.stdin.end(input);
324
+ });
454
325
  }
455
326
  function createWindowsCodexKeyring() {
456
- const source = `
327
+ const source = `
457
328
  using System;
458
329
  using System.Runtime.InteropServices;
459
330
  using System.Text;
@@ -481,968 +352,828 @@ public static class CodexCredentialManager {
481
352
  } finally { Marshal.FreeHGlobal(blob); }
482
353
  }
483
354
  }`;
484
- return {
485
- async read({ service, account }) {
486
- return readWindowsCredentialManagerPassword({
487
- targetName: `${account}.${service}`
488
- });
489
- },
490
- async write({ service, account, value }) {
491
- const script = `Add-Type -TypeDefinition $env:AI_SDK_CODEX_CREDENTIAL_SOURCE; $value = [Console]::In.ReadToEnd(); [CodexCredentialManager]::Write($env:AI_SDK_CODEX_CREDENTIAL_TARGET, $env:AI_SDK_CODEX_CREDENTIAL_USER, $value)`;
492
- await runCommandWithInput({
493
- command: "powershell.exe",
494
- args: ["-NoProfile", "-NonInteractive", "-Command", script],
495
- input: value,
496
- env: {
497
- ...process.env,
498
- AI_SDK_CODEX_CREDENTIAL_SOURCE: source,
499
- AI_SDK_CODEX_CREDENTIAL_TARGET: `${account}.${service}`,
500
- AI_SDK_CODEX_CREDENTIAL_USER: account
501
- }
502
- });
503
- }
504
- };
355
+ return {
356
+ async read({ service, account }) {
357
+ return readWindowsCredentialManagerPassword({ targetName: `${account}.${service}` });
358
+ },
359
+ async write({ service, account, value }) {
360
+ await runCommandWithInput({
361
+ command: "powershell.exe",
362
+ args: [
363
+ "-NoProfile",
364
+ "-NonInteractive",
365
+ "-Command",
366
+ `Add-Type -TypeDefinition $env:AI_SDK_CODEX_CREDENTIAL_SOURCE; $value = [Console]::In.ReadToEnd(); [CodexCredentialManager]::Write($env:AI_SDK_CODEX_CREDENTIAL_TARGET, $env:AI_SDK_CODEX_CREDENTIAL_USER, $value)`
367
+ ],
368
+ input: value,
369
+ env: {
370
+ ...process.env,
371
+ AI_SDK_CODEX_CREDENTIAL_SOURCE: source,
372
+ AI_SDK_CODEX_CREDENTIAL_TARGET: `${account}.${service}`,
373
+ AI_SDK_CODEX_CREDENTIAL_USER: account
374
+ }
375
+ });
376
+ }
377
+ };
505
378
  }
506
-
507
- // src/codex-bridge-protocol.ts
508
- import {
509
- harnessV1BridgeInboundCommandSchemas,
510
- harnessV1BridgeOutboundMessageSchema,
511
- harnessV1BridgeReadySchema,
512
- harnessV1BridgeStartBaseSchema
513
- } from "@ai-sdk/harness";
514
- import { z } from "zod/v4";
515
- var outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
516
- var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
517
- instructions: z.string().optional(),
518
- reasoningEffort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
519
- webSearch: z.boolean().optional(),
520
- codexConfig: z.record(z.string(), z.unknown()).optional(),
521
- mcpServers: z.record(z.string(), z.unknown()).optional(),
522
- headers: z.record(z.string(), z.string()).optional(),
523
- /*
524
- * The host carries this app-server thread ID in lifecycle state. Supplying it
525
- * makes the bridge call `thread/resume` instead of starting a fresh thread.
526
- */
527
- resumeThreadId: z.string().optional(),
528
- restartThread: z.boolean().optional()
379
+ //#endregion
380
+ //#region src/codex-bridge-protocol.ts
381
+ const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
382
+ const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
383
+ instructions: z.string().optional(),
384
+ reasoningEffort: z.enum([
385
+ "low",
386
+ "medium",
387
+ "high",
388
+ "xhigh",
389
+ "max"
390
+ ]).optional(),
391
+ webSearch: z.boolean().optional(),
392
+ codexConfig: z.record(z.string(), z.unknown()).optional(),
393
+ mcpServers: z.record(z.string(), z.unknown()).optional(),
394
+ headers: z.record(z.string(), z.string()).optional(),
395
+ resumeThreadId: z.string().optional(),
396
+ restartThread: z.boolean().optional()
529
397
  });
530
- var inboundMessageSchema = z.discriminatedUnion("type", [
531
- startMessageSchema,
532
- ...harnessV1BridgeInboundCommandSchemas
533
- ]);
534
-
535
- // src/version.ts
536
- var VERSION = true ? "1.0.135" : "0.0.0-test";
537
-
538
- // src/codex-harness.ts
539
- var DEFAULT_CODEX_MODEL = "gpt-5.5";
540
- var CODEX_CLIENT_APP = `ai-sdk-harness-codex/${VERSION}`;
541
- var CODEX_BUILTIN_TOOLS = {
542
- bash: commonTool("bash", {
543
- nativeName: "shell",
544
- toolUseKind: "bash",
545
- description: "Execute a shell command",
546
- inputSchema: z2.object({ command: z2.string() })
547
- }),
548
- webSearch: commonTool("webSearch", {
549
- nativeName: "web_search",
550
- toolUseKind: "readonly",
551
- description: "Search the web",
552
- inputSchema: z2.object({ query: z2.string() })
553
- }),
554
- apply_patch: {
555
- ...tool({
556
- description: "Apply a patch to files",
557
- inputSchema: z2.string()
558
- }),
559
- toolUseKind: "edit"
560
- },
561
- view_image: {
562
- ...tool({
563
- description: "View a local image file",
564
- inputSchema: z2.object({
565
- path: z2.string(),
566
- detail: z2.enum(["high", "original"]).optional(),
567
- environment_id: z2.string().optional()
568
- })
569
- }),
570
- toolUseKind: "readonly"
571
- }
398
+ z.discriminatedUnion("type", [startMessageSchema, ...harnessV1BridgeInboundCommandSchemas]);
399
+ //#endregion
400
+ //#region src/version.ts
401
+ const VERSION = "1.0.137";
402
+ //#endregion
403
+ //#region src/codex-harness.ts
404
+ const DEFAULT_CODEX_MODEL = "gpt-5.5";
405
+ /**
406
+ * Value to use in User-Agent and `x-client-app` headers.
407
+ */
408
+ const CODEX_CLIENT_APP = `ai-sdk-harness-codex/${VERSION}`;
409
+ const CODEX_BUILTIN_TOOLS = {
410
+ bash: commonTool("bash", {
411
+ nativeName: "shell",
412
+ toolUseKind: "bash",
413
+ description: "Execute a shell command",
414
+ inputSchema: z.object({ command: z.string() })
415
+ }),
416
+ webSearch: commonTool("webSearch", {
417
+ nativeName: "web_search",
418
+ toolUseKind: "readonly",
419
+ description: "Search the web",
420
+ inputSchema: z.object({ query: z.string() })
421
+ }),
422
+ apply_patch: {
423
+ ...tool({
424
+ description: "Apply a patch to files",
425
+ inputSchema: z.string()
426
+ }),
427
+ toolUseKind: "edit"
428
+ },
429
+ view_image: {
430
+ ...tool({
431
+ description: "View a local image file",
432
+ inputSchema: z.object({
433
+ path: z.string(),
434
+ detail: z.enum(["high", "original"]).optional(),
435
+ environment_id: z.string().optional()
436
+ })
437
+ }),
438
+ toolUseKind: "readonly"
439
+ }
572
440
  };
573
- var codexBridgeCoordsSchema = z2.object({
574
- port: z2.number(),
575
- token: z2.string(),
576
- lastSeenEventId: z2.number(),
577
- sandboxId: z2.string().optional()
441
+ /**
442
+ * Live bridge coordinates returned by `doDetach()` and `doSuspendTurn()`. A
443
+ * future process uses them to reopen a socket to the still-running bridge
444
+ * (`attach`) instead of re-spawning it. Absent on a `doStop()` payload.
445
+ */
446
+ const codexBridgeCoordsSchema = z.object({
447
+ port: z.number(),
448
+ token: z.string(),
449
+ lastSeenEventId: z.number(),
450
+ sandboxId: z.string().optional()
578
451
  });
579
- var codexResumeStateSchema = z2.object({
580
- threadId: z2.string().optional(),
581
- turnConfigurationFingerprint: z2.string().optional(),
582
- bridge: codexBridgeCoordsSchema.optional(),
583
- sandboxCredentialEnvironment: z2.record(z2.string(), z2.string()).optional()
452
+ /**
453
+ * Schema for the adapter-specific lifecycle `data` payload Codex produces.
454
+ * `threadId` is what app-server's `thread/resume` method requires for the replay/rerun
455
+ * rungs; the sandbox lookup is handled separately via
456
+ * `provider.resumeSession({ sessionId })`. `bridge` carries live coordinates
457
+ * for cross-process `attach` (present on `doDetach()` and `doSuspendTurn()`
458
+ * payloads).
459
+ */
460
+ const codexResumeStateSchema = z.object({
461
+ threadId: z.string().optional(),
462
+ turnConfigurationFingerprint: z.string().optional(),
463
+ bridge: codexBridgeCoordsSchema.optional(),
464
+ sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional()
584
465
  });
585
466
  function createCodex(settings = {}) {
586
- return {
587
- specificationVersion: "harness-v1",
588
- harnessId: "codex",
589
- builtinTools: CODEX_BUILTIN_TOOLS,
590
- supportsBuiltinToolFiltering: true,
591
- supportsBuiltinToolApprovals: false,
592
- lifecycleStateSchema: codexResumeStateSchema,
593
- getBootstrap: getCodexBootstrap,
594
- doStart: async (startOpts) => {
595
- const model = DEFAULT_CODEX_MODEL;
596
- if (startOpts.permissionMode != null && startOpts.permissionMode !== "allow-all") {
597
- throw new HarnessCapabilityUnsupportedError({
598
- message: "Harness 'codex' does not support built-in tool approval requests; use permissionMode: 'allow-all'.",
599
- harnessId: "codex"
600
- });
601
- }
602
- const sandboxSession = startOpts.sandboxSession;
603
- const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
604
- const sandboxId = "id" in sandboxSession ? sandboxSession.id : void 0;
605
- validateBasicSandboxSettings({
606
- sandboxSession,
607
- port: settings.port,
608
- portEndpoint: settings.portEndpoint
609
- });
610
- if (settings.mintBridgeToken != null && sandboxId == null) {
611
- throw new HarnessCapabilityUnsupportedError({
612
- harnessId: "codex",
613
- message: "The codex harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id."
614
- });
615
- }
616
- const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
617
- const isResume = lifecycleState != null;
618
- const isContinue = startOpts.continueFrom != null;
619
- const resumeData = isResume && typeof lifecycleState?.data === "object" ? lifecycleState.data : void 0;
620
- const resumeThreadId = resumeData?.threadId;
621
- const resumeThreadIdString = typeof resumeThreadId === "string" && resumeThreadId.length > 0 ? resumeThreadId : void 0;
622
- const turnConfigurationFingerprint = typeof resumeData?.turnConfigurationFingerprint === "string" ? resumeData.turnConfigurationFingerprint : void 0;
623
- const coords = resumeData?.bridge;
624
- const authenticationMode = resolveCodexAuthenticationMode(settings.auth);
625
- const resolvedAuthentication = await resolveCodexAuthentication({
626
- auth: settings.auth,
627
- authCredentialsStoreMode: settings.codexConfig?.cli_auth_credentials_store
628
- });
629
- const resolvedAuthEnvironment = resolvedAuthentication.environment;
630
- let sandboxAuthEnvironment = resolvedAuthEnvironment;
631
- let sandboxCredentialEnvironment;
632
- let credentialsBrokered = false;
633
- if ("addRequestTransformations" in sandboxSession && sandboxSession.addRequestTransformations != null) {
634
- sandboxCredentialEnvironment = await resolveSandboxCredentialEnvironment({
635
- environment: resolvedAuthEnvironment,
636
- credentialEnvironmentVariables: CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
637
- credentialForwarding: settings.credentialForwarding,
638
- previousSandboxCredentialEnvironment: resumeData?.sandboxCredentialEnvironment
639
- });
640
- sandboxAuthEnvironment = {
641
- ...resolvedAuthEnvironment,
642
- ...sandboxCredentialEnvironment
643
- };
644
- const requestTransformations = createCodexSubscriptionRequestTransformations({
645
- env: resolvedAuthEnvironment,
646
- sandboxEnv: sandboxAuthEnvironment,
647
- auth: authenticationMode,
648
- requestHeaders: resolvedAuthentication.requestHeaders
649
- });
650
- if (requestTransformations.length > 0) {
651
- await sandboxSession.addRequestTransformations(
652
- requestTransformations
653
- );
654
- }
655
- credentialsBrokered = true;
656
- if (requestTransformations.length > 0 && authenticationMode === "direct" && resolvedAuthEnvironment.OPENAI_BASE_URL == null) {
657
- sandboxAuthEnvironment.OPENAI_BASE_URL = DEFAULT_OPENAI_BASE_URL;
658
- }
659
- }
660
- const sandboxHomeDir = await resolveSandboxHomeDir({
661
- sandbox: toolSafeSandboxSession,
662
- abortSignal: startOpts.abortSignal
663
- });
664
- const stateDir = harnessStateDirectoryPath({ sandboxHomeDir });
665
- const bootstrapDir = path.posix.resolve(stateDir, CODEX_BOOTSTRAP_DIR);
666
- const workDir = startOpts.sessionWorkDir;
667
- const sessionDataDir = harnessSessionDataDirectoryPath({
668
- stateDirectory: stateDir,
669
- sessionId: startOpts.sessionId
670
- });
671
- const bridgeStateDir = `${sessionDataDir}/bridge`;
672
- const timeoutMs = settings.startupTimeoutMs ?? 12e4;
673
- const report = startOpts.observability?.report;
674
- const onDiagnostic = report ? (frame) => report(
675
- harnessV1DiagnosticFromBridgeFrame(frame, {
676
- sessionId: startOpts.sessionId,
677
- timestamp: Date.now()
678
- })
679
- ) : void 0;
680
- const onBridgeError = createBridgeErrorHandler({
681
- harnessId: "codex",
682
- sessionId: startOpts.sessionId
683
- });
684
- if (coords) {
685
- try {
686
- const endpoint2 = await resolveBridgeEndpoint({
687
- sandboxSession,
688
- override: settings.portEndpoint,
689
- port: coords.port
690
- });
691
- const attachEndpoint = withBridgeToken({
692
- endpoint: endpoint2,
693
- token: coords.token
694
- });
695
- const attachChannel = new SandboxChannel({
696
- connect: ({ abortSignal }) => openWebSocket({ ...attachEndpoint, abortSignal }),
697
- outboundSchema: outboundMessageSchema,
698
- initialLastSeenEventId: coords.lastSeenEventId,
699
- onDiagnostic,
700
- onBridgeError,
701
- reconnect: settings.reconnect
702
- });
703
- await attachChannel.open(isContinue ? { resume: true } : void 0);
704
- return createSession({
705
- sessionId: startOpts.sessionId,
706
- channel: attachChannel,
707
- // The live bridge was spawned by another process; no process handle.
708
- proc: void 0,
709
- model,
710
- reasoningEffort: settings.reasoningEffort,
711
- webSearch: settings.webSearch,
712
- builtinToolFiltering: startOpts.builtinToolFiltering,
713
- codexConfig: settings.codexConfig,
714
- mcpServers: settings.mcpServers,
715
- headers: startOpts.headers,
716
- resumeThreadId: resumeThreadIdString,
717
- isResume: true,
718
- seedResumeThreadOnFirstPrompt: false,
719
- rerunContinue: false,
720
- bridgePort: coords.port,
721
- bridgeToken: coords.token,
722
- sandboxId,
723
- sandboxCredentialEnvironment,
724
- debug: startOpts.observability?.debug,
725
- permissionMode: startOpts.permissionMode,
726
- sandbox: toolSafeSandboxSession,
727
- sandboxHomeDir,
728
- turnConfigurationFingerprint
729
- });
730
- } catch {
731
- }
732
- }
733
- let respawnStrategy = isResume ? "rerun" : void 0;
734
- if (coords && isContinue) {
735
- const logRaw = await Promise.resolve(
736
- toolSafeSandboxSession.readTextFile({
737
- path: `${bridgeStateDir}/event-log.ndjson`,
738
- abortSignal: startOpts.abortSignal
739
- })
740
- ).catch(() => null);
741
- if (await classifyDiskLog(logRaw) === "replay") {
742
- respawnStrategy = "replay";
743
- }
744
- }
745
- const port = resolveBridgePort({
746
- sandboxSession,
747
- override: settings.port
748
- });
749
- const token = settings.mintBridgeToken == null ? createBridgeToken() : settings.mintBridgeToken(sandboxId);
750
- const forwardedAuthEnvironment = credentialsBrokered ? sandboxAuthEnvironment : await applyCredentialForwarding({
751
- environment: sandboxAuthEnvironment,
752
- credentialEnvironmentVariables: CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
753
- credentialForwarding: settings.credentialForwarding
754
- });
755
- if (!credentialsBrokered) {
756
- warnCredentialBrokeringUnavailable({
757
- environment: resolvedAuthEnvironment,
758
- forwardedEnvironment: forwardedAuthEnvironment,
759
- credentialEnvironmentVariables: CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES
760
- });
761
- }
762
- const env = {
763
- ...forwardedAuthEnvironment,
764
- AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
765
- BRIDGE_CHANNEL_TOKEN: token,
766
- BRIDGE_WS_PORT: String(port),
767
- ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
768
- };
769
- if (respawnStrategy === void 0) {
770
- await toolSafeSandboxSession.run({
771
- command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
772
- abortSignal: startOpts.abortSignal
773
- });
774
- }
775
- await markBridgeStarting({
776
- sandbox: toolSafeSandboxSession,
777
- bridgeStateDir,
778
- bridgeType: "codex",
779
- abortSignal: startOpts.abortSignal
780
- });
781
- const proc = await toolSafeSandboxSession.spawn({
782
- command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)}`,
783
- env,
784
- abortSignal: startOpts.abortSignal
785
- });
786
- const stderrTail = [];
787
- const bridgeStderrDone = forwardBridgeProcessStream({
788
- stream: proc.stderr,
789
- streamName: "stderr",
790
- source: "codex",
791
- collectTail: stderrTail
792
- });
793
- const { port: boundPort } = await waitForBridgeReady({
794
- proc,
795
- sandbox: toolSafeSandboxSession,
796
- bridgeStateDir,
797
- bridgeType: "codex",
798
- timeoutMs,
799
- abortSignal: startOpts.abortSignal,
800
- createTimeoutError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
801
- message: "codex bridge did not become ready in time.",
802
- proc: proc2,
803
- stdoutTail,
804
- stderrTail,
805
- stderrDone: bridgeStderrDone
806
- }),
807
- createExitError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
808
- message: "codex bridge exited before becoming ready.",
809
- proc: proc2,
810
- stdoutTail,
811
- stderrTail,
812
- stderrDone: bridgeStderrDone
813
- })
814
- });
815
- void drainBridgeProcessStream(proc.stdout);
816
- const endpoint = await resolveBridgeEndpoint({
817
- sandboxSession,
818
- override: settings.portEndpoint,
819
- port: boundPort
820
- });
821
- const bridgeEndpoint = withBridgeToken({ endpoint, token });
822
- const channel = new SandboxChannel({
823
- connect: ({ abortSignal }) => openWebSocket({ ...bridgeEndpoint, abortSignal }),
824
- outboundSchema: outboundMessageSchema,
825
- onDiagnostic,
826
- onBridgeError,
827
- reconnect: settings.reconnect,
828
- // In replay mode the respawned bridge reloaded the finished turn from
829
- // disk; seed the cursor and resume so it streams the tail (incl.
830
- // `finish`).
831
- ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
832
- });
833
- await channel.open(
834
- respawnStrategy === "replay" ? { resume: true } : void 0
835
- );
836
- return createSession({
837
- sessionId: startOpts.sessionId,
838
- channel,
839
- proc,
840
- model,
841
- reasoningEffort: settings.reasoningEffort,
842
- webSearch: settings.webSearch,
843
- builtinToolFiltering: startOpts.builtinToolFiltering,
844
- codexConfig: settings.codexConfig,
845
- mcpServers: settings.mcpServers,
846
- headers: startOpts.headers,
847
- resumeThreadId: resumeThreadIdString,
848
- isResume: respawnStrategy !== void 0,
849
- seedResumeThreadOnFirstPrompt: respawnStrategy !== void 0,
850
- rerunContinue: respawnStrategy === "rerun",
851
- bridgePort: boundPort,
852
- bridgeToken: token,
853
- sandboxId,
854
- sandboxCredentialEnvironment,
855
- debug: startOpts.observability?.debug,
856
- permissionMode: startOpts.permissionMode,
857
- sandbox: toolSafeSandboxSession,
858
- sandboxHomeDir,
859
- turnConfigurationFingerprint
860
- });
861
- }
862
- };
467
+ return {
468
+ specificationVersion: "harness-v1",
469
+ harnessId: "codex",
470
+ builtinTools: CODEX_BUILTIN_TOOLS,
471
+ supportsBuiltinToolFiltering: true,
472
+ supportsBuiltinToolApprovals: false,
473
+ lifecycleStateSchema: codexResumeStateSchema,
474
+ getBootstrap: getCodexBootstrap,
475
+ doStart: async (startOpts) => {
476
+ const model = DEFAULT_CODEX_MODEL;
477
+ if (startOpts.permissionMode != null && startOpts.permissionMode !== "allow-all") throw new HarnessCapabilityUnsupportedError({
478
+ message: "Harness 'codex' does not support built-in tool approval requests; use permissionMode: 'allow-all'.",
479
+ harnessId: "codex"
480
+ });
481
+ const sandboxSession = startOpts.sandboxSession;
482
+ const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
483
+ const sandboxId = "id" in sandboxSession ? sandboxSession.id : void 0;
484
+ validateBasicSandboxSettings({
485
+ sandboxSession,
486
+ port: settings.port,
487
+ portEndpoint: settings.portEndpoint
488
+ });
489
+ if (settings.mintBridgeToken != null && sandboxId == null) throw new HarnessCapabilityUnsupportedError({
490
+ harnessId: "codex",
491
+ message: "The codex harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id."
492
+ });
493
+ const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
494
+ const isResume = lifecycleState != null;
495
+ const isContinue = startOpts.continueFrom != null;
496
+ const resumeData = isResume && typeof lifecycleState?.data === "object" ? lifecycleState.data : void 0;
497
+ const resumeThreadId = resumeData?.threadId;
498
+ const resumeThreadIdString = typeof resumeThreadId === "string" && resumeThreadId.length > 0 ? resumeThreadId : void 0;
499
+ const turnConfigurationFingerprint = typeof resumeData?.turnConfigurationFingerprint === "string" ? resumeData.turnConfigurationFingerprint : void 0;
500
+ const coords = resumeData?.bridge;
501
+ const authenticationMode = resolveCodexAuthenticationMode(settings.auth);
502
+ const resolvedAuthentication = await resolveCodexAuthentication({
503
+ auth: settings.auth,
504
+ authCredentialsStoreMode: settings.codexConfig?.cli_auth_credentials_store
505
+ });
506
+ const resolvedAuthEnvironment = resolvedAuthentication.environment;
507
+ let sandboxAuthEnvironment = resolvedAuthEnvironment;
508
+ let sandboxCredentialEnvironment;
509
+ let credentialsBrokered = false;
510
+ if ("addRequestTransformations" in sandboxSession && sandboxSession.addRequestTransformations != null) {
511
+ sandboxCredentialEnvironment = await resolveSandboxCredentialEnvironment({
512
+ environment: resolvedAuthEnvironment,
513
+ credentialEnvironmentVariables: CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
514
+ credentialForwarding: settings.credentialForwarding,
515
+ previousSandboxCredentialEnvironment: resumeData?.sandboxCredentialEnvironment
516
+ });
517
+ sandboxAuthEnvironment = {
518
+ ...resolvedAuthEnvironment,
519
+ ...sandboxCredentialEnvironment
520
+ };
521
+ const requestTransformations = createCodexSubscriptionRequestTransformations({
522
+ env: resolvedAuthEnvironment,
523
+ sandboxEnv: sandboxAuthEnvironment,
524
+ auth: authenticationMode,
525
+ requestHeaders: resolvedAuthentication.requestHeaders
526
+ });
527
+ if (requestTransformations.length > 0) await sandboxSession.addRequestTransformations(requestTransformations);
528
+ credentialsBrokered = true;
529
+ if (requestTransformations.length > 0 && authenticationMode === "direct" && resolvedAuthEnvironment.OPENAI_BASE_URL == null) sandboxAuthEnvironment.OPENAI_BASE_URL = DEFAULT_OPENAI_BASE_URL;
530
+ }
531
+ const sandboxHomeDir = await resolveSandboxHomeDir({
532
+ sandbox: toolSafeSandboxSession,
533
+ abortSignal: startOpts.abortSignal
534
+ });
535
+ const stateDir = harnessStateDirectoryPath({ sandboxHomeDir });
536
+ const bootstrapDir = path.posix.resolve(stateDir, CODEX_BOOTSTRAP_DIR);
537
+ const workDir = startOpts.sessionWorkDir;
538
+ const bridgeStateDir = `${harnessSessionDataDirectoryPath({
539
+ stateDirectory: stateDir,
540
+ sessionId: startOpts.sessionId
541
+ })}/bridge`;
542
+ const timeoutMs = settings.startupTimeoutMs ?? 12e4;
543
+ const report = startOpts.observability?.report;
544
+ const onDiagnostic = report ? (frame) => report(harnessV1DiagnosticFromBridgeFrame(frame, {
545
+ sessionId: startOpts.sessionId,
546
+ timestamp: Date.now()
547
+ })) : void 0;
548
+ const onBridgeError = createBridgeErrorHandler({
549
+ harnessId: "codex",
550
+ sessionId: startOpts.sessionId
551
+ });
552
+ if (coords) try {
553
+ const endpoint = await resolveBridgeEndpoint({
554
+ sandboxSession,
555
+ override: settings.portEndpoint,
556
+ port: coords.port
557
+ });
558
+ const attachEndpoint = withBridgeToken({
559
+ endpoint,
560
+ token: coords.token
561
+ });
562
+ const attachChannel = new SandboxChannel({
563
+ connect: ({ abortSignal }) => openWebSocket({
564
+ ...attachEndpoint,
565
+ abortSignal
566
+ }),
567
+ outboundSchema: outboundMessageSchema,
568
+ initialLastSeenEventId: coords.lastSeenEventId,
569
+ onDiagnostic,
570
+ onBridgeError,
571
+ reconnect: settings.reconnect
572
+ });
573
+ await attachChannel.open(isContinue ? { resume: true } : void 0);
574
+ return createSession({
575
+ sessionId: startOpts.sessionId,
576
+ channel: attachChannel,
577
+ proc: void 0,
578
+ model,
579
+ reasoningEffort: settings.reasoningEffort,
580
+ webSearch: settings.webSearch,
581
+ builtinToolFiltering: startOpts.builtinToolFiltering,
582
+ codexConfig: settings.codexConfig,
583
+ mcpServers: settings.mcpServers,
584
+ headers: startOpts.headers,
585
+ resumeThreadId: resumeThreadIdString,
586
+ isResume: true,
587
+ seedResumeThreadOnFirstPrompt: false,
588
+ rerunContinue: false,
589
+ bridgePort: coords.port,
590
+ bridgeToken: coords.token,
591
+ sandboxId,
592
+ sandboxCredentialEnvironment,
593
+ debug: startOpts.observability?.debug,
594
+ permissionMode: startOpts.permissionMode,
595
+ sandbox: toolSafeSandboxSession,
596
+ sandboxHomeDir,
597
+ turnConfigurationFingerprint
598
+ });
599
+ } catch {}
600
+ let respawnStrategy = isResume ? "rerun" : void 0;
601
+ if (coords && isContinue) {
602
+ const logRaw = await Promise.resolve(toolSafeSandboxSession.readTextFile({
603
+ path: `${bridgeStateDir}/event-log.ndjson`,
604
+ abortSignal: startOpts.abortSignal
605
+ })).catch(() => null);
606
+ if (await classifyDiskLog(logRaw) === "replay") respawnStrategy = "replay";
607
+ }
608
+ const port = resolveBridgePort({
609
+ sandboxSession,
610
+ override: settings.port
611
+ });
612
+ const token = settings.mintBridgeToken == null ? createBridgeToken() : settings.mintBridgeToken(sandboxId);
613
+ const forwardedAuthEnvironment = credentialsBrokered ? sandboxAuthEnvironment : await applyCredentialForwarding({
614
+ environment: sandboxAuthEnvironment,
615
+ credentialEnvironmentVariables: CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
616
+ credentialForwarding: settings.credentialForwarding
617
+ });
618
+ if (!credentialsBrokered) warnCredentialBrokeringUnavailable({
619
+ environment: resolvedAuthEnvironment,
620
+ forwardedEnvironment: forwardedAuthEnvironment,
621
+ credentialEnvironmentVariables: CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES
622
+ });
623
+ const env = {
624
+ ...forwardedAuthEnvironment,
625
+ AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
626
+ BRIDGE_CHANNEL_TOKEN: token,
627
+ BRIDGE_WS_PORT: String(port),
628
+ ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
629
+ };
630
+ if (respawnStrategy === void 0) await toolSafeSandboxSession.run({
631
+ command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
632
+ abortSignal: startOpts.abortSignal
633
+ });
634
+ await markBridgeStarting({
635
+ sandbox: toolSafeSandboxSession,
636
+ bridgeStateDir,
637
+ bridgeType: "codex",
638
+ abortSignal: startOpts.abortSignal
639
+ });
640
+ const proc = await toolSafeSandboxSession.spawn({
641
+ command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)}`,
642
+ env,
643
+ abortSignal: startOpts.abortSignal
644
+ });
645
+ const stderrTail = [];
646
+ const bridgeStderrDone = forwardBridgeProcessStream({
647
+ stream: proc.stderr,
648
+ streamName: "stderr",
649
+ source: "codex",
650
+ collectTail: stderrTail
651
+ });
652
+ const { port: boundPort } = await waitForBridgeReady({
653
+ proc,
654
+ sandbox: toolSafeSandboxSession,
655
+ bridgeStateDir,
656
+ bridgeType: "codex",
657
+ timeoutMs,
658
+ abortSignal: startOpts.abortSignal,
659
+ createTimeoutError: ({ proc, stdoutTail }) => createBridgeStartupError({
660
+ message: "codex bridge did not become ready in time.",
661
+ proc,
662
+ stdoutTail,
663
+ stderrTail,
664
+ stderrDone: bridgeStderrDone
665
+ }),
666
+ createExitError: ({ proc, stdoutTail }) => createBridgeStartupError({
667
+ message: "codex bridge exited before becoming ready.",
668
+ proc,
669
+ stdoutTail,
670
+ stderrTail,
671
+ stderrDone: bridgeStderrDone
672
+ })
673
+ });
674
+ drainBridgeProcessStream(proc.stdout);
675
+ const endpoint = await resolveBridgeEndpoint({
676
+ sandboxSession,
677
+ override: settings.portEndpoint,
678
+ port: boundPort
679
+ });
680
+ const bridgeEndpoint = withBridgeToken({
681
+ endpoint,
682
+ token
683
+ });
684
+ const channel = new SandboxChannel({
685
+ connect: ({ abortSignal }) => openWebSocket({
686
+ ...bridgeEndpoint,
687
+ abortSignal
688
+ }),
689
+ outboundSchema: outboundMessageSchema,
690
+ onDiagnostic,
691
+ onBridgeError,
692
+ reconnect: settings.reconnect,
693
+ ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
694
+ });
695
+ await channel.open(respawnStrategy === "replay" ? { resume: true } : void 0);
696
+ return createSession({
697
+ sessionId: startOpts.sessionId,
698
+ channel,
699
+ proc,
700
+ model,
701
+ reasoningEffort: settings.reasoningEffort,
702
+ webSearch: settings.webSearch,
703
+ builtinToolFiltering: startOpts.builtinToolFiltering,
704
+ codexConfig: settings.codexConfig,
705
+ mcpServers: settings.mcpServers,
706
+ headers: startOpts.headers,
707
+ resumeThreadId: resumeThreadIdString,
708
+ isResume: respawnStrategy !== void 0,
709
+ seedResumeThreadOnFirstPrompt: respawnStrategy !== void 0,
710
+ rerunContinue: respawnStrategy === "rerun",
711
+ bridgePort: boundPort,
712
+ bridgeToken: token,
713
+ sandboxId,
714
+ sandboxCredentialEnvironment,
715
+ debug: startOpts.observability?.debug,
716
+ permissionMode: startOpts.permissionMode,
717
+ sandbox: toolSafeSandboxSession,
718
+ sandboxHomeDir,
719
+ turnConfigurationFingerprint
720
+ });
721
+ }
722
+ };
863
723
  }
864
- function resolveBridgePort({
865
- sandboxSession,
866
- override
867
- }) {
868
- if (override !== void 0) return override;
869
- if ("ports" in sandboxSession && sandboxSession.ports.length > 0) {
870
- return sandboxSession.ports[0];
871
- }
872
- throw new HarnessCapabilityUnsupportedError({
873
- harnessId: "codex",
874
- message: "The codex harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createCodex({ port })`."
875
- });
724
+ function resolveBridgePort({ sandboxSession, override }) {
725
+ if (override !== void 0) return override;
726
+ if ("ports" in sandboxSession && sandboxSession.ports.length > 0) return sandboxSession.ports[0];
727
+ throw new HarnessCapabilityUnsupportedError({
728
+ harnessId: "codex",
729
+ message: "The codex harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createCodex({ port })`."
730
+ });
876
731
  }
877
- function validateBasicSandboxSettings({
878
- sandboxSession,
879
- port,
880
- portEndpoint
881
- }) {
882
- if ("getPortEndpoint" in sandboxSession) return;
883
- if (port == null) {
884
- throw new HarnessCapabilityUnsupportedError({
885
- harnessId: "codex",
886
- message: "The codex harness requires an explicit `port` when using a basic sandbox session."
887
- });
888
- }
889
- if (portEndpoint == null) {
890
- throw new HarnessCapabilityUnsupportedError({
891
- harnessId: "codex",
892
- message: "The codex harness requires an explicit `portEndpoint` when using a basic sandbox session."
893
- });
894
- }
732
+ function validateBasicSandboxSettings({ sandboxSession, port, portEndpoint }) {
733
+ if ("getPortEndpoint" in sandboxSession) return;
734
+ if (port == null) throw new HarnessCapabilityUnsupportedError({
735
+ harnessId: "codex",
736
+ message: "The codex harness requires an explicit `port` when using a basic sandbox session."
737
+ });
738
+ if (portEndpoint == null) throw new HarnessCapabilityUnsupportedError({
739
+ harnessId: "codex",
740
+ message: "The codex harness requires an explicit `portEndpoint` when using a basic sandbox session."
741
+ });
895
742
  }
896
- async function resolveBridgeEndpoint({
897
- sandboxSession,
898
- override,
899
- port
900
- }) {
901
- if (override != null) return override;
902
- if ("getPortEndpoint" in sandboxSession) {
903
- return sandboxSession.getPortEndpoint({ port, protocol: "ws" });
904
- }
905
- throw new HarnessCapabilityUnsupportedError({
906
- harnessId: "codex",
907
- message: "The codex harness requires an explicit `portEndpoint` when using a basic sandbox session."
908
- });
743
+ async function resolveBridgeEndpoint({ sandboxSession, override, port }) {
744
+ if (override != null) return override;
745
+ if ("getPortEndpoint" in sandboxSession) return sandboxSession.getPortEndpoint({
746
+ port,
747
+ protocol: "ws"
748
+ });
749
+ throw new HarnessCapabilityUnsupportedError({
750
+ harnessId: "codex",
751
+ message: "The codex harness requires an explicit `portEndpoint` when using a basic sandbox session."
752
+ });
909
753
  }
910
- function openWebSocket({
911
- url,
912
- headers,
913
- abortSignal
914
- }) {
915
- return new Promise((resolve2, reject) => {
916
- if (abortSignal.aborted) {
917
- reject(abortSignal.reason ?? new Error("WebSocket connection aborted"));
918
- return;
919
- }
920
- const ws = new WebSocket(url, {
921
- headers: headers == null ? void 0 : { ...headers }
922
- });
923
- let settled = false;
924
- const cleanup = () => {
925
- abortSignal.removeEventListener("abort", onAbort);
926
- ws.off("open", onOpen);
927
- ws.off("error", onError);
928
- };
929
- const rejectWithCleanup = (error) => {
930
- if (settled) return;
931
- settled = true;
932
- cleanup();
933
- ws.once("error", () => {
934
- });
935
- try {
936
- ws.terminate();
937
- } catch {
938
- try {
939
- ws.close();
940
- } catch {
941
- }
942
- }
943
- reject(error);
944
- };
945
- const onOpen = () => {
946
- if (settled) return;
947
- settled = true;
948
- cleanup();
949
- resolve2(ws);
950
- };
951
- const onError = (err) => {
952
- rejectWithCleanup(err);
953
- };
954
- const onAbort = () => rejectWithCleanup(
955
- abortSignal.reason ?? new Error("WebSocket connection aborted")
956
- );
957
- ws.once("open", onOpen);
958
- ws.once("error", onError);
959
- abortSignal.addEventListener("abort", onAbort, { once: true });
960
- });
754
+ function openWebSocket({ url, headers, abortSignal }) {
755
+ return new Promise((resolve, reject) => {
756
+ if (abortSignal.aborted) {
757
+ reject(abortSignal.reason ?? /* @__PURE__ */ new Error("WebSocket connection aborted"));
758
+ return;
759
+ }
760
+ const ws = new WebSocket(url, { headers: headers == null ? void 0 : { ...headers } });
761
+ let settled = false;
762
+ const cleanup = () => {
763
+ abortSignal.removeEventListener("abort", onAbort);
764
+ ws.off("open", onOpen);
765
+ ws.off("error", onError);
766
+ };
767
+ const rejectWithCleanup = (error) => {
768
+ if (settled) return;
769
+ settled = true;
770
+ cleanup();
771
+ ws.once("error", () => {});
772
+ try {
773
+ ws.terminate();
774
+ } catch {
775
+ try {
776
+ ws.close();
777
+ } catch {}
778
+ }
779
+ reject(error);
780
+ };
781
+ const onOpen = () => {
782
+ if (settled) return;
783
+ settled = true;
784
+ cleanup();
785
+ resolve(ws);
786
+ };
787
+ const onError = (err) => {
788
+ rejectWithCleanup(err);
789
+ };
790
+ const onAbort = () => rejectWithCleanup(abortSignal.reason ?? /* @__PURE__ */ new Error("WebSocket connection aborted"));
791
+ ws.once("open", onOpen);
792
+ ws.once("error", onError);
793
+ abortSignal.addEventListener("abort", onAbort, { once: true });
794
+ });
961
795
  }
962
- function createSession({
963
- sessionId,
964
- channel,
965
- proc,
966
- model,
967
- reasoningEffort,
968
- webSearch,
969
- builtinToolFiltering,
970
- codexConfig,
971
- mcpServers,
972
- headers,
973
- resumeThreadId,
974
- isResume,
975
- seedResumeThreadOnFirstPrompt,
976
- rerunContinue,
977
- bridgePort,
978
- bridgeToken,
979
- sandboxId,
980
- sandboxCredentialEnvironment,
981
- debug,
982
- permissionMode,
983
- sandbox,
984
- sandboxHomeDir,
985
- turnConfigurationFingerprint
986
- }) {
987
- let stopped = false;
988
- let stopPromise;
989
- let pendingResumeThreadId = seedResumeThreadOnFirstPrompt ? resumeThreadId : void 0;
990
- let latestThreadId = resumeThreadId;
991
- let latestTurnConfigurationFingerprint = turnConfigurationFingerprint;
992
- channel.on("bridge-thread", (msg) => {
993
- latestThreadId = msg.threadId;
994
- });
995
- const synchronizeTurnConfiguration = async ({
996
- skills,
997
- instructions,
998
- tools,
999
- abortSignal
1000
- }) => {
1001
- const skillsResult = await writeSkills({
1002
- sandbox,
1003
- homePath: sandboxHomeDir,
1004
- skillsDir: ".agents/skills",
1005
- skills,
1006
- abortSignal,
1007
- invalidSkillNameMessage: ({ name }) => `Invalid Codex skill name: ${name}`,
1008
- invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid Codex skill file path for ${skillName}: ${filePath}`
1009
- });
1010
- const nextFingerprint = fingerprintCodexTurnConfiguration({
1011
- instructions,
1012
- tools
1013
- });
1014
- const restartThread = latestThreadId != null && (skillsResult.changed || latestTurnConfigurationFingerprint != null && latestTurnConfigurationFingerprint !== nextFingerprint);
1015
- latestTurnConfigurationFingerprint = nextFingerprint;
1016
- if (restartThread) {
1017
- pendingResumeThreadId = void 0;
1018
- }
1019
- return { restartThread };
1020
- };
1021
- const wireTurn = (turnOpts) => {
1022
- let pendingResolve;
1023
- let pendingReject;
1024
- const done = new Promise((resolve2, reject) => {
1025
- pendingResolve = resolve2;
1026
- pendingReject = reject;
1027
- });
1028
- const userMessageSubmitter = experimental_createBridgeUserMessageSubmitter({
1029
- send: (message) => channel.send(message),
1030
- onResponse: (listener) => channel.on("user-message-response", listener),
1031
- onReconnect: (listener) => channel.onReconnect(listener)
1032
- });
1033
- const unsubs = [];
1034
- const forward = (event) => {
1035
- try {
1036
- turnOpts.emit(event);
1037
- } catch {
1038
- }
1039
- };
1040
- const eventTypes = [
1041
- "stream-start",
1042
- "text-start",
1043
- "text-delta",
1044
- "text-end",
1045
- "reasoning-start",
1046
- "reasoning-delta",
1047
- "reasoning-end",
1048
- "tool-call",
1049
- "tool-approval-request",
1050
- "tool-result",
1051
- "file-change",
1052
- "finish-step",
1053
- "raw"
1054
- ];
1055
- let isSettled = false;
1056
- const settleSuccess = () => {
1057
- if (isSettled) return;
1058
- isSettled = true;
1059
- userMessageSubmitter.close();
1060
- for (const u of unsubs) u();
1061
- pendingResolve();
1062
- };
1063
- const settleError = (err) => {
1064
- if (isSettled) return;
1065
- isSettled = true;
1066
- userMessageSubmitter.close(err);
1067
- for (const u of unsubs) u();
1068
- pendingReject(err);
1069
- };
1070
- for (const type of eventTypes) {
1071
- unsubs.push(
1072
- channel.on(type, (msg) => {
1073
- forward(msg);
1074
- })
1075
- );
1076
- }
1077
- unsubs.push(
1078
- channel.on("finish", (msg) => {
1079
- forward(msg);
1080
- settleSuccess();
1081
- })
1082
- );
1083
- unsubs.push(
1084
- channel.on("error", (msg) => {
1085
- forward(msg);
1086
- settleError(msg.error);
1087
- })
1088
- );
1089
- const onClose = (_code, reason) => {
1090
- if (isSettled) return;
1091
- if (reason === "suspended") {
1092
- settleSuccess();
1093
- return;
1094
- }
1095
- settleError(new Error("codex bridge closed before the turn finished."));
1096
- };
1097
- channel.onClose(onClose);
1098
- const onAbort = () => {
1099
- if (isSettled) return;
1100
- try {
1101
- channel.send({ type: "abort" });
1102
- } catch {
1103
- }
1104
- settleError(
1105
- turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError")
1106
- );
1107
- };
1108
- if (turnOpts.abortSignal) {
1109
- if (turnOpts.abortSignal.aborted) {
1110
- onAbort();
1111
- } else {
1112
- turnOpts.abortSignal.addEventListener("abort", onAbort, {
1113
- once: true
1114
- });
1115
- }
1116
- }
1117
- const control = {
1118
- submitToolResult: async (input) => {
1119
- channel.send({
1120
- type: "tool-result",
1121
- toolCallId: input.toolCallId,
1122
- output: input.output,
1123
- isError: input.isError
1124
- });
1125
- },
1126
- submitToolApproval: async (input) => {
1127
- channel.send({
1128
- type: "tool-approval-response",
1129
- approvalId: input.approvalId,
1130
- approved: input.approved,
1131
- reason: input.reason
1132
- });
1133
- },
1134
- submitUserMessage: async (text) => {
1135
- await userMessageSubmitter.submit(text);
1136
- },
1137
- done
1138
- };
1139
- return {
1140
- control,
1141
- sendStart: (send) => {
1142
- const timer = setTimeout(() => {
1143
- if (isSettled) return;
1144
- try {
1145
- send();
1146
- } catch (err) {
1147
- settleError(err);
1148
- }
1149
- }, 0);
1150
- timer.unref?.();
1151
- }
1152
- };
1153
- };
1154
- return {
1155
- sessionId,
1156
- isResume,
1157
- doPromptTurn: async (promptOpts) => {
1158
- if (promptOpts.responseFormat?.type === "json" && promptOpts.responseFormat.schema == null) {
1159
- throw new HarnessCapabilityUnsupportedError({
1160
- message: "Harness 'codex' requires a JSON schema for structured output.",
1161
- harnessId: "codex"
1162
- });
1163
- }
1164
- const tools = (promptOpts.tools ?? []).map((t) => ({
1165
- name: t.name,
1166
- description: t.description,
1167
- inputSchema: t.inputSchema
1168
- }));
1169
- const { restartThread } = await synchronizeTurnConfiguration({
1170
- skills: promptOpts.skills,
1171
- instructions: promptOpts.instructions,
1172
- tools,
1173
- abortSignal: promptOpts.abortSignal
1174
- });
1175
- const turn = wireTurn({
1176
- emit: promptOpts.emit,
1177
- abortSignal: promptOpts.abortSignal
1178
- });
1179
- const startMessage = {
1180
- type: "start",
1181
- prompt: extractUserText(promptOpts.prompt),
1182
- tools,
1183
- ...promptOpts.responseFormat == null ? {} : { responseFormat: promptOpts.responseFormat },
1184
- ...promptOpts.instructions ? { instructions: promptOpts.instructions } : {},
1185
- model: promptOpts.model ?? model,
1186
- reasoningEffort,
1187
- webSearch,
1188
- ...builtinToolFiltering == null ? {} : { builtinToolFiltering },
1189
- ...codexConfig == null ? {} : { codexConfig },
1190
- ...mcpServers == null ? {} : { mcpServers },
1191
- ...headers == null ? {} : { headers },
1192
- ...permissionMode ? { permissionMode } : {},
1193
- ...pendingResumeThreadId ? { resumeThreadId: pendingResumeThreadId } : {},
1194
- ...restartThread ? { restartThread: true } : {},
1195
- ...debug ? { debug } : {}
1196
- };
1197
- pendingResumeThreadId = void 0;
1198
- turn.sendStart(() => channel.send(startMessage));
1199
- return turn.control;
1200
- },
1201
- doContinueTurn: async (continueOpts) => {
1202
- if (continueOpts.responseFormat?.type === "json" && continueOpts.responseFormat.schema == null) {
1203
- throw new HarnessCapabilityUnsupportedError({
1204
- message: "Harness 'codex' requires a JSON schema for structured output.",
1205
- harnessId: "codex"
1206
- });
1207
- }
1208
- const tools = (continueOpts.tools ?? []).map((t) => ({
1209
- name: t.name,
1210
- description: t.description,
1211
- inputSchema: t.inputSchema
1212
- }));
1213
- const { restartThread } = await synchronizeTurnConfiguration({
1214
- skills: continueOpts.skills,
1215
- instructions: continueOpts.instructions,
1216
- tools,
1217
- abortSignal: continueOpts.abortSignal
1218
- });
1219
- const turn = wireTurn({
1220
- emit: continueOpts.emit,
1221
- abortSignal: continueOpts.abortSignal
1222
- });
1223
- if (rerunContinue) {
1224
- const threadId = pendingResumeThreadId ?? latestThreadId;
1225
- pendingResumeThreadId = void 0;
1226
- turn.sendStart(
1227
- () => channel.send({
1228
- type: "start",
1229
- /*
1230
- * A continuation nudge rather than an empty prompt: `resumeThreadId`
1231
- * rehydrates the prior thread, and this is the new user turn that
1232
- * drives it forward. Keeping it non-empty avoids handing the runtime
1233
- * an empty user message (and mirrors the claude-code adapter, where an
1234
- * empty text block trips the Anthropic API's `cache_control` rule).
1235
- */
1236
- prompt: "Continue.",
1237
- tools,
1238
- ...continueOpts.responseFormat == null ? {} : { responseFormat: continueOpts.responseFormat },
1239
- ...continueOpts.instructions ? { instructions: continueOpts.instructions } : {},
1240
- model: continueOpts.model ?? model,
1241
- reasoningEffort,
1242
- webSearch,
1243
- ...builtinToolFiltering == null ? {} : { builtinToolFiltering },
1244
- ...codexConfig == null ? {} : { codexConfig },
1245
- ...mcpServers == null ? {} : { mcpServers },
1246
- ...headers == null ? {} : { headers },
1247
- ...permissionMode ? { permissionMode } : {},
1248
- ...threadId ? { resumeThreadId: threadId } : {},
1249
- ...restartThread ? { restartThread: true } : {},
1250
- ...debug ? { debug } : {}
1251
- })
1252
- );
1253
- }
1254
- return turn.control;
1255
- },
1256
- doCompact: async () => {
1257
- throw new HarnessCapabilityUnsupportedError({
1258
- message: "Harness 'codex' does not support manual compaction; Codex auto-compacts its context internally.",
1259
- harnessId: "codex"
1260
- });
1261
- },
1262
- doDetach: async () => {
1263
- if (stopped) {
1264
- throw new Error(
1265
- `codex session ${sessionId} is already stopped; cannot detach.`
1266
- );
1267
- }
1268
- stopped = true;
1269
- const lastSeenEventId = await channel.suspend();
1270
- const payload = {
1271
- type: "resume-session",
1272
- harnessId: "codex",
1273
- specificationVersion: "harness-v1",
1274
- data: {
1275
- ...latestThreadId ? { threadId: latestThreadId } : {},
1276
- ...latestTurnConfigurationFingerprint ? {
1277
- turnConfigurationFingerprint: latestTurnConfigurationFingerprint
1278
- } : {},
1279
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1280
- bridge: {
1281
- port: bridgePort,
1282
- token: bridgeToken,
1283
- lastSeenEventId,
1284
- ...sandboxId == null ? {} : { sandboxId }
1285
- }
1286
- }
1287
- };
1288
- return payload;
1289
- },
1290
- doDestroy: async () => {
1291
- if (stopped) return stopPromise;
1292
- stopped = true;
1293
- stopPromise = (async () => {
1294
- channel.beginClose();
1295
- try {
1296
- if (!channel.isClosed()) {
1297
- channel.send({ type: "destroy" });
1298
- }
1299
- } catch {
1300
- }
1301
- let stopTimer;
1302
- try {
1303
- if (proc) {
1304
- await Promise.race([
1305
- proc.wait(),
1306
- new Promise((resolve2) => {
1307
- stopTimer = setTimeout(resolve2, 5e3);
1308
- stopTimer.unref?.();
1309
- })
1310
- ]);
1311
- }
1312
- } finally {
1313
- if (stopTimer) clearTimeout(stopTimer);
1314
- try {
1315
- await proc?.kill();
1316
- } catch {
1317
- }
1318
- channel.close();
1319
- }
1320
- })();
1321
- return stopPromise;
1322
- },
1323
- doStop: async () => {
1324
- if (stopped) {
1325
- throw new Error(
1326
- `codex session ${sessionId} is already stopped; cannot stop.`
1327
- );
1328
- }
1329
- stopped = true;
1330
- channel.beginClose();
1331
- const data = channel.isClosed() ? {} : await new Promise((resolve2, reject) => {
1332
- const timer = setTimeout(() => {
1333
- unsub();
1334
- reject(
1335
- new Error(
1336
- `codex session ${sessionId} did not reply to stop within 5s.`
1337
- )
1338
- );
1339
- }, 5e3);
1340
- timer.unref?.();
1341
- const unsub = channel.on("bridge-stop", (msg) => {
1342
- clearTimeout(timer);
1343
- unsub();
1344
- resolve2(msg.data);
1345
- });
1346
- try {
1347
- channel.send({ type: "stop" });
1348
- } catch (err) {
1349
- clearTimeout(timer);
1350
- unsub();
1351
- reject(err);
1352
- }
1353
- });
1354
- let stopTimer;
1355
- try {
1356
- if (proc) {
1357
- await Promise.race([
1358
- proc.wait(),
1359
- new Promise((resolve2) => {
1360
- stopTimer = setTimeout(resolve2, 5e3);
1361
- stopTimer.unref?.();
1362
- })
1363
- ]);
1364
- }
1365
- } finally {
1366
- if (stopTimer) clearTimeout(stopTimer);
1367
- try {
1368
- await proc?.kill();
1369
- } catch {
1370
- }
1371
- channel.close();
1372
- }
1373
- const lifecycleData = data != null && typeof data === "object" && !Array.isArray(data) ? { ...data } : {};
1374
- const payload = {
1375
- type: "resume-session",
1376
- harnessId: "codex",
1377
- specificationVersion: "harness-v1",
1378
- data: {
1379
- ...lifecycleData,
1380
- ...latestTurnConfigurationFingerprint ? {
1381
- turnConfigurationFingerprint: latestTurnConfigurationFingerprint
1382
- } : {},
1383
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment }
1384
- }
1385
- };
1386
- return payload;
1387
- },
1388
- doSuspendTurn: async () => {
1389
- if (stopped) {
1390
- throw new Error(
1391
- `codex session ${sessionId} is stopped; cannot suspend.`
1392
- );
1393
- }
1394
- stopped = true;
1395
- const lastSeenEventId = await channel.suspend();
1396
- const payload = {
1397
- type: "continue-turn",
1398
- harnessId: "codex",
1399
- specificationVersion: "harness-v1",
1400
- data: {
1401
- ...latestThreadId ? { threadId: latestThreadId } : {},
1402
- ...latestTurnConfigurationFingerprint ? {
1403
- turnConfigurationFingerprint: latestTurnConfigurationFingerprint
1404
- } : {},
1405
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1406
- bridge: {
1407
- port: bridgePort,
1408
- token: bridgeToken,
1409
- lastSeenEventId,
1410
- ...sandboxId == null ? {} : { sandboxId }
1411
- }
1412
- }
1413
- };
1414
- return payload;
1415
- }
1416
- };
796
+ function createSession({ sessionId, channel, proc, model, reasoningEffort, webSearch, builtinToolFiltering, codexConfig, mcpServers, headers, resumeThreadId, isResume, seedResumeThreadOnFirstPrompt, rerunContinue, bridgePort, bridgeToken, sandboxId, sandboxCredentialEnvironment, debug, permissionMode, sandbox, sandboxHomeDir, turnConfigurationFingerprint }) {
797
+ let stopped = false;
798
+ let stopPromise;
799
+ let pendingResumeThreadId = seedResumeThreadOnFirstPrompt ? resumeThreadId : void 0;
800
+ let latestThreadId = resumeThreadId;
801
+ let latestTurnConfigurationFingerprint = turnConfigurationFingerprint;
802
+ channel.on("bridge-thread", (msg) => {
803
+ latestThreadId = msg.threadId;
804
+ });
805
+ const synchronizeTurnConfiguration = async ({ skills, instructions, tools, abortSignal }) => {
806
+ const skillsResult = await writeSkills({
807
+ sandbox,
808
+ homePath: sandboxHomeDir,
809
+ skillsDir: ".agents/skills",
810
+ skills,
811
+ abortSignal,
812
+ invalidSkillNameMessage: ({ name }) => `Invalid Codex skill name: ${name}`,
813
+ invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid Codex skill file path for ${skillName}: ${filePath}`
814
+ });
815
+ const nextFingerprint = fingerprintCodexTurnConfiguration({
816
+ instructions,
817
+ tools
818
+ });
819
+ const restartThread = latestThreadId != null && (skillsResult.changed || latestTurnConfigurationFingerprint != null && latestTurnConfigurationFingerprint !== nextFingerprint);
820
+ latestTurnConfigurationFingerprint = nextFingerprint;
821
+ if (restartThread) pendingResumeThreadId = void 0;
822
+ return { restartThread };
823
+ };
824
+ const wireTurn = (turnOpts) => {
825
+ let pendingResolve;
826
+ let pendingReject;
827
+ const done = new Promise((resolve, reject) => {
828
+ pendingResolve = resolve;
829
+ pendingReject = reject;
830
+ });
831
+ const userMessageSubmitter = experimental_createBridgeUserMessageSubmitter({
832
+ send: (message) => channel.send(message),
833
+ onResponse: (listener) => channel.on("user-message-response", listener),
834
+ onReconnect: (listener) => channel.onReconnect(listener)
835
+ });
836
+ const unsubs = [];
837
+ const forward = (event) => {
838
+ try {
839
+ turnOpts.emit(event);
840
+ } catch {}
841
+ };
842
+ const eventTypes = [
843
+ "stream-start",
844
+ "text-start",
845
+ "text-delta",
846
+ "text-end",
847
+ "reasoning-start",
848
+ "reasoning-delta",
849
+ "reasoning-end",
850
+ "tool-call",
851
+ "tool-approval-request",
852
+ "tool-result",
853
+ "file-change",
854
+ "finish-step",
855
+ "raw"
856
+ ];
857
+ let isSettled = false;
858
+ const settleSuccess = () => {
859
+ if (isSettled) return;
860
+ isSettled = true;
861
+ userMessageSubmitter.close();
862
+ for (const u of unsubs) u();
863
+ pendingResolve();
864
+ };
865
+ const settleError = (err) => {
866
+ if (isSettled) return;
867
+ isSettled = true;
868
+ userMessageSubmitter.close(err);
869
+ for (const u of unsubs) u();
870
+ pendingReject(err);
871
+ };
872
+ for (const type of eventTypes) unsubs.push(channel.on(type, (msg) => {
873
+ forward(msg);
874
+ }));
875
+ unsubs.push(channel.on("finish", (msg) => {
876
+ forward(msg);
877
+ settleSuccess();
878
+ }));
879
+ unsubs.push(channel.on("error", (msg) => {
880
+ forward(msg);
881
+ settleError(msg.error);
882
+ }));
883
+ const onClose = (_code, reason) => {
884
+ if (isSettled) return;
885
+ if (reason === "suspended") {
886
+ settleSuccess();
887
+ return;
888
+ }
889
+ settleError(/* @__PURE__ */ new Error("codex bridge closed before the turn finished."));
890
+ };
891
+ channel.onClose(onClose);
892
+ const onAbort = () => {
893
+ if (isSettled) return;
894
+ try {
895
+ channel.send({ type: "abort" });
896
+ } catch {}
897
+ settleError(turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError"));
898
+ };
899
+ if (turnOpts.abortSignal) {
900
+ if (turnOpts.abortSignal.aborted) onAbort();
901
+ else turnOpts.abortSignal.addEventListener("abort", onAbort, { once: true });
902
+ }
903
+ return {
904
+ control: {
905
+ submitToolResult: async (input) => {
906
+ channel.send({
907
+ type: "tool-result",
908
+ toolCallId: input.toolCallId,
909
+ output: input.output,
910
+ isError: input.isError
911
+ });
912
+ },
913
+ submitToolApproval: async (input) => {
914
+ channel.send({
915
+ type: "tool-approval-response",
916
+ approvalId: input.approvalId,
917
+ approved: input.approved,
918
+ reason: input.reason
919
+ });
920
+ },
921
+ submitUserMessage: async (text) => {
922
+ await userMessageSubmitter.submit(text);
923
+ },
924
+ done
925
+ },
926
+ sendStart: (send) => {
927
+ setTimeout(() => {
928
+ if (isSettled) return;
929
+ try {
930
+ send();
931
+ } catch (err) {
932
+ settleError(err);
933
+ }
934
+ }, 0).unref?.();
935
+ }
936
+ };
937
+ };
938
+ return {
939
+ sessionId,
940
+ isResume,
941
+ doPromptTurn: async (promptOpts) => {
942
+ if (promptOpts.responseFormat?.type === "json" && promptOpts.responseFormat.schema == null) throw new HarnessCapabilityUnsupportedError({
943
+ message: "Harness 'codex' requires a JSON schema for structured output.",
944
+ harnessId: "codex"
945
+ });
946
+ const tools = (promptOpts.tools ?? []).map((t) => ({
947
+ name: t.name,
948
+ description: t.description,
949
+ inputSchema: t.inputSchema
950
+ }));
951
+ const { restartThread } = await synchronizeTurnConfiguration({
952
+ skills: promptOpts.skills,
953
+ instructions: promptOpts.instructions,
954
+ tools,
955
+ abortSignal: promptOpts.abortSignal
956
+ });
957
+ const turn = wireTurn({
958
+ emit: promptOpts.emit,
959
+ abortSignal: promptOpts.abortSignal
960
+ });
961
+ const startMessage = {
962
+ type: "start",
963
+ prompt: extractUserText(promptOpts.prompt),
964
+ tools,
965
+ ...promptOpts.responseFormat == null ? {} : { responseFormat: promptOpts.responseFormat },
966
+ ...promptOpts.instructions ? { instructions: promptOpts.instructions } : {},
967
+ model: promptOpts.model ?? model,
968
+ reasoningEffort,
969
+ webSearch,
970
+ ...builtinToolFiltering == null ? {} : { builtinToolFiltering },
971
+ ...codexConfig == null ? {} : { codexConfig },
972
+ ...mcpServers == null ? {} : { mcpServers },
973
+ ...headers == null ? {} : { headers },
974
+ ...permissionMode ? { permissionMode } : {},
975
+ ...pendingResumeThreadId ? { resumeThreadId: pendingResumeThreadId } : {},
976
+ ...restartThread ? { restartThread: true } : {},
977
+ ...debug ? { debug } : {}
978
+ };
979
+ pendingResumeThreadId = void 0;
980
+ turn.sendStart(() => channel.send(startMessage));
981
+ return turn.control;
982
+ },
983
+ doContinueTurn: async (continueOpts) => {
984
+ if (continueOpts.responseFormat?.type === "json" && continueOpts.responseFormat.schema == null) throw new HarnessCapabilityUnsupportedError({
985
+ message: "Harness 'codex' requires a JSON schema for structured output.",
986
+ harnessId: "codex"
987
+ });
988
+ const tools = (continueOpts.tools ?? []).map((t) => ({
989
+ name: t.name,
990
+ description: t.description,
991
+ inputSchema: t.inputSchema
992
+ }));
993
+ const { restartThread } = await synchronizeTurnConfiguration({
994
+ skills: continueOpts.skills,
995
+ instructions: continueOpts.instructions,
996
+ tools,
997
+ abortSignal: continueOpts.abortSignal
998
+ });
999
+ const turn = wireTurn({
1000
+ emit: continueOpts.emit,
1001
+ abortSignal: continueOpts.abortSignal
1002
+ });
1003
+ if (rerunContinue) {
1004
+ const threadId = pendingResumeThreadId ?? latestThreadId;
1005
+ pendingResumeThreadId = void 0;
1006
+ turn.sendStart(() => channel.send({
1007
+ type: "start",
1008
+ prompt: "Continue.",
1009
+ tools,
1010
+ ...continueOpts.responseFormat == null ? {} : { responseFormat: continueOpts.responseFormat },
1011
+ ...continueOpts.instructions ? { instructions: continueOpts.instructions } : {},
1012
+ model: continueOpts.model ?? model,
1013
+ reasoningEffort,
1014
+ webSearch,
1015
+ ...builtinToolFiltering == null ? {} : { builtinToolFiltering },
1016
+ ...codexConfig == null ? {} : { codexConfig },
1017
+ ...mcpServers == null ? {} : { mcpServers },
1018
+ ...headers == null ? {} : { headers },
1019
+ ...permissionMode ? { permissionMode } : {},
1020
+ ...threadId ? { resumeThreadId: threadId } : {},
1021
+ ...restartThread ? { restartThread: true } : {},
1022
+ ...debug ? { debug } : {}
1023
+ }));
1024
+ }
1025
+ return turn.control;
1026
+ },
1027
+ doCompact: async () => {
1028
+ throw new HarnessCapabilityUnsupportedError({
1029
+ message: "Harness 'codex' does not support manual compaction; Codex auto-compacts its context internally.",
1030
+ harnessId: "codex"
1031
+ });
1032
+ },
1033
+ doDetach: async () => {
1034
+ if (stopped) throw new Error(`codex session ${sessionId} is already stopped; cannot detach.`);
1035
+ stopped = true;
1036
+ const lastSeenEventId = await channel.suspend();
1037
+ return {
1038
+ type: "resume-session",
1039
+ harnessId: "codex",
1040
+ specificationVersion: "harness-v1",
1041
+ data: {
1042
+ ...latestThreadId ? { threadId: latestThreadId } : {},
1043
+ ...latestTurnConfigurationFingerprint ? { turnConfigurationFingerprint: latestTurnConfigurationFingerprint } : {},
1044
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1045
+ bridge: {
1046
+ port: bridgePort,
1047
+ token: bridgeToken,
1048
+ lastSeenEventId,
1049
+ ...sandboxId == null ? {} : { sandboxId }
1050
+ }
1051
+ }
1052
+ };
1053
+ },
1054
+ doDestroy: async () => {
1055
+ if (stopped) return stopPromise;
1056
+ stopped = true;
1057
+ stopPromise = (async () => {
1058
+ channel.beginClose();
1059
+ try {
1060
+ if (!channel.isClosed()) channel.send({ type: "destroy" });
1061
+ } catch {}
1062
+ let stopTimer;
1063
+ try {
1064
+ if (proc) await Promise.race([proc.wait(), new Promise((resolve) => {
1065
+ stopTimer = setTimeout(resolve, 5e3);
1066
+ stopTimer.unref?.();
1067
+ })]);
1068
+ } finally {
1069
+ if (stopTimer) clearTimeout(stopTimer);
1070
+ try {
1071
+ await proc?.kill();
1072
+ } catch {}
1073
+ channel.close();
1074
+ }
1075
+ })();
1076
+ return stopPromise;
1077
+ },
1078
+ doStop: async () => {
1079
+ if (stopped) throw new Error(`codex session ${sessionId} is already stopped; cannot stop.`);
1080
+ stopped = true;
1081
+ channel.beginClose();
1082
+ const data = channel.isClosed() ? {} : await new Promise((resolve, reject) => {
1083
+ const timer = setTimeout(() => {
1084
+ unsub();
1085
+ reject(/* @__PURE__ */ new Error(`codex session ${sessionId} did not reply to stop within 5s.`));
1086
+ }, 5e3);
1087
+ timer.unref?.();
1088
+ const unsub = channel.on("bridge-stop", (msg) => {
1089
+ clearTimeout(timer);
1090
+ unsub();
1091
+ resolve(msg.data);
1092
+ });
1093
+ try {
1094
+ channel.send({ type: "stop" });
1095
+ } catch (err) {
1096
+ clearTimeout(timer);
1097
+ unsub();
1098
+ reject(err);
1099
+ }
1100
+ });
1101
+ let stopTimer;
1102
+ try {
1103
+ if (proc) await Promise.race([proc.wait(), new Promise((resolve) => {
1104
+ stopTimer = setTimeout(resolve, 5e3);
1105
+ stopTimer.unref?.();
1106
+ })]);
1107
+ } finally {
1108
+ if (stopTimer) clearTimeout(stopTimer);
1109
+ try {
1110
+ await proc?.kill();
1111
+ } catch {}
1112
+ channel.close();
1113
+ }
1114
+ return {
1115
+ type: "resume-session",
1116
+ harnessId: "codex",
1117
+ specificationVersion: "harness-v1",
1118
+ data: {
1119
+ ...data != null && typeof data === "object" && !Array.isArray(data) ? { ...data } : {},
1120
+ ...latestTurnConfigurationFingerprint ? { turnConfigurationFingerprint: latestTurnConfigurationFingerprint } : {},
1121
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment }
1122
+ }
1123
+ };
1124
+ },
1125
+ doSuspendTurn: async () => {
1126
+ if (stopped) throw new Error(`codex session ${sessionId} is stopped; cannot suspend.`);
1127
+ stopped = true;
1128
+ const lastSeenEventId = await channel.suspend();
1129
+ return {
1130
+ type: "continue-turn",
1131
+ harnessId: "codex",
1132
+ specificationVersion: "harness-v1",
1133
+ data: {
1134
+ ...latestThreadId ? { threadId: latestThreadId } : {},
1135
+ ...latestTurnConfigurationFingerprint ? { turnConfigurationFingerprint: latestTurnConfigurationFingerprint } : {},
1136
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1137
+ bridge: {
1138
+ port: bridgePort,
1139
+ token: bridgeToken,
1140
+ lastSeenEventId,
1141
+ ...sandboxId == null ? {} : { sandboxId }
1142
+ }
1143
+ }
1144
+ };
1145
+ }
1146
+ };
1417
1147
  }
1418
- function fingerprintCodexTurnConfiguration({
1419
- instructions,
1420
- tools
1421
- }) {
1422
- return createHash2("sha256").update(JSON.stringify({ instructions: instructions ?? null, tools })).digest("hex");
1148
+ function fingerprintCodexTurnConfiguration({ instructions, tools }) {
1149
+ return createHash("sha256").update(JSON.stringify({
1150
+ instructions: instructions ?? null,
1151
+ tools
1152
+ })).digest("hex");
1423
1153
  }
1424
1154
  function extractUserText(prompt) {
1425
- if (typeof prompt === "string") return prompt;
1426
- const { content } = prompt;
1427
- if (typeof content === "string") return content;
1428
- const parts = [];
1429
- for (const part of content) {
1430
- if (part.type !== "text") {
1431
- throw new HarnessCapabilityUnsupportedError({
1432
- harnessId: "codex",
1433
- message: `The codex harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
1434
- });
1435
- }
1436
- parts.push(part.text);
1437
- }
1438
- return parts.join("\n\n");
1155
+ if (typeof prompt === "string") return prompt;
1156
+ const { content } = prompt;
1157
+ if (typeof content === "string") return content;
1158
+ const parts = [];
1159
+ for (const part of content) {
1160
+ if (part.type !== "text") throw new HarnessCapabilityUnsupportedError({
1161
+ harnessId: "codex",
1162
+ message: `The codex harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
1163
+ });
1164
+ parts.push(part.text);
1165
+ }
1166
+ return parts.join("\n\n");
1439
1167
  }
1168
+ //#endregion
1169
+ //#region src/index.ts
1170
+ /**
1171
+ * Default `codex` harness instance with no overrides — suitable for the
1172
+ * common case where the underlying `codex` CLI's defaults are fine.
1173
+ * Equivalent to `createCodex()`.
1174
+ */
1175
+ const codex = createCodex();
1176
+ //#endregion
1177
+ export { VERSION, codex, createCodex };
1440
1178
 
1441
- // src/index.ts
1442
- var codex = createCodex();
1443
- export {
1444
- VERSION,
1445
- codex,
1446
- createCodex
1447
- };
1448
1179
  //# sourceMappingURL=index.js.map