@bitkyc08/opencodex 2.13.0 → 2.14.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/gui/dist/assets/index-Co12XTT-.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +5 -1
- package/src/adapters/cursor/request-builder.ts +3 -3
- package/src/adapters/google.ts +25 -5
- package/src/adapters/openai-chat.ts +182 -6
- package/src/adapters/openai-responses.ts +17 -7
- package/src/codex/catalog/bundled.ts +16 -0
- package/src/codex/catalog/metadata.ts +180 -5
- package/src/codex/catalog/parsing.ts +7 -6
- package/src/codex/catalog/sync.ts +73 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence.ts +20 -0
- package/src/codex/prompt-journal.ts +50 -13
- package/src/codex/prompt-layers.ts +1 -1
- package/src/config.ts +57 -0
- package/src/generated/compatibility-version.json +64 -48
- package/src/generated/model-metadata.ts +3 -3
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/oauth/login-cli.ts +52 -26
- package/src/providers/derive.ts +2 -0
- package/src/providers/openai-sidecar.ts +9 -2
- package/src/providers/quota.ts +57 -0
- package/src/providers/registry.ts +53 -25
- package/src/responses/state.ts +22 -0
- package/src/router.ts +1 -0
- package/src/server/claude-messages.ts +57 -11
- package/src/server/direct-local-http.ts +7 -3
- package/src/server/images.ts +6 -0
- package/src/server/index.ts +51 -11
- package/src/server/live.ts +117 -13
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/config-routes.ts +20 -3
- package/src/server/management/logs-usage-routes.ts +28 -0
- package/src/server/management/model-routes.ts +11 -3
- package/src/server/management/model-rows.ts +18 -3
- package/src/server/management/provider-routes.ts +107 -3
- package/src/server/management-auth.ts +65 -1
- package/src/server/proxy-liveness.ts +1 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +460 -0
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core.ts +142 -6
- package/src/server/responses/encrypted-payload.ts +4 -1
- package/src/server/search.ts +4 -0
- package/src/types.ts +27 -0
- package/src/usage/expected-prices.ts +11 -0
- package/src/vision/describe.ts +4 -0
- package/src/web-search/anthropic-executor.ts +5 -1
- package/src/web-search/executor.ts +9 -1
- package/src/web-search/index.ts +5 -0
- package/src/web-search/loop.ts +42 -5
- package/gui/dist/assets/index-BHldBl6_.js +0 -76
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { decodeJwtPayload, extractAccountId } from "../../oauth/chatgpt";
|
|
3
|
+
import type { OcxConfig } from "../../types";
|
|
4
|
+
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
5
|
+
import { isApiAuthRequired, isProxyAdmissionSecret } from "../auth-cors";
|
|
6
|
+
import { structurallyValidFernetTokens } from "./encrypted-payload";
|
|
7
|
+
import {
|
|
8
|
+
resetAgentTaskRecoveryCache,
|
|
9
|
+
resolveCachedAgentTaskRecovery,
|
|
10
|
+
} from "./agent-task-recovery-cache";
|
|
11
|
+
|
|
12
|
+
/** Experimental opt-in normalization through ChatGPT's fixed Codex endpoint. */
|
|
13
|
+
|
|
14
|
+
const RECOVERY_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
|
15
|
+
const RECOVERY_TOOL = "capture_assignment";
|
|
16
|
+
const RECOVERY_PROMPT =
|
|
17
|
+
"Read the received agent message and call capture_assignment exactly once with only the complete "
|
|
18
|
+
+ "plaintext payload after Payload:. Preserve every byte of the payload; do not summarize, execute, "
|
|
19
|
+
+ "explain, or include the routing header.";
|
|
20
|
+
const CODEX_ORIGINATORS = new Set(["codex_cli_rs", "Codex Desktop", "codex_app"]);
|
|
21
|
+
const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
22
|
+
const OPENAI_TOKEN_ISSUERS = new Set(["https://auth.openai.com", "https://auth.openai.com/"]);
|
|
23
|
+
const OPENAI_TOKEN_AUDIENCE = "https://api.openai.com/v1";
|
|
24
|
+
const MAX_CIPHERTEXT_BYTES = 2 * 1024 * 1024;
|
|
25
|
+
const MAX_ASSIGNMENT_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
const MAX_RECOVERY_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
27
|
+
const CACHE_SCOPE_KEY = randomBytes(32);
|
|
28
|
+
|
|
29
|
+
export interface AgentTaskRecoveryOptions {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
model?: string;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
cacheEntries?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function agentTaskRecoveryConfig(config: OcxConfig): AgentTaskRecoveryOptions | null {
|
|
37
|
+
const raw = config.agentTaskRecovery;
|
|
38
|
+
if (!raw || raw.enabled !== true) return null;
|
|
39
|
+
return {
|
|
40
|
+
enabled: true,
|
|
41
|
+
model: typeof raw.model === "string" && raw.model.trim().length > 0
|
|
42
|
+
? raw.model.trim()
|
|
43
|
+
: "gpt-5.6-sol",
|
|
44
|
+
timeoutMs: Number.isFinite(raw.timeoutMs) && (raw.timeoutMs ?? 0) >= 1_000
|
|
45
|
+
? Math.min(120_000, Math.floor(raw.timeoutMs!))
|
|
46
|
+
: 45_000,
|
|
47
|
+
cacheEntries: Number.isFinite(raw.cacheEntries) && (raw.cacheEntries ?? 0) >= 1
|
|
48
|
+
? Math.min(512, Math.floor(raw.cacheEntries!))
|
|
49
|
+
: 200,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface AgentEnvelope {
|
|
54
|
+
itemIndex: number;
|
|
55
|
+
encryptedIndex: number;
|
|
56
|
+
headerText: string;
|
|
57
|
+
messageType: "NEW_TASK";
|
|
58
|
+
taskName: string;
|
|
59
|
+
sender: string;
|
|
60
|
+
ciphertext: string;
|
|
61
|
+
author: string;
|
|
62
|
+
recipient: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ROUTING_HEADER = /(?:^|\n)Message Type\s*:\s*(NEW_TASK)\s*\nTask name\s*:\s*(\S+)\s*\nSender\s*:\s*(\S+)\s*\nPayload\s*:\s*(?:\n|$)/;
|
|
66
|
+
|
|
67
|
+
function findEnvelope(input: unknown): AgentEnvelope | null {
|
|
68
|
+
if (!Array.isArray(input)) return null;
|
|
69
|
+
let itemIndex = input.length - 1;
|
|
70
|
+
while (itemIndex >= 0) {
|
|
71
|
+
const type = input[itemIndex] && typeof input[itemIndex] === "object"
|
|
72
|
+
? (input[itemIndex] as { type?: unknown }).type
|
|
73
|
+
: undefined;
|
|
74
|
+
if (type !== "compaction_trigger" && type !== "additional_tools") break;
|
|
75
|
+
itemIndex -= 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const item = input[itemIndex];
|
|
79
|
+
if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const content = (item as { content?: unknown }).content;
|
|
83
|
+
if (!Array.isArray(content)) return null;
|
|
84
|
+
|
|
85
|
+
let headerText: string | null = null;
|
|
86
|
+
let messageType: "NEW_TASK" | null = null;
|
|
87
|
+
let taskName: string | null = null;
|
|
88
|
+
let sender: string | null = null;
|
|
89
|
+
let encryptedIndex = -1;
|
|
90
|
+
let ciphertext = "";
|
|
91
|
+
let encryptedPartCount = 0;
|
|
92
|
+
let ciphertextCount = 0;
|
|
93
|
+
|
|
94
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
95
|
+
const part = content[index] as { type?: unknown; text?: unknown; encrypted_content?: unknown } | null;
|
|
96
|
+
if (!part) continue;
|
|
97
|
+
if (
|
|
98
|
+
(part.type === "input_text" || part.type === "text")
|
|
99
|
+
&& typeof part.text === "string"
|
|
100
|
+
) {
|
|
101
|
+
const match = ROUTING_HEADER.exec(part.text);
|
|
102
|
+
if (match) {
|
|
103
|
+
if (headerText !== null) return null;
|
|
104
|
+
if (
|
|
105
|
+
part.text.slice(0, match.index).trim().length > 0
|
|
106
|
+
|| part.text.slice(match.index + match[0].length).trim().length > 0
|
|
107
|
+
) return null;
|
|
108
|
+
headerText = match[0].startsWith("\n") ? match[0].slice(1) : match[0];
|
|
109
|
+
messageType = "NEW_TASK";
|
|
110
|
+
taskName = match[2]!;
|
|
111
|
+
sender = match[3]!;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (part.type !== "encrypted_content" || typeof part.encrypted_content !== "string") continue;
|
|
115
|
+
encryptedPartCount += 1;
|
|
116
|
+
for (const token of structurallyValidFernetTokens(part.encrypted_content)) {
|
|
117
|
+
ciphertextCount += 1;
|
|
118
|
+
encryptedIndex = index;
|
|
119
|
+
ciphertext = token;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
!headerText
|
|
125
|
+
|| !messageType
|
|
126
|
+
|| !taskName
|
|
127
|
+
|| !sender
|
|
128
|
+
|| encryptedIndex < 0
|
|
129
|
+
|| encryptedPartCount !== 1
|
|
130
|
+
|| ciphertextCount !== 1
|
|
131
|
+
|| (content[encryptedIndex] as { encrypted_content?: unknown }).encrypted_content !== ciphertext
|
|
132
|
+
|| Buffer.byteLength(ciphertext) > MAX_CIPHERTEXT_BYTES
|
|
133
|
+
) return null;
|
|
134
|
+
|
|
135
|
+
const itemRecord = item as { author?: unknown; recipient?: unknown };
|
|
136
|
+
if (typeof itemRecord.author !== "string" || typeof itemRecord.recipient !== "string") return null;
|
|
137
|
+
if (itemRecord.author !== sender || itemRecord.recipient !== taskName) return null;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
itemIndex,
|
|
141
|
+
encryptedIndex,
|
|
142
|
+
headerText,
|
|
143
|
+
messageType,
|
|
144
|
+
taskName,
|
|
145
|
+
sender,
|
|
146
|
+
ciphertext,
|
|
147
|
+
author: itemRecord.author,
|
|
148
|
+
recipient: itemRecord.recipient,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function stripMatchingEnvelope(assignment: string, envelope: AgentEnvelope): string | null {
|
|
153
|
+
const match = ROUTING_HEADER.exec(assignment);
|
|
154
|
+
if (!match) return assignment;
|
|
155
|
+
if (match.index !== 0) return null;
|
|
156
|
+
if (
|
|
157
|
+
match[1] !== envelope.messageType
|
|
158
|
+
|| match[2] !== envelope.taskName
|
|
159
|
+
|| match[3] !== envelope.sender
|
|
160
|
+
) return null;
|
|
161
|
+
return assignment.slice(match[0].length);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateAssignment(assignment: unknown, envelope: AgentEnvelope): string | null {
|
|
165
|
+
if (typeof assignment !== "string") return null;
|
|
166
|
+
const payload = stripMatchingEnvelope(assignment, envelope);
|
|
167
|
+
if (payload === null || payload.trim().length === 0) return null;
|
|
168
|
+
if (Buffer.byteLength(payload) > MAX_ASSIGNMENT_BYTES) return null;
|
|
169
|
+
if (structurallyValidFernetTokens(payload).length > 0) return null;
|
|
170
|
+
return payload;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function injectAssignment(input: unknown, envelope: AgentEnvelope, assignment: string): boolean {
|
|
174
|
+
if (!Array.isArray(input)) return false;
|
|
175
|
+
const item = input[envelope.itemIndex];
|
|
176
|
+
if (!item || typeof item !== "object") return false;
|
|
177
|
+
const content = (item as { content?: unknown }).content;
|
|
178
|
+
if (!Array.isArray(content)) return false;
|
|
179
|
+
const part = content[envelope.encryptedIndex] as { type?: unknown; encrypted_content?: unknown } | undefined;
|
|
180
|
+
if (
|
|
181
|
+
!part
|
|
182
|
+
|| part.type !== "encrypted_content"
|
|
183
|
+
|| part.encrypted_content !== envelope.ciphertext
|
|
184
|
+
) return false;
|
|
185
|
+
|
|
186
|
+
content[envelope.encryptedIndex] = { type: "input_text", text: assignment };
|
|
187
|
+
const message = item as Record<string, unknown>;
|
|
188
|
+
message.type = "message";
|
|
189
|
+
message.role = "user";
|
|
190
|
+
delete message.id;
|
|
191
|
+
delete message.author;
|
|
192
|
+
delete message.recipient;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface RecoveryAdmission {
|
|
197
|
+
headers: Headers;
|
|
198
|
+
cacheScope: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isNativeChatGptAccessToken(token: string): boolean {
|
|
202
|
+
const segments = token.split(".");
|
|
203
|
+
if (segments.length !== 3 || !segments[0] || !segments[1] || !segments[2]) return false;
|
|
204
|
+
let header: Record<string, unknown>;
|
|
205
|
+
try {
|
|
206
|
+
header = JSON.parse(Buffer.from(segments[0], "base64url").toString("utf8")) as Record<string, unknown>;
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (header.alg !== "RS256" || header.typ !== "JWT" || typeof header.kid !== "string" || !header.kid) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const payload = decodeJwtPayload(token);
|
|
214
|
+
if (!payload || !OPENAI_TOKEN_ISSUERS.has(payload.iss as string)) return false;
|
|
215
|
+
const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
216
|
+
if (!audiences.includes(OPENAI_TOKEN_AUDIENCE)) return false;
|
|
217
|
+
if (payload.client_id !== CODEX_OAUTH_CLIENT_ID && payload.azp !== CODEX_OAUTH_CLIENT_ID) return false;
|
|
218
|
+
const now = Math.floor(Date.now() / 1_000);
|
|
219
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= now) return false;
|
|
220
|
+
if (
|
|
221
|
+
payload.nbf !== undefined
|
|
222
|
+
&& (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > now + 60)
|
|
223
|
+
) return false;
|
|
224
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
225
|
+
return !!auth && typeof auth === "object" && !Array.isArray(auth);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function recoveryAdmission(req: Request, config: OcxConfig): RecoveryAdmission | null {
|
|
229
|
+
if (isApiAuthRequired(config)) return null;
|
|
230
|
+
if (!CODEX_ORIGINATORS.has(req.headers.get("originator") ?? "")) return null;
|
|
231
|
+
// Remote/shared proxy admission is intentionally unsupported: caller-controlled
|
|
232
|
+
// Codex metadata is not strong enough to authorize use of a stored ChatGPT session.
|
|
233
|
+
if (req.headers.has("x-opencodex-api-key") || req.headers.has("x-api-key")) return null;
|
|
234
|
+
|
|
235
|
+
const authorization = req.headers.get("authorization")?.trim() ?? "";
|
|
236
|
+
const match = /^Bearer\s+(\S+)$/i.exec(authorization);
|
|
237
|
+
if (!match) return null;
|
|
238
|
+
const token = match[1]!;
|
|
239
|
+
if (isProxyAdmissionSecret(token, config)) return null;
|
|
240
|
+
if (!isNativeChatGptAccessToken(token)) return null;
|
|
241
|
+
const accountId = extractAccountId(undefined, token);
|
|
242
|
+
const explicitAccountId = req.headers.get("chatgpt-account-id")?.trim();
|
|
243
|
+
if (!accountId || !explicitAccountId || accountId !== explicitAccountId) return null;
|
|
244
|
+
|
|
245
|
+
const headers = new Headers({
|
|
246
|
+
authorization: `Bearer ${token}`,
|
|
247
|
+
"chatgpt-account-id": explicitAccountId,
|
|
248
|
+
"content-type": "application/json",
|
|
249
|
+
accept: "text/event-stream",
|
|
250
|
+
originator: req.headers.get("originator")!,
|
|
251
|
+
});
|
|
252
|
+
for (const name of ["openai-beta", "user-agent"]) {
|
|
253
|
+
const value = req.headers.get(name);
|
|
254
|
+
if (value) headers.set(name, value);
|
|
255
|
+
}
|
|
256
|
+
const cacheScope = createHmac("sha256", CACHE_SCOPE_KEY)
|
|
257
|
+
.update(token)
|
|
258
|
+
.update("\0")
|
|
259
|
+
.update(explicitAccountId)
|
|
260
|
+
.digest("hex");
|
|
261
|
+
return { headers, cacheScope };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function recoveryPayload(envelope: AgentEnvelope, model: string): string {
|
|
265
|
+
return JSON.stringify({
|
|
266
|
+
model,
|
|
267
|
+
stream: true,
|
|
268
|
+
store: false,
|
|
269
|
+
instructions: RECOVERY_PROMPT,
|
|
270
|
+
tools: [{
|
|
271
|
+
type: "function",
|
|
272
|
+
name: RECOVERY_TOOL,
|
|
273
|
+
description: "Return only the exact decrypted agent task payload.",
|
|
274
|
+
parameters: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: { assignment: { type: "string" } },
|
|
277
|
+
required: ["assignment"],
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
},
|
|
280
|
+
strict: true,
|
|
281
|
+
}],
|
|
282
|
+
tool_choice: { type: "function", name: RECOVERY_TOOL },
|
|
283
|
+
input: [{
|
|
284
|
+
type: "agent_message",
|
|
285
|
+
author: envelope.author,
|
|
286
|
+
recipient: envelope.recipient,
|
|
287
|
+
content: [
|
|
288
|
+
{ type: "input_text", text: envelope.headerText },
|
|
289
|
+
{ type: "encrypted_content", encrypted_content: envelope.ciphertext },
|
|
290
|
+
],
|
|
291
|
+
}],
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sseDataPayloads(raw: string): string[] {
|
|
296
|
+
const payloads: string[] = [];
|
|
297
|
+
let data: string[] = [];
|
|
298
|
+
const dispatch = (): void => {
|
|
299
|
+
if (data.length > 0) payloads.push(data.join("\n"));
|
|
300
|
+
data = [];
|
|
301
|
+
};
|
|
302
|
+
for (const line of raw.replace(/\r\n?/g, "\n").split("\n")) {
|
|
303
|
+
if (line === "") {
|
|
304
|
+
dispatch();
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (line.startsWith(":")) continue;
|
|
308
|
+
if (line === "data") {
|
|
309
|
+
data.push("");
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (!line.startsWith("data:")) continue;
|
|
313
|
+
const value = line.slice(5);
|
|
314
|
+
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
315
|
+
}
|
|
316
|
+
dispatch();
|
|
317
|
+
return payloads;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function assignmentFromRecoverySse(raw: string, envelope: AgentEnvelope): string | null {
|
|
321
|
+
let assignment: string | null = null;
|
|
322
|
+
let completed = false;
|
|
323
|
+
let terminalFailure = false;
|
|
324
|
+
let conflictingAssignments = false;
|
|
325
|
+
let malformedEvent = false;
|
|
326
|
+
let invalidAssignment = false;
|
|
327
|
+
for (const data of sseDataPayloads(raw)) {
|
|
328
|
+
if (!data || data === "[DONE]") continue;
|
|
329
|
+
let event: any;
|
|
330
|
+
try { event = JSON.parse(data); } catch {
|
|
331
|
+
malformedEvent = true;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (
|
|
335
|
+
event?.type === "response.failed"
|
|
336
|
+
|| event?.type === "response.incomplete"
|
|
337
|
+
|| event?.type === "error"
|
|
338
|
+
) terminalFailure = true;
|
|
339
|
+
if (event?.type === "response.completed" && event.response?.status === "completed") {
|
|
340
|
+
completed = true;
|
|
341
|
+
}
|
|
342
|
+
const items = event?.type === "response.output_item.done"
|
|
343
|
+
? [event.item]
|
|
344
|
+
: event?.type === "response.function_call_arguments.done"
|
|
345
|
+
? [{ type: "function_call", name: event.name, arguments: event.arguments }]
|
|
346
|
+
: event?.type === "response.completed"
|
|
347
|
+
? (Array.isArray(event.response?.output) ? event.response.output : []).filter((candidate: any) => (
|
|
348
|
+
candidate?.type === "function_call" && candidate?.name === RECOVERY_TOOL
|
|
349
|
+
))
|
|
350
|
+
: [];
|
|
351
|
+
for (const item of items) {
|
|
352
|
+
if (item?.type !== "function_call" || item.name !== RECOVERY_TOOL) continue;
|
|
353
|
+
let args: unknown = item.arguments;
|
|
354
|
+
if (typeof args === "string") {
|
|
355
|
+
try { args = JSON.parse(args); } catch {
|
|
356
|
+
invalidAssignment = true;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!args || typeof args !== "object") {
|
|
361
|
+
invalidAssignment = true;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const candidate = validateAssignment((args as { assignment?: unknown }).assignment, envelope);
|
|
365
|
+
if (candidate === null) {
|
|
366
|
+
invalidAssignment = true;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (assignment === null) assignment = candidate;
|
|
370
|
+
else if (assignment !== candidate) conflictingAssignments = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return completed && !terminalFailure && !conflictingAssignments && !malformedEvent && !invalidAssignment
|
|
374
|
+
? assignment
|
|
375
|
+
: null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function requestRecovery(
|
|
379
|
+
admission: RecoveryAdmission,
|
|
380
|
+
envelope: AgentEnvelope,
|
|
381
|
+
options: AgentTaskRecoveryOptions,
|
|
382
|
+
abortSignal?: AbortSignal,
|
|
383
|
+
): Promise<string | null> {
|
|
384
|
+
const controller = new AbortController();
|
|
385
|
+
const timeout = setTimeout(
|
|
386
|
+
() => controller.abort(new DOMException("Agent task recovery timed out", "TimeoutError")),
|
|
387
|
+
options.timeoutMs ?? 45_000,
|
|
388
|
+
);
|
|
389
|
+
const signal = abortSignal
|
|
390
|
+
? AbortSignal.any([abortSignal, controller.signal])
|
|
391
|
+
: controller.signal;
|
|
392
|
+
try {
|
|
393
|
+
const response = await fetch(RECOVERY_ENDPOINT, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers: admission.headers,
|
|
396
|
+
body: recoveryPayload(envelope, options.model ?? "gpt-5.6-sol"),
|
|
397
|
+
signal,
|
|
398
|
+
redirect: "error",
|
|
399
|
+
});
|
|
400
|
+
if (!response.ok) {
|
|
401
|
+
try { await response.body?.cancel(); } catch { /* already closed */ }
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const body = await readBoundedResponseBody(response, {
|
|
405
|
+
signal,
|
|
406
|
+
fatalUtf8: true,
|
|
407
|
+
maxBytes: MAX_RECOVERY_RESPONSE_BYTES,
|
|
408
|
+
totalTimeoutMs: options.timeoutMs ?? 45_000,
|
|
409
|
+
inactivityTimeoutMs: options.timeoutMs ?? 45_000,
|
|
410
|
+
firstByteTimeoutMs: options.timeoutMs ?? 45_000,
|
|
411
|
+
});
|
|
412
|
+
if (body.truncated || body.oversized || body.timedOut || !body.displaySafe) return null;
|
|
413
|
+
return assignmentFromRecoverySse(body.text, envelope);
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
} finally {
|
|
417
|
+
clearTimeout(timeout);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function recoverEncryptedAgentTask(
|
|
422
|
+
req: Request,
|
|
423
|
+
input: unknown,
|
|
424
|
+
options: AgentTaskRecoveryOptions,
|
|
425
|
+
config: OcxConfig,
|
|
426
|
+
context: { parentThreadId?: string | null; abortSignal?: AbortSignal } = {},
|
|
427
|
+
): Promise<boolean> {
|
|
428
|
+
const envelope = findEnvelope(input);
|
|
429
|
+
if (!envelope) return false;
|
|
430
|
+
// Admission is deliberately checked before cache access. A cache hit must not
|
|
431
|
+
// turn this process into a plaintext oracle for an unauthenticated caller.
|
|
432
|
+
const admission = recoveryAdmission(req, config);
|
|
433
|
+
if (!admission) return false;
|
|
434
|
+
|
|
435
|
+
const cacheKey = createHash("sha256")
|
|
436
|
+
.update(admission.cacheScope)
|
|
437
|
+
.update("\0")
|
|
438
|
+
.update(context.parentThreadId ?? "")
|
|
439
|
+
.update("\0")
|
|
440
|
+
.update(envelope.messageType)
|
|
441
|
+
.update("\0")
|
|
442
|
+
.update(envelope.taskName)
|
|
443
|
+
.update("\0")
|
|
444
|
+
.update(envelope.sender)
|
|
445
|
+
.update("\0")
|
|
446
|
+
.update(envelope.ciphertext)
|
|
447
|
+
.digest("hex");
|
|
448
|
+
const assignment = await resolveCachedAgentTaskRecovery(
|
|
449
|
+
cacheKey,
|
|
450
|
+
options.cacheEntries ?? 200,
|
|
451
|
+
signal => requestRecovery(admission, envelope, options, signal),
|
|
452
|
+
context.abortSignal,
|
|
453
|
+
);
|
|
454
|
+
if (!assignment || context.abortSignal?.aborted) return false;
|
|
455
|
+
return injectAssignment(input, envelope, assignment);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function resetAgentTaskRecoveryState(): void {
|
|
459
|
+
resetAgentTaskRecoveryCache();
|
|
460
|
+
}
|
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
} from "../../codex/upstream-host-health";
|
|
83
83
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
|
|
84
84
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
85
|
-
import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
85
|
+
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
86
86
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
87
87
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
88
88
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
@@ -392,7 +392,9 @@ export async function handleResponsesCompact(
|
|
|
392
392
|
}
|
|
393
393
|
throw err;
|
|
394
394
|
}
|
|
395
|
-
const base = (compactProvider
|
|
395
|
+
const base = isCanonicalOpenAiForwardProvider(compactProvider)
|
|
396
|
+
? CODEX_FORWARD_BASE_URL
|
|
397
|
+
: (compactProvider.baseUrl ?? "").replace(/\/+$/, "");
|
|
396
398
|
if (compactProvider.authMode !== "forward" && compactProvider.apiKey) {
|
|
397
399
|
headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
|
|
398
400
|
}
|