@meistrari/agent-core 0.2.0-experimental.5 → 0.2.0-experimental.7
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 +9 -1
- package/package.json +1 -1
- package/src/agents/claude/claude-code-process.ts +12 -6
- package/src/agents/claude/claude-provider.ts +27 -7
- package/src/agents/claude/claude-run.ts +19 -6
- package/src/agents/claude/claude-steered-notifications.ts +2 -2
- package/src/agents/codex/codex-auth.ts +15 -1
- package/src/agents/codex/codex-json-rpc-client.ts +42 -6
- package/src/agents/codex/codex-provider.ts +90 -4
- package/src/agents/codex/codex-run.ts +16 -3
- package/src/agents/input-attachment-preparation.ts +4 -2
- package/src/agents/materialized-input-attachment.ts +2 -0
- package/src/provenance.gen.ts +3 -3
package/README.md
CHANGED
|
@@ -122,6 +122,13 @@ stale managed instruction file.
|
|
|
122
122
|
`resolveInferenceModel`, `pluginsEnabled`, and `browserAuth` (a per-run Browser Use
|
|
123
123
|
credential socket for ChatGPT authentication; off by default).
|
|
124
124
|
|
|
125
|
+
API-key and Bedrock logins stay in the app-server's memory (`cli_auth_credentials_store="ephemeral"`),
|
|
126
|
+
so no `auth.json` under `CODEX_HOME` holds the key for the agent's tools to read. An API-key login
|
|
127
|
+
may name `baseUrl`, the root of another OpenAI Responses API (a gateway, or a loopback proxy that
|
|
128
|
+
holds the real credential and gets a placeholder): Codex then reaches it as a model provider of its
|
|
129
|
+
own, named by `modelProvider` (`api-key-endpoint` by default, never a built-in id), with the key as
|
|
130
|
+
its bearer. `codex-api-key-endpoint.cli.spec.ts` pins that contract against the real CLI.
|
|
131
|
+
|
|
125
132
|
`AgentRun.notify` and `AgentRun.compactContext` are optional on the interface; the Claude and
|
|
126
133
|
Codex runs implement both. `sendPrompt` accepts `submissionOrder`, defaulting to `in-order`.
|
|
127
134
|
|
|
@@ -134,7 +141,8 @@ product instructions/tools, and supervisor integration tests. CI type-checks and
|
|
|
134
141
|
with Remy's Bun 1.3.10 / Claude SDK 0.3.284 and agent-api's Claude SDK 0.3.257 pins.
|
|
135
142
|
|
|
136
143
|
`bun run test` runs the portable suites. `bun run test:cli` runs the `*.cli.spec.ts` suites
|
|
137
|
-
against the real bundled Claude Code CLI and a fake Messages API
|
|
144
|
+
against the real bundled Claude Code CLI and a fake Messages API, and the Codex endpoint spec
|
|
145
|
+
against the pinned Codex CLI (through `bunx`, or `CODEX_EXECUTABLE`) and a fake Responses API. CI runs them on Linux; the
|
|
138
146
|
process lifecycle spec reads `/proc`, so it fails on macOS. Claude Code rejects its
|
|
139
147
|
bypass-permissions mode as root, so run them as a regular user.
|
|
140
148
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/agent-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.0-experimental.
|
|
4
|
+
"version": "0.2.0-experimental.7",
|
|
5
5
|
"packageManager": "bun@1.3.12",
|
|
6
6
|
"description": "Shared contracts and runtime modules for Tela coding-agent sandboxes: agent protocol, supervisor wire protocol, resident supervisor, worker runtime client, and Claude/Codex harness adapters.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
@@ -25,7 +25,7 @@ export class ClaudeCodeProcess {
|
|
|
25
25
|
private termination: Promise<void> | undefined
|
|
26
26
|
private stderr = ''
|
|
27
27
|
|
|
28
|
-
constructor(private readonly exitTimeoutMs = CLAUDE_PROCESS_EXIT_TIMEOUT_MS) {}
|
|
28
|
+
constructor(private readonly exitTimeoutMs = CLAUDE_PROCESS_EXIT_TIMEOUT_MS, private readonly processLauncher?: string) {}
|
|
29
29
|
|
|
30
30
|
/** The SDK `spawnClaudeCodeProcess` hook. A run spawns at most one process. */
|
|
31
31
|
readonly spawn = (options: SpawnOptions): SpawnedProcess => {
|
|
@@ -33,10 +33,10 @@ export class ClaudeCodeProcess {
|
|
|
33
33
|
throw new Error('Claude Code process already spawned for this run.')
|
|
34
34
|
// `detached` makes the CLI a process-group leader. The forwarded signal aborts only after the
|
|
35
35
|
// SDK's graceful stdin-EOF window, as for the SDK's own spawn.
|
|
36
|
-
const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, signal: options.signal, stdio: ['pipe', 'pipe', 'pipe'], detached: true, windowsHide: true })
|
|
36
|
+
const child = spawn(this.processLauncher ?? options.command, this.processLauncher ? [options.command, ...options.args] : options.args, { cwd: options.cwd, env: options.env, signal: options.signal, stdio: ['pipe', 'pipe', 'pipe'], detached: true, windowsHide: true })
|
|
37
37
|
this.child = child
|
|
38
38
|
this.exit = new Promise((resolve) => {
|
|
39
|
-
child.once('exit', () => resolve())
|
|
39
|
+
child.once(this.processLauncher ? 'close' : 'exit', () => resolve())
|
|
40
40
|
// A failed spawn emits `error` without `exit`; there is no process to wait for.
|
|
41
41
|
child.once('error', () => {
|
|
42
42
|
if (child.pid === undefined)
|
|
@@ -70,15 +70,21 @@ export class ClaudeCodeProcess {
|
|
|
70
70
|
const exit = this.exit
|
|
71
71
|
if (!child || !exit || child.pid === undefined)
|
|
72
72
|
return
|
|
73
|
-
|
|
73
|
+
// A fork/wait launcher can exit while its CLI still holds the stdio pipes.
|
|
74
|
+
// For launchers, await close and clean the group even if the wrapper already exited.
|
|
75
|
+
if (await settlesWithin(exit, this.exitTimeoutMs) && !this.processLauncher)
|
|
74
76
|
return
|
|
75
77
|
try {
|
|
76
78
|
process.kill(-child.pid, 'SIGKILL')
|
|
77
79
|
}
|
|
78
|
-
catch {
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (this.processLauncher && process.platform !== 'win32' && (error as NodeJS.ErrnoException).code !== 'ESRCH')
|
|
82
|
+
throw error
|
|
79
83
|
child.kill('SIGKILL')
|
|
80
84
|
}
|
|
81
|
-
await settlesWithin(exit, killedExitWaitMs)
|
|
85
|
+
const closed = await settlesWithin(exit, killedExitWaitMs)
|
|
86
|
+
if (this.processLauncher && !closed)
|
|
87
|
+
throw new Error('Claude launcher process group did not close after SIGKILL.')
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -71,6 +71,14 @@ export interface ClaudeProviderConfig {
|
|
|
71
71
|
desktop?: ClaudeDesktopConfig | null
|
|
72
72
|
tools?: readonly AgentTool[]
|
|
73
73
|
executable?: string
|
|
74
|
+
/**
|
|
75
|
+
* Trusted launcher invoked as <CLI executable> <CLI args>; owns any privilege drop.
|
|
76
|
+
* Preserve stdio and the process group: exec the CLI or keep children in that group.
|
|
77
|
+
* On Windows, the launcher must exec the CLI in place (group signals are unavailable).
|
|
78
|
+
*/
|
|
79
|
+
processLauncher?: string
|
|
80
|
+
/** Private provider stores; defaults to the CLI transcript directory when omitted. */
|
|
81
|
+
runtimeStateRoot?: string
|
|
74
82
|
environment?: Record<string, string | undefined>
|
|
75
83
|
nativeToolActorBridge?: ClaudeNativeToolActorBridge
|
|
76
84
|
startupDeadlineMs?: number
|
|
@@ -139,6 +147,8 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
139
147
|
private readonly desktop: ClaudeDesktopConfig | null
|
|
140
148
|
private readonly tools: readonly AgentTool[]
|
|
141
149
|
private readonly executable: string | undefined
|
|
150
|
+
private readonly processLauncher: string | undefined
|
|
151
|
+
private readonly runtimeStateRoot: string | undefined
|
|
142
152
|
private readonly environment: Record<string, string | undefined>
|
|
143
153
|
private readonly nativeToolActorBridge: ClaudeNativeToolActorBridge | undefined
|
|
144
154
|
private readonly startupDeadlineMs: number
|
|
@@ -156,6 +166,8 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
156
166
|
this.desktop = config.desktop ?? null
|
|
157
167
|
this.tools = config.tools ?? []
|
|
158
168
|
this.executable = config.executable
|
|
169
|
+
this.processLauncher = config.processLauncher
|
|
170
|
+
this.runtimeStateRoot = config.runtimeStateRoot
|
|
159
171
|
this.environment = config.environment ?? {}
|
|
160
172
|
this.nativeToolActorBridge = config.nativeToolActorBridge
|
|
161
173
|
this.startupDeadlineMs = config.startupDeadlineMs ?? CLAUDE_STARTUP_DEADLINE_MS
|
|
@@ -202,10 +214,10 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
202
214
|
const runtime = createClaudeToolRuntimeRef({ turnId: () => state.currentTurnId })
|
|
203
215
|
// The transcript location is a pre-input fact; session-local stores live beside it.
|
|
204
216
|
const transcriptPath = join(configDir, 'projects', encodeClaudeProjectPath(input.cwd), `${expectedSessionId}.jsonl`)
|
|
205
|
-
const steeredNotifications = ClaudeSteeredNotificationStore.open({ sessionId: input.sessionId, transcriptPath })
|
|
217
|
+
const steeredNotifications = ClaudeSteeredNotificationStore.open({ sessionId: input.sessionId, transcriptPath, rootDir: this.runtimeStateRoot })
|
|
206
218
|
const resources = await this.openRunResources({ input, expectedSessionId, signal: abortController.signal })
|
|
207
219
|
const { nativeToolActorRun, workspaceSkills, selectedRepositoryInstructions, browserAuthBroker, browserTurnMetadata } = resources
|
|
208
|
-
const claudeProcess = new ClaudeCodeProcess()
|
|
220
|
+
const claudeProcess = new ClaudeCodeProcess(undefined, this.processLauncher)
|
|
209
221
|
const declaredSubscriptionType = this.auth ? fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) : undefined
|
|
210
222
|
const runtimeMcpServers = toClaudeMcpServers({
|
|
211
223
|
tools,
|
|
@@ -302,7 +314,7 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
302
314
|
const [userInputRequests, initialization, , sessionContext] = await Promise.all([
|
|
303
315
|
UserInputRequestStore.open({
|
|
304
316
|
sessionId: metadata.sessionId,
|
|
305
|
-
rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
317
|
+
rootDir: this.runtimeStateRoot ?? userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
306
318
|
}),
|
|
307
319
|
awaitClaudeInitialization({ query, signal: abortController.signal, deadlineMs: this.startupDeadlineMs }),
|
|
308
320
|
this.auth?.acquire({ signal: abortController.signal }),
|
|
@@ -314,8 +326,12 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
314
326
|
if (this.auth && declaredSubscriptionType !== undefined && declaredSubscriptionType !== fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) && !params.respawnedForSubscriptionType) {
|
|
315
327
|
query.close()
|
|
316
328
|
inputQueue.close()
|
|
317
|
-
|
|
318
|
-
|
|
329
|
+
try {
|
|
330
|
+
await claudeProcess.terminate()
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
await closeRunResources(resources)
|
|
334
|
+
}
|
|
319
335
|
return await this.openRun({ ...params, respawnedForSubscriptionType: true })
|
|
320
336
|
}
|
|
321
337
|
|
|
@@ -372,8 +388,12 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
|
|
|
372
388
|
query.close()
|
|
373
389
|
inputQueue.close()
|
|
374
390
|
// A failed open (for example a missed initialize deadline) must not leave its CLI behind.
|
|
375
|
-
|
|
376
|
-
|
|
391
|
+
try {
|
|
392
|
+
await claudeProcess.terminate()
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
await closeRunResources(resources)
|
|
396
|
+
}
|
|
377
397
|
throw error
|
|
378
398
|
}
|
|
379
399
|
}
|
|
@@ -879,18 +879,31 @@ export class ClaudeRun implements AgentRun {
|
|
|
879
879
|
cause: error,
|
|
880
880
|
}))
|
|
881
881
|
this.ended = this.endAfterProcessExit(reason, error)
|
|
882
|
+
// Autonomous stream failure may finish without a stop caller; keep rejection observed.
|
|
883
|
+
void this.ended.catch(() => undefined)
|
|
882
884
|
}
|
|
883
885
|
|
|
884
|
-
// session.ended means the CLI is gone: the terminal events wait for its exit (bounded by
|
|
886
|
+
// A successful session.ended means the CLI is gone: the terminal events wait for its exit (bounded by
|
|
885
887
|
// CLAUDE_PROCESS_EXIT_TIMEOUT_MS, then a process-group kill). close() is idempotent and sends
|
|
886
888
|
// the stdin EOF the CLI exits on.
|
|
887
889
|
private async endAfterProcessExit(reason: 'completed' | 'stopped' | 'interrupted' | 'failed', error: AgentError | undefined): Promise<void> {
|
|
888
890
|
this.closeQuery()
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
891
|
+
try {
|
|
892
|
+
await this.process.terminate()
|
|
893
|
+
this.events.push({ type: 'session.ended', payload: error ? { reason, error } : { reason } })
|
|
894
|
+
this.events.push({ type: 'session.state.changed', payload: { state: reason === 'failed' ? 'failed' : 'stopped' } })
|
|
895
|
+
}
|
|
896
|
+
catch (cause) {
|
|
897
|
+
const shutdownError = { ...serializeAgentError(cause), code: 'agents.provider-shutdown-failed' }
|
|
898
|
+
this.events.push({ type: 'error', payload: { message: 'Claude CLI shutdown could not be confirmed.', fatal: true, source: 'runtime', error: shutdownError } })
|
|
899
|
+
this.events.push({ type: 'session.ended', payload: { reason: 'failed', error: shutdownError } })
|
|
900
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'failed' } })
|
|
901
|
+
throw cause
|
|
902
|
+
}
|
|
903
|
+
finally {
|
|
904
|
+
this.events.end()
|
|
905
|
+
await this.onFinished().catch(() => undefined)
|
|
906
|
+
}
|
|
894
907
|
}
|
|
895
908
|
|
|
896
909
|
// Reports the request's one result; later calls return the reported one. The caller decides
|
|
@@ -48,9 +48,9 @@ export interface SteeredNotificationDelivery {
|
|
|
48
48
|
export class ClaudeSteeredNotificationStore {
|
|
49
49
|
private constructor(private readonly filePath: string, private file: StoreFile) {}
|
|
50
50
|
|
|
51
|
-
static open(input: { sessionId: string, transcriptPath: string }): ClaudeSteeredNotificationStore {
|
|
51
|
+
static open(input: { sessionId: string, transcriptPath: string, rootDir?: string }): ClaudeSteeredNotificationStore {
|
|
52
52
|
const filePath = join(
|
|
53
|
-
dirname(input.transcriptPath),
|
|
53
|
+
input.rootDir ?? dirname(input.transcriptPath),
|
|
54
54
|
storeDirectoryName,
|
|
55
55
|
Buffer.from(input.sessionId, 'utf8').toString('base64url'),
|
|
56
56
|
storeFileName,
|
|
@@ -44,9 +44,23 @@ export interface CodexBedrockAuthProvider {
|
|
|
44
44
|
getCredentials: (input?: CodexAuthTokenRequest) => Promise<CodexBedrockCredentials>
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* An API key login, kept in the app-server's memory only. Without `baseUrl` it reaches the API of the model
|
|
49
|
+
* provider `modelProvider` names (OpenAI's by default). With `baseUrl`, the root of another OpenAI Responses
|
|
50
|
+
* API (`https://gateway.example/v1`, or a loopback proxy that holds the real credential), Codex reaches it as
|
|
51
|
+
* a model provider of its own, named by `modelProvider` (`api-key-endpoint` by default) and given the key as
|
|
52
|
+
* its bearer.
|
|
53
|
+
*/
|
|
54
|
+
export interface CodexApiKeyAuthentication {
|
|
55
|
+
kind: 'api-key'
|
|
56
|
+
provider: CodexApiKeyAuthProvider
|
|
57
|
+
modelProvider?: string
|
|
58
|
+
baseUrl?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
export type CodexAuthentication
|
|
48
62
|
= | { kind: 'chatgpt', provider: CodexAuthProvider }
|
|
49
|
-
|
|
|
63
|
+
| CodexApiKeyAuthentication
|
|
50
64
|
| { kind: 'amazon-bedrock', region: string, provider: CodexBedrockAuthProvider }
|
|
51
65
|
/** Expiry scheduling hint only; this neither authenticates nor verifies a JWT signature. */
|
|
52
66
|
export function codexAuthTokensExpireSoon(tokens: CodexAuthTokens): boolean {
|
|
@@ -59,7 +59,9 @@ export class CodexJsonRpcClient {
|
|
|
59
59
|
readonly child: ChildProcessWithoutNullStreams
|
|
60
60
|
private nextId = 0
|
|
61
61
|
private stopped = false
|
|
62
|
+
private shutdown: Promise<void> | undefined
|
|
62
63
|
private readonly closed: Promise<void>
|
|
64
|
+
private readonly processGroup: boolean
|
|
63
65
|
private readonly pending = new Map<string, {
|
|
64
66
|
method: ClientRequestMethod
|
|
65
67
|
resolve: (value: unknown) => void
|
|
@@ -70,6 +72,7 @@ export class CodexJsonRpcClient {
|
|
|
70
72
|
executable: string,
|
|
71
73
|
options: {
|
|
72
74
|
cwd: string
|
|
75
|
+
processLauncher?: string
|
|
73
76
|
environment?: Record<string, string | undefined>
|
|
74
77
|
configOverrides?: readonly string[]
|
|
75
78
|
onMessage: (message: CodexJsonRpcMessage) => void
|
|
@@ -78,10 +81,13 @@ export class CodexJsonRpcClient {
|
|
|
78
81
|
maxLineBytes?: number
|
|
79
82
|
},
|
|
80
83
|
) {
|
|
81
|
-
this.
|
|
84
|
+
this.processGroup = options.processLauncher !== undefined && process.platform !== 'win32'
|
|
85
|
+
const args = ['app-server', '--listen', 'stdio://', ...(options.configOverrides ?? []).flatMap(value => ['-c', value])]
|
|
86
|
+
this.child = spawn(options.processLauncher ?? executable, options.processLauncher ? [executable, ...args] : args, {
|
|
82
87
|
cwd: options.cwd,
|
|
83
88
|
env: options.environment,
|
|
84
89
|
stdio: 'pipe',
|
|
90
|
+
detached: this.processGroup,
|
|
85
91
|
})
|
|
86
92
|
this.closed = new Promise(resolve => this.child.once('close', () => resolve()))
|
|
87
93
|
|
|
@@ -229,9 +235,16 @@ export class CodexJsonRpcClient {
|
|
|
229
235
|
}
|
|
230
236
|
|
|
231
237
|
async closeInputAndStop(): Promise<void> {
|
|
238
|
+
this.shutdown ??= this.drainAndStop()
|
|
239
|
+
await this.shutdown
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async drainAndStop(): Promise<void> {
|
|
232
243
|
if (this.stopped) {
|
|
233
244
|
if (!await waitForClosed(this.closed, PROCESS_SHUTDOWN_GRACE_MS))
|
|
234
|
-
await terminateChild(this.child, this.closed)
|
|
245
|
+
await terminateChild(this.child, this.closed, this.processGroup)
|
|
246
|
+
if (this.processGroup)
|
|
247
|
+
signalProcessGroup(this.child, 'SIGKILL')
|
|
235
248
|
return
|
|
236
249
|
}
|
|
237
250
|
this.stopped = true
|
|
@@ -242,9 +255,10 @@ export class CodexJsonRpcClient {
|
|
|
242
255
|
// EOF/drain and process exit share one grace window. Never wait separately for a
|
|
243
256
|
// buffered stdin write: a child that stops reading must still reach signal escalation.
|
|
244
257
|
this.child.stdin.end()
|
|
245
|
-
if (await waitForClosed(this.closed, PROCESS_SHUTDOWN_GRACE_MS))
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
if (!await waitForClosed(this.closed, PROCESS_SHUTDOWN_GRACE_MS))
|
|
259
|
+
await terminateChild(this.child, this.closed, this.processGroup)
|
|
260
|
+
if (this.processGroup)
|
|
261
|
+
signalProcessGroup(this.child, 'SIGKILL')
|
|
248
262
|
}
|
|
249
263
|
|
|
250
264
|
async stop(): Promise<void> {
|
|
@@ -288,7 +302,17 @@ export class CodexJsonRpcClient {
|
|
|
288
302
|
}
|
|
289
303
|
}
|
|
290
304
|
|
|
291
|
-
async function terminateChild(child: ChildProcessWithoutNullStreams, closed: Promise<void
|
|
305
|
+
async function terminateChild(child: ChildProcessWithoutNullStreams, closed: Promise<void>, processGroup: boolean): Promise<void> {
|
|
306
|
+
if (processGroup) {
|
|
307
|
+
// The launcher may already be gone while the CLI retains its pipes. Signal the
|
|
308
|
+
// retained group regardless of the wrapper's exitCode, and bound the close wait.
|
|
309
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGKILL'] as const) {
|
|
310
|
+
signalProcessGroup(child, signal)
|
|
311
|
+
if (await waitForClosed(closed, signal === 'SIGKILL' ? 1_000 : PROCESS_SHUTDOWN_GRACE_MS))
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
throw new Error('Codex launcher process group did not close after SIGKILL.')
|
|
315
|
+
}
|
|
292
316
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
293
317
|
await closed
|
|
294
318
|
return
|
|
@@ -308,6 +332,18 @@ async function terminateChild(child: ChildProcessWithoutNullStreams, closed: Pro
|
|
|
308
332
|
await closed
|
|
309
333
|
}
|
|
310
334
|
|
|
335
|
+
function signalProcessGroup(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): void {
|
|
336
|
+
if (child.pid === undefined)
|
|
337
|
+
return
|
|
338
|
+
try {
|
|
339
|
+
process.kill(-child.pid, signal)
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if ((error as NodeJS.ErrnoException).code !== 'ESRCH')
|
|
343
|
+
throw error
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
311
347
|
async function waitForClosed(closed: Promise<void>, timeoutMs: number): Promise<boolean> {
|
|
312
348
|
return await new Promise<boolean>((resolve) => {
|
|
313
349
|
const timer = setTimeout(() => resolve(false), timeoutMs)
|
|
@@ -5,7 +5,7 @@ import type { AgentRun } from '../agent-run'
|
|
|
5
5
|
import type { AgentTool } from '../agent-tool'
|
|
6
6
|
import type { InstructionComposer } from '../instructions'
|
|
7
7
|
import type { SessionContextComposer } from '../session-context'
|
|
8
|
-
import type { CodexAuthentication, CodexAuthProvider, CodexAuthTokens, CodexBedrockCredentials } from './codex-auth'
|
|
8
|
+
import type { CodexApiKeyAuthentication, CodexAuthentication, CodexAuthProvider, CodexAuthTokens, CodexBedrockCredentials } from './codex-auth'
|
|
9
9
|
import type { CodexEventMapperState, CodexNotification } from './codex-event-mapper'
|
|
10
10
|
import type { CodexJsonRpcMessage, CodexServerRequest } from './codex-json-rpc-client'
|
|
11
11
|
import type { ClientRequestResponsesByMethod } from './codex-protocol'
|
|
@@ -13,7 +13,7 @@ import { realpath } from 'node:fs/promises'
|
|
|
13
13
|
import { resolve } from 'node:path'
|
|
14
14
|
import { codexModelIdSchema } from '../../protocol'
|
|
15
15
|
import { serializeAgentError } from '../agent-error-serializer'
|
|
16
|
-
import { AgentProviderMetadataError, AgentProviderProtocolError, AgentProviderRuntimeError } from '../agent-runtime-error'
|
|
16
|
+
import { AgentProviderMetadataError, AgentProviderProtocolError, AgentProviderRuntimeError, AgentRuntimeValidationError } from '../agent-runtime-error'
|
|
17
17
|
import { sessionConfiguredEvent } from '../agent-session-events'
|
|
18
18
|
import { selectAgentTools, toolByName } from '../agent-tool'
|
|
19
19
|
import { emptyInstructionComposer } from '../instructions'
|
|
@@ -33,6 +33,15 @@ import { CodexRunAuth, CodexRunAuthClosedError } from './codex-run-auth'
|
|
|
33
33
|
import { catalogRepositorySkillRoots, workspaceSkillRoots } from './codex-skill-roots'
|
|
34
34
|
import { runCodexDynamicTool, toCodexDynamicTools } from './codex-tool-mapper'
|
|
35
35
|
|
|
36
|
+
/** The model provider an API key endpoint gets when `modelProvider` names none. */
|
|
37
|
+
const API_KEY_ENDPOINT_PROVIDER = 'api-key-endpoint'
|
|
38
|
+
/** Codex's own provider ids: an endpoint defined under one of these would not be the one Codex uses. */
|
|
39
|
+
const BUILT_IN_MODEL_PROVIDERS = new Set(['openai', 'amazon-bedrock', 'ollama', 'lmstudio', 'oss'])
|
|
40
|
+
/** A bare TOML key, so the id is safe inside the dotted `-c model_providers.<id>.…` overrides. */
|
|
41
|
+
const MODEL_PROVIDER_ID = /^[\w-]+$/
|
|
42
|
+
/** Where plain http keeps the key on the machine. */
|
|
43
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]'])
|
|
44
|
+
|
|
36
45
|
export interface CodexStartupStageFact {
|
|
37
46
|
event: 'sandbox.startup-stage'
|
|
38
47
|
stage: 'codex-initialize' | 'auth-acquisition-login' | 'thread-start' | 'thread-resume'
|
|
@@ -45,6 +54,14 @@ export interface CodexProviderConfig {
|
|
|
45
54
|
auth: CodexAuthProvider | CodexAuthentication
|
|
46
55
|
tools?: readonly AgentTool[]
|
|
47
56
|
executable?: string
|
|
57
|
+
/**
|
|
58
|
+
* Trusted launcher invoked as <CLI executable> <CLI args>; owns any privilege drop.
|
|
59
|
+
* Preserve stdio and the process group: exec the CLI or keep children in that group.
|
|
60
|
+
* On Windows, the launcher must exec the CLI in place (group signals are unavailable).
|
|
61
|
+
*/
|
|
62
|
+
processLauncher?: string
|
|
63
|
+
/** Private provider stores; defaults to the CLI transcript directory when omitted. */
|
|
64
|
+
runtimeStateRoot?: string
|
|
48
65
|
environment?: Record<string, string | undefined>
|
|
49
66
|
onStartupStage?: (fact: CodexStartupStageFact) => void
|
|
50
67
|
instructions?: InstructionComposer
|
|
@@ -102,7 +119,11 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
102
119
|
private readonly tools: readonly AgentTool[]
|
|
103
120
|
private readonly executable: string
|
|
104
121
|
private readonly environment: Record<string, string | undefined>
|
|
122
|
+
private readonly processLauncher: string | undefined
|
|
123
|
+
private readonly runtimeStateRoot: string | undefined
|
|
105
124
|
private readonly auth: CodexAuthentication
|
|
125
|
+
/** The validated endpoint of an API key login that names one. */
|
|
126
|
+
private readonly apiKeyEndpoint: string | undefined
|
|
106
127
|
private readonly onStartupStage: ((fact: CodexStartupStageFact) => void) | undefined
|
|
107
128
|
private readonly instructions: InstructionComposer
|
|
108
129
|
private readonly sessionContext: SessionContextComposer | undefined
|
|
@@ -115,9 +136,12 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
115
136
|
|
|
116
137
|
private constructor(config: CodexProviderConfig) {
|
|
117
138
|
this.auth = normalizeCodexAuthentication(config.auth)
|
|
139
|
+
this.apiKeyEndpoint = this.auth.kind === 'api-key' ? apiKeyEndpoint(this.auth) : undefined
|
|
118
140
|
this.tools = config.tools ?? []
|
|
119
141
|
this.executable = config.executable ?? 'codex'
|
|
120
142
|
this.environment = config.environment ?? {}
|
|
143
|
+
this.processLauncher = config.processLauncher
|
|
144
|
+
this.runtimeStateRoot = config.runtimeStateRoot
|
|
121
145
|
this.onStartupStage = config.onStartupStage
|
|
122
146
|
this.instructions = config.instructions ?? emptyInstructionComposer
|
|
123
147
|
this.sessionContext = config.sessionContext
|
|
@@ -422,7 +446,11 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
422
446
|
}
|
|
423
447
|
|
|
424
448
|
private modelProvider(): string | undefined {
|
|
425
|
-
|
|
449
|
+
if (this.auth.kind === 'amazon-bedrock')
|
|
450
|
+
return 'amazon-bedrock'
|
|
451
|
+
if (this.auth.kind === 'api-key')
|
|
452
|
+
return this.auth.modelProvider ?? (this.apiKeyEndpoint === undefined ? undefined : API_KEY_ENDPOINT_PROVIDER)
|
|
453
|
+
return undefined
|
|
426
454
|
}
|
|
427
455
|
|
|
428
456
|
private async openConnection(input: {
|
|
@@ -442,6 +470,7 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
442
470
|
|
|
443
471
|
try {
|
|
444
472
|
client = new CodexJsonRpcClient(this.executable, {
|
|
473
|
+
processLauncher: this.processLauncher,
|
|
445
474
|
cwd,
|
|
446
475
|
environment: holder.browserAuthBroker
|
|
447
476
|
? { ...this.environment, CODING_AGENT_BROWSER_AUTH_SOCKET: holder.browserAuthBroker.socketPath }
|
|
@@ -450,6 +479,7 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
450
479
|
...(this.auth.kind === 'amazon-bedrock'
|
|
451
480
|
? ['cli_auth_credentials_store="ephemeral"', 'model_provider="amazon-bedrock"', `model_providers.amazon-bedrock.aws.region=${JSON.stringify(this.auth.region)}`, 'model_providers.amazon-bedrock.aws.wire_api="responses"']
|
|
452
481
|
: []),
|
|
482
|
+
...(this.auth.kind === 'api-key' ? apiKeyConfigOverrides(this.auth.modelProvider, this.apiKeyEndpoint) : []),
|
|
453
483
|
...(this.pluginsEnabled === undefined ? [] : [`features.plugins=${this.pluginsEnabled}`]),
|
|
454
484
|
],
|
|
455
485
|
onMessage: (message) => {
|
|
@@ -620,7 +650,7 @@ export class CodexProvider implements AgentProvider<'codex'> {
|
|
|
620
650
|
}),
|
|
621
651
|
UserInputRequestStore.open({
|
|
622
652
|
sessionId: metadata.sessionId,
|
|
623
|
-
rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
653
|
+
rootDir: this.runtimeStateRoot ?? userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
|
|
624
654
|
}),
|
|
625
655
|
pendingSessionContext({
|
|
626
656
|
compose: this.sessionContext,
|
|
@@ -895,6 +925,62 @@ function normalizeCodexAuthentication(auth: CodexAuthProvider | CodexAuthenticat
|
|
|
895
925
|
return 'kind' in auth ? auth : { kind: 'chatgpt', provider: auth }
|
|
896
926
|
}
|
|
897
927
|
|
|
928
|
+
/**
|
|
929
|
+
* The endpoint as Codex is given it, or none: an http(s) URL with no credentials, query or fragment (not even
|
|
930
|
+
* an empty `?` or `#`), plain http only on loopback, where the key never crosses the network. Without a
|
|
931
|
+
* trailing slash, so Codex appends `/responses` to the path it names.
|
|
932
|
+
*/
|
|
933
|
+
function apiKeyEndpoint(auth: CodexApiKeyAuthentication): string | undefined {
|
|
934
|
+
const invalid = (message: string) => new AgentRuntimeValidationError({
|
|
935
|
+
code: 'agents.codex-api-key-endpoint-invalid',
|
|
936
|
+
message,
|
|
937
|
+
publicMessage: 'The Codex API key endpoint is misconfigured.',
|
|
938
|
+
details: { modelProvider: auth.modelProvider },
|
|
939
|
+
})
|
|
940
|
+
// Without an endpoint the provider id only selects a provider configured elsewhere, as it always has.
|
|
941
|
+
if (auth.baseUrl === undefined)
|
|
942
|
+
return undefined
|
|
943
|
+
if (auth.modelProvider !== undefined && !MODEL_PROVIDER_ID.test(auth.modelProvider))
|
|
944
|
+
throw invalid('Codex model provider ids are letters, digits, underscores and hyphens.')
|
|
945
|
+
let url: URL
|
|
946
|
+
try {
|
|
947
|
+
url = new URL(auth.baseUrl)
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
throw invalid('The Codex API key endpoint is not a URL.')
|
|
951
|
+
}
|
|
952
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
953
|
+
throw invalid('The Codex API key endpoint must be an http or https URL.')
|
|
954
|
+
if (url.protocol === 'http:' && !LOOPBACK_HOSTS.has(url.hostname))
|
|
955
|
+
throw invalid('The Codex API key endpoint sends the key as its bearer: plain http is accepted on loopback only.')
|
|
956
|
+
if (url.username || url.password || /[?#]/u.test(auth.baseUrl))
|
|
957
|
+
throw invalid('The Codex API key endpoint carries no credentials, query or fragment; the key travels as the bearer.')
|
|
958
|
+
if (BUILT_IN_MODEL_PROVIDERS.has(auth.modelProvider ?? API_KEY_ENDPOINT_PROVIDER))
|
|
959
|
+
throw invalid('An API key endpoint needs a model provider id of its own, not one of Codex\'s built-in providers.')
|
|
960
|
+
return `${url.origin}${url.pathname.replace(/\/+$/u, '')}`
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* An API key login is always kept in memory, never written to `auth.json` under CODEX_HOME, where the agent's
|
|
965
|
+
* own tools could read it. With an endpoint, Codex reaches it through a Responses provider of its own that
|
|
966
|
+
* authenticates with that login.
|
|
967
|
+
*/
|
|
968
|
+
function apiKeyConfigOverrides(modelProvider: string | undefined, endpoint: string | undefined): string[] {
|
|
969
|
+
const ephemeral = ['cli_auth_credentials_store="ephemeral"']
|
|
970
|
+
if (endpoint === undefined)
|
|
971
|
+
return ephemeral
|
|
972
|
+
const id = modelProvider ?? API_KEY_ENDPOINT_PROVIDER
|
|
973
|
+
const provider = `model_providers.${id}`
|
|
974
|
+
return [
|
|
975
|
+
...ephemeral,
|
|
976
|
+
`model_provider=${JSON.stringify(id)}`,
|
|
977
|
+
`${provider}.name=${JSON.stringify(id)}`,
|
|
978
|
+
`${provider}.base_url=${JSON.stringify(endpoint)}`,
|
|
979
|
+
`${provider}.wire_api="responses"`,
|
|
980
|
+
`${provider}.requires_openai_auth=true`,
|
|
981
|
+
]
|
|
982
|
+
}
|
|
983
|
+
|
|
898
984
|
async function requireSelectedRepositoryInstructionSource(input: {
|
|
899
985
|
instructionBridgePath: string
|
|
900
986
|
instructionSources?: readonly string[]
|
|
@@ -21,6 +21,7 @@ import type { CodexEventMapperState } from './codex-event-mapper'
|
|
|
21
21
|
import type { CodexJsonRpcClient } from './codex-json-rpc-client'
|
|
22
22
|
import type { ClientRequestResponsesByMethod, CodexUserInput } from './codex-protocol'
|
|
23
23
|
import { serializeAgentNotificationToolOutput } from '../../protocol'
|
|
24
|
+
import { serializeAgentError } from '../agent-error-serializer'
|
|
24
25
|
import { AgentEventStream } from '../agent-event-stream'
|
|
25
26
|
import { AgentDeliveryUnknownError, AgentNotAcceptedError, AgentPromptDeferredError, AgentRunStateError } from '../agent-runtime-error'
|
|
26
27
|
import { sessionSettingsChangedEvent } from '../agent-session-events'
|
|
@@ -146,7 +147,7 @@ export class CodexRun implements AgentRun {
|
|
|
146
147
|
sessionId: this.metadata.sessionId,
|
|
147
148
|
providerSessionId: this.metadata.providerSessionId,
|
|
148
149
|
reason: 'aborted',
|
|
149
|
-
}))
|
|
150
|
+
}).catch(() => undefined))
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
pushEvent(draft: AgentEventDraft): void {
|
|
@@ -639,8 +640,20 @@ export class CodexRun implements AgentRun {
|
|
|
639
640
|
const revocation = new Promise<void>((resolve) => {
|
|
640
641
|
resolve(this.onStop())
|
|
641
642
|
})
|
|
642
|
-
void Promise.
|
|
643
|
-
.then(
|
|
643
|
+
void Promise.allSettled([this.client.stop(), revocation])
|
|
644
|
+
.then((outcomes) => {
|
|
645
|
+
const failure = outcomes.find(outcome => outcome.status === 'rejected')
|
|
646
|
+
if (!failure) {
|
|
647
|
+
finalize()
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
const error = { ...serializeAgentError(failure.reason), code: 'agents.provider-shutdown-failed' }
|
|
651
|
+
this.events.push({ type: 'error', payload: { message: 'Codex run shutdown could not be confirmed.', fatal: true, source: 'runtime', error } })
|
|
652
|
+
this.events.push({ type: 'session.ended', payload: { reason: 'failed', error } })
|
|
653
|
+
this.events.push({ type: 'session.state.changed', payload: { state: 'failed' } })
|
|
654
|
+
this.events.end()
|
|
655
|
+
throw failure.reason
|
|
656
|
+
})
|
|
644
657
|
.then(stopping.resolve, stopping.reject)
|
|
645
658
|
}
|
|
646
659
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Buffer } from 'node:buffer'
|
|
2
1
|
import type { MaterializedAgentInputAttachment } from './materialized-input-attachment'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { AgentInputAttachmentPreparationFailure } from './input-attachment.errors'
|
|
5
5
|
|
|
@@ -8,7 +8,9 @@ type NativeImageMimeType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/web
|
|
|
8
8
|
export async function readAttachmentBytes(attachment: MaterializedAgentInputAttachment, signal: AbortSignal): Promise<Buffer> {
|
|
9
9
|
signal.throwIfAborted()
|
|
10
10
|
try {
|
|
11
|
-
const bytes =
|
|
11
|
+
const bytes = attachment.readBytes
|
|
12
|
+
? Buffer.from(await attachment.readBytes(signal))
|
|
13
|
+
: await readFile(attachment.localPath, { signal })
|
|
12
14
|
signal.throwIfAborted()
|
|
13
15
|
return bytes
|
|
14
16
|
}
|
|
@@ -6,6 +6,8 @@ export type MaterializedAgentInputAttachment = Readonly<{
|
|
|
6
6
|
byteSize: number
|
|
7
7
|
sha256: string
|
|
8
8
|
localPath: string
|
|
9
|
+
/** Read as the file owner when the provider uses a different process identity. */
|
|
10
|
+
readBytes?: (signal: AbortSignal) => Promise<Uint8Array>
|
|
9
11
|
}>
|
|
10
12
|
|
|
11
13
|
export type AgentInputAttachmentProjection = Readonly<{
|
package/src/provenance.gen.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/write-provenance.ts. Do not edit by hand.
|
|
2
2
|
export const generatedProvenance = {
|
|
3
|
-
version: "0.2.0-experimental.
|
|
4
|
-
sha: "
|
|
5
|
-
buildTime: "2026-
|
|
3
|
+
version: "0.2.0-experimental.7",
|
|
4
|
+
sha: "c27666274441cd3c81ffc5934ca115dffbe234ee",
|
|
5
|
+
buildTime: "2026-10-14T20:20:44.640Z",
|
|
6
6
|
} as const
|