@ai-sdk/harness-codex 1.0.93 → 1.0.95
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/CHANGELOG.md +23 -0
- package/dist/bridge/index.mjs +32 -6
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/bridge/package.json +1 -1
- package/dist/bridge/pnpm-lock.yaml +34 -34
- package/dist/index.d.ts +6 -27
- package/dist/index.js +112 -102
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bridge/index.ts +3 -1
- package/src/bridge/package.json +1 -1
- package/src/bridge/pnpm-lock.yaml +34 -34
- package/src/codex-auth.ts +28 -107
- package/src/codex-bridge-protocol.ts +1 -0
- package/src/codex-harness.ts +139 -48
- package/src/index.ts +1 -1
package/src/codex-auth.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
HarnessV1Authentication,
|
|
2
3
|
HarnessV1RequestTransformation,
|
|
3
4
|
HarnessV1RequestTransformationSources,
|
|
4
5
|
} from '@ai-sdk/harness';
|
|
5
6
|
import {
|
|
6
7
|
createCredentialRequestTransformation,
|
|
7
8
|
getAiGatewayAuthFromEnv,
|
|
9
|
+
isHarnessAuthenticationEnvironment,
|
|
8
10
|
} from '@ai-sdk/harness/utils';
|
|
9
11
|
|
|
10
12
|
const DEFAULT_AI_GATEWAY_BASE_URL = 'https://ai-gateway.vercel.sh/v1';
|
|
@@ -42,83 +44,48 @@ export function createCodexRequestTransformations({
|
|
|
42
44
|
|
|
43
45
|
export type CodexResolvedAuthenticationMode = 'direct' | 'ai-gateway';
|
|
44
46
|
|
|
45
|
-
export type CodexAuthenticationMode =
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @deprecated Passing an object to auth options is deprecated. Use a `CodexAuthenticationMode` string value ("auto" | "direct" | "ai-gateway") instead, and pass credentials via environment variables.
|
|
49
|
-
*/
|
|
50
|
-
export type LegacyCodexAuthOptions = {
|
|
51
|
-
readonly openaiCompatible?: {
|
|
52
|
-
readonly apiKey?: string;
|
|
53
|
-
readonly baseUrl?: string;
|
|
54
|
-
readonly modelProviderName?: string;
|
|
55
|
-
readonly queryParamsJson?: string;
|
|
56
|
-
};
|
|
57
|
-
readonly openai?: {
|
|
58
|
-
readonly apiKey?: string;
|
|
59
|
-
readonly baseUrl?: string;
|
|
60
|
-
readonly organization?: string;
|
|
61
|
-
readonly project?: string;
|
|
62
|
-
};
|
|
63
|
-
readonly gateway?: {
|
|
64
|
-
readonly apiKey?: string;
|
|
65
|
-
readonly baseUrl?: string;
|
|
66
|
-
};
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export type CodexAuthOptions = CodexAuthenticationMode | LegacyCodexAuthOptions;
|
|
47
|
+
export type CodexAuthenticationMode = HarnessV1Authentication;
|
|
70
48
|
|
|
71
49
|
/**
|
|
72
50
|
* Resolve the environment-variable blob the codex bridge needs. Precedence:
|
|
73
51
|
*
|
|
74
|
-
* 1.
|
|
75
|
-
* 2.
|
|
76
|
-
* 3. Explicit `auth.gateway` — pin to Vercel AI Gateway.
|
|
77
|
-
* 4. Auto-detect from the host process env: gateway first
|
|
52
|
+
* 1. An explicit authentication mode pins the selected route.
|
|
53
|
+
* 2. Auto-detect from the host process env: gateway first
|
|
78
54
|
* (`AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN`), then `CODEX_API_KEY` /
|
|
79
55
|
* `OPENAI_API_KEY`.
|
|
80
56
|
*/
|
|
81
57
|
export function resolveCodexEnv(
|
|
82
|
-
auth:
|
|
58
|
+
auth: CodexAuthenticationMode | undefined,
|
|
83
59
|
processEnv: Record<string, string | undefined> = process.env,
|
|
84
60
|
): Record<string, string> {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (normalizedAuth?.openai) {
|
|
91
|
-
return pickOpenAI({ explicit: normalizedAuth.openai, processEnv });
|
|
61
|
+
const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
|
|
62
|
+
const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
|
|
63
|
+
if (auth === 'direct') {
|
|
64
|
+
return pickOpenAI({ processEnv: authenticationEnvironment });
|
|
92
65
|
}
|
|
66
|
+
|
|
93
67
|
const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
|
|
94
|
-
env:
|
|
68
|
+
env: authenticationEnvironment,
|
|
95
69
|
});
|
|
96
|
-
if (
|
|
97
|
-
return pickGateway({
|
|
98
|
-
explicit: normalizedAuth.gateway,
|
|
99
|
-
gatewayAuthFromEnv,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (gatewayAuthFromEnv.apiKey) {
|
|
103
|
-
return pickGateway({
|
|
104
|
-
explicit: {},
|
|
105
|
-
gatewayAuthFromEnv,
|
|
106
|
-
});
|
|
70
|
+
if (auth === 'ai-gateway' || gatewayAuthFromEnv.apiKey) {
|
|
71
|
+
return pickGateway({ gatewayAuthFromEnv });
|
|
107
72
|
}
|
|
108
|
-
return pickOpenAI({ processEnv });
|
|
73
|
+
return pickOpenAI({ processEnv: authenticationEnvironment });
|
|
109
74
|
}
|
|
110
75
|
|
|
111
76
|
export function resolveCodexAuthenticationMode(
|
|
112
|
-
auth:
|
|
77
|
+
auth: CodexAuthenticationMode | undefined,
|
|
113
78
|
processEnv: Record<string, string | undefined> = process.env,
|
|
114
79
|
): CodexResolvedAuthenticationMode {
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
80
|
+
if (isHarnessAuthenticationEnvironment(auth)) {
|
|
81
|
+
return getAiGatewayAuthFromEnv({ env: auth }).apiKey
|
|
82
|
+
? 'ai-gateway'
|
|
83
|
+
: 'direct';
|
|
117
84
|
}
|
|
118
|
-
if (auth === 'direct'
|
|
85
|
+
if (auth === 'direct') {
|
|
119
86
|
return 'direct';
|
|
120
87
|
}
|
|
121
|
-
if (auth === 'ai-gateway'
|
|
88
|
+
if (auth === 'ai-gateway') {
|
|
122
89
|
return 'ai-gateway';
|
|
123
90
|
}
|
|
124
91
|
return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey
|
|
@@ -126,76 +93,30 @@ export function resolveCodexAuthenticationMode(
|
|
|
126
93
|
: 'direct';
|
|
127
94
|
}
|
|
128
95
|
|
|
129
|
-
function normalizeCodexAuthToLegacyAuth(
|
|
130
|
-
auth: CodexAuthOptions | undefined,
|
|
131
|
-
): LegacyCodexAuthOptions | undefined {
|
|
132
|
-
if (auth == null || auth === 'auto') {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
if (typeof auth === 'string') {
|
|
136
|
-
switch (auth) {
|
|
137
|
-
case 'direct':
|
|
138
|
-
return { openai: {} };
|
|
139
|
-
case 'ai-gateway':
|
|
140
|
-
return { gateway: {} };
|
|
141
|
-
default:
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
console.warn(
|
|
147
|
-
'[codex] Passing an object to auth options is deprecated. Use a string mode ("auto" | "direct" | "ai-gateway") instead, and pass credentials via environment variables.',
|
|
148
|
-
);
|
|
149
|
-
return auth;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function pickOpenAICompatible(
|
|
153
|
-
explicit: NonNullable<LegacyCodexAuthOptions['openaiCompatible']>,
|
|
154
|
-
processEnv: Record<string, string | undefined>,
|
|
155
|
-
): Record<string, string> {
|
|
156
|
-
const env: Record<string, string> = {};
|
|
157
|
-
const apiKey =
|
|
158
|
-
explicit.apiKey ?? processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
|
|
159
|
-
if (apiKey) env.CODEX_API_KEY = apiKey;
|
|
160
|
-
if (explicit.baseUrl) env.OPENAI_BASE_URL = explicit.baseUrl;
|
|
161
|
-
if (explicit.modelProviderName)
|
|
162
|
-
env.CODEX_MODEL_PROVIDER_NAME = explicit.modelProviderName;
|
|
163
|
-
if (explicit.queryParamsJson)
|
|
164
|
-
env.OPENAI_QUERY_PARAMS_JSON = explicit.queryParamsJson;
|
|
165
|
-
return env;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
96
|
function pickOpenAI({
|
|
169
|
-
explicit,
|
|
170
97
|
processEnv,
|
|
171
98
|
}: {
|
|
172
|
-
explicit?: NonNullable<LegacyCodexAuthOptions['openai']>;
|
|
173
99
|
processEnv: Record<string, string | undefined>;
|
|
174
100
|
}): Record<string, string> {
|
|
175
101
|
const env: Record<string, string> = {};
|
|
176
|
-
const apiKey =
|
|
177
|
-
explicit?.apiKey ?? processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
|
|
102
|
+
const apiKey = processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
|
|
178
103
|
if (apiKey) env.CODEX_API_KEY = apiKey;
|
|
179
|
-
const baseUrl =
|
|
104
|
+
const baseUrl = processEnv.OPENAI_BASE_URL;
|
|
180
105
|
if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
|
|
181
|
-
const organization =
|
|
106
|
+
const organization = processEnv.OPENAI_ORGANIZATION;
|
|
182
107
|
if (organization) env.OPENAI_ORGANIZATION = organization;
|
|
183
|
-
const project =
|
|
108
|
+
const project = processEnv.OPENAI_PROJECT;
|
|
184
109
|
if (project) env.OPENAI_PROJECT = project;
|
|
185
110
|
return env;
|
|
186
111
|
}
|
|
187
112
|
|
|
188
113
|
function pickGateway({
|
|
189
|
-
explicit,
|
|
190
114
|
gatewayAuthFromEnv,
|
|
191
115
|
}: {
|
|
192
|
-
explicit: NonNullable<LegacyCodexAuthOptions['gateway']>;
|
|
193
116
|
gatewayAuthFromEnv: ReturnType<typeof getAiGatewayAuthFromEnv>;
|
|
194
117
|
}): Record<string, string> {
|
|
195
|
-
const apiKey =
|
|
196
|
-
const baseUrl = toCodexGatewayBaseUrl(
|
|
197
|
-
explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl,
|
|
198
|
-
);
|
|
118
|
+
const apiKey = gatewayAuthFromEnv.apiKey;
|
|
119
|
+
const baseUrl = toCodexGatewayBaseUrl(gatewayAuthFromEnv.baseUrl);
|
|
199
120
|
const env: Record<string, string> = {};
|
|
200
121
|
if (apiKey) {
|
|
201
122
|
env.AI_GATEWAY_API_KEY = apiKey;
|
|
@@ -27,6 +27,7 @@ export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
|
|
|
27
27
|
// The host sources the id from lifecycle state `data` cached from a prior
|
|
28
28
|
// `agent.detach`.
|
|
29
29
|
resumeThreadId: z.string().optional(),
|
|
30
|
+
restartThread: z.boolean().optional(),
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
export type StartMessage = z.infer<typeof startMessageSchema>;
|
package/src/codex-harness.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
commonTool,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
warnCredentialBrokeringUnavailable,
|
|
37
37
|
waitForBridgeReady,
|
|
38
38
|
writeSkills as writeHarnessSkills,
|
|
39
|
+
type WriteSkillsResult,
|
|
39
40
|
} from '@ai-sdk/harness/utils';
|
|
40
41
|
import {
|
|
41
42
|
type Experimental_SandboxProcess,
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
53
54
|
DEFAULT_OPENAI_BASE_URL,
|
|
54
55
|
resolveCodexAuthenticationMode,
|
|
55
56
|
resolveCodexEnv,
|
|
56
|
-
type
|
|
57
|
+
type CodexAuthenticationMode,
|
|
57
58
|
} from './codex-auth';
|
|
58
59
|
import {
|
|
59
60
|
outboundMessageSchema,
|
|
@@ -66,11 +67,6 @@ import { VERSION } from './version';
|
|
|
66
67
|
type CodexChannel = SandboxChannel<OutboundMessage, InboundMessage>;
|
|
67
68
|
type CodexRespawnStrategy = 'replay' | 'rerun';
|
|
68
69
|
|
|
69
|
-
type WriteSkillsResult = {
|
|
70
|
-
readonly homeDir: string;
|
|
71
|
-
readonly codexHomeDir: string;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
70
|
/*
|
|
75
71
|
* This intentionally is not the latest Codex model. Newer GPT-5.6 models use
|
|
76
72
|
* Responses Lite, which does not expose their code-mode tools as callable
|
|
@@ -89,7 +85,7 @@ const DEFAULT_CODEX_MODEL = 'gpt-5.5';
|
|
|
89
85
|
const CODEX_CLIENT_APP = `ai-sdk/harness-codex/${VERSION}`;
|
|
90
86
|
|
|
91
87
|
export type CodexHarnessSettings = {
|
|
92
|
-
readonly auth?:
|
|
88
|
+
readonly auth?: CodexAuthenticationMode;
|
|
93
89
|
/**
|
|
94
90
|
* Customizes each credential value before it is forwarded into a sandbox
|
|
95
91
|
* process. This does not restrict which credentials the harness adapter can
|
|
@@ -110,6 +106,8 @@ export type CodexHarnessSettings = {
|
|
|
110
106
|
/**
|
|
111
107
|
* OpenAI model id the underlying `codex` CLI should use. Leaving this unset
|
|
112
108
|
* pins the adapter default (`DEFAULT_CODEX_MODEL`).
|
|
109
|
+
*
|
|
110
|
+
* @deprecated Use `model` on `HarnessAgent` instead.
|
|
113
111
|
*/
|
|
114
112
|
readonly model?: string;
|
|
115
113
|
/**
|
|
@@ -189,6 +187,7 @@ const codexBridgeCoordsSchema = z.object({
|
|
|
189
187
|
*/
|
|
190
188
|
const codexResumeStateSchema = z.object({
|
|
191
189
|
threadId: z.string().optional(),
|
|
190
|
+
turnConfigurationFingerprint: z.string().optional(),
|
|
192
191
|
bridge: codexBridgeCoordsSchema.optional(),
|
|
193
192
|
sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
|
|
194
193
|
});
|
|
@@ -206,6 +205,7 @@ export function createCodex(
|
|
|
206
205
|
lifecycleStateSchema: codexResumeStateSchema,
|
|
207
206
|
getBootstrap: getCodexBootstrap,
|
|
208
207
|
doStart: async startOpts => {
|
|
208
|
+
const model = startOpts.model ?? settings.model ?? DEFAULT_CODEX_MODEL;
|
|
209
209
|
if (startOpts.builtinToolFiltering != null) {
|
|
210
210
|
throw new HarnessCapabilityUnsupportedError({
|
|
211
211
|
message:
|
|
@@ -251,6 +251,7 @@ export function createCodex(
|
|
|
251
251
|
isResume && typeof lifecycleState?.data === 'object'
|
|
252
252
|
? (lifecycleState.data as {
|
|
253
253
|
threadId?: unknown;
|
|
254
|
+
turnConfigurationFingerprint?: unknown;
|
|
254
255
|
bridge?: CodexBridgeCoords;
|
|
255
256
|
sandboxCredentialEnvironment?: Record<string, string>;
|
|
256
257
|
})
|
|
@@ -260,6 +261,10 @@ export function createCodex(
|
|
|
260
261
|
typeof resumeThreadId === 'string' && resumeThreadId.length > 0
|
|
261
262
|
? resumeThreadId
|
|
262
263
|
: undefined;
|
|
264
|
+
const turnConfigurationFingerprint =
|
|
265
|
+
typeof resumeData?.turnConfigurationFingerprint === 'string'
|
|
266
|
+
? resumeData.turnConfigurationFingerprint
|
|
267
|
+
: undefined;
|
|
263
268
|
const coords = resumeData?.bridge;
|
|
264
269
|
const authenticationMode = resolveCodexAuthenticationMode(settings.auth);
|
|
265
270
|
const resolvedAuthEnvironment = resolveCodexEnv(settings.auth);
|
|
@@ -315,6 +320,10 @@ export function createCodex(
|
|
|
315
320
|
);
|
|
316
321
|
|
|
317
322
|
const workDir = startOpts.sessionWorkDir;
|
|
323
|
+
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
324
|
+
sandbox: toolSafeSandboxSession,
|
|
325
|
+
abortSignal: startOpts.abortSignal,
|
|
326
|
+
});
|
|
318
327
|
const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
|
|
319
328
|
const bridgeStateDir = `${sessionDataDir}/bridge`;
|
|
320
329
|
const cliShimDir = `${sessionDataDir}/codex`;
|
|
@@ -372,7 +381,7 @@ export function createCodex(
|
|
|
372
381
|
cliShimPath,
|
|
373
382
|
// The live bridge was spawned by another process; no process handle.
|
|
374
383
|
proc: undefined,
|
|
375
|
-
model
|
|
384
|
+
model,
|
|
376
385
|
reasoningEffort: settings.reasoningEffort,
|
|
377
386
|
webSearch: settings.webSearch,
|
|
378
387
|
codexConfig: settings.codexConfig,
|
|
@@ -387,6 +396,9 @@ export function createCodex(
|
|
|
387
396
|
sandboxCredentialEnvironment,
|
|
388
397
|
debug: startOpts.observability?.debug,
|
|
389
398
|
permissionMode: startOpts.permissionMode,
|
|
399
|
+
sandbox: toolSafeSandboxSession,
|
|
400
|
+
sandboxHomeDir,
|
|
401
|
+
turnConfigurationFingerprint,
|
|
390
402
|
});
|
|
391
403
|
} catch {
|
|
392
404
|
// Bridge no longer reachable — recover by respawning below.
|
|
@@ -432,25 +444,11 @@ export function createCodex(
|
|
|
432
444
|
CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
|
|
433
445
|
credentialForwarding: settings.credentialForwarding,
|
|
434
446
|
});
|
|
435
|
-
const codexSkillSetup =
|
|
436
|
-
startOpts.skills && startOpts.skills.length > 0
|
|
437
|
-
? await writeCodexSkills({
|
|
438
|
-
sandbox: toolSafeSandboxSession,
|
|
439
|
-
skills: startOpts.skills,
|
|
440
|
-
abortSignal: startOpts.abortSignal,
|
|
441
|
-
})
|
|
442
|
-
: undefined;
|
|
443
447
|
const env = {
|
|
444
448
|
...forwardedAuthEnvironment,
|
|
445
449
|
AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
|
|
446
450
|
BRIDGE_CHANNEL_TOKEN: token,
|
|
447
451
|
BRIDGE_WS_PORT: String(port),
|
|
448
|
-
...(codexSkillSetup
|
|
449
|
-
? {
|
|
450
|
-
HOME: codexSkillSetup.homeDir,
|
|
451
|
-
CODEX_HOME: codexSkillSetup.codexHomeDir,
|
|
452
|
-
}
|
|
453
|
-
: {}),
|
|
454
452
|
...(respawnStrategy === 'replay'
|
|
455
453
|
? { BRIDGE_REPLAY_FROM_DISK: '1' }
|
|
456
454
|
: {}),
|
|
@@ -537,7 +535,7 @@ export function createCodex(
|
|
|
537
535
|
channel,
|
|
538
536
|
cliShimPath,
|
|
539
537
|
proc,
|
|
540
|
-
model
|
|
538
|
+
model,
|
|
541
539
|
reasoningEffort: settings.reasoningEffort,
|
|
542
540
|
webSearch: settings.webSearch,
|
|
543
541
|
codexConfig: settings.codexConfig,
|
|
@@ -552,6 +550,9 @@ export function createCodex(
|
|
|
552
550
|
sandboxCredentialEnvironment,
|
|
553
551
|
debug: startOpts.observability?.debug,
|
|
554
552
|
permissionMode: startOpts.permissionMode,
|
|
553
|
+
sandbox: toolSafeSandboxSession,
|
|
554
|
+
sandboxHomeDir,
|
|
555
|
+
turnConfigurationFingerprint,
|
|
555
556
|
});
|
|
556
557
|
},
|
|
557
558
|
};
|
|
@@ -624,22 +625,17 @@ async function resolveBridgeEndpoint({
|
|
|
624
625
|
|
|
625
626
|
async function writeCodexSkills({
|
|
626
627
|
sandbox,
|
|
628
|
+
sandboxHomeDir,
|
|
627
629
|
skills,
|
|
628
630
|
abortSignal,
|
|
629
631
|
}: {
|
|
630
632
|
sandbox: SandboxSession;
|
|
633
|
+
sandboxHomeDir: string;
|
|
631
634
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
632
635
|
abortSignal?: AbortSignal;
|
|
633
636
|
}): Promise<WriteSkillsResult> {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
await sandbox.run({
|
|
637
|
-
command: `mkdir -p ${shellQuote(codexHomeDir)}`,
|
|
638
|
-
abortSignal,
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
const rootDir = path.posix.join(homeDir, '.agents', 'skills');
|
|
642
|
-
await writeHarnessSkills({
|
|
637
|
+
const rootDir = path.posix.join(sandboxHomeDir, '.agents', 'skills');
|
|
638
|
+
return writeHarnessSkills({
|
|
643
639
|
sandbox,
|
|
644
640
|
rootDir,
|
|
645
641
|
skills,
|
|
@@ -648,11 +644,6 @@ async function writeCodexSkills({
|
|
|
648
644
|
invalidSkillFilePathMessage: ({ skillName, filePath }) =>
|
|
649
645
|
`Invalid Codex skill file path for ${skillName}: ${filePath}`,
|
|
650
646
|
});
|
|
651
|
-
|
|
652
|
-
return {
|
|
653
|
-
homeDir,
|
|
654
|
-
codexHomeDir,
|
|
655
|
-
};
|
|
656
647
|
}
|
|
657
648
|
|
|
658
649
|
function openWebSocket({
|
|
@@ -708,6 +699,9 @@ function createSession({
|
|
|
708
699
|
sandboxCredentialEnvironment,
|
|
709
700
|
debug,
|
|
710
701
|
permissionMode,
|
|
702
|
+
sandbox,
|
|
703
|
+
sandboxHomeDir,
|
|
704
|
+
turnConfigurationFingerprint,
|
|
711
705
|
}: {
|
|
712
706
|
sessionId: string;
|
|
713
707
|
channel: CodexChannel;
|
|
@@ -729,6 +723,9 @@ function createSession({
|
|
|
729
723
|
sandboxCredentialEnvironment: Record<string, string> | undefined;
|
|
730
724
|
debug: HarnessV1DebugConfig | undefined;
|
|
731
725
|
permissionMode: HarnessV1PermissionMode | undefined;
|
|
726
|
+
sandbox: SandboxSession;
|
|
727
|
+
sandboxHomeDir: string;
|
|
728
|
+
turnConfigurationFingerprint: string | undefined;
|
|
732
729
|
}): HarnessV1Session {
|
|
733
730
|
let stopped = false;
|
|
734
731
|
let stopPromise: Promise<void> | undefined;
|
|
@@ -755,10 +752,55 @@ function createSession({
|
|
|
755
752
|
* can include a thread id even before this process has run a turn.
|
|
756
753
|
*/
|
|
757
754
|
let latestThreadId = resumeThreadId;
|
|
755
|
+
let latestTurnConfigurationFingerprint = turnConfigurationFingerprint;
|
|
758
756
|
channel.on('bridge-thread', msg => {
|
|
759
757
|
latestThreadId = msg.threadId;
|
|
760
758
|
});
|
|
761
759
|
|
|
760
|
+
const synchronizeTurnConfiguration = async ({
|
|
761
|
+
skills,
|
|
762
|
+
instructions,
|
|
763
|
+
tools,
|
|
764
|
+
abortSignal,
|
|
765
|
+
}: {
|
|
766
|
+
skills: ReadonlyArray<HarnessV1Skill>;
|
|
767
|
+
instructions: string | undefined;
|
|
768
|
+
tools: ReadonlyArray<{
|
|
769
|
+
name: string;
|
|
770
|
+
description?: string;
|
|
771
|
+
inputSchema: unknown;
|
|
772
|
+
}>;
|
|
773
|
+
abortSignal?: AbortSignal;
|
|
774
|
+
}): Promise<{ restartThread: boolean }> => {
|
|
775
|
+
const skillsResult = await writeCodexSkills({
|
|
776
|
+
sandbox,
|
|
777
|
+
sandboxHomeDir,
|
|
778
|
+
skills,
|
|
779
|
+
abortSignal,
|
|
780
|
+
});
|
|
781
|
+
const nextFingerprint = fingerprintCodexTurnConfiguration({
|
|
782
|
+
instructions,
|
|
783
|
+
tools,
|
|
784
|
+
});
|
|
785
|
+
const restartThread =
|
|
786
|
+
latestThreadId != null &&
|
|
787
|
+
(skillsResult.changed ||
|
|
788
|
+
(latestTurnConfigurationFingerprint != null &&
|
|
789
|
+
latestTurnConfigurationFingerprint !== nextFingerprint));
|
|
790
|
+
latestTurnConfigurationFingerprint = nextFingerprint;
|
|
791
|
+
if (restartThread) {
|
|
792
|
+
/*
|
|
793
|
+
* `codex exec resume` retains the native thread's original developer
|
|
794
|
+
* instructions and skill catalog. A fresh native thread is therefore
|
|
795
|
+
* required for replacement semantics. Host-tool guidance is framed
|
|
796
|
+
* again because that guidance also belongs to the new native thread.
|
|
797
|
+
*/
|
|
798
|
+
initialPromptGuidanceApplied = false;
|
|
799
|
+
pendingResumeThreadId = undefined;
|
|
800
|
+
}
|
|
801
|
+
return { restartThread };
|
|
802
|
+
};
|
|
803
|
+
|
|
762
804
|
/*
|
|
763
805
|
* Wire the channel into one turn's worth of events and return the control
|
|
764
806
|
* surface. Shared by `doPromptTurn` (which sends a `start` afterwards) and
|
|
@@ -928,16 +970,22 @@ function createSession({
|
|
|
928
970
|
harnessId: 'codex',
|
|
929
971
|
});
|
|
930
972
|
}
|
|
931
|
-
const turn = wireTurn({
|
|
932
|
-
emit: promptOpts.emit,
|
|
933
|
-
abortSignal: promptOpts.abortSignal,
|
|
934
|
-
});
|
|
935
|
-
|
|
936
973
|
const tools = (promptOpts.tools ?? []).map(t => ({
|
|
937
974
|
name: t.name,
|
|
938
975
|
description: t.description,
|
|
939
976
|
inputSchema: t.inputSchema,
|
|
940
977
|
}));
|
|
978
|
+
const { restartThread } = await synchronizeTurnConfiguration({
|
|
979
|
+
skills: promptOpts.skills,
|
|
980
|
+
instructions: promptOpts.instructions,
|
|
981
|
+
tools,
|
|
982
|
+
abortSignal: promptOpts.abortSignal,
|
|
983
|
+
});
|
|
984
|
+
const turn = wireTurn({
|
|
985
|
+
emit: promptOpts.emit,
|
|
986
|
+
abortSignal: promptOpts.abortSignal,
|
|
987
|
+
});
|
|
988
|
+
|
|
941
989
|
let promptText = extractUserText(promptOpts.prompt);
|
|
942
990
|
if (!initialPromptGuidanceApplied) {
|
|
943
991
|
promptText = frameInitialPromptGuidance({
|
|
@@ -972,6 +1020,7 @@ function createSession({
|
|
|
972
1020
|
...(pendingResumeThreadId
|
|
973
1021
|
? { resumeThreadId: pendingResumeThreadId }
|
|
974
1022
|
: {}),
|
|
1023
|
+
...(restartThread ? { restartThread: true } : {}),
|
|
975
1024
|
...(debug ? { debug } : {}),
|
|
976
1025
|
};
|
|
977
1026
|
pendingResumeThreadId = undefined;
|
|
@@ -990,6 +1039,17 @@ function createSession({
|
|
|
990
1039
|
harnessId: 'codex',
|
|
991
1040
|
});
|
|
992
1041
|
}
|
|
1042
|
+
const tools = (continueOpts.tools ?? []).map(t => ({
|
|
1043
|
+
name: t.name,
|
|
1044
|
+
description: t.description,
|
|
1045
|
+
inputSchema: t.inputSchema,
|
|
1046
|
+
}));
|
|
1047
|
+
const { restartThread } = await synchronizeTurnConfiguration({
|
|
1048
|
+
skills: continueOpts.skills,
|
|
1049
|
+
instructions: continueOpts.instructions,
|
|
1050
|
+
tools,
|
|
1051
|
+
abortSignal: continueOpts.abortSignal,
|
|
1052
|
+
});
|
|
993
1053
|
const turn = wireTurn({
|
|
994
1054
|
emit: continueOpts.emit,
|
|
995
1055
|
abortSignal: continueOpts.abortSignal,
|
|
@@ -1021,11 +1081,7 @@ function createSession({
|
|
|
1021
1081
|
* empty text block trips the Anthropic API's `cache_control` rule).
|
|
1022
1082
|
*/
|
|
1023
1083
|
prompt: 'Continue.',
|
|
1024
|
-
tools
|
|
1025
|
-
name: t.name,
|
|
1026
|
-
description: t.description,
|
|
1027
|
-
inputSchema: t.inputSchema,
|
|
1028
|
-
})),
|
|
1084
|
+
tools,
|
|
1029
1085
|
...(continueOpts.responseFormat == null
|
|
1030
1086
|
? {}
|
|
1031
1087
|
: { responseFormat: continueOpts.responseFormat }),
|
|
@@ -1039,6 +1095,7 @@ function createSession({
|
|
|
1039
1095
|
...(mcpServers == null ? {} : { mcpServers }),
|
|
1040
1096
|
...(permissionMode ? { permissionMode } : {}),
|
|
1041
1097
|
...(threadId ? { resumeThreadId: threadId } : {}),
|
|
1098
|
+
...(restartThread ? { restartThread: true } : {}),
|
|
1042
1099
|
...(debug ? { debug } : {}),
|
|
1043
1100
|
}),
|
|
1044
1101
|
);
|
|
@@ -1074,6 +1131,12 @@ function createSession({
|
|
|
1074
1131
|
specificationVersion: 'harness-v1',
|
|
1075
1132
|
data: {
|
|
1076
1133
|
...(latestThreadId ? { threadId: latestThreadId } : {}),
|
|
1134
|
+
...(latestTurnConfigurationFingerprint
|
|
1135
|
+
? {
|
|
1136
|
+
turnConfigurationFingerprint:
|
|
1137
|
+
latestTurnConfigurationFingerprint,
|
|
1138
|
+
}
|
|
1139
|
+
: {}),
|
|
1077
1140
|
...(sandboxCredentialEnvironment == null
|
|
1078
1141
|
? {}
|
|
1079
1142
|
: { sandboxCredentialEnvironment }),
|
|
@@ -1195,6 +1258,12 @@ function createSession({
|
|
|
1195
1258
|
specificationVersion: 'harness-v1',
|
|
1196
1259
|
data: {
|
|
1197
1260
|
...lifecycleData,
|
|
1261
|
+
...(latestTurnConfigurationFingerprint
|
|
1262
|
+
? {
|
|
1263
|
+
turnConfigurationFingerprint:
|
|
1264
|
+
latestTurnConfigurationFingerprint,
|
|
1265
|
+
}
|
|
1266
|
+
: {}),
|
|
1198
1267
|
...(sandboxCredentialEnvironment == null
|
|
1199
1268
|
? {}
|
|
1200
1269
|
: { sandboxCredentialEnvironment }),
|
|
@@ -1224,6 +1293,12 @@ function createSession({
|
|
|
1224
1293
|
specificationVersion: 'harness-v1',
|
|
1225
1294
|
data: {
|
|
1226
1295
|
...(latestThreadId ? { threadId: latestThreadId } : {}),
|
|
1296
|
+
...(latestTurnConfigurationFingerprint
|
|
1297
|
+
? {
|
|
1298
|
+
turnConfigurationFingerprint:
|
|
1299
|
+
latestTurnConfigurationFingerprint,
|
|
1300
|
+
}
|
|
1301
|
+
: {}),
|
|
1227
1302
|
...(sandboxCredentialEnvironment == null
|
|
1228
1303
|
? {}
|
|
1229
1304
|
: { sandboxCredentialEnvironment }),
|
|
@@ -1240,6 +1315,22 @@ function createSession({
|
|
|
1240
1315
|
};
|
|
1241
1316
|
}
|
|
1242
1317
|
|
|
1318
|
+
function fingerprintCodexTurnConfiguration({
|
|
1319
|
+
instructions,
|
|
1320
|
+
tools,
|
|
1321
|
+
}: {
|
|
1322
|
+
instructions: string | undefined;
|
|
1323
|
+
tools: ReadonlyArray<{
|
|
1324
|
+
name: string;
|
|
1325
|
+
description?: string;
|
|
1326
|
+
inputSchema: unknown;
|
|
1327
|
+
}>;
|
|
1328
|
+
}): string {
|
|
1329
|
+
return createHash('sha256')
|
|
1330
|
+
.update(JSON.stringify({ instructions: instructions ?? null, tools }))
|
|
1331
|
+
.digest('hex');
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1243
1334
|
/*
|
|
1244
1335
|
* Frame host-tool relay guidance and the user's text so Codex treats the
|
|
1245
1336
|
* prepended block as operating guidance rather than user prose. Applied only
|
package/src/index.ts
CHANGED
|
@@ -10,4 +10,4 @@ export const codex = createCodex();
|
|
|
10
10
|
export { createCodex } from './codex-harness';
|
|
11
11
|
export { VERSION } from './version';
|
|
12
12
|
export type { CodexHarnessSettings } from './codex-harness';
|
|
13
|
-
export type { CodexAuthenticationMode
|
|
13
|
+
export type { CodexAuthenticationMode } from './codex-auth';
|