icebear0828 Claude Opus 4.6 commited on
Commit
d6c3bb0
·
1 Parent(s): 1dcf4e8

refactor: architecture audit fixes round 2 (P0-P2)

Browse files

- Fix curl subprocess leak on client disconnect (abort signal propagation)
- Cap SSE parse buffer at 10MB to prevent memory exhaustion
- Fatal error triggers graceful shutdown instead of hard exit
- Session hash: 128-bit + JSON.stringify to prevent collisions
- Gemini SSE line endings normalized to \n\n
- Extract shared translation utils (desktop context, instructions, budgetToEffort)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

src/index.ts CHANGED
@@ -150,5 +150,7 @@ async function main() {
150
 
151
  main().catch((err) => {
152
  console.error("Fatal error:", err);
153
- process.exit(1);
 
 
154
  });
 
150
 
151
  main().catch((err) => {
152
  console.error("Fatal error:", err);
153
+ // Trigger graceful shutdown instead of hard exit
154
+ process.kill(process.pid, "SIGTERM");
155
+ setTimeout(() => process.exit(1), 2000).unref();
156
  });
src/proxy/codex-api.ts CHANGED
@@ -356,6 +356,7 @@ export class CodexApi {
356
  .pipeThrough(new TextDecoderStream())
357
  .getReader();
358
 
 
359
  let buffer = "";
360
  try {
361
  while (true) {
@@ -363,6 +364,9 @@ export class CodexApi {
363
  if (done) break;
364
 
365
  buffer += value;
 
 
 
366
  const parts = buffer.split("\n\n");
367
  buffer = parts.pop()!;
368
 
 
356
  .pipeThrough(new TextDecoderStream())
357
  .getReader();
358
 
359
+ const MAX_SSE_BUFFER = 10 * 1024 * 1024; // 10MB
360
  let buffer = "";
361
  try {
362
  while (true) {
 
364
  if (done) break;
365
 
366
  buffer += value;
367
+ if (buffer.length > MAX_SSE_BUFFER) {
368
+ throw new Error(`SSE buffer exceeded ${MAX_SSE_BUFFER} bytes — aborting stream`);
369
+ }
370
  const parts = buffer.split("\n\n");
371
  buffer = parts.pop()!;
372
 
src/routes/shared/proxy-handler.ts CHANGED
@@ -91,6 +91,7 @@ export async function handleProxyRequest(
91
 
92
  // P0-2: AbortController to kill curl when client disconnects
93
  const abortController = new AbortController();
 
94
 
95
  try {
96
  // 3. Retry + send to Codex
@@ -106,6 +107,7 @@ export async function handleProxyRequest(
106
  c.header("Connection", "keep-alive");
107
 
108
  return stream(c, async (s) => {
 
109
  let sessionTaskId: string | null = null;
110
  try {
111
  for await (const chunk of fmt.streamTranslator(
 
91
 
92
  // P0-2: AbortController to kill curl when client disconnects
93
  const abortController = new AbortController();
94
+ c.req.raw.signal.addEventListener("abort", () => abortController.abort(), { once: true });
95
 
96
  try {
97
  // 3. Retry + send to Codex
 
107
  c.header("Connection", "keep-alive");
108
 
109
  return stream(c, async (s) => {
110
+ s.onAbort(() => abortController.abort());
111
  let sessionTaskId: string | null = null;
112
  try {
113
  for await (const chunk of fmt.streamTranslator(
src/session/manager.ts CHANGED
@@ -36,8 +36,8 @@ export class SessionManager {
36
  hashMessages(
37
  messages: Array<{ role: string; content: string }>,
38
  ): string {
39
- const data = messages.map((m) => `${m.role}:${m.content}`).join("|");
40
- return createHash("sha256").update(data).digest("hex").slice(0, 16);
41
  }
42
 
43
  /**
 
36
  hashMessages(
37
  messages: Array<{ role: string; content: string }>,
38
  ): string {
39
+ const data = JSON.stringify(messages.map((m) => [m.role, m.content]));
40
+ return createHash("sha256").update(data).digest("hex").slice(0, 32);
41
  }
42
 
43
  /**
src/translation/anthropic-to-codex.ts CHANGED
@@ -2,8 +2,6 @@
2
  * Translate Anthropic Messages API request → Codex Responses API request.
3
  */
4
 
5
- import { readFileSync } from "fs";
6
- import { resolve } from "path";
7
  import type { AnthropicMessagesRequest } from "../types/anthropic.js";
8
  import type {
9
  CodexResponsesRequest,
@@ -11,19 +9,7 @@ import type {
11
  } from "../proxy/codex-api.js";
12
  import { resolveModelId, getModelInfo } from "../routes/models.js";
13
  import { getConfig } from "../config.js";
14
-
15
- const DESKTOP_CONTEXT = loadDesktopContext();
16
-
17
- function loadDesktopContext(): string {
18
- try {
19
- return readFileSync(
20
- resolve(process.cwd(), "config/prompts/desktop-context.md"),
21
- "utf-8",
22
- );
23
- } catch {
24
- return "";
25
- }
26
- }
27
 
28
  /**
29
  * Map Anthropic thinking budget_tokens to Codex reasoning effort.
@@ -32,11 +18,7 @@ function mapThinkingToEffort(
32
  thinking: AnthropicMessagesRequest["thinking"],
33
  ): string | undefined {
34
  if (!thinking || thinking.type === "disabled") return undefined;
35
- const budget = thinking.budget_tokens;
36
- if (budget < 2000) return "low";
37
- if (budget < 8000) return "medium";
38
- if (budget < 20000) return "high";
39
- return "xhigh";
40
  }
41
 
42
  /**
@@ -76,9 +58,7 @@ export function translateAnthropicToCodexRequest(
76
  } else {
77
  userInstructions = "You are a helpful assistant.";
78
  }
79
- const instructions = DESKTOP_CONTEXT
80
- ? `${DESKTOP_CONTEXT}\n\n${userInstructions}`
81
- : userInstructions;
82
 
83
  // Build input items from messages
84
  const input: CodexInputItem[] = [];
 
2
  * Translate Anthropic Messages API request → Codex Responses API request.
3
  */
4
 
 
 
5
  import type { AnthropicMessagesRequest } from "../types/anthropic.js";
6
  import type {
7
  CodexResponsesRequest,
 
9
  } from "../proxy/codex-api.js";
10
  import { resolveModelId, getModelInfo } from "../routes/models.js";
11
  import { getConfig } from "../config.js";
12
+ import { buildInstructions, budgetToEffort } from "./shared-utils.js";
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  /**
15
  * Map Anthropic thinking budget_tokens to Codex reasoning effort.
 
18
  thinking: AnthropicMessagesRequest["thinking"],
19
  ): string | undefined {
20
  if (!thinking || thinking.type === "disabled") return undefined;
21
+ return budgetToEffort(thinking.budget_tokens);
 
 
 
 
22
  }
23
 
24
  /**
 
58
  } else {
59
  userInstructions = "You are a helpful assistant.";
60
  }
61
+ const instructions = buildInstructions(userInstructions);
 
 
62
 
63
  // Build input items from messages
64
  const input: CodexInputItem[] = [];
src/translation/codex-to-gemini.ts CHANGED
@@ -53,7 +53,7 @@ export async function* streamCodexToGemini(
53
  ],
54
  modelVersion: model,
55
  };
56
- yield `data: ${JSON.stringify(chunk)}\r\n\r\n`;
57
  }
58
  break;
59
  }
@@ -84,7 +84,7 @@ export async function* streamCodexToGemini(
84
  },
85
  modelVersion: model,
86
  };
87
- yield `data: ${JSON.stringify(finalChunk)}\r\n\r\n`;
88
  break;
89
  }
90
  }
 
53
  ],
54
  modelVersion: model,
55
  };
56
+ yield `data: ${JSON.stringify(chunk)}\n\n`;
57
  }
58
  break;
59
  }
 
84
  },
85
  modelVersion: model,
86
  };
87
+ yield `data: ${JSON.stringify(finalChunk)}\n\n`;
88
  break;
89
  }
90
  }
src/translation/gemini-to-codex.ts CHANGED
@@ -2,8 +2,6 @@
2
  * Translate Google Gemini generateContent request → Codex Responses API request.
3
  */
4
 
5
- import { readFileSync } from "fs";
6
- import { resolve } from "path";
7
  import type {
8
  GeminiGenerateContentRequest,
9
  GeminiContent,
@@ -14,30 +12,7 @@ import type {
14
  } from "../proxy/codex-api.js";
15
  import { resolveModelId, getModelInfo } from "../routes/models.js";
16
  import { getConfig } from "../config.js";
17
-
18
- const DESKTOP_CONTEXT = loadDesktopContext();
19
-
20
- function loadDesktopContext(): string {
21
- try {
22
- return readFileSync(
23
- resolve(process.cwd(), "config/prompts/desktop-context.md"),
24
- "utf-8",
25
- );
26
- } catch {
27
- return "";
28
- }
29
- }
30
-
31
- /**
32
- * Map Gemini thinkingBudget to Codex reasoning effort.
33
- */
34
- function budgetToEffort(budget?: number): string | undefined {
35
- if (!budget || budget <= 0) return undefined;
36
- if (budget < 2000) return "low";
37
- if (budget < 8000) return "medium";
38
- if (budget < 20000) return "high";
39
- return "xhigh";
40
- }
41
 
42
  /**
43
  * Extract text from Gemini content parts.
@@ -96,9 +71,7 @@ export function translateGeminiToCodexRequest(
96
  } else {
97
  userInstructions = "You are a helpful assistant.";
98
  }
99
- const instructions = DESKTOP_CONTEXT
100
- ? `${DESKTOP_CONTEXT}\n\n${userInstructions}`
101
- : userInstructions;
102
 
103
  // Build input items from contents
104
  const input: CodexInputItem[] = [];
 
2
  * Translate Google Gemini generateContent request → Codex Responses API request.
3
  */
4
 
 
 
5
  import type {
6
  GeminiGenerateContentRequest,
7
  GeminiContent,
 
12
  } from "../proxy/codex-api.js";
13
  import { resolveModelId, getModelInfo } from "../routes/models.js";
14
  import { getConfig } from "../config.js";
15
+ import { buildInstructions, budgetToEffort } from "./shared-utils.js";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  /**
18
  * Extract text from Gemini content parts.
 
71
  } else {
72
  userInstructions = "You are a helpful assistant.";
73
  }
74
+ const instructions = buildInstructions(userInstructions);
 
 
75
 
76
  // Build input items from contents
77
  const input: CodexInputItem[] = [];
src/translation/openai-to-codex.ts CHANGED
@@ -2,8 +2,6 @@
2
  * Translate OpenAI Chat Completions request → Codex Responses API request.
3
  */
4
 
5
- import { readFileSync } from "fs";
6
- import { resolve } from "path";
7
  import type { ChatCompletionRequest } from "../types/openai.js";
8
  import type {
9
  CodexResponsesRequest,
@@ -11,19 +9,7 @@ import type {
11
  } from "../proxy/codex-api.js";
12
  import { resolveModelId, getModelInfo } from "../routes/models.js";
13
  import { getConfig } from "../config.js";
14
-
15
- const DESKTOP_CONTEXT = loadDesktopContext();
16
-
17
- function loadDesktopContext(): string {
18
- try {
19
- return readFileSync(
20
- resolve(process.cwd(), "config/prompts/desktop-context.md"),
21
- "utf-8",
22
- );
23
- } catch {
24
- return "";
25
- }
26
- }
27
 
28
  /**
29
  * Convert a ChatCompletionRequest to a CodexResponsesRequest.
@@ -43,9 +29,7 @@ export function translateToCodexRequest(
43
  const userInstructions =
44
  systemMessages.map((m) => m.content).join("\n\n") ||
45
  "You are a helpful assistant.";
46
- const instructions = DESKTOP_CONTEXT
47
- ? `${DESKTOP_CONTEXT}\n\n${userInstructions}`
48
- : userInstructions;
49
 
50
  // Build input items from non-system messages
51
  const input: CodexInputItem[] = [];
 
2
  * Translate OpenAI Chat Completions request → Codex Responses API request.
3
  */
4
 
 
 
5
  import type { ChatCompletionRequest } from "../types/openai.js";
6
  import type {
7
  CodexResponsesRequest,
 
9
  } from "../proxy/codex-api.js";
10
  import { resolveModelId, getModelInfo } from "../routes/models.js";
11
  import { getConfig } from "../config.js";
12
+ import { buildInstructions } from "./shared-utils.js";
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  /**
15
  * Convert a ChatCompletionRequest to a CodexResponsesRequest.
 
29
  const userInstructions =
30
  systemMessages.map((m) => m.content).join("\n\n") ||
31
  "You are a helpful assistant.";
32
+ const instructions = buildInstructions(userInstructions);
 
 
33
 
34
  // Build input items from non-system messages
35
  const input: CodexInputItem[] = [];
src/translation/shared-utils.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Shared utilities for request translation modules.
3
+ *
4
+ * Deduplicates: desktop context loading, instruction building, budget→effort mapping.
5
+ */
6
+
7
+ import { readFileSync } from "fs";
8
+ import { resolve } from "path";
9
+
10
+ let cachedDesktopContext: string | null = null;
11
+
12
+ /**
13
+ * Lazily load and cache the desktop context prompt.
14
+ * File is maintained by apply-update.ts; cached once per process lifetime.
15
+ */
16
+ export function getDesktopContext(): string {
17
+ if (cachedDesktopContext !== null) return cachedDesktopContext;
18
+ try {
19
+ cachedDesktopContext = readFileSync(
20
+ resolve(process.cwd(), "config/prompts/desktop-context.md"),
21
+ "utf-8",
22
+ );
23
+ } catch {
24
+ cachedDesktopContext = "";
25
+ }
26
+ return cachedDesktopContext;
27
+ }
28
+
29
+ /**
30
+ * Assemble final instructions from desktop context + user instructions.
31
+ */
32
+ export function buildInstructions(userInstructions: string): string {
33
+ const ctx = getDesktopContext();
34
+ return ctx ? `${ctx}\n\n${userInstructions}` : userInstructions;
35
+ }
36
+
37
+ /**
38
+ * Map a token budget (e.g. Anthropic thinking.budget_tokens or Gemini thinkingBudget)
39
+ * to a Codex reasoning effort level.
40
+ */
41
+ export function budgetToEffort(budget: number | undefined): string | undefined {
42
+ if (!budget || budget <= 0) return undefined;
43
+ if (budget < 2000) return "low";
44
+ if (budget < 8000) return "medium";
45
+ if (budget < 20000) return "high";
46
+ return "xhigh";
47
+ }