@meistrari/agent-core 0.2.0-experimental.0 → 0.2.0-experimental.2
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 +82 -0
- package/docs/consumer-capability-matrix.md +374 -0
- package/package.json +17 -10
- package/scripts/generate-codex-protocol.ts +711 -0
- package/src/agents/agent-operation.ts +8 -0
- package/src/agents/agent-provider.ts +3 -0
- package/src/agents/agent-run.ts +14 -2
- package/src/agents/agent-runtime-error.ts +28 -0
- package/src/agents/agent-session-events.ts +33 -0
- package/src/agents/agent-tool-runner.ts +12 -11
- package/src/agents/agent-tool.ts +12 -4
- package/src/agents/author-context.ts +1 -1
- package/src/agents/claude/claude-auth.ts +40 -0
- package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
- package/src/agents/claude/claude-code-process.ts +98 -0
- package/src/agents/claude/claude-command-mapper.ts +63 -3
- package/src/agents/claude/claude-desktop.ts +55 -0
- package/src/agents/claude/claude-event-mapper.ts +876 -124
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +344 -29
- package/src/agents/claude/claude-run.ts +922 -91
- package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
- package/src/agents/claude/claude-skill-catalog.ts +75 -0
- package/src/agents/claude/claude-steered-notifications.ts +180 -0
- package/src/agents/claude/claude-tool-mapper.ts +81 -2
- package/src/agents/claude/claude-workspace-skills.ts +87 -0
- package/src/agents/claude/index.ts +2 -0
- package/src/agents/codex/codex-auth-mapper.ts +27 -0
- package/src/agents/codex/codex-auth.ts +19 -0
- package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
- package/src/agents/codex/codex-browser-auth-status.ts +55 -0
- package/src/agents/codex/codex-command-mapper.ts +38 -3
- package/src/agents/codex/codex-event-mapper.ts +375 -64
- package/src/agents/codex/codex-json-rpc-client.ts +66 -21
- package/src/agents/codex/codex-provider.ts +118 -236
- package/src/agents/codex/codex-run-auth.ts +113 -0
- package/src/agents/codex/codex-run.ts +428 -48
- package/src/agents/codex/codex-tool-mapper.ts +2 -2
- package/src/agents/codex/codex.errors.ts +8 -0
- package/src/agents/codex/generated/meta.gen.ts +144 -6
- package/src/agents/codex/generated/namespaces.gen.ts +92 -3
- package/src/agents/codex/generated/schema.gen.ts +24698 -15638
- package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/input-attachment-preparation.ts +13 -0
- package/src/agents/mcp-tool-content.ts +82 -0
- package/src/agents/model-rebinding-run.ts +14 -3
- package/src/agents/selected-repository-instructions.ts +202 -0
- package/src/agents/session-context.ts +41 -0
- package/src/agents/subscription-token-refresh.ts +73 -0
- package/src/agents/user-input-request.ts +19 -20
- package/src/errors/index.ts +1 -1
- package/src/errors/validation-error.ts +6 -0
- package/src/protocol/agent-catalog.ts +159 -0
- package/src/protocol/agent-command.ts +54 -7
- package/src/protocol/agent-content.ts +36 -16
- package/src/protocol/agent-event.ts +210 -73
- package/src/protocol/agent-identity.ts +29 -0
- package/src/protocol/agent-json.ts +122 -8
- package/src/protocol/agent-metadata.ts +2 -2
- package/src/protocol/agent-model.ts +13 -1
- package/src/protocol/agent-notification.ts +110 -0
- package/src/protocol/agent-presentation.ts +71 -20
- package/src/protocol/agent-tool.ts +66 -10
- package/src/protocol/agent-usage.ts +4 -1
- package/src/protocol/agent-user-input.ts +4 -4
- package/src/protocol/agent-work.ts +12 -10
- package/src/protocol/index.ts +2 -0
- package/src/protocol/rejected-keys.ts +11 -0
- package/src/provenance.gen.ts +3 -3
- package/src/steps/contract.ts +4 -1
- package/src/steps/projector.ts +17 -1
- package/src/supervisor/agent-provider-factory.ts +60 -5
- package/src/supervisor/bootstrap-binder.ts +3 -2
- package/src/supervisor/index.ts +1 -1
- package/src/supervisor/persistence/supervisor-store.ts +22 -8
- package/src/supervisor/runtime-handler.ts +30 -2
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
- package/src/supervisor-protocol/bootstrap.ts +4 -1
- package/src/supervisor-protocol/command-body.ts +4 -1
- package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
- package/src/testing/in-memory-runtime-control-plane.ts +118 -12
- package/src/testing/index.ts +1 -1
- package/src/worker-runtime-client/command-pump.ts +85 -10
- package/src/worker-runtime-client/connection-attempt.ts +95 -20
- package/src/worker-runtime-client/frame-processor.ts +242 -58
- package/src/worker-runtime-client/index.ts +8 -2
- package/src/worker-runtime-client/ports.ts +34 -2
- package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type { Server, Socket } from 'node:net'
|
|
2
|
+
import type { CodexAuthTokens } from './codex-auth'
|
|
3
|
+
import type { ClientRequestParamsByMethod } from './codex-protocol'
|
|
4
|
+
import type { CodexRunAuth } from './codex-run-auth'
|
|
5
|
+
import { Buffer } from 'node:buffer'
|
|
6
|
+
import { chmod, mkdtemp, rm } from 'node:fs/promises'
|
|
7
|
+
import { createServer } from 'node:net'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { AgentProviderProtocolError } from '../agent-runtime-error'
|
|
11
|
+
import { codexAuthTokensExpireSoon } from './codex-auth'
|
|
12
|
+
import { toAuthRefreshInput, toCodexLoginParams, toCodexRefreshResponse } from './codex-auth-mapper'
|
|
13
|
+
import { projectCodexBrowserAuthStatus } from './codex-browser-auth-status'
|
|
14
|
+
import { CodexJsonRpcClient, CodexJsonRpcResponseError, isCodexServerRequest } from './codex-json-rpc-client'
|
|
15
|
+
|
|
16
|
+
const MAX_BYTES = 1024 * 1024
|
|
17
|
+
const DEADLINE_MS = 30_000
|
|
18
|
+
const MAX_CONNECTIONS = 8
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The credential source a broker serves. Codex runs pass their own `CodexRunAuth`, shared with the
|
|
22
|
+
* main native process; Claude runs pass a `LazyCodexRunAuth` that binds an account on first use.
|
|
23
|
+
*/
|
|
24
|
+
export type CodexBrowserAuthSource = Pick<CodexRunAuth, 'getCurrentTokens' | 'refreshAuthTokens' | 'close'>
|
|
25
|
+
|
|
26
|
+
interface BrokerInput {
|
|
27
|
+
executable: string
|
|
28
|
+
cwd: string
|
|
29
|
+
environment: Record<string, string | undefined>
|
|
30
|
+
auth: CodexBrowserAuthSource
|
|
31
|
+
}
|
|
32
|
+
type BrowserMethod = 'initialize' | 'getAuthStatus' | 'configRequirements/read' | 'config/read' | 'config/value/write' | 'config/batchWrite'
|
|
33
|
+
type BrowserRequest = {
|
|
34
|
+
[Method in BrowserMethod]: { id: string | number, method: Method, params: ClientRequestParamsByMethod[Method] }
|
|
35
|
+
}[BrowserMethod]
|
|
36
|
+
|
|
37
|
+
/** Runtime-only capability: never put this socket path into session metadata. */
|
|
38
|
+
export class CodexBrowserAuthBroker {
|
|
39
|
+
readonly socketPath: string
|
|
40
|
+
private readonly server: Server
|
|
41
|
+
private readonly connections = new Set<{ close: () => Promise<void> }>()
|
|
42
|
+
private closing: Promise<void> | undefined
|
|
43
|
+
|
|
44
|
+
private constructor(private readonly directory: string, private readonly input: BrokerInput) {
|
|
45
|
+
this.socketPath = join(directory, 'rpc.sock')
|
|
46
|
+
this.server = createServer((socket) => {
|
|
47
|
+
if (this.closing || this.connections.size >= MAX_CONNECTIONS) {
|
|
48
|
+
socket.destroy()
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const connection = new BrowserConnection(socket, this.input)
|
|
52
|
+
this.connections.add(connection)
|
|
53
|
+
socket.once('close', () => {
|
|
54
|
+
void connection.close().finally(() => this.connections.delete(connection))
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async open(input: BrokerInput): Promise<CodexBrowserAuthBroker> {
|
|
60
|
+
const directory = await mkdtemp(join(tmpdir(), 'codex-browser-'))
|
|
61
|
+
const broker = new CodexBrowserAuthBroker(directory, input)
|
|
62
|
+
try {
|
|
63
|
+
await chmod(directory, 0o700)
|
|
64
|
+
await new Promise<void>((resolve, reject) => {
|
|
65
|
+
broker.server.once('error', reject)
|
|
66
|
+
broker.server.listen(broker.socketPath, resolve)
|
|
67
|
+
})
|
|
68
|
+
await chmod(broker.socketPath, 0o600)
|
|
69
|
+
return broker
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
await broker.close()
|
|
73
|
+
throw brokerFailure()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async close(): Promise<void> {
|
|
78
|
+
this.closing ??= (async () => {
|
|
79
|
+
this.input.auth.close()
|
|
80
|
+
const listenerClosed = new Promise<void>(resolve => this.server.close(() => resolve()))
|
|
81
|
+
await Promise.all([...this.connections].map(async connection => await connection.close()))
|
|
82
|
+
await listenerClosed
|
|
83
|
+
await rm(this.directory, { recursive: true, force: true })
|
|
84
|
+
})()
|
|
85
|
+
await this.closing
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class BrowserConnection {
|
|
90
|
+
private readonly client: CodexJsonRpcClient
|
|
91
|
+
private readonly abort = new AbortController()
|
|
92
|
+
private tokens: CodexAuthTokens | undefined
|
|
93
|
+
private initialized = false
|
|
94
|
+
private buffer = Buffer.alloc(0)
|
|
95
|
+
private queuedBytes = 0
|
|
96
|
+
private queuedFrames = 0
|
|
97
|
+
private queuedRefreshes = 0
|
|
98
|
+
private queue = Promise.resolve()
|
|
99
|
+
private refreshQueue = Promise.resolve()
|
|
100
|
+
private closing: Promise<void> | undefined
|
|
101
|
+
private readonly startupTimer: ReturnType<typeof setTimeout>
|
|
102
|
+
|
|
103
|
+
constructor(private readonly socket: Socket, private readonly input: BrokerInput) {
|
|
104
|
+
this.client = new CodexJsonRpcClient(input.executable, {
|
|
105
|
+
cwd: input.cwd,
|
|
106
|
+
environment: input.environment,
|
|
107
|
+
maxLineBytes: MAX_BYTES,
|
|
108
|
+
onStderr: () => undefined,
|
|
109
|
+
onInvalidLine: () => {
|
|
110
|
+
void this.close()
|
|
111
|
+
},
|
|
112
|
+
onMessage: (message) => {
|
|
113
|
+
if (!isCodexServerRequest(message))
|
|
114
|
+
return
|
|
115
|
+
if (message.method !== 'account/chatgptAuthTokens/refresh') {
|
|
116
|
+
this.client.respondError(message.id, { code: -32601, message: 'Unsupported request.' })
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
this.queuedRefreshes += 1
|
|
120
|
+
if (this.queuedRefreshes > 8) {
|
|
121
|
+
void this.close()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
this.refreshQueue = this.refreshQueue.then(async () => {
|
|
125
|
+
this.abort.signal.throwIfAborted()
|
|
126
|
+
if (!this.tokens || message.params.reason !== 'unauthorized')
|
|
127
|
+
throw brokerFailure()
|
|
128
|
+
const refreshed = await this.deadline(input.auth.refreshAuthTokens(toAuthRefreshInput(message.params, this.tokens)))
|
|
129
|
+
if (this.abort.signal.aborted)
|
|
130
|
+
return
|
|
131
|
+
this.tokens = refreshed
|
|
132
|
+
this.client.respond(message.id, toCodexRefreshResponse(refreshed))
|
|
133
|
+
}).catch(() => {
|
|
134
|
+
void this.close()
|
|
135
|
+
}).finally(() => { this.queuedRefreshes -= 1 })
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
this.startupTimer = setTimeout(() => {
|
|
139
|
+
void this.close()
|
|
140
|
+
}, DEADLINE_MS)
|
|
141
|
+
this.client.child.once('close', () => {
|
|
142
|
+
void this.close()
|
|
143
|
+
})
|
|
144
|
+
socket.on('error', () => {
|
|
145
|
+
void this.close()
|
|
146
|
+
})
|
|
147
|
+
socket.on('end', () => {
|
|
148
|
+
void this.close()
|
|
149
|
+
})
|
|
150
|
+
socket.on('data', (chunk: Buffer) => {
|
|
151
|
+
this.queuedBytes += chunk.length
|
|
152
|
+
if (this.queuedBytes > MAX_BYTES) {
|
|
153
|
+
void this.close()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
this.buffer = Buffer.concat([this.buffer, chunk])
|
|
157
|
+
let newline = this.buffer.indexOf(10)
|
|
158
|
+
while (newline !== -1) {
|
|
159
|
+
this.queuedFrames += 1
|
|
160
|
+
if (this.queuedFrames > 64) {
|
|
161
|
+
void this.close()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
const line = this.buffer.subarray(0, newline)
|
|
165
|
+
this.buffer = this.buffer.subarray(newline + 1)
|
|
166
|
+
newline = this.buffer.indexOf(10)
|
|
167
|
+
this.queue = this.queue.then(async () => {
|
|
168
|
+
if (!this.closing)
|
|
169
|
+
await this.deadline(this.handleLine(line))
|
|
170
|
+
}).catch(() => {
|
|
171
|
+
void this.close()
|
|
172
|
+
}).finally(() => {
|
|
173
|
+
this.queuedBytes -= line.length + 1
|
|
174
|
+
this.queuedFrames -= 1
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async close(): Promise<void> {
|
|
181
|
+
this.closing ??= (async () => {
|
|
182
|
+
clearTimeout(this.startupTimer)
|
|
183
|
+
this.tokens = undefined
|
|
184
|
+
this.abort.abort()
|
|
185
|
+
this.socket.destroy()
|
|
186
|
+
await this.client.stop()
|
|
187
|
+
this.buffer = Buffer.alloc(0)
|
|
188
|
+
})()
|
|
189
|
+
await this.closing
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async handleLine(line: Buffer): Promise<void> {
|
|
193
|
+
const raw: unknown = JSON.parse(line.toString('utf8'))
|
|
194
|
+
if (isInitializedNotification(raw))
|
|
195
|
+
return
|
|
196
|
+
const request = parseRequest(raw)
|
|
197
|
+
let result: unknown
|
|
198
|
+
if (request.method === 'initialize') {
|
|
199
|
+
if (this.initialized)
|
|
200
|
+
throw brokerFailure()
|
|
201
|
+
result = await this.client.request(request.method, request.params)
|
|
202
|
+
this.abort.signal.throwIfAborted()
|
|
203
|
+
this.client.notify('initialized', undefined)
|
|
204
|
+
const tokens = await this.input.auth.getCurrentTokens({ signal: this.abort.signal })
|
|
205
|
+
await this.installTokens(tokens)
|
|
206
|
+
this.initialized = true
|
|
207
|
+
clearTimeout(this.startupTimer)
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
if (!this.initialized)
|
|
211
|
+
throw brokerFailure()
|
|
212
|
+
try {
|
|
213
|
+
if (request.method === 'getAuthStatus') {
|
|
214
|
+
await this.prepareAuthStatus(request.params)
|
|
215
|
+
const response = await this.client.request(request.method, request.params)
|
|
216
|
+
if (!this.tokens)
|
|
217
|
+
throw brokerFailure()
|
|
218
|
+
result = projectCodexBrowserAuthStatus({ response, params: request.params, installedTokens: this.tokens })
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
result = await this.client.request(request.method, request.params)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// Native config errors (including version conflicts) are command results, not
|
|
226
|
+
// transport failures. Keep only the numeric code; messages/data may hold secrets.
|
|
227
|
+
if (request.method === 'getAuthStatus' || !(error instanceof CodexJsonRpcResponseError)
|
|
228
|
+
|| error.responseErrorCode === undefined || !Number.isSafeInteger(error.responseErrorCode)
|
|
229
|
+
|| error.responseErrorCode < -2147483648 || error.responseErrorCode > 2147483647) {
|
|
230
|
+
throw brokerFailure()
|
|
231
|
+
}
|
|
232
|
+
await this.writeResponse({ id: request.id, error: { code: error.responseErrorCode, message: 'Codex configuration request failed.' } })
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
await this.writeResponse({ id: request.id, result })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async prepareAuthStatus(params: ClientRequestParamsByMethod['getAuthStatus']): Promise<void> {
|
|
240
|
+
const current = await this.input.auth.getCurrentTokens({ signal: this.abort.signal })
|
|
241
|
+
await this.installTokens(current)
|
|
242
|
+
const installed = this.tokens
|
|
243
|
+
if (!installed)
|
|
244
|
+
throw brokerFailure()
|
|
245
|
+
if (params.refreshToken === true || codexAuthTokensExpireSoon(installed)) {
|
|
246
|
+
this.abort.signal.throwIfAborted()
|
|
247
|
+
const refreshed = await this.input.auth.refreshAuthTokens(toAuthRefreshInput({ reason: 'unauthorized' }, installed))
|
|
248
|
+
await this.installTokens(refreshed)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private async installTokens(tokens: CodexAuthTokens): Promise<void> {
|
|
253
|
+
this.abort.signal.throwIfAborted()
|
|
254
|
+
if (this.tokens?.chatgptAccountId === tokens.chatgptAccountId
|
|
255
|
+
&& this.tokens.accessTokenGeneration === tokens.accessTokenGeneration
|
|
256
|
+
&& this.tokens.accessTokenFingerprint === tokens.accessTokenFingerprint
|
|
257
|
+
&& (this.tokens.chatgptPlanType ?? null) === (tokens.chatgptPlanType ?? null)) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
this.tokens = tokens
|
|
261
|
+
await this.client.request('account/login/start', toCodexLoginParams(tokens))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async writeResponse(message: object): Promise<void> {
|
|
265
|
+
this.abort.signal.throwIfAborted()
|
|
266
|
+
const response = `${JSON.stringify(message)}\n`
|
|
267
|
+
if (Buffer.byteLength(response) > MAX_BYTES)
|
|
268
|
+
throw brokerFailure()
|
|
269
|
+
await new Promise<void>((resolve, reject) => this.socket.write(response, error => error ? reject(brokerFailure()) : resolve()))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async deadline<T>(operation: Promise<T>): Promise<T> {
|
|
273
|
+
this.abort.signal.throwIfAborted()
|
|
274
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
275
|
+
let rejectAbort: (() => void) | undefined
|
|
276
|
+
try {
|
|
277
|
+
return await Promise.race([
|
|
278
|
+
operation,
|
|
279
|
+
new Promise<never>((_, reject) => {
|
|
280
|
+
rejectAbort = () => reject(brokerFailure())
|
|
281
|
+
this.abort.signal.addEventListener('abort', rejectAbort, { once: true })
|
|
282
|
+
}),
|
|
283
|
+
new Promise<never>((_, reject) => { timer = setTimeout(() => reject(brokerFailure()), DEADLINE_MS) }),
|
|
284
|
+
])
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
clearTimeout(timer)
|
|
288
|
+
if (rejectAbort)
|
|
289
|
+
this.abort.signal.removeEventListener('abort', rejectAbort)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isInitializedNotification(raw: unknown): boolean {
|
|
295
|
+
return typeof raw === 'object' && raw !== null && 'method' in raw && raw.method === 'initialized' && !('id' in raw)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseRequest(raw: unknown): BrowserRequest {
|
|
299
|
+
if (typeof raw !== 'object' || raw === null || !('id' in raw) || !('method' in raw)
|
|
300
|
+
|| !(typeof raw.id === 'string' || (typeof raw.id === 'number' && Number.isSafeInteger(raw.id)))
|
|
301
|
+
|| typeof raw.method !== 'string'
|
|
302
|
+
|| !['initialize', 'getAuthStatus', 'configRequirements/read', 'config/read', 'config/value/write', 'config/batchWrite'].includes(raw.method)) {
|
|
303
|
+
throw brokerFailure()
|
|
304
|
+
}
|
|
305
|
+
if (raw.method === 'configRequirements/read') {
|
|
306
|
+
if ('params' in raw && raw.params !== null)
|
|
307
|
+
throw brokerFailure()
|
|
308
|
+
// Both native wire forms denote no payload; the generated client uses undefined.
|
|
309
|
+
return { id: raw.id, method: raw.method, params: undefined }
|
|
310
|
+
}
|
|
311
|
+
if (!('params' in raw) || typeof raw.params !== 'object' || raw.params === null || Array.isArray(raw.params))
|
|
312
|
+
throw brokerFailure()
|
|
313
|
+
// Raw Unix transport boundary only: Codex validates the generated native params;
|
|
314
|
+
// this broker validates framing, correlation and its narrow method policy.
|
|
315
|
+
return raw as BrowserRequest
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function brokerFailure(): AgentProviderProtocolError {
|
|
319
|
+
return new AgentProviderProtocolError({ message: 'Browser authentication connection failed.' })
|
|
320
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CodexAuthTokens } from './codex-auth'
|
|
2
|
+
import type { ClientRequestParamsByMethod, ClientRequestResponsesByMethod } from './codex-protocol'
|
|
3
|
+
import { Buffer } from 'node:buffer'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { AgentProviderProtocolError } from '../agent-runtime-error'
|
|
6
|
+
|
|
7
|
+
const externalAuthStatusSchema = z.object({
|
|
8
|
+
authMethod: z.literal('chatgptAuthTokens'),
|
|
9
|
+
authToken: z.string().nullable(),
|
|
10
|
+
requiresOpenaiAuth: z.boolean().nullable(),
|
|
11
|
+
}).passthrough() satisfies z.ZodType<ClientRequestResponsesByMethod['getAuthStatus']>
|
|
12
|
+
|
|
13
|
+
const accountClaimsNamespace = 'https://api.openai.com/auth'
|
|
14
|
+
const browserAccountClaimsSchema = z.object({
|
|
15
|
+
[accountClaimsNamespace]: z.object({
|
|
16
|
+
chatgpt_account_id: z.string().refine(value => value.trim().length > 0),
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/** Pinned Browser Use accepts the legacy label only; native auth and refresh stay external. */
|
|
21
|
+
export function projectCodexBrowserAuthStatus(input: {
|
|
22
|
+
response: ClientRequestResponsesByMethod['getAuthStatus']
|
|
23
|
+
params: ClientRequestParamsByMethod['getAuthStatus']
|
|
24
|
+
installedTokens: CodexAuthTokens
|
|
25
|
+
}): ClientRequestResponsesByMethod['getAuthStatus'] {
|
|
26
|
+
// The generated client types the RPC result; validate the actual native wire value here.
|
|
27
|
+
const validated = externalAuthStatusSchema.safeParse(input.response)
|
|
28
|
+
if (!validated.success)
|
|
29
|
+
throw invalidBrowserAuthStatus()
|
|
30
|
+
const expectedToken = input.params.includeToken === true ? input.installedTokens.accessToken : null
|
|
31
|
+
if (validated.data.authToken !== expectedToken || !installedAccountMatchesToken(input.installedTokens))
|
|
32
|
+
throw invalidBrowserAuthStatus()
|
|
33
|
+
// Preserve the exact native token, auth requirement, and extension fields. This is not
|
|
34
|
+
// a claim that Codex owns refresh, nor a change to JWT claims or remote authorization.
|
|
35
|
+
return { ...input.response, authMethod: 'chatgpt' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function installedAccountMatchesToken(tokens: CodexAuthTokens): boolean {
|
|
39
|
+
const parts = tokens.accessToken.split('.')
|
|
40
|
+
if (parts.length !== 3 || parts.some(part => part.length === 0 || /[^\w-]/.test(part)))
|
|
41
|
+
return false
|
|
42
|
+
try {
|
|
43
|
+
// Account consistency only, not signature verification. The native consumer uses
|
|
44
|
+
// this original claim for its account header; never supply a fallback or rewrite it.
|
|
45
|
+
const claims = browserAccountClaimsSchema.safeParse(JSON.parse(Buffer.from(parts[1]!, 'base64url').toString('utf8')))
|
|
46
|
+
return claims.success && claims.data[accountClaimsNamespace].chatgpt_account_id === tokens.chatgptAccountId
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function invalidBrowserAuthStatus(): AgentProviderProtocolError {
|
|
54
|
+
return new AgentProviderProtocolError({ message: 'Browser authentication status is invalid.' })
|
|
55
|
+
}
|
|
@@ -11,13 +11,18 @@ interface CodexUserInputMapperInput {
|
|
|
11
11
|
origin?: 'user' | 'supervisor'
|
|
12
12
|
author?: AgentAuthor
|
|
13
13
|
senderContext?: AgentSenderContext
|
|
14
|
+
authorContext?: string
|
|
15
|
+
// The session's one-time context block, carried by its first prompt only.
|
|
16
|
+
sessionContext?: string
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function toCodexUserInput(input: CodexUserInputMapperInput): CodexUserInput[] {
|
|
17
|
-
const { prompt, origin = 'user', author, senderContext } = input
|
|
18
|
-
const blocks = prompt.blocks.map(toCodexInputBlock)
|
|
20
|
+
const { prompt, origin = 'user', author, senderContext, authorContext, sessionContext } = input
|
|
21
|
+
const blocks: CodexUserInput[] = prompt.blocks.map(toCodexInputBlock)
|
|
22
|
+
if (sessionContext)
|
|
23
|
+
blocks.unshift({ type: 'text', text: sessionContext, text_elements: [] })
|
|
19
24
|
return origin === 'user' && author
|
|
20
|
-
? [{ type: 'text', text: renderAuthorContext(author, senderContext), text_elements: [] }, ...blocks]
|
|
25
|
+
? [{ type: 'text', text: authorContext ?? renderAuthorContext(author, senderContext), text_elements: [] }, ...blocks]
|
|
21
26
|
: blocks
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -65,6 +70,36 @@ export function codexEffort(effort: AgentReasoningEffort | undefined): CodexTurn
|
|
|
65
70
|
return effort
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
export const codexReasoningSummary: CodexTurnStartParams['summary'] = 'auto'
|
|
74
|
+
|
|
75
|
+
// A type alias: the generated thread config is an index-signature record, which interfaces don't satisfy.
|
|
76
|
+
// eslint-disable-next-line ts/consistent-type-definitions
|
|
77
|
+
type CodexThreadConfig = {
|
|
78
|
+
'tools.update_plan.enabled': true
|
|
79
|
+
'features.step_model_switching'?: true
|
|
80
|
+
'model_reasoning_effort'?: CodexTurnStartParams['effort']
|
|
81
|
+
'model_reasoning_summary': CodexTurnStartParams['summary']
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Codex prewarms each thread by sending a baseline request over its WebSocket right after
|
|
85
|
+
// thread/start, using the thread's configured effort and summary. The first turn/start reuses that
|
|
86
|
+
// baseline incrementally (previous_response_id + delta items) only when its reasoning settings match;
|
|
87
|
+
// otherwise Codex logs "incremental request failed, websocket reuse properties didn't match" and
|
|
88
|
+
// resends the whole prompt. Configure the thread with exactly what every turn sends.
|
|
89
|
+
//
|
|
90
|
+
// `features.step_model_switching` enables `turn/settings/update`, which a steer uses to change the
|
|
91
|
+
// running turn's model/effort; without it the app-server answers -32600. The feature is experimental
|
|
92
|
+
// upstream and pinned by the sandbox image's Codex version: re-verify it on every Codex bump.
|
|
93
|
+
export function codexThreadConfig(effort: AgentReasoningEffort | undefined, stepModelSwitching = false): CodexThreadConfig {
|
|
94
|
+
const reasoningEffort = codexEffort(effort)
|
|
95
|
+
return {
|
|
96
|
+
'tools.update_plan.enabled': true,
|
|
97
|
+
...(stepModelSwitching ? { 'features.step_model_switching': true as const } : {}),
|
|
98
|
+
...(reasoningEffort === undefined ? {} : { model_reasoning_effort: reasoningEffort }),
|
|
99
|
+
'model_reasoning_summary': codexReasoningSummary,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
68
103
|
function toCodexInputBlock(block: AgentInputBlock): CodexUserInput {
|
|
69
104
|
if (block.type === 'text') {
|
|
70
105
|
return { type: 'text', text: block.text, text_elements: [] }
|