@meistrari/agent-core 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/bin/supervisor.ts +116 -0
- package/package.json +42 -3
- package/scripts/build-supervisor-executable.ts +32 -0
- package/src/agents/agent-error-serializer.ts +23 -0
- package/src/agents/agent-event-stream.ts +81 -0
- package/src/agents/agent-id.ts +17 -0
- package/src/agents/agent-operation.ts +11 -0
- package/src/agents/agent-provider.ts +37 -0
- package/src/agents/agent-run.ts +32 -0
- package/src/agents/agent-runtime-error.ts +161 -0
- package/src/agents/agent-session-events.ts +32 -0
- package/src/agents/agent-tool-runner.ts +83 -0
- package/src/agents/agent-tool.ts +111 -0
- package/src/agents/author-context.ts +36 -0
- package/src/agents/claude/claude-command-mapper.ts +234 -0
- package/src/agents/claude/claude-event-mapper.ts +736 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +464 -0
- package/src/agents/claude/claude-tool-mapper.ts +186 -0
- package/src/agents/claude/index.ts +1 -0
- package/src/agents/codex/codex-auth.ts +34 -0
- package/src/agents/codex/codex-command-mapper.ts +78 -0
- package/src/agents/codex/codex-event-mapper.ts +708 -0
- package/src/agents/codex/codex-json-rpc-client.ts +326 -0
- package/src/agents/codex/codex-protocol.ts +36 -0
- package/src/agents/codex/codex-provider.ts +1050 -0
- package/src/agents/codex/codex-run.ts +404 -0
- package/src/agents/codex/codex-skill-catalog.ts +158 -0
- package/src/agents/codex/codex-skill-roots.ts +19 -0
- package/src/agents/codex/codex-tool-mapper.ts +55 -0
- package/src/agents/codex/codex.errors.ts +68 -0
- package/src/agents/codex/generated/meta.gen.ts +606 -0
- package/src/agents/codex/generated/namespaces.gen.ts +311 -0
- package/src/agents/codex/generated/schema.gen.ts +34883 -0
- package/src/agents/codex/index.ts +2 -0
- package/src/agents/index.ts +14 -0
- package/src/agents/input-attachment-preparation.ts +86 -0
- package/src/agents/input-attachment.errors.ts +16 -0
- package/src/agents/instructions.ts +8 -0
- package/src/agents/materialized-input-attachment.ts +26 -0
- package/src/agents/message-id.ts +23 -0
- package/src/agents/normalize.ts +8 -0
- package/src/agents/sandbox-environment.ts +1 -0
- package/src/agents/tools/ping.tool.ts +13 -0
- package/src/agents/user-input-request.ts +470 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +125 -0
- package/src/supervisor/config.ts +49 -0
- package/src/supervisor/control-authority-verifier.ts +135 -0
- package/src/supervisor/create-supervisor-runtime.ts +25 -0
- package/src/supervisor/errors.ts +24 -0
- package/src/supervisor/index.ts +34 -0
- package/src/supervisor/persistence/json.ts +21 -0
- package/src/supervisor/persistence/state-discovery.ts +56 -0
- package/src/supervisor/persistence/supervisor-store.ts +364 -0
- package/src/supervisor/ports/index.ts +109 -0
- package/src/supervisor/provider-factory.ts +37 -0
- package/src/supervisor/resident.ts +143 -0
- package/src/supervisor/rpc-client.ts +120 -0
- package/src/supervisor/runtime-handler.ts +309 -0
- package/src/supervisor/websocket-server.ts +434 -0
- package/src/supervisor-protocol/bootstrap.ts +1 -1
- package/src/template-onboarding.ts +47 -0
- package/src/testing/es256-test-keys.ts +73 -0
- package/src/testing/in-memory-runtime-control-plane.ts +205 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/loopback-supervisor-connection.ts +71 -0
- package/src/testing/scripted-provider.ts +64 -0
- package/src/worker-runtime-client/command-pump.ts +132 -0
- package/src/worker-runtime-client/connection-attempt.ts +340 -0
- package/src/worker-runtime-client/control-authority-signer.ts +100 -0
- package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
- package/src/worker-runtime-client/frame-processor.ts +178 -0
- package/src/worker-runtime-client/index.ts +27 -0
- package/src/worker-runtime-client/lease-reconciler.ts +14 -0
- package/src/worker-runtime-client/ports.ts +137 -0
- package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
- package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
- package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +300 -0
- package/src/worker-runtime-client/token-crypto.ts +46 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import type { AgentReasoningEffort, AgentToolDefinition, CodexModelId } from '../../protocol'
|
|
2
|
+
import type { AgentOperationOptions } from '../agent-operation'
|
|
3
|
+
import type { AgentProvider, AgentSessionOpenInput, AgentSessionResumeInput } from '../agent-provider'
|
|
4
|
+
import type { AgentTool } from '../agent-tool'
|
|
5
|
+
import type { InstructionComposer } from '../instructions'
|
|
6
|
+
import type { CodexAuthentication, CodexAuthProvider, CodexAuthRefreshInput, CodexAuthTokens } from './codex-auth'
|
|
7
|
+
import type { CodexEventMapperState, CodexNotification } from './codex-event-mapper'
|
|
8
|
+
import type { CodexJsonRpcMessage, CodexServerRequest } from './codex-json-rpc-client'
|
|
9
|
+
import type { ClientRequestResponsesByMethod, CodexChatGPTAuthTokensRefreshParams } from './codex-protocol'
|
|
10
|
+
import { randomUUID } from 'node:crypto'
|
|
11
|
+
import { lstat, open, readFile, realpath, rename, unlink } from 'node:fs/promises'
|
|
12
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
13
|
+
import { codexModelIdSchema } from '../../protocol'
|
|
14
|
+
import { serializeAgentError } from '../agent-error-serializer'
|
|
15
|
+
import { AgentNotAcceptedError, AgentProviderMetadataError, AgentProviderProtocolError, AgentProviderRuntimeError } from '../agent-runtime-error'
|
|
16
|
+
import { sessionConfiguredEvent } from '../agent-session-events'
|
|
17
|
+
import { selectAgentTools, toolByName } from '../agent-tool'
|
|
18
|
+
import { emptyInstructionComposer } from '../instructions'
|
|
19
|
+
import { UserInputRequestStore, userInputRequestStoreRootFromTranscriptPath } from '../user-input-request'
|
|
20
|
+
import { cacheCodexThreadMetadata, codexNotificationFromMessage, createCodexEventMapperState, mapCodexNotification } from './codex-event-mapper'
|
|
21
|
+
import { CodexJsonRpcClient, isCodexServerRequest } from './codex-json-rpc-client'
|
|
22
|
+
import { CodexRun } from './codex-run'
|
|
23
|
+
import { catalogRepositorySkillRoots, stableRepositoryRoots, workspaceSkillRoots } from './codex-skill-roots'
|
|
24
|
+
import { runCodexDynamicTool, toCodexDynamicTools } from './codex-tool-mapper'
|
|
25
|
+
|
|
26
|
+
export interface CodexStartupStageFact {
|
|
27
|
+
event: 'sandbox.startup-stage'
|
|
28
|
+
stage: 'codex-initialize' | 'auth-acquisition-login' | 'thread-start' | 'thread-resume'
|
|
29
|
+
outcome: 'success' | 'failure'
|
|
30
|
+
duration: number
|
|
31
|
+
sessionId: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CodexProviderConfig {
|
|
35
|
+
auth: CodexAuthProvider | CodexAuthentication
|
|
36
|
+
tools?: readonly AgentTool[]
|
|
37
|
+
executable?: string
|
|
38
|
+
environment?: Record<string, string | undefined>
|
|
39
|
+
onStartupStage?: (fact: CodexStartupStageFact) => void
|
|
40
|
+
instructions?: InstructionComposer
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CodexRunHolder {
|
|
44
|
+
toolsByName: Map<string, AgentTool>
|
|
45
|
+
run?: CodexRun
|
|
46
|
+
authTokens?: CodexAuthTokens
|
|
47
|
+
startupFailure?: Error
|
|
48
|
+
}
|
|
49
|
+
type CodexAuthMaterial
|
|
50
|
+
= | { kind: 'chatgpt', tokens: CodexAuthTokens }
|
|
51
|
+
| { kind: 'api-key', apiKey: string }
|
|
52
|
+
interface CodexConnection {
|
|
53
|
+
client: CodexJsonRpcClient
|
|
54
|
+
abortController: AbortController
|
|
55
|
+
state: CodexEventMapperState
|
|
56
|
+
}
|
|
57
|
+
interface StartupTaskResult<Source extends 'connection' | 'auth', Value> {
|
|
58
|
+
source: Source
|
|
59
|
+
result: PromiseSettledResult<Value>
|
|
60
|
+
}
|
|
61
|
+
type CodexThreadBootstrapResult
|
|
62
|
+
= | ClientRequestResponsesByMethod['thread/start']
|
|
63
|
+
| ClientRequestResponsesByMethod['thread/resume']
|
|
64
|
+
interface CodexSessionBootstrapConfig {
|
|
65
|
+
sessionId: string
|
|
66
|
+
cwd: string
|
|
67
|
+
workspaceRepositoryRoots: readonly string[]
|
|
68
|
+
model?: string
|
|
69
|
+
reasoningEffort?: AgentReasoningEffort
|
|
70
|
+
tools?: AgentToolDefinition[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class CodexProvider implements AgentProvider<'codex'> {
|
|
74
|
+
readonly id = 'codex' as const
|
|
75
|
+
private readonly tools: readonly AgentTool[]
|
|
76
|
+
private readonly executable: string
|
|
77
|
+
private readonly environment: Record<string, string | undefined>
|
|
78
|
+
private readonly auth: CodexAuthentication
|
|
79
|
+
private readonly onStartupStage: ((fact: CodexStartupStageFact) => void) | undefined
|
|
80
|
+
private readonly instructions: InstructionComposer
|
|
81
|
+
|
|
82
|
+
private constructor(config: CodexProviderConfig) {
|
|
83
|
+
this.auth = normalizeCodexAuthentication(config.auth)
|
|
84
|
+
this.tools = config.tools ?? []
|
|
85
|
+
this.executable = config.executable ?? 'codex'
|
|
86
|
+
this.environment = config.environment ?? {}
|
|
87
|
+
this.onStartupStage = config.onStartupStage
|
|
88
|
+
this.instructions = config.instructions ?? emptyInstructionComposer
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static create(config: CodexProviderConfig): CodexProvider {
|
|
92
|
+
return new CodexProvider(config)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async openSession(input: AgentSessionOpenInput<'codex'>, options: AgentOperationOptions = {}): Promise<CodexRun> {
|
|
96
|
+
return await this.startSession({
|
|
97
|
+
config: input,
|
|
98
|
+
options,
|
|
99
|
+
stage: 'thread-start',
|
|
100
|
+
requestThread: async ({ client, tools }): Promise<CodexThreadBootstrapResult> => await client.request('thread/start', {
|
|
101
|
+
cwd: input.cwd,
|
|
102
|
+
model: input.model,
|
|
103
|
+
modelProvider: this.auth.kind === 'api-key' ? this.auth.modelProvider : undefined,
|
|
104
|
+
ephemeral: false,
|
|
105
|
+
approvalPolicy: 'never',
|
|
106
|
+
sandbox: 'danger-full-access',
|
|
107
|
+
developerInstructions: this.instructions({ provider: 'codex', tools }),
|
|
108
|
+
dynamicTools: toCodexDynamicTools(tools),
|
|
109
|
+
}),
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async resumeSession(input: AgentSessionResumeInput<'codex'>, options: AgentOperationOptions = {}): Promise<CodexRun> {
|
|
114
|
+
return await this.startSession({
|
|
115
|
+
config: input,
|
|
116
|
+
options,
|
|
117
|
+
stage: 'thread-resume',
|
|
118
|
+
requestThread: async ({ client, tools }): Promise<CodexThreadBootstrapResult> => await client.request('thread/resume', {
|
|
119
|
+
threadId: input.providerSessionId,
|
|
120
|
+
cwd: input.cwd,
|
|
121
|
+
approvalPolicy: 'never',
|
|
122
|
+
sandbox: 'danger-full-access',
|
|
123
|
+
model: input.model,
|
|
124
|
+
modelProvider: this.auth.kind === 'api-key' ? this.auth.modelProvider : undefined,
|
|
125
|
+
developerInstructions: this.instructions({ provider: 'codex', tools }),
|
|
126
|
+
}),
|
|
127
|
+
validateThread: (result) => {
|
|
128
|
+
// Resume must rebind the same provider session; adopting a different thread would silently
|
|
129
|
+
// divorce the durable binding from the transcript the user expects.
|
|
130
|
+
if (result.thread?.id && result.thread.id !== input.providerSessionId) {
|
|
131
|
+
throw new AgentProviderProtocolError({
|
|
132
|
+
message: 'Codex thread/resume returned a different thread id than requested.',
|
|
133
|
+
details: { expected: input.providerSessionId, actual: result.thread.id },
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async startSession(input: {
|
|
141
|
+
config: CodexSessionBootstrapConfig
|
|
142
|
+
options: AgentOperationOptions
|
|
143
|
+
stage: 'thread-start' | 'thread-resume'
|
|
144
|
+
requestThread: (context: {
|
|
145
|
+
client: CodexJsonRpcClient
|
|
146
|
+
tools: readonly AgentTool[]
|
|
147
|
+
}) => Promise<CodexThreadBootstrapResult>
|
|
148
|
+
validateThread?: (result: CodexThreadBootstrapResult) => void
|
|
149
|
+
}): Promise<CodexRun> {
|
|
150
|
+
input.options.signal?.throwIfAborted()
|
|
151
|
+
const tools = selectAgentTools(this.tools, input.config.tools)
|
|
152
|
+
const holder: CodexRunHolder = { toolsByName: toolByName(tools) }
|
|
153
|
+
const authStartedAt = performance.now()
|
|
154
|
+
let authObserved = false
|
|
155
|
+
const instructionBridge = this.prepareSessionBootstrap({
|
|
156
|
+
cwd: input.config.cwd,
|
|
157
|
+
workspaceRepositoryRoots: input.config.workspaceRepositoryRoots,
|
|
158
|
+
startupSignal: input.options.signal,
|
|
159
|
+
})
|
|
160
|
+
void instructionBridge.catch(() => undefined)
|
|
161
|
+
const { client, abortController, state, authMaterial } = await this.openConnectionWithAuth({
|
|
162
|
+
sessionId: input.config.sessionId,
|
|
163
|
+
cwd: input.config.cwd,
|
|
164
|
+
options: input.options,
|
|
165
|
+
holder,
|
|
166
|
+
}).catch(async (error: unknown) => {
|
|
167
|
+
await instructionBridge.catch(() => undefined)
|
|
168
|
+
this.observeStartupStage({
|
|
169
|
+
stage: 'auth-acquisition-login',
|
|
170
|
+
outcome: 'failure',
|
|
171
|
+
startedAt: authStartedAt,
|
|
172
|
+
sessionId: input.config.sessionId,
|
|
173
|
+
})
|
|
174
|
+
authObserved = true
|
|
175
|
+
throw error
|
|
176
|
+
})
|
|
177
|
+
const removeBootstrapAbort = this.bindBootstrapAbort({
|
|
178
|
+
signal: input.options.signal,
|
|
179
|
+
client,
|
|
180
|
+
abortController,
|
|
181
|
+
})
|
|
182
|
+
try {
|
|
183
|
+
input.options.signal?.throwIfAborted()
|
|
184
|
+
await this.login({ client, holder, authMaterial })
|
|
185
|
+
this.observeStartupStage({
|
|
186
|
+
stage: 'auth-acquisition-login',
|
|
187
|
+
outcome: 'success',
|
|
188
|
+
startedAt: authStartedAt,
|
|
189
|
+
sessionId: input.config.sessionId,
|
|
190
|
+
})
|
|
191
|
+
authObserved = true
|
|
192
|
+
const instructionBridgePath = await instructionBridge
|
|
193
|
+
const result = await this.runStartupStage({
|
|
194
|
+
stage: input.stage,
|
|
195
|
+
sessionId: input.config.sessionId,
|
|
196
|
+
operation: async () => await input.requestThread({ client, tools }),
|
|
197
|
+
})
|
|
198
|
+
input.options.signal?.throwIfAborted()
|
|
199
|
+
input.validateThread?.(result)
|
|
200
|
+
|
|
201
|
+
return await this.bootstrapRun({
|
|
202
|
+
config: input.config,
|
|
203
|
+
result,
|
|
204
|
+
client,
|
|
205
|
+
abortController,
|
|
206
|
+
state,
|
|
207
|
+
holder,
|
|
208
|
+
instructionBridgePath,
|
|
209
|
+
startupSignal: input.options.signal,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (!authObserved) {
|
|
214
|
+
this.observeStartupStage({
|
|
215
|
+
stage: 'auth-acquisition-login',
|
|
216
|
+
outcome: 'failure',
|
|
217
|
+
startedAt: authStartedAt,
|
|
218
|
+
sessionId: input.config.sessionId,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
await stopCodexClientBestEffort(client)
|
|
222
|
+
input.options.signal?.throwIfAborted()
|
|
223
|
+
throw holder.startupFailure ?? error
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
removeBootstrapAbort()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async prepareSessionBootstrap(input: {
|
|
231
|
+
cwd: string
|
|
232
|
+
workspaceRepositoryRoots: readonly string[]
|
|
233
|
+
startupSignal?: AbortSignal
|
|
234
|
+
}): Promise<string> {
|
|
235
|
+
input.startupSignal?.throwIfAborted()
|
|
236
|
+
const instructionBridgePath = await reconcileSelectedRepositoryInstructionBridge({
|
|
237
|
+
cwd: input.cwd,
|
|
238
|
+
repositoryRoots: input.workspaceRepositoryRoots,
|
|
239
|
+
})
|
|
240
|
+
input.startupSignal?.throwIfAborted()
|
|
241
|
+
return instructionBridgePath
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async attestSessionBootstrap(input: {
|
|
245
|
+
holder: CodexRunHolder
|
|
246
|
+
instructionBridgePath: string
|
|
247
|
+
instructionSources?: readonly string[]
|
|
248
|
+
workspaceRepositoryRoots: readonly string[]
|
|
249
|
+
startupSignal?: AbortSignal
|
|
250
|
+
}): Promise<void> {
|
|
251
|
+
throwIfStartupFailed(input.holder)
|
|
252
|
+
await requireSelectedRepositoryInstructionSource({
|
|
253
|
+
instructionBridgePath: input.instructionBridgePath,
|
|
254
|
+
instructionSources: input.instructionSources,
|
|
255
|
+
repositoryRoots: input.workspaceRepositoryRoots,
|
|
256
|
+
})
|
|
257
|
+
input.startupSignal?.throwIfAborted()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async openConnectionWithAuth(input: {
|
|
261
|
+
sessionId: string
|
|
262
|
+
cwd: string
|
|
263
|
+
options: AgentOperationOptions
|
|
264
|
+
holder: CodexRunHolder
|
|
265
|
+
}): Promise<CodexConnection & { authMaterial: CodexAuthMaterial }> {
|
|
266
|
+
const authAbortController = new AbortController()
|
|
267
|
+
const connectionAbortController = new AbortController()
|
|
268
|
+
const abortStartup = () => {
|
|
269
|
+
authAbortController.abort(input.options.signal?.reason)
|
|
270
|
+
connectionAbortController.abort(input.options.signal?.reason)
|
|
271
|
+
}
|
|
272
|
+
if (input.options.signal?.aborted)
|
|
273
|
+
abortStartup()
|
|
274
|
+
else
|
|
275
|
+
input.options.signal?.addEventListener('abort', abortStartup, { once: true })
|
|
276
|
+
|
|
277
|
+
const connectionTask = this.openConnection({
|
|
278
|
+
...input,
|
|
279
|
+
startupSignal: connectionAbortController.signal,
|
|
280
|
+
}).then(
|
|
281
|
+
(value): StartupTaskResult<'connection', CodexConnection> => ({ source: 'connection', result: { status: 'fulfilled', value } }),
|
|
282
|
+
(reason): StartupTaskResult<'connection', CodexConnection> => ({ source: 'connection', result: { status: 'rejected', reason } }),
|
|
283
|
+
)
|
|
284
|
+
const authTask = this.getAuthMaterial(authAbortController.signal).then(
|
|
285
|
+
(value): StartupTaskResult<'auth', CodexAuthMaterial> => ({ source: 'auth', result: { status: 'fulfilled', value } }),
|
|
286
|
+
(reason): StartupTaskResult<'auth', CodexAuthMaterial> => ({ source: 'auth', result: { status: 'rejected', reason } }),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const first = await Promise.race([connectionTask, authTask])
|
|
291
|
+
if (first.source === 'connection') {
|
|
292
|
+
if (first.result.status === 'rejected') {
|
|
293
|
+
authAbortController.abort(first.result.reason)
|
|
294
|
+
throw first.result.reason
|
|
295
|
+
}
|
|
296
|
+
const auth = await authTask
|
|
297
|
+
if (auth.result.status === 'rejected') {
|
|
298
|
+
await stopCodexClientBestEffort(first.result.value.client)
|
|
299
|
+
throw auth.result.reason
|
|
300
|
+
}
|
|
301
|
+
return { ...first.result.value, authMaterial: auth.result.value }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (first.result.status === 'fulfilled') {
|
|
305
|
+
const connection = await connectionTask
|
|
306
|
+
if (connection.result.status === 'rejected')
|
|
307
|
+
throw connection.result.reason
|
|
308
|
+
return { ...connection.result.value, authMaterial: first.result.value }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
connectionAbortController.abort(first.result.reason)
|
|
312
|
+
const interruptedConnection = await connectionTask
|
|
313
|
+
if (interruptedConnection.result.status === 'fulfilled')
|
|
314
|
+
await stopCodexClientBestEffort(interruptedConnection.result.value.client)
|
|
315
|
+
throw first.result.reason
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
input.options.signal?.removeEventListener('abort', abortStartup)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private async getAuthMaterial(signal: AbortSignal): Promise<CodexAuthMaterial> {
|
|
323
|
+
if (this.auth.kind === 'api-key') {
|
|
324
|
+
const credentials = await this.auth.provider.getApiKey({ signal })
|
|
325
|
+
return { kind: 'api-key', apiKey: credentials.apiKey }
|
|
326
|
+
}
|
|
327
|
+
const tokens = await this.auth.provider.getAuthTokens({ signal })
|
|
328
|
+
return { kind: 'chatgpt', tokens }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async openConnection(input: {
|
|
332
|
+
sessionId: string
|
|
333
|
+
cwd: string
|
|
334
|
+
options: AgentOperationOptions
|
|
335
|
+
holder: CodexRunHolder
|
|
336
|
+
startupSignal: AbortSignal
|
|
337
|
+
}): Promise<CodexConnection> {
|
|
338
|
+
const { cwd, holder, startupSignal } = input
|
|
339
|
+
const state = createCodexEventMapperState()
|
|
340
|
+
const abortController = new AbortController()
|
|
341
|
+
const initializeStartedAt = performance.now()
|
|
342
|
+
let initializeObserved = false
|
|
343
|
+
let messageQueue = Promise.resolve()
|
|
344
|
+
let client: CodexJsonRpcClient
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
client = new CodexJsonRpcClient(this.executable, {
|
|
348
|
+
cwd,
|
|
349
|
+
environment: this.environment,
|
|
350
|
+
onMessage: (message) => {
|
|
351
|
+
messageQueue = this.enqueueMessageHandling(messageQueue, { message, holder, state, client, signal: abortController.signal })
|
|
352
|
+
},
|
|
353
|
+
onInvalidLine: (line, error) => holder.run?.pushEvent({ type: 'error', payload: { message: `Invalid Codex JSON-RPC line: ${line.slice(0, 200)}`, fatal: false, source: 'transport', error: serializeAgentError(error) } }),
|
|
354
|
+
onStderr: line => holder.run?.pushEvent({ type: 'error', payload: { message: line, fatal: false, source: 'provider' } }),
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
this.observeStartupStage({
|
|
359
|
+
stage: 'codex-initialize',
|
|
360
|
+
outcome: 'failure',
|
|
361
|
+
startedAt: initializeStartedAt,
|
|
362
|
+
sessionId: input.sessionId,
|
|
363
|
+
})
|
|
364
|
+
throw error
|
|
365
|
+
}
|
|
366
|
+
const stopForStartupAbort = () => {
|
|
367
|
+
abortController.abort(startupSignal.reason)
|
|
368
|
+
void stopCodexClientBestEffort(client)
|
|
369
|
+
}
|
|
370
|
+
if (startupSignal.aborted)
|
|
371
|
+
stopForStartupAbort()
|
|
372
|
+
else
|
|
373
|
+
startupSignal.addEventListener('abort', stopForStartupAbort, { once: true })
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
await client.request('initialize', {
|
|
377
|
+
clientInfo: { name: 'coding-agent', title: 'Coding Agent', version: '0.1.0' },
|
|
378
|
+
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
379
|
+
})
|
|
380
|
+
client.notify('initialized', undefined)
|
|
381
|
+
await yieldToPendingServerRequests()
|
|
382
|
+
await messageQueue
|
|
383
|
+
throwIfStartupFailed(holder)
|
|
384
|
+
this.observeStartupStage({
|
|
385
|
+
stage: 'codex-initialize',
|
|
386
|
+
outcome: 'success',
|
|
387
|
+
startedAt: initializeStartedAt,
|
|
388
|
+
sessionId: input.sessionId,
|
|
389
|
+
})
|
|
390
|
+
initializeObserved = true
|
|
391
|
+
|
|
392
|
+
return { client, abortController, state }
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (!initializeObserved) {
|
|
396
|
+
this.observeStartupStage({
|
|
397
|
+
stage: 'codex-initialize',
|
|
398
|
+
outcome: 'failure',
|
|
399
|
+
startedAt: initializeStartedAt,
|
|
400
|
+
sessionId: input.sessionId,
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
await stopCodexClientBestEffort(client)
|
|
404
|
+
startupSignal.throwIfAborted()
|
|
405
|
+
throw error
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
startupSignal.removeEventListener('abort', stopForStartupAbort)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private async runStartupStage<T>(input: {
|
|
413
|
+
stage: CodexStartupStageFact['stage']
|
|
414
|
+
sessionId: string
|
|
415
|
+
operation: () => Promise<T>
|
|
416
|
+
}): Promise<T> {
|
|
417
|
+
const startedAt = performance.now()
|
|
418
|
+
try {
|
|
419
|
+
const result = await input.operation()
|
|
420
|
+
this.observeStartupStage({ ...input, outcome: 'success', startedAt })
|
|
421
|
+
return result
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
this.observeStartupStage({ ...input, outcome: 'failure', startedAt })
|
|
425
|
+
throw error
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private observeStartupStage(input: {
|
|
430
|
+
stage: CodexStartupStageFact['stage']
|
|
431
|
+
outcome: CodexStartupStageFact['outcome']
|
|
432
|
+
startedAt: number
|
|
433
|
+
sessionId: string
|
|
434
|
+
}): void {
|
|
435
|
+
try {
|
|
436
|
+
this.onStartupStage?.({
|
|
437
|
+
event: 'sandbox.startup-stage',
|
|
438
|
+
stage: input.stage,
|
|
439
|
+
outcome: input.outcome,
|
|
440
|
+
duration: Math.max(0, Math.round(performance.now() - input.startedAt)),
|
|
441
|
+
sessionId: input.sessionId,
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Startup telemetry is best effort and cannot alter provider behavior.
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private bindBootstrapAbort(input: {
|
|
450
|
+
signal?: AbortSignal
|
|
451
|
+
client: CodexJsonRpcClient
|
|
452
|
+
abortController: AbortController
|
|
453
|
+
}): () => void {
|
|
454
|
+
const abortBootstrap = () => {
|
|
455
|
+
input.abortController.abort(input.signal?.reason)
|
|
456
|
+
void stopCodexClientBestEffort(input.client)
|
|
457
|
+
}
|
|
458
|
+
if (input.signal?.aborted)
|
|
459
|
+
abortBootstrap()
|
|
460
|
+
else
|
|
461
|
+
input.signal?.addEventListener('abort', abortBootstrap, { once: true })
|
|
462
|
+
|
|
463
|
+
return () => input.signal?.removeEventListener('abort', abortBootstrap)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async bootstrapRun(input: {
|
|
467
|
+
config: CodexSessionBootstrapConfig
|
|
468
|
+
result: CodexThreadBootstrapResult
|
|
469
|
+
client: CodexJsonRpcClient
|
|
470
|
+
abortController: AbortController
|
|
471
|
+
state: CodexEventMapperState
|
|
472
|
+
holder: CodexRunHolder
|
|
473
|
+
instructionBridgePath: string
|
|
474
|
+
startupSignal?: AbortSignal
|
|
475
|
+
}): Promise<CodexRun> {
|
|
476
|
+
const { config, result, client, abortController, state, holder, instructionBridgePath, startupSignal } = input
|
|
477
|
+
const thread = result.thread
|
|
478
|
+
const effectiveModel = result.model ?? config.model
|
|
479
|
+
if (!thread?.id || !thread.path || !effectiveModel) {
|
|
480
|
+
throw new AgentProviderMetadataError({
|
|
481
|
+
message: 'Codex did not return required persistent thread metadata.',
|
|
482
|
+
details: { provider: 'codex' },
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const metadata = {
|
|
487
|
+
provider: 'codex' as const,
|
|
488
|
+
sessionId: config.sessionId,
|
|
489
|
+
providerSessionId: thread.id,
|
|
490
|
+
cwd: thread.cwd ?? config.cwd,
|
|
491
|
+
model: firstCodexModelId(effectiveModel),
|
|
492
|
+
transcriptPath: thread.path,
|
|
493
|
+
}
|
|
494
|
+
throwIfStartupFailed(holder)
|
|
495
|
+
const [, userInputRequests] = await Promise.all([
|
|
496
|
+
this.attestSessionBootstrap({
|
|
497
|
+
holder,
|
|
498
|
+
instructionBridgePath,
|
|
499
|
+
instructionSources: result.instructionSources,
|
|
500
|
+
workspaceRepositoryRoots: config.workspaceRepositoryRoots,
|
|
501
|
+
startupSignal,
|
|
502
|
+
}),
|
|
503
|
+
UserInputRequestStore.open({
|
|
504
|
+
sessionId: metadata.sessionId,
|
|
505
|
+
rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
506
|
+
}),
|
|
507
|
+
])
|
|
508
|
+
startupSignal?.throwIfAborted()
|
|
509
|
+
const run = new CodexRun({
|
|
510
|
+
metadata,
|
|
511
|
+
reasoningEffort: config.reasoningEffort,
|
|
512
|
+
client,
|
|
513
|
+
requestedCwd: config.cwd,
|
|
514
|
+
workspaceRepositoryRoots: config.workspaceRepositoryRoots,
|
|
515
|
+
registeredSkillRoots: workspaceSkillRoots(config.workspaceRepositoryRoots),
|
|
516
|
+
repositorySkillRoots: catalogRepositorySkillRoots({
|
|
517
|
+
repositoryRoots: config.workspaceRepositoryRoots,
|
|
518
|
+
requestedCwd: config.cwd,
|
|
519
|
+
runtimeCwd: metadata.cwd,
|
|
520
|
+
}),
|
|
521
|
+
reconcileInstructionBridge: async (repositoryRoots) => {
|
|
522
|
+
await reconcileSelectedRepositoryInstructionBridge({ cwd: config.cwd, repositoryRoots })
|
|
523
|
+
},
|
|
524
|
+
userInputRequests,
|
|
525
|
+
eventMapperState: state,
|
|
526
|
+
abortSignal: abortController.signal,
|
|
527
|
+
onStop: () => abortController.abort(),
|
|
528
|
+
})
|
|
529
|
+
holder.run = run
|
|
530
|
+
|
|
531
|
+
run.pushEvent({ type: 'session.started', payload: run.metadata })
|
|
532
|
+
run.pushEvent(sessionConfiguredEvent({ model: effectiveModel, cwd: run.metadata.cwd, reasoningEffort: config.reasoningEffort, tools: config.tools, environment: this.environment }))
|
|
533
|
+
run.pushEvent({ type: 'session.state.changed', payload: { state: 'ready' } })
|
|
534
|
+
return run
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private async enqueueMessageHandling(queue: Promise<void>, input: {
|
|
538
|
+
message: CodexJsonRpcMessage
|
|
539
|
+
holder: CodexRunHolder
|
|
540
|
+
state: CodexEventMapperState
|
|
541
|
+
client: CodexJsonRpcClient
|
|
542
|
+
signal: AbortSignal
|
|
543
|
+
}): Promise<void> {
|
|
544
|
+
return await queue
|
|
545
|
+
.then(async () => await this.handleMessage(input))
|
|
546
|
+
.catch((error: unknown) => {
|
|
547
|
+
input.holder.run?.pushEvent({ type: 'error', payload: { message: 'Codex app-server message handling failed.', fatal: false, source: 'runtime', error: serializeAgentError(error) } })
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private async handleMessage(input: {
|
|
552
|
+
message: CodexJsonRpcMessage
|
|
553
|
+
holder: CodexRunHolder
|
|
554
|
+
state: CodexEventMapperState
|
|
555
|
+
client: CodexJsonRpcClient
|
|
556
|
+
signal: AbortSignal
|
|
557
|
+
}): Promise<void> {
|
|
558
|
+
const { message, holder, state, client, signal } = input
|
|
559
|
+
const run = holder.run
|
|
560
|
+
if (isCodexServerRequest(message)) {
|
|
561
|
+
const handling = this.handleServerRequest(message, holder, client, signal)
|
|
562
|
+
// Startup requests must flush their response before start/resume surfaces the failure.
|
|
563
|
+
if (!run)
|
|
564
|
+
await handling
|
|
565
|
+
else
|
|
566
|
+
void handling
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
if (!run)
|
|
570
|
+
return
|
|
571
|
+
const notification = codexNotificationFromMessage(message)
|
|
572
|
+
if (!notification)
|
|
573
|
+
return
|
|
574
|
+
if (notification.method === 'skills/changed') {
|
|
575
|
+
run.requestSkillsRefresh()
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
// Child-thread notifications must reach the mapper, which buffers them until spawn correlation.
|
|
579
|
+
await this.cacheSpawnedSubagentThreadMetadata({ notification, state, client })
|
|
580
|
+
const drafts = mapCodexNotification(notification, state)
|
|
581
|
+
for (const draft of drafts) {
|
|
582
|
+
run.pushEvent(draft)
|
|
583
|
+
}
|
|
584
|
+
if (notification.method === 'turn/started' && notification.params.threadId === run.metadata.providerSessionId) {
|
|
585
|
+
run.setActiveTurn(notification.params.turn.id)
|
|
586
|
+
run.pushEvent({ type: 'session.state.changed', payload: { state: 'running' } })
|
|
587
|
+
}
|
|
588
|
+
if (notification.method === 'turn/completed' && notification.params.threadId === run.metadata.providerSessionId) {
|
|
589
|
+
run.clearActiveTurn()
|
|
590
|
+
run.pushEvent({ type: 'session.state.changed', payload: { state: 'idle' } })
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private async cacheSpawnedSubagentThreadMetadata(input: { notification: CodexNotification, state: CodexEventMapperState, client: CodexJsonRpcClient }): Promise<void> {
|
|
595
|
+
const threadId = spawnedSubagentThreadId(input.notification)
|
|
596
|
+
if (!threadId)
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
let thread: Parameters<typeof cacheCodexThreadMetadata>[0]
|
|
600
|
+
try {
|
|
601
|
+
const result = await input.client.request('thread/read', { threadId, includeTurns: false })
|
|
602
|
+
thread = result.thread
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// Metadata improves display labels only; lifecycle mapping can proceed without it.
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
cacheCodexThreadMetadata(thread, input.state)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private async login(input: {
|
|
612
|
+
client: CodexJsonRpcClient
|
|
613
|
+
holder: CodexRunHolder
|
|
614
|
+
authMaterial: CodexAuthMaterial
|
|
615
|
+
}): Promise<void> {
|
|
616
|
+
const { client, holder, authMaterial } = input
|
|
617
|
+
throwIfStartupFailed(holder)
|
|
618
|
+
if (authMaterial.kind === 'api-key') {
|
|
619
|
+
await client.request('account/login/start', { type: 'apiKey', apiKey: authMaterial.apiKey })
|
|
620
|
+
throwIfStartupFailed(holder)
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
await client.request('account/login/start', toCodexLoginParams(authMaterial.tokens))
|
|
624
|
+
throwIfStartupFailed(holder)
|
|
625
|
+
holder.authTokens = authMaterial.tokens
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private async handleAuthRefreshRequest(
|
|
629
|
+
request: Extract<CodexServerRequest, { method: 'account/chatgptAuthTokens/refresh' }>,
|
|
630
|
+
holder: CodexRunHolder,
|
|
631
|
+
client: CodexJsonRpcClient,
|
|
632
|
+
): Promise<void> {
|
|
633
|
+
const current = holder.authTokens
|
|
634
|
+
if (!current) {
|
|
635
|
+
const error = new AgentProviderProtocolError({
|
|
636
|
+
message: 'Codex requested ChatGPT auth refresh before external auth tokens were available.',
|
|
637
|
+
details: { method: request.method },
|
|
638
|
+
})
|
|
639
|
+
holder.startupFailure = error
|
|
640
|
+
await client.respondErrorAndFlush(request.id, { code: -32000, message: 'Codex auth tokens are not available for refresh.' })
|
|
641
|
+
await client.closeInputAndStop()
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
if (this.auth.kind !== 'chatgpt')
|
|
646
|
+
throw new AgentProviderProtocolError({ message: 'Codex requested ChatGPT token refresh while using API-key authentication.' })
|
|
647
|
+
const refreshed = await this.auth.provider.refreshAuthTokens(toAuthRefreshInput(request.params, current))
|
|
648
|
+
holder.authTokens = refreshed
|
|
649
|
+
client.respond(request.id, toCodexRefreshResponse(refreshed))
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
const serializedError = redactSerializedError(error, current.accessToken)
|
|
653
|
+
const run = holder.run
|
|
654
|
+
if (!run) {
|
|
655
|
+
holder.startupFailure = new AgentProviderProtocolError({
|
|
656
|
+
message: 'Codex ChatGPT auth refresh failed during startup.',
|
|
657
|
+
details: { method: request.method },
|
|
658
|
+
cause: serializedError,
|
|
659
|
+
})
|
|
660
|
+
try {
|
|
661
|
+
await client.respondErrorAndFlush(request.id, { code: -32001, message: 'Codex auth refresh failed.' })
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
await client.closeInputAndStop()
|
|
665
|
+
}
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
await client.respondErrorAndFlush(request.id, { code: -32001, message: 'Codex auth refresh failed.' })
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
await run.failActiveTurnAndEndRun({ reason: 'codex_auth_refresh_failed', error: serializedError })
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private async handleServerRequest(
|
|
678
|
+
request: CodexServerRequest,
|
|
679
|
+
holder: CodexRunHolder,
|
|
680
|
+
client: CodexJsonRpcClient,
|
|
681
|
+
signal: AbortSignal,
|
|
682
|
+
): Promise<void> {
|
|
683
|
+
const run = holder.run
|
|
684
|
+
if (request.method === 'account/chatgptAuthTokens/refresh') {
|
|
685
|
+
await this.handleAuthRefreshRequest(request, holder, client)
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
if (request.method !== 'item/tool/call') {
|
|
689
|
+
client.respond(request.id, {})
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
if (!run) {
|
|
693
|
+
client.respondError(request.id, { code: -32000, message: 'Codex run is not ready for server requests.' })
|
|
694
|
+
return
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const toolParams = request.params
|
|
698
|
+
const runtimeTool = holder.toolsByName.get(toolParams.tool)
|
|
699
|
+
if (!runtimeTool) {
|
|
700
|
+
client.respond(request.id, { success: false, contentItems: [{ type: 'inputText', text: `Unknown tool: ${toolParams.tool}` }] })
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const toolCallId = toolParams.callId ?? String(request.id)
|
|
705
|
+
try {
|
|
706
|
+
const result = await runCodexDynamicTool({
|
|
707
|
+
tool: runtimeTool,
|
|
708
|
+
params: toolParams,
|
|
709
|
+
context: {
|
|
710
|
+
provider: 'codex',
|
|
711
|
+
sessionId: run.metadata.sessionId,
|
|
712
|
+
providerSessionId: run.metadata.providerSessionId,
|
|
713
|
+
turnId: toolParams.turnId,
|
|
714
|
+
cwd: run.metadata.cwd,
|
|
715
|
+
toolCallId,
|
|
716
|
+
signal,
|
|
717
|
+
requestUserInput: async request => await run.requestUserInput({
|
|
718
|
+
...request,
|
|
719
|
+
turnId: toolParams.turnId,
|
|
720
|
+
toolCallId,
|
|
721
|
+
}),
|
|
722
|
+
},
|
|
723
|
+
})
|
|
724
|
+
client.respond(request.id, result)
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
728
|
+
client.respond(request.id, { success: false, contentItems: [{ type: 'inputText', text: message }] })
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function throwIfStartupFailed(holder: CodexRunHolder): void {
|
|
734
|
+
if (holder.startupFailure)
|
|
735
|
+
throw holder.startupFailure
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_FILENAME = 'AGENTS.override.md'
|
|
739
|
+
const SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_SENTINEL = '<!-- coding-agent-managed-selected-repository-instructions -->'
|
|
740
|
+
|
|
741
|
+
type InstructionBridgeOwnership = 'missing' | 'managed' | 'unowned'
|
|
742
|
+
|
|
743
|
+
async function reconcileSelectedRepositoryInstructionBridge(input: { cwd: string, repositoryRoots: readonly string[] }): Promise<string> {
|
|
744
|
+
const instructionBridgePath = resolve(input.cwd, SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_FILENAME)
|
|
745
|
+
let ownership: InstructionBridgeOwnership
|
|
746
|
+
try {
|
|
747
|
+
ownership = await readInstructionBridgeOwnership(instructionBridgePath)
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
if (!(error instanceof AgentProviderRuntimeError))
|
|
751
|
+
throw error
|
|
752
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
|
|
753
|
+
}
|
|
754
|
+
const repositoryRoots = stableRepositoryRoots(input.repositoryRoots)
|
|
755
|
+
|
|
756
|
+
if (repositoryRoots.length === 0) {
|
|
757
|
+
if (ownership === 'managed') {
|
|
758
|
+
try {
|
|
759
|
+
await removeManagedInstructionBridge(instructionBridgePath)
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
if (!(error instanceof AgentProviderRuntimeError))
|
|
763
|
+
throw error
|
|
764
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return instructionBridgePath
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (ownership === 'unowned') {
|
|
771
|
+
const cause = new AgentProviderRuntimeError({
|
|
772
|
+
message: 'Refusing to overwrite an AGENTS.override.md not owned by the Coding Agent runtime.',
|
|
773
|
+
details: { instructionBridgePath },
|
|
774
|
+
retryable: false,
|
|
775
|
+
})
|
|
776
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'terminal', cause })
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
await writeInstructionBridgeAtomically({
|
|
781
|
+
instructionBridgePath,
|
|
782
|
+
content: selectedRepositoryInstructionBridge(repositoryRoots),
|
|
783
|
+
})
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
if (!(error instanceof AgentProviderRuntimeError))
|
|
787
|
+
throw error
|
|
788
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
|
|
789
|
+
}
|
|
790
|
+
return instructionBridgePath
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function readInstructionBridgeOwnership(instructionBridgePath: string): Promise<InstructionBridgeOwnership> {
|
|
794
|
+
let entry
|
|
795
|
+
try {
|
|
796
|
+
entry = await lstat(instructionBridgePath)
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
if (isNodeError(error) && error.code === 'ENOENT')
|
|
800
|
+
return 'missing'
|
|
801
|
+
throw new AgentProviderRuntimeError({
|
|
802
|
+
message: 'Failed to inspect the selected-repository instruction bridge.',
|
|
803
|
+
details: { instructionBridgePath },
|
|
804
|
+
retryable: true,
|
|
805
|
+
cause: error,
|
|
806
|
+
})
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
810
|
+
return 'unowned'
|
|
811
|
+
|
|
812
|
+
let content: string
|
|
813
|
+
try {
|
|
814
|
+
content = await readFile(instructionBridgePath, 'utf8')
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
throw new AgentProviderRuntimeError({
|
|
818
|
+
message: 'Failed to inspect the selected-repository instruction bridge.',
|
|
819
|
+
details: { instructionBridgePath },
|
|
820
|
+
retryable: true,
|
|
821
|
+
cause: error,
|
|
822
|
+
})
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return firstLine(content) === SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_SENTINEL ? 'managed' : 'unowned'
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function writeInstructionBridgeAtomically(input: { instructionBridgePath: string, content: string }): Promise<void> {
|
|
829
|
+
const tempPath = join(
|
|
830
|
+
dirname(input.instructionBridgePath),
|
|
831
|
+
`.${basename(input.instructionBridgePath)}.${process.pid}.${randomUUID()}.tmp`,
|
|
832
|
+
)
|
|
833
|
+
let tempFile: Awaited<ReturnType<typeof open>> | undefined
|
|
834
|
+
try {
|
|
835
|
+
tempFile = await open(tempPath, 'wx', 0o640)
|
|
836
|
+
await tempFile.chmod(0o640)
|
|
837
|
+
await tempFile.writeFile(input.content, 'utf8')
|
|
838
|
+
await tempFile.close()
|
|
839
|
+
tempFile = undefined
|
|
840
|
+
await rename(tempPath, input.instructionBridgePath)
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
await tempFile?.close().catch(() => undefined)
|
|
844
|
+
await unlink(tempPath).catch(() => undefined)
|
|
845
|
+
throw new AgentProviderRuntimeError({
|
|
846
|
+
message: 'Failed to publish the selected-repository instruction bridge.',
|
|
847
|
+
details: { instructionBridgePath: input.instructionBridgePath },
|
|
848
|
+
retryable: true,
|
|
849
|
+
cause: error,
|
|
850
|
+
})
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function removeManagedInstructionBridge(instructionBridgePath: string): Promise<void> {
|
|
855
|
+
try {
|
|
856
|
+
await unlink(instructionBridgePath)
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
if (isNodeError(error) && error.code === 'ENOENT')
|
|
860
|
+
return
|
|
861
|
+
throw new AgentProviderRuntimeError({
|
|
862
|
+
message: 'Failed to remove the stale selected-repository instruction bridge.',
|
|
863
|
+
details: { instructionBridgePath },
|
|
864
|
+
retryable: true,
|
|
865
|
+
cause: error,
|
|
866
|
+
})
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function requireSelectedRepositoryInstructionSource(input: {
|
|
871
|
+
instructionBridgePath: string
|
|
872
|
+
instructionSources?: readonly string[]
|
|
873
|
+
repositoryRoots: readonly string[]
|
|
874
|
+
}): Promise<void> {
|
|
875
|
+
if (input.repositoryRoots.length === 0)
|
|
876
|
+
return
|
|
877
|
+
|
|
878
|
+
const instructionSources = input.instructionSources ?? []
|
|
879
|
+
let expectedInstructionSource: string
|
|
880
|
+
let attestedInstructionSources: string[]
|
|
881
|
+
try {
|
|
882
|
+
expectedInstructionSource = await realpath(input.instructionBridgePath)
|
|
883
|
+
attestedInstructionSources = await Promise.all(instructionSources.map(async source => await realpath(source)))
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
if (!isFileSystemErrno(error))
|
|
887
|
+
throw error
|
|
888
|
+
|
|
889
|
+
const details = {
|
|
890
|
+
expectedInstructionSource: input.instructionBridgePath,
|
|
891
|
+
instructionSources,
|
|
892
|
+
}
|
|
893
|
+
if (isDeterministicRealpathFailure(error)) {
|
|
894
|
+
const cause = new AgentProviderProtocolError({
|
|
895
|
+
message: 'Failed to attest Codex selected-repository instruction sources.',
|
|
896
|
+
details,
|
|
897
|
+
cause: error,
|
|
898
|
+
})
|
|
899
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'terminal', cause })
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const cause = new AgentProviderRuntimeError({
|
|
903
|
+
message: 'Failed to resolve Codex selected-repository instruction sources.',
|
|
904
|
+
details,
|
|
905
|
+
retryable: true,
|
|
906
|
+
cause: error,
|
|
907
|
+
})
|
|
908
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause })
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (attestedInstructionSources.includes(expectedInstructionSource))
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
const cause = new AgentProviderProtocolError({
|
|
915
|
+
message: 'Codex did not load the selected-repository instruction bridge.',
|
|
916
|
+
details: {
|
|
917
|
+
expectedInstructionSource,
|
|
918
|
+
instructionSources: attestedInstructionSources,
|
|
919
|
+
},
|
|
920
|
+
})
|
|
921
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'terminal', cause })
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function repositoryInstructionsUnavailable(input: {
|
|
925
|
+
recoveryKind: 'retry' | 'terminal'
|
|
926
|
+
cause: AgentProviderRuntimeError
|
|
927
|
+
}): AgentNotAcceptedError {
|
|
928
|
+
return new AgentNotAcceptedError({
|
|
929
|
+
message: 'Selected repository instructions are unavailable.',
|
|
930
|
+
recovery: { kind: input.recoveryKind, reason: 'repository_instructions_unavailable' },
|
|
931
|
+
cause: input.cause,
|
|
932
|
+
})
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function selectedRepositoryInstructionBridge(repositoryRoots: readonly string[]): string {
|
|
936
|
+
return [
|
|
937
|
+
SELECTED_REPOSITORY_INSTRUCTION_BRIDGE_SENTINEL,
|
|
938
|
+
'# Selected repository instructions',
|
|
939
|
+
'',
|
|
940
|
+
'Selected repository roots:',
|
|
941
|
+
...repositoryRoots.map(root => `- ${JSON.stringify(root)}`),
|
|
942
|
+
'',
|
|
943
|
+
'For each repository root, you must:',
|
|
944
|
+
'- Inspect `AGENTS.override.md` first; if it does not exist, inspect `AGENTS.md`.',
|
|
945
|
+
'- Follow every documentation file referenced by the applicable AGENTS file.',
|
|
946
|
+
'- Before editing nested files, re-check for more-local `AGENTS.override.md` or `AGENTS.md` files.',
|
|
947
|
+
'',
|
|
948
|
+
'Runtime and developer instructions take precedence over these repository instructions.',
|
|
949
|
+
'Repository instructions apply only within their own repository.',
|
|
950
|
+
'If selected repositories contain irreconcilable instructions, report the conflict instead of resolving it by repository input order.',
|
|
951
|
+
'',
|
|
952
|
+
].join('\n')
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function firstLine(content: string): string {
|
|
956
|
+
const lineEnd = content.indexOf('\n')
|
|
957
|
+
return lineEnd === -1 ? content : content.slice(0, lineEnd)
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
961
|
+
return error instanceof Error && 'code' in error
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function isFileSystemErrno(error: unknown): error is NodeJS.ErrnoException & { code: string, syscall: string } {
|
|
965
|
+
return isNodeError(error)
|
|
966
|
+
&& typeof error.code === 'string'
|
|
967
|
+
&& /^E[A-Z0-9]+$/.test(error.code)
|
|
968
|
+
&& typeof error.syscall === 'string'
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function isDeterministicRealpathFailure(error: NodeJS.ErrnoException): boolean {
|
|
972
|
+
return error.code === 'ENOENT' || error.code === 'ENOTDIR' || error.code === 'ELOOP'
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function yieldToPendingServerRequests(): Promise<void> {
|
|
976
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function stopCodexClientBestEffort(client: CodexJsonRpcClient): Promise<void> {
|
|
980
|
+
await client.stop().catch(() => undefined)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function toCodexLoginParams(tokens: CodexAuthTokens): {
|
|
984
|
+
type: 'chatgptAuthTokens'
|
|
985
|
+
accessToken: string
|
|
986
|
+
chatgptAccountId: string
|
|
987
|
+
chatgptPlanType?: string | null
|
|
988
|
+
} {
|
|
989
|
+
return {
|
|
990
|
+
type: 'chatgptAuthTokens',
|
|
991
|
+
accessToken: tokens.accessToken,
|
|
992
|
+
chatgptAccountId: tokens.chatgptAccountId,
|
|
993
|
+
chatgptPlanType: tokens.chatgptPlanType ?? null,
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function normalizeCodexAuthentication(auth: CodexAuthProvider | CodexAuthentication): CodexAuthentication {
|
|
998
|
+
return 'kind' in auth ? auth : { kind: 'chatgpt', provider: auth }
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function toAuthRefreshInput(params: CodexChatGPTAuthTokensRefreshParams, tokens: CodexAuthTokens): CodexAuthRefreshInput {
|
|
1002
|
+
return {
|
|
1003
|
+
reason: params.reason,
|
|
1004
|
+
rejectedTokenGeneration: tokens.accessTokenGeneration,
|
|
1005
|
+
rejectedTokenFingerprint: tokens.accessTokenFingerprint,
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function toCodexRefreshResponse(tokens: CodexAuthTokens): { accessToken: string, chatgptAccountId: string, chatgptPlanType?: string | null } {
|
|
1010
|
+
return {
|
|
1011
|
+
accessToken: tokens.accessToken,
|
|
1012
|
+
chatgptAccountId: tokens.chatgptAccountId,
|
|
1013
|
+
chatgptPlanType: tokens.chatgptPlanType ?? null,
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function redactSerializedError(error: unknown, accessToken: string): { message: string, code?: string, name?: string, stack?: string } {
|
|
1018
|
+
return redactAgentError(serializeAgentError(error), accessToken)
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function redactAgentError<T extends { message: string, stack?: string, cause?: T }>(error: T, secret: string): T {
|
|
1022
|
+
const redacted = '<redacted>'
|
|
1023
|
+
const replaceSecret = (value: string | undefined) => value?.split(secret).join(redacted)
|
|
1024
|
+
return {
|
|
1025
|
+
...error,
|
|
1026
|
+
message: replaceSecret(error.message) ?? error.message,
|
|
1027
|
+
...(error.stack === undefined ? {} : { stack: replaceSecret(error.stack) }),
|
|
1028
|
+
...(error.cause === undefined ? {} : { cause: redactAgentError(error.cause, secret) }),
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function firstCodexModelId(...values: Array<string | null | undefined>): CodexModelId | undefined {
|
|
1033
|
+
for (const value of values) {
|
|
1034
|
+
const parsed = codexModelIdSchema.safeParse(value)
|
|
1035
|
+
if (parsed.success)
|
|
1036
|
+
return parsed.data
|
|
1037
|
+
}
|
|
1038
|
+
return undefined
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function spawnedSubagentThreadId(notification: CodexNotification): string | undefined {
|
|
1042
|
+
if (notification.method !== 'item/completed')
|
|
1043
|
+
return undefined
|
|
1044
|
+
|
|
1045
|
+
const item = notification.params.item
|
|
1046
|
+
if (item.type !== 'collabAgentToolCall' || item.tool !== 'spawnAgent' || item.status === 'failed')
|
|
1047
|
+
return undefined
|
|
1048
|
+
|
|
1049
|
+
return item.receiverThreadIds.at(0)
|
|
1050
|
+
}
|