Five tightening fixes driven by the reviewer pass on top of round 4:
1. Drop OPENAI_API_KEY from the codex subprocess safe-list.
The OpenAI provider key belongs to the OpenAI provider; a shimmed
`codex` binary on PATH must not receive it. Users who want to wire
the same key into Codex now set CODEX_OPENAI_API_KEY, which is the
one OpenAI-shaped key we still forward.
2. Fail-closed env scrub on the SDK path.
When AppServerConfig is missing from the installed openai_codex
build, the bare AsyncCodex() constructor used to inherit the full
os.environ via the SDK's internal os.environ.copy(). Replaced the
fallback with a _ScrubbedEnvAsyncCodex wrapper that swaps
os.environ for the lifetime of the session so HF_TOKEN, GH_TOKEN,
WANDB_API_KEY etc never reach the spawned app-server.
3. Prefer the upstream-canonical base_instructions kwarg.
The real openai_codex SDK takes the system prompt as
`base_instructions`; our previous helper only knew `system`. Now
tries base_instructions first, falls back to system, then inlines
the system text in the user prompt as a last resort.
4. Filter visible text to answer-bearing event types only.
_coerce_text used to render any payload that exposed a `delta` /
`text` / `content` field, which let command output, file paths and
tool call arguments leak into the Chat Completions reply. Gated on
a _ANSWER_EVENT_TYPES allow-list (message.delta, completed,
text_delta, etc.); untyped legacy dicts still pass through.
5. Treat the SDK as the install gate.
openai-codex-cli-bin ships the codex runtime that backs
AsyncCodex(...), so SDK alone is sufficient to drive the provider.
`installed` no longer also requires a standalone codex binary on
PATH; cli_path remains reported separately so the UI can still
show whether a CLI is also installed.
Also added the matching positive-match regex line ("Authenticated:
Yes") for one more login-status wording the CLI ships in some
locales.
Tests: grown to 39 cases. New regressions cover the SDK-only install
gate, base_instructions kwarg priority + system fallback, the
fail-closed env scrub wrapper, the answer-only delta filter, the
Authenticated: Yes wording, and the CODEX_OPENAI_API_KEY-vs-
OPENAI_API_KEY split.
Fourth reviewer.py pass surfaced one more security finding and a
handful of correctness gaps. Each is small but the env-scrub for the
SDK path closes the asymmetric-fix loop opened in the previous round.
* Codex SDK construction now passes an `AppServerConfig(env=...)`
that overrides every non-safe-listed env key to an empty string.
Upstream openai/codex/sdk/python/client.py builds the spawn env as
`os.environ.copy()` then `env.update(self.config.env)`, so this
scrubs HF_TOKEN / GH_TOKEN / WANDB_API_KEY / ANTHROPIC_API_KEY etc.
out of the codex app-server subprocess env on the chat / parallel
/ synthesis paths, matching the CLI/login paths from the previous
round. The helper falls back to bare `AsyncCodex()` when the SDK
version does not expose AppServerConfig, with logged warning.
* Install hint now names the actual upstream PyPI project,
`openai-codex` (canonical), with `codex_app_server` documented as
the legacy alias. The probe still accepts both import names so
forward compat is preserved.
* Device-auth URL regex broadened to accept upstream's current
`chatgpt.com/activate` shape and any `/device|/activate|/verify`
variant, not just `/codex/device`. The frontend can now open the
verification page on CLI builds that print the documented
ChatGPT-style URL.
* `_run_codex_synthesis` now takes a `system` arg and forwards it
to `thread_start(system=...)`, falling back to a prompt-prefix on
older SDK revs that reject the kwarg. Previously a fan-out with
"Always answer in Spanish" produced Spanish per-tab attempts but
an English synthesis.
* `_detect_logged_in` negative regex now also matches "Not signed
in", "Please sign in" (alternative localisations / future CLI
releases). Same word-boundary anchoring as before.
* Frontend `CodexLoginEvent` union gains `device_code` and a `code`
field. `CodexLoginButton` now renders the one-time code under the
verification URL so users on a headless / remote install can copy
the code without scraping the log pane. Also fixes a closure-stale
bug where setError(message) was followed by a stale `error` read,
losing specific backend errors; the new path keeps `lastStreamError`
inside the closure.
* Replaced four hardcoded `/mnt/disks/...` paths in the new
regression tests with `_backend_file()` resolved from `__file__`,
so the suite runs in any checkout (CI, local dev, the review
worker tree). Found by the round-4 reviewer.
Four new pytest cases pin the behaviour:
`test_not_signed_in_wording_also_handled`,
`test_device_url_accepts_generic_verification_url`,
`test_synthesis_call_forwards_system_prompt`, and
`test_sdk_env_scrubbed_via_appserverconfig`. 32/32 codex_provider
tests pass; `tsc --noEmit` clean.
Third reviewer.py pass found three remaining sharp edges. Each fix
is small and paired with a regression test where applicable.
* Codex subprocess env is now scrubbed to a safe-list before spawn.
Both `_run_cli` in codex_availability and the device-auth spawn
in stream_codex_device_login switch from `env=os.environ.copy()`
to `env=_codex_subprocess_env()`, which forwards only PATH /
HOME / USER / Windows-equivalents / CODEX_HOME / OPENAI_API_KEY /
OPENAI_BASE_URL. Other-provider secrets like HF_TOKEN, GH_TOKEN,
WANDB_API_KEY, ANTHROPIC_API_KEY no longer reach the local codex
binary, so a shimmed `codex` earlier on PATH cannot harvest them.
* `_stream_thread_run` now tracks `emitted_any` and refuses to fall
through to the buffered `await thread.run(prompt)` after either
streaming helper has already yielded text. Previously a network
glitch mid-stream re-executed the same Codex turn, which can
duplicate file writes, shell commands, and other Codex side
effects. The buffered path is now reserved for the zero-output
case (no streaming helper resolved, or streaming returned empty).
* `CodexLoginButton` now aborts the SSE reader on unmount via a
useEffect cleanup that calls `abortRef.current?.abort()`. The
underlying `codex login --device-auth` subprocess no longer
keeps streaming (and holding a device-auth session) after the
dialog closes.
Two new pytest cases pin the behaviour: `test_codex_subprocess_env_scrubbed`
sets HF/GH/WANDB/ANTHROPIC keys and asserts none reach the codex
env while OPENAI_API_KEY / CODEX_HOME survive; and
`test_partial_stream_failure_does_not_replay_turn` injects a fake
`turn().stream()` that yields "partial output " then raises, and
asserts `thread.run()` is never called. 28/28 codex_provider tests
pass; `tsc --noEmit` clean.
Followups on the post-merge review pass for the Codex SDK chat
provider. Verified against codex-cli 0.133.0 + the upstream
`openai/codex` Rust + Python sources, then pinned each fix with
a regression test in `test_codex_provider.py` (24/24 passing).
* Probe both `openai_codex` (canonical upstream Python package at
`openai/codex/sdk/python`) and the legacy `codex_app_server`
alias. Without this the availability probe always reported
`sdk_importable: false` even when the SDK was installed, so the
provider was permanently hidden.
* Switch the device-auth and login-status invocations from
`codex auth login --device-auth` / `codex auth status` to the
real upstream subcommands `codex login --device-auth` and
`codex login status`. The former path returns
`unrecognized subcommand 'auth'` on a real CLI.
* Strip ANSI control sequences before extracting the device URL
(upstream wraps the URL in `\x1b[34m...\x1b[0m`) and tighten the
pattern to the canonical `.../codex/device` shape. Also surface
the one-time code as a `device_code` SSE event so the UI can
show it alongside the URL.
* Fix `_detect_logged_in` substring footgun: `"logged in" in
combined` matched inside `"not logged in"`, flipping logged-out
users to logged-in. Anchor on word boundaries with negative
prefixes winning regardless of return code.
* Cancel in-flight fan-out workers on SSE disconnect. Previously
every parallel Codex turn ran to completion against a
disconnected client and burned quota; now `_stream_codex_parallel`
cancels its worker + drain tasks in a try/finally on
`CancelledError`/`GeneratorExit`.
* Tear down the device-login subprocess on disconnect via
`start_new_session=True` + `os.killpg(SIGTERM)` (Unix) or
`CREATE_NEW_PROCESS_GROUP` + `CTRL_BREAK_EVENT` (Windows), with
a bounded `proc.wait()` and `proc.kill()` fallback. Previously
`finally: await proc.wait()` blocked the SSE close path because
`codex login --device-auth` only exits on user action.
* Render the full conversation transcript in `_last_user_prompt`
instead of returning only the most recent user message. The PR
opens a fresh thread per request so prior assistant turns were
dropped, degrading multi-turn chats to single-shot prompts.
Single-turn input is unchanged.
* Make `ChatCompletionRequest.parallel_calls` default to 1 (`int`
with `ge=1, le=20`) instead of `Optional[int] = None`. The
runtime already coerced `None` -> 1, but the schema now matches
the documented `[1, 20]` range.
* Replace the registry's hardcoded `default_models` (which
contained `o3`, not in the upstream catalog) with the current
`gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex / 5.2` set from
`codex-rs/models-manager/models.json`.
* Stop echoing `str(exc)` in SSE error frames in both
`routes/inference.py` and `routes/codex.py`. The Codex SDK can
raise with local paths, env-var content, or traceback fragments
(CodeQL `py/information-exposure-through-exception`). Surface a
generic message + `exception_type` discriminator; log the full
reason server-side via `logger.error(..., exc_type=..., error=...)`.
Doc / comment updates throughout to refer to `codex login` /
`openai_codex` rather than the older incorrect strings.
Tested: pytest 24 cases in `test_codex_provider.py` (the original
14 + 10 new `TestCodexHardenedRegressions`) plus the rest of the
Studio-backend test suite the PR touches (209 passing). Also
verified live against Studio launched from this branch on a
Blackwell B200 via `UNSLOTH_STUDIO_HOME=$WORKSPACE/temp/...
./install.sh --local` then a Playwright probe.
Wires the OpenAI Codex CLI / Python SDK (codex_app_server) into Studio
as a new chat provider type. Hosts that don't have the CLI or the SDK
installed never see the entry; on logged-out hosts the provider config
dialog renders a device-auth Sign-in button that surfaces the
verification URL and streams CLI progress back over SSE.
Backend
- new core/inference/codex_availability.py probes the CLI + SDK and
reports {installed, logged_in, version, supported_models}; it never
imports codex_app_server at module top level so the rest of the
backend keeps starting cleanly on hosts that don't have the SDK.
- new core/inference/codex_provider.py wraps AsyncCodex and translates
Codex events into OpenAI chat-completion chunks. Supports the
thread.run_streaming path with a non-streaming fallback for older
SDK revs.
- parallel_calls > 1 fans the turn out across N tasks (capped at 20)
via asyncio.gather and emits codex_tab_open / codex_tab_chunk /
codex_tab_close tool-events per attempt plus a final codex_gather
synthesis event. A separate standalone Codex call produces the
unified answer.
- new routes/codex.py exposes GET /api/codex/status and POST
/api/codex/login. The login route shells out to
codex auth login --device-auth and streams events; the first event
carries the verification URL so the frontend can window.open it.
- ChatCompletionRequest gains a parallel_calls field bounded [1, 20]
by pydantic. The codex registry entry stays hidden by default; the
/api/codex/status probe is the authoritative gate.
- routes/inference.py dispatches provider_type=codex through the
local CLI/SDK pipeline instead of the standard HTTP client, with
graceful error surfacing for CodexUnavailableError.
Frontend
- new api/codex-api.ts exposes fetchCodexStatus() and an async
generator streamCodexDeviceLogin() that drives the SSE stream and
yields parsed events.
- new components/codex-parallel-tabs.tsx renders the tabbed parallel-
calls UI with a Synthesis tab highlighted once the codex_gather
event arrives. Pure reducer keeps the state transitions unit-
testable.
- new components/codex-login-button.tsx posts to /api/codex/login,
opens the verification URL in a new tab via window.open, and shows
the streamed CLI log as it lands.
- external-providers.ts exports CODEX_PROVIDER_TYPE,
CODEX_MAX_PARALLEL_CALLS, isCodexProviderType, and
clampCodexParallelCalls. Codex is marked text-only so the composer
hides image-attach affordances when selected.
Tests
- tests/test_codex_provider.py (14 cases) covers the availability
probe across the four install / login states, the streaming +
parallel-calls translation against a fake codex_app_server module
injected into sys.modules, the [1, 20] pydantic clamp, the
CodexUnavailableError surfacing path, and the parallel_calls=1
single-call shape (no tab tool-events).