Skip to content

Commit c7a05ab

Browse files
committed
codex: don't inject --profile on subcommands that reject it (app-server)
`ucode codex` prepends the global `--profile ucode` flag to every launch. codex only accepts that flag on runtime commands (interactive, `exec`) and `codex mcp`; management subcommands like `app-server` reject it outright ("--profile only applies to runtime commands and codex mcp") and fail to start. This breaks callers that drive `codex app-server` through ucode (e.g. omnigent's managed codex-native harness). Skip `--profile` when the target subcommand is one that rejects it. Those subcommands still resolve the `[profiles.ucode]` provider config via the top-level `profile = "ucode"` selector ucode writes into the config file, so the flag is redundant for them anyway. A small subcommand scanner skips leading global options (e.g. `--model X`) so it isn't mistaken for the subcommand. Co-authored-by: Isaac
1 parent a0a0d1c commit c7a05ab

2 files changed

Lines changed: 118 additions & 1 deletion

File tree

src/ucode/agents/codex.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,74 @@ def _gpt_version_key(entry: tuple[str, tuple[int, int | None, int | None, str]])
391391
return max(parsed, key=_gpt_version_key)[0]
392392

393393

394+
# codex accepts the global ``--profile`` flag only on runtime commands (the
395+
# interactive TUI and ``exec``) and ``codex mcp``. Management subcommands such
396+
# as ``app-server`` reject it outright ("--profile only applies to runtime
397+
# commands and codex mcp") and fail to start. Those subcommands still resolve
398+
# the ``[profiles.ucode]`` provider config through the top-level
399+
# ``profile = "ucode"`` selector ucode writes into the config file, so the CLI
400+
# flag is redundant for them anyway — skip it rather than break the launch.
401+
_SUBCOMMANDS_REJECTING_PROFILE = frozenset({"app-server"})
402+
403+
# codex global options that consume the following token as their value. Used to
404+
# skip past a leading global (e.g. ``--model X``) when locating the subcommand,
405+
# so it isn't mistaken for the subcommand itself. Boolean flags take no value;
406+
# ``--flag=value`` forms are self-contained and handled inline.
407+
_CODEX_GLOBAL_VALUE_FLAGS = frozenset(
408+
{
409+
"-c",
410+
"--config",
411+
"-i",
412+
"--image",
413+
"-m",
414+
"--model",
415+
"-p",
416+
"--profile",
417+
"-s",
418+
"--sandbox",
419+
"-C",
420+
"--cd",
421+
"-a",
422+
"--ask-for-approval",
423+
"--enable",
424+
"--disable",
425+
"--remote",
426+
"--remote-auth-token-env",
427+
"--local-provider",
428+
"--add-dir",
429+
}
430+
)
431+
432+
433+
def _codex_subcommand(tool_args: list[str]) -> str | None:
434+
"""Return the codex subcommand in *tool_args*, skipping global options.
435+
436+
``None`` when there is no subcommand (the interactive TUI).
437+
"""
438+
i = 0
439+
while i < len(tool_args):
440+
arg = tool_args[i]
441+
if not arg.startswith("-"):
442+
return arg
443+
# A bare value flag (``--model X``) consumes the next token too; a
444+
# ``--flag=value`` form or a boolean flag consumes only itself.
445+
if "=" not in arg and arg in _CODEX_GLOBAL_VALUE_FLAGS:
446+
i += 1
447+
i += 1
448+
return None
449+
450+
394451
def launch(state: dict, tool_args: list[str]) -> None:
395452
binary = SPEC["binary"]
396453
workspace = state.get("workspace")
397454
if workspace:
398455
os.environ["OAUTH_TOKEN"] = get_databricks_token(workspace, state.get("profile"))
399-
exec_or_spawn([binary, "--profile", CODEX_PROFILE_NAME, *tool_args])
456+
profile_argv = (
457+
[]
458+
if _codex_subcommand(tool_args) in _SUBCOMMANDS_REJECTING_PROFILE
459+
else ["--profile", CODEX_PROFILE_NAME]
460+
)
461+
exec_or_spawn([binary, *profile_argv, *tool_args])
400462

401463

402464
def validate_cmd(binary: str) -> list[str]:

tests/test_agent_codex.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,58 @@ def fake_execvp(binary: str, args: list[str]) -> None:
436436

437437
assert os.environ["OAUTH_TOKEN"] == "fresh-token"
438438
assert exec_calls == [("codex", ["codex", "--profile", "ucode", "--search"])]
439+
440+
def test_omits_profile_for_app_server(self, monkeypatch):
441+
# `codex app-server` rejects the global --profile flag, so ucode must
442+
# not inject it there — even when a global option precedes it.
443+
exec_calls: list[tuple[str, list[str]]] = []
444+
445+
def fake_execvp(binary: str, args: list[str]) -> None:
446+
exec_calls.append((binary, args))
447+
raise RuntimeError("stop")
448+
449+
monkeypatch.setattr(codex, "get_databricks_token", lambda workspace, profile=None: "tok")
450+
monkeypatch.setattr(os, "execvp", fake_execvp)
451+
452+
try:
453+
codex.launch({"workspace": WS}, ["--model", "gpt-5", "app-server", "--listen", "u"])
454+
except RuntimeError:
455+
pass
456+
457+
assert exec_calls == [
458+
("codex", ["codex", "--model", "gpt-5", "app-server", "--listen", "u"])
459+
]
460+
461+
def test_keeps_profile_for_exec_subcommand(self, monkeypatch):
462+
exec_calls: list[tuple[str, list[str]]] = []
463+
464+
def fake_execvp(binary: str, args: list[str]) -> None:
465+
exec_calls.append((binary, args))
466+
raise RuntimeError("stop")
467+
468+
monkeypatch.setattr(codex, "get_databricks_token", lambda workspace, profile=None: "tok")
469+
monkeypatch.setattr(os, "execvp", fake_execvp)
470+
471+
try:
472+
codex.launch({"workspace": WS}, ["exec", "say hi"])
473+
except RuntimeError:
474+
pass
475+
476+
assert exec_calls == [("codex", ["codex", "--profile", "ucode", "exec", "say hi"])]
477+
478+
479+
class TestCodexSubcommand:
480+
def test_none_for_interactive(self):
481+
assert codex._codex_subcommand([]) is None
482+
assert codex._codex_subcommand(["--search"]) is None
483+
484+
def test_finds_bare_subcommand(self):
485+
assert codex._codex_subcommand(["exec", "hi"]) == "exec"
486+
assert codex._codex_subcommand(["app-server"]) == "app-server"
487+
488+
def test_skips_leading_value_flags(self):
489+
assert codex._codex_subcommand(["--model", "gpt-5", "app-server"]) == "app-server"
490+
assert codex._codex_subcommand(["-c", "k=v", "exec"]) == "exec"
491+
492+
def test_handles_equals_form(self):
493+
assert codex._codex_subcommand(["--model=gpt-5", "app-server"]) == "app-server"

0 commit comments

Comments
 (0)