When Codex runs through the ccr middleware, every command execution
fails with three repeated Windows error dialogs:
codex-windows-sandbox-setup.exe
COM+ 注册表数据库检测到一个系统错误
(COM+ registry database system error)
Root cause: the middleware's config/read handler hard-coded
`sandbox_mode: "workspace-write"` in the virtual config returned to
Codex (codex-cli-middleware-runtime.ts configRead). Unlike the
`elevated` mode Codex reads from its own config.toml when run directly
(or via tools that only swap config like cc-switch), `workspace-write`
triggers `codex-windows-sandbox-setup.exe` setup refresh on every
command. On systems where the COM+ catalog is unavailable (COMSysApp
stopped or the catalog itself misbehaves — see openai/codex#29332),
that refresh fails and surfaces as the repeated error dialogs.
Empirically verified across the three legal sandbox_mode values:
- read-only : does not trigger setup, but cannot write files
- workspace-write : triggers setup refresh every command -> fails
on affected systems (this bug)
- danger-full-access : does not trigger setup, can write files, but
has no isolation
Forcing workspace-write therefore both overrides the user's own
config.toml preference AND guarantees the failure on affected systems.
Fix: omit sandbox_mode from the virtual config entirely, letting Codex
read it from its own config.toml ([windows] sandbox / sandbox_mode) —
matching the behavior of config-only tools like cc-switch. The user
keeps full control: those who want isolation set workspace-write (and
must ensure their COM+ works); those on affected systems set
danger-full-access.
This is a one-line removal plus an explanatory comment; configValues
was already empty ({}), so Codex now falls back to its config file
exactly as it does without any middleware.
Refs: openai/codex#29332
Three independent crashes in src/server/gateway/service.ts, each surfacing
as either a 502 or an Electron Uncaught Exception dialog.
== 1. Non-ASCII provider/model names -> 502 ByteString error ==
When a provider name or model selector contains non-ASCII characters
(CJK, emoji, accented Latin — e.g. "小米mimo/mimo-v2.5-pro-ultraspeed"),
every routed request fails with:
Unexpected status 502 Bad Gateway: Cannot convert argument to a
ByteString because the character at index 0 has a value of 23567
which is greater than 255.
Several observability headers derived their values from raw,
unsanitized user-facing strings (x-ccr-logical-provider, x-ccr-routed-
model, x-ccr-route-reason, the model-discovery/app-rewrite/codex-patch
diagnostics, x-ccr-fallback-model). Node/undici enforces the HTTP
ByteString contract (code point <= 255), so any non-ASCII char throws a
TypeError that bubbles to the catch-all as 502.
Fix: add sanitizeHeaderValue() that replaces characters outside the
printable ASCII range with "-", and wrap all ten affected assignments.
The core gateway (@the-next-ai/ai-gateway) does not read any x-ccr-*
header (verified), so values stay observability-only.
== 2. Client disconnect mid-stream -> Uncaught Exception ==
When a client closes the SSE connection mid-stream — normal during
local tool execution (file creation) — the gateway emitted an Uncaught
Exception that crashed the Electron main process:
Uncaught Exception:
Error: Client connection closed before response completed.
Two gaps let the error escape:
- responseBody.once("error", ...) consumes only the FIRST error, but
a disconnect triggers several in quick succession (abort, destroy,
EPIPE). Remaining errors re-throw as unhandled.
- The destination ServerResponse had NO "error" listener, so write
failures on a closed socket bubbled up directly.
Fix: once("error") -> on("error") on the response body so every error
is logged via writeStreamLog; add response.on("error") that marks the
disconnect and aborts upstream. Only the spurious crash is removed —
disconnects are still recorded and upstream still aborted.
== 3. Codex apply_patch -> 502 "immutable" ==
Creating a file via Codex (apply_patch) fails with:
Unexpected status 502 Bad Gateway: immutable
Undici's fetch Response.headers can be immutable; mergeFallbackResponse
Headers returns it as-is when no fallback occurred, and the codex patch
bridge then calls responseHeaders.delete("content-length"), which
throws TypeError: immutable.
Fix: wrap the merged headers in new Headers(...) to guarantee a mutable
copy regardless of upstream immutability or merge early-return.
== Verification ==
- sanitizeHeaderValue: "小米mimo/mimo-v2.5-pro-ultraspeed" ->
"mimo/mimo-v2.5-pro-ultraspeed" (ASCII-safe); real repro confirmed.
- Disconnect crash: previously fired on every file-creating tool call;
after fix, no crash, model continues normally.
- immutable 502: previously fired on every apply_patch; after fix,
files are created successfully.
- ASCII-only configs unaffected (sanitization is a no-op; error
handlers only affect the failure path).