Skip to content

codex: try --profile, fall back on codex's rejection error#184

Open
Edwinhe03 wants to merge 1 commit into
databricks:mainfrom
Edwinhe03:edwin-he/ucode-composable-flag-injection
Open

codex: try --profile, fall back on codex's rejection error#184
Edwinhe03 wants to merge 1 commit into
databricks:mainfrom
Edwinhe03:edwin-he/ucode-composable-flag-injection

Conversation

@Edwinhe03

@Edwinhe03 Edwinhe03 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

ucode codex's launch always ran codex --profile ucode <args>. But codex only accepts the global --profile on runtime subcommands. Server-family subcommands (app-server, mcp-server, exec-server, remote-control) and utilities reject it immediately:

Error: --profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, ...

So ucode codex app-server (and the other server subcommands) was functionally broken.

Change

No version-dependent lists to maintain — instead of an allow-list, catch codex's own rejection, inline in launch():

  • launch() spawns codex with --profile first, with stderr captured to a temp file, read once codex exits. A nonzero exit whose stderr contains codex's rejection message (--profile only applies to runtime commands) relaunches without --profile, swallowing the expected noise. Rejected subcommands are caller-configured anyway (omnigent runs codex app-server with its own CODEX_HOME), so omitting --profile there just stays out of the way.
  • Any other outcome replays the captured stderr and propagates the exit code unchanged — an unrelated error (e.g. a broken config) is never silently retried without ucode's routing.

A temp file, unlike a pipe, has no backpressure: codex can never block on a stderr write, so nothing has to read alongside the running process (no pump thread). The trade is that codex's stderr appears at exit rather than live — and at exit is exactly when failures, the case where stderr matters, are read anyway. Ctrl-C is forwarded to codex just as exec_or_spawn does on its Windows path.

This is self-adapting: a future codex version that adds a new runtime subcommand gets --profile correctly on the first attempt, and a new server subcommand falls back — neither requires a ucode change. The rejection is instant and side-effect-free, so the fallback costs ~150ms once. If codex ever rewords the error, the launch fails loudly with codex's own message rather than silently misrouting.

The whole thing is inlined in launch() (no separate helper) and touches only codex.py; launcher.py is untouched.

Verification

  • Full unit suite green (852 passed, 8 skipped); ruff + ty clean. launch() tests drive a fake Popen and cover: spawns with --profile + sets OAUTH_TOKEN; success → stderr replayed, no fallback; --profile rejection → relaunch without it, rejection swallowed; unrelated failure → propagated, no fallback; marker on a zero exit → no fallback.
  • Rejection mechanics verified empirically on codex 0.137.0 (the pinned image version) and 0.141: exits nonzero in ~150ms, message on stderr with the stable prefix, no side effects; a different fast failure (invalid TOML) does not contain the marker.
  • E2E driving codex.launch() against real codex:
    • app-server --listen: first attempt rejected → relaunch → socket bound, server alive, and the rejection line is absent from output (swallowed).
    • exec --help: accepted first try, exit 0, no fallback.
    • broken ucode.config.toml + exec: original error replayed to the caller, exit 1, no fallback (no silent bypass of the Databricks gateway).

Manual verification (real ucode codex CLI)

Configured ucode isolated against a live staging workspace (ucode configure --profiles <p> --agents codex --use-pat), then drove the real CLI:

  • ucode codex app-server --listen unix://… (the previously-broken case): first attempt rejected → relaunch → app-server socket bound and process alive, with the rejection line absent from output. Only ucode's normal launch banner is printed.
  • ucode codex mcp list (runtime subcommand): accepted with --profile on the first attempt, exit 0, no fallback.
  • ucode codex exec --skip-git-repo-check "Reply with exactly: CODEX_OK": full model round-trip through the Databricks AI Gateway on the pinned model (system.ai.gpt-5-2) — replied CODEX_OK in 18s, proving --profile routing is intact end-to-end.

This pull request and its description were written by Isaac.

@Edwinhe03
Edwinhe03 marked this pull request as ready for review July 6, 2026 22:29
@rohita5l
rohita5l requested a review from lilly-luo July 7, 2026 13:19
@Edwinhe03 Edwinhe03 changed the title ucode: compose injected flags with the caller's command (claude --settings, codex --profile) ucode: compose claude --settings; passthrough non-interactive subcommands (codex app-server, --version/--help) Jul 7, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from c577f42 to bcff2ab Compare July 7, 2026 22:31
@Edwinhe03 Edwinhe03 changed the title ucode: compose claude --settings; passthrough non-interactive subcommands (codex app-server, --version/--help) ucode: exec the real binary for non-interactive subcommands (codex app-server, --version/--help) Jul 7, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from bcff2ab to 7d540fb Compare July 7, 2026 22:56
@lilly-luo

Copy link
Copy Markdown
Collaborator

am i testing this right? it seems to error on start?

i checked out your branch

no app-server:

$ uv run ucode codex
✔ Databricks auth already available for https://dbc-a5d4177a-49dc.cloud.databricks.com
✔ Unity AI Gateway detected      
                                
╭──────────────────╮
│ ucode with Codex │
╰──────────────────╯
  Model: system.ai.gpt-5-5
✔ Starting Codex
╭────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.143.0-alpha.14)                    │
│                                                        │
│ model:     system.ai.gpt-5-5 medium   /model to change │
│ directory: ~/ucode                                     │
╰────────────────────────────────────────────────────────╯

  Tip: New Build faster with Codex.

⚠ MCP startup interrupted. The following servers were not initialized: confluence, databricks-v2, devportal, github, glean,
  google, jira, logs-summariser, pagerduty, slack

with app-server

# lilly.luo at ip-10-93-41-23 in ~/ucode (git:edwin-he/ucode-composable-flag-injection) [14:50:55]
$ uv run ucode codex app-server
2026-07-08T14:50:56.919524Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.
    1. /home/lilly.luo/ucode/.codex
       To load project-local config, hooks, and exec policies, add /home/lilly.luo/ucode as a trusted project in /home/lilly.luo/.codex/config.toml.

2026-07-08T14:50:57.317311Z ERROR codex_models_manager::manager: failed to refresh available models: {"error_code":"BAD_REQUEST","message":"Request path '/codex/v1/models?client_version=0.143.0' doesn't match any known API type and is classified as an unmanaged api request. Set the Databricks-Model-Provider-Service header to the name of the model provider service to forward this request to."}

@lilly-luo

lilly-luo commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator
  1. could you help me understand why omnigent is even using ucode if it just wants the codex pass thru? why doesn't omnigent just call codex instead of ucode codex app-server
  2. instead of making this a separate command, would we make this a flag? something like the below. it could be nice for people to use ucode directlyucode codex --skip-init

@lilly-luo

Copy link
Copy Markdown
Collaborator

follow up:
Edwin: temp bump omnigent timeout
Lilly: introduce new config setting that skips all initialization, doesnt pull fresh models, doesn't test the tools

@Edwinhe03 Edwinhe03 closed this Jul 8, 2026
@Edwinhe03 Edwinhe03 changed the title ucode: exec the real binary for non-interactive subcommands (codex app-server, --version/--help) codex: allow-list --profile by subcommand; deliver config via -c where --profile is rejected Jul 8, 2026
@Edwinhe03 Edwinhe03 reopened this Jul 8, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from 7d540fb to a9961a2 Compare July 8, 2026 23:32
@Edwinhe03 Edwinhe03 changed the title codex: allow-list --profile by subcommand; deliver config via -c where --profile is rejected codex: allow-list --profile by subcommand; omit it where codex rejects it Jul 9, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from a9961a2 to b37ab3d Compare July 9, 2026 00:32
Comment thread src/ucode/agents/codex.py Outdated
Comment thread src/ucode/agents/codex.py Outdated
Comment on lines +448 to +450
_CODEX_PROFILE_SUBCOMMANDS = frozenset(
{"exec", "review", "resume", "archive", "delete", "unarchive", "fork", "mcp", "sandbox"}
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will we also need to maintain this list ? it also will depend on the cli version of codex that the customer is on

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I can't see how we can get around this. but problem statement is ucode unconditionally adds --profile to all codex commands when codex doesn't support --profile on all subcommands. do you have any ideas?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you just do a try catch on the specific error?

@Edwinhe03 Edwinhe03 changed the title codex: allow-list --profile by subcommand; omit it where codex rejects it codex: try --profile, fall back on codex's rejection error Jul 13, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch 3 times, most recently from 63da551 to 52830b0 Compare July 13, 2026 23:35
@Edwinhe03
Edwinhe03 requested a review from lilly-luo July 13, 2026 23:37
Comment thread src/ucode/agents/codex.py Outdated
Comment on lines +398 to +405
# codex accepts the global --profile only on its runtime subcommands (plus the
# bare interactive TUI); server-family subcommands (app-server, mcp-server,
# exec-server, remote-control) and utilities reject it up front by printing
# this to stderr and exiting nonzero. The accepted set drifts across codex
# versions, so rather than maintain a version-dependent list we try with
# --profile and fall back exactly on this rejection. Stable prefix verified on
# codex 0.137 and 0.141; if a future codex rewords it, the launch fails loudly
# with codex's own error instead of silently dropping ucode's routing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make this less verbose? or just rm this comment

Comment thread src/ucode/agents/codex.py
stderr write, so nothing has to read alongside the running process. The
trade is that codex's stderr appears when it exits, not live.
"""
with tempfile.TemporaryFile() as stderr_file:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to wrap in this temp file? it seems diff behavior from before - exec_or_spawn doesnt have the temp file

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec_or_spawn doesn't capture any output. we could do piping, but then need to consider error output streaming and pipe buffer

Comment thread src/ucode/agents/codex.py Outdated
Comment on lines +462 to +466
_exec_or_spawn_with_fallback(
[binary, "--profile", CODEX_PROFILE_NAME, *tool_args],
[binary, *tool_args],
_PROFILE_REJECTED_MARKER,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of a new method, is it possible to do something inline + more simple like:

try: 
    exec_or_spawn
except Exception as e: 
    if e.msg == _PROFILE_REJECTED_MARKER:
         exec_or_spawn([binary, *tool_args])

ucode's codex launch always ran `codex --profile ucode <args>`, but codex only
accepts the global `--profile` on runtime subcommands. Server-family
subcommands (app-server, mcp-server, exec-server, remote-control) and
utilities reject it up front:

  Error: --profile only applies to runtime commands and `codex mcp`: ...

so e.g. `ucode codex app-server` was functionally broken.

Rather than maintain a version-dependent allow-list of which subcommands
accept `--profile` (the accepted set drifts across codex releases), catch
codex's own rejection: launch() spawns codex with `--profile`, capturing
stderr to a temp file, and reads it once codex exits. Only a nonzero exit
whose stderr contains the rejection message relaunches without `--profile`
(swallowing the expected noise); any other outcome replays the captured
stderr and propagates the exit code unchanged, so an unrelated error is never
silently retried without ucode's routing. A temp file, unlike a pipe, has no
backpressure, so codex can never block on a stderr write. The rejection is
instant and side-effect-free (verified on codex 0.137 and 0.141), so the
fallback costs ~150ms once.

Inlined directly in launch() (no separate helper); launcher.py is untouched.

Co-authored-by: Isaac
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from 52830b0 to 881ca21 Compare July 15, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants