@bitkyc08/opencodex 2.7.42 → 2.7.43

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.
Files changed (143) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +13 -14
  3. package/gui/dist/assets/index-Czw-jpTU.css +1 -0
  4. package/gui/dist/assets/index-cmds12BG.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  10. package/src/adapters/cursor/protobuf-events.ts +24 -2
  11. package/src/adapters/cursor/protobuf-request.ts +1 -2
  12. package/src/adapters/cursor/tool-definitions.ts +68 -29
  13. package/src/adapters/google-wire-compiler.ts +4 -0
  14. package/src/adapters/google.ts +128 -2
  15. package/src/adapters/identity.ts +12 -2
  16. package/src/adapters/kiro.ts +49 -6
  17. package/src/adapters/mimo-free.ts +2 -0
  18. package/src/adapters/openai-responses.ts +246 -59
  19. package/src/claude/agents-inject.ts +5 -0
  20. package/src/claude/inbound.ts +26 -9
  21. package/src/cli/account-auth.ts +1 -1
  22. package/src/cli/agent-driven.ts +37 -0
  23. package/src/cli/claude.ts +3 -3
  24. package/src/cli/doctor.ts +23 -18
  25. package/src/cli/help.ts +42 -6
  26. package/src/cli/index.ts +88 -19
  27. package/src/cli/interactive-confirm.ts +129 -0
  28. package/src/cli/opencode.ts +701 -0
  29. package/src/cli/provider-runtime.ts +3 -0
  30. package/src/cli/provider.ts +31 -10
  31. package/src/cli/star-prompt.ts +55 -16
  32. package/src/cli/status.ts +47 -13
  33. package/src/codex/account-id.ts +34 -0
  34. package/src/codex/account-lifecycle.ts +4 -1
  35. package/src/codex/account-namespace-match.ts +63 -0
  36. package/src/codex/account-namespaces.ts +149 -0
  37. package/src/codex/account-pause.ts +20 -0
  38. package/src/codex/account-usability.ts +6 -1
  39. package/src/codex/app-server-processes.ts +511 -0
  40. package/src/codex/auth-api.ts +293 -34
  41. package/src/codex/auth-collision.ts +2 -1
  42. package/src/codex/auth-context.ts +60 -17
  43. package/src/codex/catalog/parsing.ts +16 -1
  44. package/src/codex/catalog/provider-fetch.ts +48 -12
  45. package/src/codex/catalog/sync.ts +18 -3
  46. package/src/codex/catalog.ts +1 -1
  47. package/src/codex/history-provider.ts +145 -1
  48. package/src/codex/inject.ts +114 -14
  49. package/src/codex/main-account.ts +2 -8
  50. package/src/codex/pool-rotation.ts +186 -0
  51. package/src/codex/quota.ts +15 -0
  52. package/src/codex/routing.ts +695 -106
  53. package/src/codex/shim.ts +4 -1
  54. package/src/codex/subagent-defaults.ts +550 -0
  55. package/src/codex/subagent-model-fallback.ts +2 -0
  56. package/src/codex/sync.ts +3 -0
  57. package/src/config.ts +566 -25
  58. package/src/images/artifacts.ts +516 -0
  59. package/src/images/fulfill-video.ts +163 -0
  60. package/src/images/fulfill.ts +111 -0
  61. package/src/images/index.ts +4 -0
  62. package/src/images/loop.ts +789 -0
  63. package/src/images/plan.ts +133 -0
  64. package/src/images/synthetic-tool.ts +133 -0
  65. package/src/images/types.ts +41 -0
  66. package/src/images/xai-client.ts +141 -0
  67. package/src/images/xai-video-client.ts +163 -0
  68. package/src/lib/admin-secrets.ts +25 -0
  69. package/src/lib/config-ownership.ts +327 -0
  70. package/src/lib/crash-guard.ts +2 -0
  71. package/src/lib/destination-policy.ts +122 -4
  72. package/src/lib/pinned-http.ts +151 -0
  73. package/src/lib/process-control.ts +2 -2
  74. package/src/lib/provider-outbound.ts +164 -0
  75. package/src/lib/provider-url.ts +14 -0
  76. package/src/lib/proxy-env.ts +18 -0
  77. package/src/lib/windows-elevation.ts +81 -3
  78. package/src/lib/windows-secret-acl.ts +189 -12
  79. package/src/lib/winsw.ts +2 -0
  80. package/src/oauth/anthropic-routing.ts +570 -0
  81. package/src/oauth/health.ts +6 -0
  82. package/src/oauth/index.ts +283 -72
  83. package/src/oauth/key-providers.ts +17 -6
  84. package/src/oauth/kimi.ts +2 -0
  85. package/src/oauth/kiro-credentials.ts +322 -10
  86. package/src/oauth/kiro.ts +423 -43
  87. package/src/oauth/login-cli.ts +32 -5
  88. package/src/oauth/store.ts +54 -4
  89. package/src/oauth/types.ts +11 -0
  90. package/src/providers/alibaba-region-migration.ts +16 -3
  91. package/src/providers/antigravity-models.ts +3 -0
  92. package/src/providers/api-keys.ts +13 -6
  93. package/src/providers/derive.ts +6 -0
  94. package/src/providers/key-failover.ts +24 -4
  95. package/src/providers/quota.ts +233 -29
  96. package/src/providers/registry.ts +11 -3
  97. package/src/responses/parser.ts +11 -0
  98. package/src/responses/state.ts +22 -8
  99. package/src/responses/tool-groups.ts +19 -0
  100. package/src/router.ts +14 -4
  101. package/src/server/auth-cors.ts +110 -22
  102. package/src/server/claude-messages.ts +8 -1
  103. package/src/server/gui-static.ts +30 -6
  104. package/src/server/images.ts +303 -9
  105. package/src/server/index.ts +77 -9
  106. package/src/server/lifecycle.ts +25 -1
  107. package/src/server/management/agent-settings-routes.ts +28 -4
  108. package/src/server/management/combo-routes.ts +7 -0
  109. package/src/server/management/config-routes.ts +5 -2
  110. package/src/server/management/logs-usage-routes.ts +167 -3
  111. package/src/server/management/oauth-account-routes.ts +163 -17
  112. package/src/server/management/provider-routes.ts +35 -7
  113. package/src/server/management/system-restart.ts +172 -0
  114. package/src/server/management/system-routes.ts +33 -10
  115. package/src/server/management-api.ts +2 -2
  116. package/src/server/management-auth.ts +216 -0
  117. package/src/server/proxy-liveness.ts +14 -3
  118. package/src/server/responses/compact.ts +21 -13
  119. package/src/server/responses/core.ts +583 -152
  120. package/src/server/responses-image-gen-repair.ts +118 -0
  121. package/src/server/responses-item-id-repair.ts +10 -85
  122. package/src/server/sse-payload-rewrite.ts +116 -0
  123. package/src/server/system-env.ts +28 -10
  124. package/src/service.ts +49 -2
  125. package/src/storage/cleanup-job.ts +57 -0
  126. package/src/storage/cleanup.ts +1504 -28
  127. package/src/storage/policy-job.ts +366 -0
  128. package/src/storage/policy-scheduler.ts +40 -0
  129. package/src/storage/policy-worker.ts +53 -0
  130. package/src/storage/policy.ts +522 -0
  131. package/src/storage/restore-job.ts +242 -0
  132. package/src/storage/restore-worker.ts +52 -0
  133. package/src/storage/storage-mutation-coordinator.ts +109 -0
  134. package/src/tray/windows.ts +2 -0
  135. package/src/types.ts +96 -1
  136. package/src/update/index.ts +36 -18
  137. package/src/update/job.ts +103 -12
  138. package/src/update/npm-invocation.d.mts +23 -0
  139. package/src/update/npm-invocation.mjs +94 -0
  140. package/src/usage/debug.ts +2 -0
  141. package/src/usage/log.ts +4 -0
  142. package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
  143. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-Bl_VBGoI.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-DfVGuN88.css">
19
+ <script type="module" crossorigin src="/assets/index-cmds12BG.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-Czw-jpTU.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.7.42",
3
+ "version": "2.7.43",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
package/src/AGENTS.md ADDED
@@ -0,0 +1,28 @@
1
+ # Source runtime instructions
2
+
3
+ This file applies to `src/` and inherits the repository-wide rules in `/AGENTS.md`.
4
+
5
+ ## Runtime and module rules
6
+
7
+ - `src/` is Bun-native TypeScript in strict mode and uses ES modules only.
8
+ - Do not assume a separate server compilation step.
9
+ - Prefer Bun and Web-platform APIs. Introduce a Node-only runtime dependency only when the task explicitly requires compatibility code and the owning module already has that role.
10
+ - Preserve existing public exports and configuration compatibility unless the task explicitly changes them.
11
+ - Read the applicable documents in `structure/` before changing shared routing, adapters, transports, sidecars, authentication, configuration, or server architecture.
12
+
13
+ ## Implementation rules
14
+
15
+ - Follow the existing subsystem boundaries and naming patterns.
16
+ - Do not combine unrelated responsibilities to avoid creating another large shared module.
17
+ - Handle asynchronous failures at request, transport, and sidecar boundaries. Optional integrations must degrade through the existing failure representation rather than crash the request path.
18
+ - Provider catalog metadata belongs in the canonical provider registry and derivation flow. Do not duplicate provider facts across independent pickers or seeds.
19
+ - Adapter changes must preserve the internal event contract, streaming behavior, tool calls, cancellation, error mapping, and image handling relevant to that adapter.
20
+ - Authentication, OAuth, token, credential, management API, and CORS changes are security-boundary changes.
21
+
22
+ ## Tests and validation
23
+
24
+ - Place focused regression coverage near the existing tests for the affected subsystem.
25
+ - For focused behavior, run the relevant `bun test tests/<name>.test.ts` and `bun run typecheck`.
26
+ - For shared routing, adapters, config, OAuth, or server behavior, also run `bun run test`.
27
+ - For logging, requests, credentials, account data, or fixtures, also run `bun run privacy:scan`.
28
+ - Update `docs-site/` when the change affects user-visible behavior or configuration.
@@ -250,6 +250,10 @@ function usesNativeAnthropicEndpoint(provider: OcxProviderConfig): boolean {
250
250
  }
251
251
  }
252
252
 
253
+ function anthropicKeyUsesBearer(provider: OcxProviderConfig): boolean {
254
+ return provider.apiKeyTransport === "bearer";
255
+ }
256
+
253
257
  /** Map a Responses reasoning effort to an Anthropic extended-thinking budget (tokens, >= 1024). */
254
258
  function reasoningBudget(effort: string): number {
255
259
  switch (effort) {
@@ -399,29 +403,33 @@ function messagesToAnthropicFormat(
399
403
  }
400
404
  case "assistant": {
401
405
  const aMsg = msg as OcxAssistantMessage;
402
- const content: unknown[] = [];
406
+ const preface: unknown[] = [];
407
+ const toolUses: unknown[] = [];
403
408
  const toolUseIds: string[] = [];
404
409
  for (const part of aMsg.content) {
405
410
  if (part.type === "text") {
406
411
  const text = (part as OcxTextContent).text;
407
- if (text) content.push({ type: "text", text });
412
+ if (text) preface.push({ type: "text", text });
408
413
  } else if (part.type === "thinking") {
409
414
  const t = part as OcxThinkingContent;
410
415
  // Redacted blocks replay verbatim FIRST (they preceded the visible thinking block
411
416
  // in the original stream order preserved by the bridge envelope).
412
417
  for (const data of t.redacted ?? []) {
413
- content.push({ type: "redacted_thinking", data });
418
+ preface.push({ type: "redacted_thinking", data });
414
419
  }
415
420
  if (isLikelyRealAnthropicThinkingSignature(t.signature)) {
416
- content.push({ type: "thinking", thinking: t.thinking, signature: t.signature });
421
+ preface.push({ type: "thinking", thinking: t.thinking, signature: t.signature });
417
422
  }
418
423
  } else if (part.type === "toolCall") {
419
424
  const tc = part as OcxToolCall;
420
425
  const flatName = namespacedToolName(tc.namespace, tc.name);
421
426
  toolUseIds.push(tc.id);
422
- content.push({ type: "tool_use", id: tc.id, name: toolNames.toWire(flatName), input: tc.arguments });
427
+ toolUses.push({ type: "tool_use", id: tc.id, name: toolNames.toWire(flatName), input: tc.arguments });
423
428
  }
424
429
  }
430
+ // Anthropic treats text/thinking after tool_use as ending the tool turn, which makes
431
+ // earlier tool_use ids look unpaired (#620 / common multi-step history shape).
432
+ const content = [...preface, ...toolUses];
425
433
  if (content.length === 0) break;
426
434
  messages.push({ role: "assistant", content });
427
435
  if (toolUseIds.length > 0) {
@@ -694,7 +702,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
694
702
  headers["X-Claude-Code-Session-Id"] = claudeCodeSessionId(provider.apiKey);
695
703
  headers["x-client-request-id"] = crypto.randomUUID();
696
704
  } else {
697
- headers["x-api-key"] = provider.apiKey;
705
+ if (anthropicKeyUsesBearer(provider)) headers["Authorization"] = `Bearer ${provider.apiKey}`;
706
+ else headers["x-api-key"] = provider.apiKey;
698
707
  }
699
708
  if (provider.headers) Object.assign(headers, provider.headers);
700
709
 
@@ -23,8 +23,20 @@ import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common
23
23
  const backgroundShells = new Map<number, { child: ChildProcessWithoutNullStreams; outputLength: number }>();
24
24
  let nextShellId = 1;
25
25
 
26
- const NATIVE_SHELL_DISABLED =
27
- "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool with the same command.";
26
+ /** Rejection text when Cursor-native shell is denied by policy (issue #604). */
27
+ export function nativeShellDisabledMessage(): string {
28
+ // Do not insist on "the same command" — that steers models into replaying bash/CMD
29
+ // idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604).
30
+ // Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex
31
+ // client that executes the bridge (LAN/SSH remote-proxy).
32
+ return (
33
+ "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). "
34
+ + "Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool. "
35
+ + "Adapt the command for the Codex client host shell before calling the bridge "
36
+ + "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`). "
37
+ + "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands."
38
+ );
39
+ }
28
40
 
29
41
  function rejectedShellResult(command: string, cwd: string, started: number) {
30
42
  return create(ShellResultSchema, {
@@ -36,7 +48,7 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
36
48
  exitCode: 1,
37
49
  signal: "",
38
50
  stdout: "",
39
- stderr: NATIVE_SHELL_DISABLED,
51
+ stderr: nativeShellDisabledMessage(),
40
52
  executionTime: Date.now() - started,
41
53
  aborted: true,
42
54
  }),
@@ -95,7 +107,7 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint
95
107
  event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
96
108
  })),
97
109
  execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
98
- event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: NATIVE_SHELL_DISABLED }) },
110
+ event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) },
99
111
  })),
100
112
  execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
101
113
  event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
@@ -196,7 +208,7 @@ export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessa
196
208
  const args = execMsg.message.value;
197
209
  const cwd = resolve(args.workingDirectory || process.cwd());
198
210
  return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
199
- result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: NATIVE_SHELL_DISABLED }) },
211
+ result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage() }) },
200
212
  }));
201
213
  }
202
214
 
@@ -233,7 +245,7 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage): Uint8Array
233
245
  export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
234
246
  if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
235
247
  return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
236
- result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: NATIVE_SHELL_DISABLED }) },
248
+ result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage() }) },
237
249
  }));
238
250
  }
239
251
 
@@ -3,6 +3,10 @@ import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
3
3
  import { decodeCursorArgsMap } from "./arg-codec";
4
4
  import { normalizeArgKeys } from "./arg-normalize";
5
5
  import {
6
+ cursorShellBridgeArgsValid,
7
+ cursorShellBridgeDropError,
8
+ defaultShellBridgeArgNormalizeSchema,
9
+ isCodexShellBridgeToolName,
6
10
  normalizeCursorWireName,
7
11
  OCX_RESPONSES_TOOL_PROVIDER,
8
12
  resolveShellBridgeAliasKey,
@@ -324,10 +328,18 @@ export function mapSyntheticMcpExecToToolEvents(
324
328
  out.push(...commitToolCall(options.state, callId, finalArgs));
325
329
  return out;
326
330
  }
331
+ const responsesName = responsesToolNameFromCursorWire(cursorWireName);
332
+ const normSchema = defaultShellBridgeArgNormalizeSchema(responsesName);
333
+ const normalizedArgs = JSON.stringify(normalizeArgKeys(decodeCursorArgsMap(args?.args), normSchema));
334
+ if (!cursorShellBridgeArgsValid(normalizedArgs, responsesName, normSchema)) {
335
+ if (isCodexShellBridgeToolName(responsesName)) {
336
+ return [{ type: "error", message: cursorShellBridgeDropError(responsesName) }];
337
+ }
338
+ }
327
339
  // Stateless fallback (no shared event state): emit a complete, self-contained tool call.
328
340
  return [
329
- { type: "tool_call_start", id: callId, name: responsesToolNameFromCursorWire(cursorWireName) },
330
- { type: "tool_call_delta", arguments: decodeMcpArgs(args) },
341
+ { type: "tool_call_start", id: callId, name: responsesName },
342
+ ...(normalizedArgs.length > 2 ? [{ type: "tool_call_delta" as const, arguments: normalizedArgs }] : []),
331
343
  { type: "tool_call_end", id: callId },
332
344
  ];
333
345
  }
@@ -361,9 +373,19 @@ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorW
361
373
  * recorded in `openToolCalls`. Because each completion emits a whole non-interleaved unit, the bridge
362
374
  * (which tracks a single current tool call) serializes parallel Cursor calls correctly.
363
375
  */
376
+ function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
377
+ state.openToolCalls.delete(callId);
378
+ state.completedToolCalls.add(callId);
379
+ return [{ type: "error", message: cursorShellBridgeDropError(toolName) }];
380
+ }
381
+
364
382
  function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
365
383
  const open = state.openToolCalls.get(callId);
366
384
  if (!open) return [];
385
+ const schema = toolSchemaForWireName(state, open.name);
386
+ if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
387
+ if (isCodexShellBridgeToolName(open.name)) return dropShellBridgeCall(state, callId, open.name);
388
+ }
367
389
  const out: CursorServerMessage[] = [{ type: "tool_call_start", id: callId, name: open.name }];
368
390
  if (finalArgs.length > 0) out.push({ type: "tool_call_delta", arguments: finalArgs });
369
391
  out.push(...endToolCall(state, callId));
@@ -38,7 +38,6 @@ import {
38
38
  } from "./gen/agent_pb";
39
39
  import {
40
40
  appendCursorGenericToolUseHint,
41
- appendCursorShellAliasHint,
42
41
  cursorToolsForActivePrompt,
43
42
  buildCursorToolGuidanceSystemNote,
44
43
  buildCursorToolDefinitions,
@@ -560,7 +559,7 @@ export function prepareCursorRunRequest(
560
559
  const rawText = activePromptText(request);
561
560
  const lastRole = request.messages.at(-1)?.role;
562
561
  const text = lastRole === "user" || lastRole === "developer"
563
- ? appendCursorShellAliasHint(request.tools, appendCursorGenericToolUseHint(request.tools, rawText))
562
+ ? appendCursorGenericToolUseHint(request.tools, rawText)
564
563
  : rawText;
565
564
  // Tool-result-only turns resume the remembered Cursor conversation with results in history.
566
565
  const lastRawIsToolResult = request.rawMessages?.at(-1)?.role === "toolResult";
@@ -12,8 +12,6 @@ export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
12
12
  export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const;
13
13
  export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
14
14
  'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell; never say native shell is blocked.';
15
- export const CURSOR_SHELL_ALIAS_USER_HINT =
16
- "Use the Codex shell bridge tool listed this turn (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` name). Prefer that bridge over Cursor-native Shell/Read. Never say phrases like \"Native shell access is blocked\" or \"Shell access is blocked\" — silently use the bridge unless that bridge tool itself fails.";
17
15
  const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
18
16
 
19
17
  export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
@@ -213,17 +211,6 @@ function shellBridgeArgNormalizeSchema(tool: OcxTool): unknown {
213
211
  };
214
212
  }
215
213
 
216
- function activeTextMentionsExecCommand(text: string): boolean {
217
- return /\b(?:exec_command|shell_command)\b/i.test(text);
218
- }
219
-
220
- function looksLikeShellCommandRequest(text: string): boolean {
221
- const hasKnownCommand = /(?:^|[\s`$])(?:echo|pwd|ls|cat|grep|rg|find|python3?|node|bun|npm|pnpm|yarn|git|curl|wget|chmod|mkdir|rm|cp|mv|touch|docker|kubectl|make|cargo|go|pytest)(?=\s|$|[`:;|&])/i.test(text);
222
- const hasRunIntent = /\b(?:run|execute|exec)\b/i.test(text) || /\b(?:stdout|stderr|exit\s+code)\b/i.test(text);
223
- const hasShellTarget = /\b(?:shell|terminal|command|cmd)\b/i.test(text);
224
- return /\b(?:run|execute|exec)\s*:/i.test(text) || hasKnownCommand || (hasRunIntent && hasShellTarget);
225
- }
226
-
227
214
  export function isGenericToolUseCountDemoPrompt(text: string): boolean {
228
215
  const trimmed = text.trim();
229
216
  if (trimmed.length === 0) return false;
@@ -316,23 +303,69 @@ export function cursorToolsForActivePrompt<T extends Pick<OcxTool, "namespace" |
316
303
  return execTools && execTools.length > 0 ? execTools : tools;
317
304
  }
318
305
 
319
- export function shouldAppendCursorShellAliasHint(
320
- tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
321
- text: string,
322
- ): boolean {
323
- const trimmed = text.trim();
324
- return trimmed.length > 0
325
- && cursorRequestHasShellAlias(tools)
326
- && !activeTextMentionsExecCommand(trimmed)
327
- && looksLikeShellCommandRequest(trimmed);
306
+ /**
307
+ * Required command payload keys for a shell bridge tool, derived from the advertised schema when present.
308
+ */
309
+ export function shellBridgeRequiredCommandKeys(
310
+ toolName: string,
311
+ schema?: unknown,
312
+ ): readonly ("cmd" | "command")[] {
313
+ if (schema && typeof schema === "object") {
314
+ const required = (schema as Record<string, unknown>).required;
315
+ if (Array.isArray(required)) {
316
+ const keys = required.filter((key): key is "cmd" | "command" => key === "cmd" || key === "command");
317
+ if (keys.length > 0) return keys;
318
+ }
319
+ }
320
+ return toolName === CODEX_SHELL_COMMAND_TOOL ? ["command"] : ["cmd"];
328
321
  }
329
322
 
330
- export function appendCursorShellAliasHint(
331
- tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
332
- text: string,
333
- ): string {
334
- if (!shouldAppendCursorShellAliasHint(tools, text)) return text;
335
- return `${text}${text.endsWith("\n") ? "\n" : "\n\n"}${CURSOR_SHELL_ALIAS_USER_HINT}`;
323
+ /** Normalize-schema defaults used when validating stateless synthetic shell-bridge calls. */
324
+ export function defaultShellBridgeArgNormalizeSchema(toolName: string): unknown {
325
+ return toolName === CODEX_SHELL_COMMAND_TOOL
326
+ ? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA
327
+ : {
328
+ type: "object",
329
+ properties: CURSOR_EXEC_COMMAND_INPUT_SCHEMA.properties,
330
+ required: ["cmd"],
331
+ };
332
+ }
333
+
334
+ export function cursorShellBridgeDropError(toolName: string): string {
335
+ return `Cursor emitted ${toolName} without a non-empty command; the tool call was dropped.`;
336
+ }
337
+
338
+ /**
339
+ * Extract a non-empty shell command from completed Cursor bridge args using the schema's required
340
+ * command key (`cmd` for bare exec_command, `command` for shell_command).
341
+ */
342
+ export function nonEmptyShellBridgeCommandFromArgs(
343
+ finalArgs: string,
344
+ toolName: string,
345
+ schema?: unknown,
346
+ ): string | undefined {
347
+ let parsed: unknown;
348
+ try {
349
+ parsed = finalArgs.length > 0 ? JSON.parse(finalArgs) : {};
350
+ } catch {
351
+ return undefined;
352
+ }
353
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
354
+ const record = parsed as Record<string, unknown>;
355
+ for (const key of shellBridgeRequiredCommandKeys(toolName, schema)) {
356
+ const value = record[key];
357
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
358
+ }
359
+ return undefined;
360
+ }
361
+
362
+ export function cursorShellBridgeArgsValid(
363
+ finalArgs: string,
364
+ toolName: string,
365
+ schema?: unknown,
366
+ ): boolean {
367
+ return !isCodexShellBridgeToolName(toolName)
368
+ || nonEmptyShellBridgeCommandFromArgs(finalArgs, toolName, schema) !== undefined;
336
369
  }
337
370
 
338
371
  export function cursorToolAllowedByChoice(
@@ -384,6 +417,11 @@ export function buildCursorToolGuidanceSystemNote(
384
417
  const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice);
385
418
  const discoveryTools = discoveryToolLabel(wireNames);
386
419
  const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
420
+ // Host-shell-neutral: the Codex client executes bridge commands, and may differ from
421
+ // the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls.
422
+ const hostShellNote = hasBareExec
423
+ ? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`<<EOF`); `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`. POSIX: use portable commands. After a shell failure, make at most one corrected bridge attempt, then report the error and stop — do not repeat equivalent failing commands."
424
+ : undefined;
387
425
  const notes = [
388
426
  `Cursor tool calls: available tool names are exactly ${listedNames}.`,
389
427
  "Use the current tool catalog as ground truth and call only those exact names with their listed argument keys.",
@@ -399,6 +437,7 @@ export function buildCursorToolGuidanceSystemNote(
399
437
  hasBareExec
400
438
  ? "Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Prefer the bridge over Cursor-native Shell/Read; do not narrate phrases like \"Native shell access is blocked\" — silently call `shell_command` / `exec_command`."
401
439
  : undefined,
440
+ hostShellNote,
402
441
  "Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
403
442
  hasBareExec
404
443
  ? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.`
@@ -418,7 +457,7 @@ export function buildCursorToolGuidanceSystemNote(
418
457
  : undefined,
419
458
  "Do not count or report a tool call unless a tool result was actually returned.",
420
459
  hasBareExec
421
- ? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with the equivalent shell command instead (e.g. \`cat\`, \`ls\`, \`rg\`, \`grep\`). Do not tell the user access is blocked. For file edits, use \`apply_patch\` when available.`
460
+ ? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use \`apply_patch\` when available.`
422
461
  : undefined,
423
462
  ].filter((note): note is string => typeof note === "string");
424
463
  return notes.join(" ");
@@ -137,6 +137,10 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
137
137
  : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
138
138
  if (thinkingLevel) out.thinkingConfig = { thinkingLevel };
139
139
  }
140
+ if (Array.isArray(value.responseModalities)) {
141
+ const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m));
142
+ if (valid.length > 0) out.responseModalities = valid;
143
+ }
140
144
  return Object.keys(out).length > 0 ? out : undefined;
141
145
  }
142
146
 
@@ -1,6 +1,7 @@
1
1
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
2
  import { debugDroppedFrame } from "../lib/debug";
3
3
  import { createHash } from "node:crypto";
4
+ import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
4
5
  import type {
5
6
  AdapterEvent,
6
7
  OcxAssistantMessage,
@@ -233,6 +234,38 @@ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage |
233
234
  };
234
235
  }
235
236
 
237
+ /**
238
+ * Cap on the buffered non-streaming response body (100 MiB), matching
239
+ * IMAGES_RESPONSE_MAX_BYTES in src/server/images.ts. Enforced by streaming the
240
+ * body with a hard byte cap before JSON.parse — Content-Length alone is not
241
+ * trusted (missing/lying headers must still reject oversized payloads).
242
+ * Streaming SSE responses also cap each data frame before JSON.parse.
243
+ */
244
+ const MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
245
+ const MAX_SSE_FRAME_BYTES = MAX_RESPONSE_BYTES;
246
+
247
+ // Note: imagen-* models use a different API surface (prediction/image-generation
248
+ // schema) and must NOT be treated as responseModalities-capable Gemini models.
249
+ // Explicit allowlist only — never `/gemini/ && /image/` (resurrects media-gen IDs).
250
+ const IMAGE_CAPABLE_MODELS = new Set([
251
+ "gemini-3.1-flash-image",
252
+ "gemini-2.0-flash-preview-image-generation",
253
+ "gemini-3-pro-image-preview",
254
+ ]);
255
+
256
+ function isImageCapableModel(modelId: string): boolean {
257
+ return IMAGE_CAPABLE_MODELS.has(modelId);
258
+ }
259
+
260
+ /**
261
+ * Model-visible markdown link for a materialized artifact. Uses the authenticated
262
+ * opaque HTTP route so remote/container clients can fetch the image without host
263
+ * filesystem paths leaking into the transcript.
264
+ */
265
+ function artifactMarkdownUrl(filePath: string): string {
266
+ return artifactHttpUrl(filePath).replace(/([()])/g, "\\$1");
267
+ }
268
+
236
269
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
237
270
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
238
271
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
@@ -272,6 +305,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
272
305
  ? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
273
306
  : undefined;
274
307
  if (directFlashThinking) generationConfig.thinkingConfig = { thinkingLevel: directFlashThinking };
308
+ if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
309
+ generationConfig.responseModalities = ["TEXT", "IMAGE"];
310
+ }
275
311
  if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
276
312
 
277
313
  const method = parsed.stream ? "streamGenerateContent" : "generateContent";
@@ -377,6 +413,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
377
413
  yield { type: "error", message: "No response body" };
378
414
  return;
379
415
  }
416
+ // Streaming responses are processed incrementally (SSE chunks), so the full body
417
+ // is never buffered — no Content-Length pre-check is needed here. Per-image size
418
+ // protection is enforced on each chunk via MAX_ENCODED_BYTES_PER_IMAGE before
419
+ // materializeInlineImage is called (see the inline.data check below).
380
420
 
381
421
  const reader = response.body.getReader();
382
422
  const decoder = new TextDecoder();
@@ -387,9 +427,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
387
427
  let sawAnyFrame = false;
388
428
  let sawTerminalSignal = false;
389
429
 
390
- const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "content" | "terminate"> {
430
+ const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
391
431
  const payload = line.slice(5).trim();
392
432
  if (!payload) return "continue";
433
+ if (payload.length > MAX_SSE_FRAME_BYTES) {
434
+ yield { type: "error", message: `upstream SSE data frame exceeds ${MAX_SSE_FRAME_BYTES} bytes` };
435
+ return "terminate";
436
+ }
393
437
  let emittedContentEvent = false;
394
438
 
395
439
  let chunk: Record<string, unknown>;
@@ -452,6 +496,21 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
452
496
  emittedContentEvent = true;
453
497
  yield { type: "text_delta", text: part.text };
454
498
  }
499
+ const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
500
+ if (inline && typeof inline.data === "string") {
501
+ if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
502
+ yield { type: "error", message: "inline image exceeds per-image size cap" };
503
+ } else {
504
+ try {
505
+ const filePath = await materializeInlineImage(inline.data, imageBudget);
506
+ const escapedPath = artifactMarkdownUrl(filePath);
507
+ emittedContentEvent = true;
508
+ yield { type: "text_delta", text: `\n![image](${escapedPath})\n` };
509
+ } catch {
510
+ yield { type: "error", message: "failed to materialize inline image" };
511
+ }
512
+ }
513
+ }
455
514
  if (part.functionCall) {
456
515
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
457
516
  toolCallsStarted++;
@@ -464,12 +523,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
464
523
  }
465
524
  return emittedContentEvent ? "content" : "continue";
466
525
  };
526
+ const imageBudget = createImageBudget();
467
527
 
468
528
  try {
469
529
  while (true) {
470
530
  const { done, value } = await reader.read();
471
531
  if (done) break;
472
532
  buffer += decoder.decode(value, { stream: true });
533
+ // Cap incomplete frames before waiting for a newline — otherwise a single
534
+ // unterminated data: payload can grow without bound.
535
+ if (buffer.length > MAX_SSE_FRAME_BYTES) {
536
+ yield { type: "error", message: `upstream SSE data frame exceeds ${MAX_SSE_FRAME_BYTES} bytes` };
537
+ try { await reader.cancel(); } catch { /* ignore */ }
538
+ return;
539
+ }
473
540
 
474
541
  const lines = buffer.split("\n");
475
542
  buffer = lines.pop() ?? "";
@@ -526,7 +593,51 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
526
593
  },
527
594
 
528
595
  async parseResponse(response: Response): Promise<AdapterEvent[]> {
529
- const raw = await response.json() as Record<string, unknown>;
596
+ // Reject oversized responses before JSON parse. Prefer Content-Length when
597
+ // present and truthful; always stream-read with a hard byte cap so a missing
598
+ // or lying Content-Length cannot force a full in-memory buffer + parse.
599
+ const contentLength = Number(response.headers.get("content-length"));
600
+ if (Number.isFinite(contentLength) && contentLength > MAX_RESPONSE_BYTES) {
601
+ try { await response.body?.cancel(); } catch { /* ignore */ }
602
+ return [{ type: "error", message: `google response too large (content-length ${contentLength} exceeds ${MAX_RESPONSE_BYTES} bytes)` }];
603
+ }
604
+ let rawText: string;
605
+ try {
606
+ const reader = response.body?.getReader();
607
+ if (!reader) return [{ type: "error", message: "google response had no body" }];
608
+ const chunks: Uint8Array[] = [];
609
+ let total = 0;
610
+ try {
611
+ for (;;) {
612
+ const { done, value } = await reader.read();
613
+ if (done) break;
614
+ total += value.byteLength;
615
+ if (total > MAX_RESPONSE_BYTES) {
616
+ await reader.cancel().catch(() => {});
617
+ return [{ type: "error", message: `google response too large (exceeded ${MAX_RESPONSE_BYTES} bytes)` }];
618
+ }
619
+ chunks.push(value);
620
+ }
621
+ } finally {
622
+ try { await reader.cancel(); } catch { /* ignore */ }
623
+ reader.releaseLock();
624
+ }
625
+ const bytes = new Uint8Array(total);
626
+ let offset = 0;
627
+ for (const chunk of chunks) {
628
+ bytes.set(chunk, offset);
629
+ offset += chunk.byteLength;
630
+ }
631
+ rawText = new TextDecoder().decode(bytes);
632
+ } catch (err) {
633
+ return [{ type: "error", message: err instanceof Error ? err.message : "failed to read google response body" }];
634
+ }
635
+ let raw: Record<string, unknown>;
636
+ try {
637
+ raw = JSON.parse(rawText) as Record<string, unknown>;
638
+ } catch {
639
+ return [{ type: "error", message: "google response was not valid JSON" }];
640
+ }
530
641
  if (raw.error) {
531
642
  const err = raw.error as { message?: string };
532
643
  return [{ type: "error", message: err.message ?? "upstream error" }];
@@ -547,6 +658,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
547
658
  return [{ type: "error", message: "google response contained no candidates" }];
548
659
  }
549
660
  let toolCallsStarted = 0;
661
+ const imageBudget = createImageBudget();
550
662
  if (candidates?.[0]?.content?.parts) {
551
663
  // Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
552
664
  if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession) {
@@ -554,6 +666,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
554
666
  }
555
667
  for (const part of candidates[0].content.parts) {
556
668
  if (part.text) events.push({ type: "text_delta", text: part.text });
669
+ const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData;
670
+ if (inline && typeof inline.data === "string") {
671
+ if (inline.data.length > MAX_ENCODED_BYTES_PER_IMAGE) {
672
+ events.push({ type: "error", message: "inline image exceeds per-image size cap" });
673
+ } else {
674
+ try {
675
+ const filePath = await materializeInlineImage(inline.data, imageBudget);
676
+ const escapedPath = artifactMarkdownUrl(filePath);
677
+ events.push({ type: "text_delta", text: `\n![image](${escapedPath})\n` });
678
+ } catch {
679
+ events.push({ type: "error", message: "failed to materialize inline image" });
680
+ }
681
+ }
682
+ }
557
683
  if (part.functionCall) {
558
684
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
559
685
  toolCallsStarted++;
@@ -14,9 +14,19 @@
14
14
  * to be a specific first-party client.
15
15
  */
16
16
 
17
- /** The exact identity line Codex injects for every model. */
17
+ /** Historical exact identity line Codex injected for every model. */
18
18
  export const CODEX_GPT5_IDENTITY_LINE = "You are Codex, a coding agent based on GPT-5.";
19
19
 
20
+ /** Codex CLI 0.145.0+ wording (#622) — still GPT-5 identity, slightly different phrasing. */
21
+ export const CODEX_GPT5_IDENTITY_LINE_AGENT = "You are Codex, an agent based on GPT-5.";
22
+
23
+ /**
24
+ * Known Codex GPT-5 identity sentences. Narrow: only "coding agent" / "an agent" + GPT-5(.x)?
25
+ * Avoid a broad `You are Codex.*` rewrite that could touch unrelated content.
26
+ */
27
+ const CODEX_GPT5_IDENTITY_RE =
28
+ /You are Codex, (?:a coding agent|an agent) based on GPT-5(?:\.[0-9]+)*\./g;
29
+
20
30
  /** Proxy-neutral replacement: no "opencodex proxy" mention, just the GPT-5/OpenAI disclaimer. */
21
31
  export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be GPT-5 or to be made by OpenAI.";
22
32
 
@@ -27,7 +37,7 @@ export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be
27
37
  * the leak can't reappear in one adapter while being fixed in another.
28
38
  */
29
39
  export function neutralizeIdentity(systemText: string): string {
30
- return systemText.replace(CODEX_GPT5_IDENTITY_LINE, NEUTRAL_IDENTITY_LINE);
40
+ return systemText.replace(CODEX_GPT5_IDENTITY_RE, NEUTRAL_IDENTITY_LINE);
31
41
  }
32
42
 
33
43
  /** The catalog (static, on-disk) replacement for `base_instructions`. Same neutral wording. */