Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,21 @@ If OpenCode's configured default model is disabled, the first prompt fails with

Bridge drives Desktop-bundled or PATH `codex` through `codex exec --json`. It does not open ChatGPT.exe. Discovery order: `CODEX_CLI_PATH` (env or `$CODEX_HOME/config.toml` / `~/.codex/config.toml`), then `%LOCALAPPDATA%\\OpenAI\\Codex\\bin\\<hash>\\codex.exe` (newest mtime among binaries that pass `exec --help` capability checks), then PATH `codex`.

Login stays in `$CODEX_HOME` when set, otherwise `~/.codex`. `--ignore-user-config` skips Desktop `config.toml` (MCP / Computer Use), and Bridge adds `-c cli_auth_credentials_store="auto"` so ChatGPT login still resolves from keyring or `auth.json`. Project `.codex/config.toml`, `AGENTS.md`, and execpolicy rules can still apply. Bridge inherits `CODEX_HOME` so a custom home used by Desktop is visible to the worker.
Login stays in `$CODEX_HOME` when set, otherwise `~/.codex`. `--ignore-user-config` skips Desktop `config.toml` (MCP / Computer Use), and Bridge adds `-c cli_auth_credentials_store="auto"` so ChatGPT login still resolves from keyring or `auth.json`. Project `.codex/config.toml` and project hooks/rules normally do not load, because Codex reads project trust from the user config this flag skips; `AGENTS.md` and rules under `$CODEX_HOME/rules` still apply. Bridge inherits `CODEX_HOME` so a custom home used by Desktop is visible to the worker.

`[agents.codex] config_overrides` adds raw `codex exec -c <item>` pairs — for settings `--ignore-user-config` would skip, e.g. a custom model provider. The block below merges field-wise into the bundled codex entry, so only `config_overrides` is needed:

```toml
[agents.codex]
config_overrides = [
'model_provider="my-gateway"',
'model_providers.my-gateway.name="My Gateway"',
'model_providers.my-gateway.base_url="https://example.com/v1"',
'model_providers.my-gateway.env_key="MY_GATEWAY_API_KEY"',
]
```

Keep secrets out of overrides: a key can only be referenced via `env_key` (command-line arguments are visible in the process list). Since Codex clears the MCP child environment, a variable named by `env_key` must also be listed in the host's `env_vars` / `[env.inherit]` like any other worker credential. Task-level `model`/`effort` are appended after overrides, so they keep priority.

Default approval is `--approve-for-me` (auto review + workspace-write). `--yolo` is only used when `[agents.codex] session_meta = { yolo = true }`. Prompt is UTF-8 stdin (`-`), never an argv string.

Expand Down
1 change: 1 addition & 0 deletions src/agent_bridge/adapters/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def _build_cmd(self, session: Session, task: Task, env: dict[str, str] | None =
effort=task.effort or session.effort,
resume_id=session.native_session_id,
yolo=yolo_requested(self.agent.session_meta),
config_overrides=self.agent.config_overrides,
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Configured Codex model absent from result

With config_overrides = ['model="custom-model"'] and no dispatch model, get_result.observed_model stays null. run_turn reads only task and session models, losing the configured selection.

Learn more

A Codex override can set a model even when neither dispatch_task.model nor the session model is set. The worker then runs with that explicit configured model, but run_turn computes observed_model only from those two fields. The registry passes that value directly to the result, so callers cannot see the applied override.

Example: Configure config_overrides = ['model="bridge-override-probe"'] and dispatch without model. Codex receives the model in its argv, but get_result.observed_model is null rather than bridge-override-probe.

Recommended fix: Derive the effective model from overrides when no task or session model was selected, or obtain the selected model from Codex's turn events if available. Keep task and session model precedence when present.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

)

async def _drain_stderr(self, proc: asyncio.subprocess.Process, session_id: str) -> str:
Expand Down
4 changes: 4 additions & 0 deletions src/agent_bridge/codex_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ def build_codex_exec_argv(
effort: str | None = None,
resume_id: str | None = None,
yolo: bool = False,
config_overrides: Sequence[str] = (),
) -> list[str]:
cmd = [
*command,
Expand All @@ -241,6 +242,9 @@ def build_codex_exec_argv(
"-C",
cwd,
]
for item in config_overrides:
# Earlier than the task's -m / effort -c so per-dispatch values win.
cmd += ["-c", item]
if model:
cmd += ["-m", model]
mapped = codex_effort(effort)
Expand Down
4 changes: 4 additions & 0 deletions src/agent_bridge/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ class AgentConfig(BaseModel):
# privilege), "allow_always" persists the grant inside the worker,
# "deny" cancels every request.
permission_policy: str = "allow_once"
# Codex only: each item is passed as `codex exec -c <item>` (raw TOML
# key=value). No key validation, no filtering, user config.toml is
# never read.
config_overrides: list[str] = Field(default_factory=list)
Comment on lines +129 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disable Codex quota lookup for config overrides

When config_overrides selects a custom model provider, the worker uses that provider, but quota_block_reason() in src/agent_bridge/quota_endpoints.py only detects -c overrides embedded in cfg.command. Because this new field is stored separately, list_agents can query and report the official Codex account quota for a worker actually using a custom gateway, violating the quota guard's fail-closed behavior. Treat any nonempty config_overrides as unsupported for quota lookup, or parse it with equivalent endpoint checks.

Useful? React with 👍 / 👎.

Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment on lines +129 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

严重程度:高

问题: config_overrides 会被 build_codex_exec_argv 逐条加成真实的 codex exec -c <item>,但 _codex() / quota_block_reason() 只扫 cfg.command 里的 -c / --config。这个新字段不在 command 里,quota 守卫完全看不到。

触发条件: [agents.codex] config_overrides 非空,尤其是 SETUP.md 示例里的 model_provider="my-gateway"。Worker 走自定义网关,但 command 仍是 bundled codex,不含 -c。

影响: list_agents 会把官方 ChatGPT 账号的缓存 quota,或 codex app-server 的 account/rateLimits/read,挂到实际上在用另一个 provider 的 worker。这违反 quota_endpoints.py 的 fail-closed 约定(自定义服务不能复用官方账号的 token 或之前的成功读数)。现有测试 test_fallback_endpoint_overrides_cannot_reuse_official_cache 已覆盖 command = [..., "-c", 'model_provider="custom"'],却盖不到这个新字段。协调端可能按错误的官方配额路由或放过这个 worker。

建议改法: 在 _codex() 里,若 cfg.config_overrides 非空,直接返回与 command 内 -c 相同的拒绝理由(quota lookup is unsupported with Codex CLI configuration overrides)。fetch_quota 已经在读 cache 之前调 quota_block_reason,改 _codex 即可同时挡住 cache 与直接查询。补覆盖:非空 overrides 时缓存的官方 quota 必须被 forget,返回 unknown。

Comment on lines +129 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

严重程度:高

问题: config_overrides 会被 build_codex_exec_argv 逐条加成真实的 codex exec -c <item>,但 _codex() / quota_block_reason() 只扫 cfg.command 里的 -c / --config。这个新字段不在 command 里,quota 守卫完全看不到。

触发条件: [agents.codex] config_overrides 非空,尤其是 SETUP.md 示例里的 model_provider="my-gateway"。Worker 走自定义网关,但 command 仍是 bundled codex,不含 -c。

影响: list_agents 会把官方 ChatGPT 账号的缓存 quota,或 codex app-server 的 account/rateLimits/read,挂到实际上在用另一个 provider 的 worker。这违反 quota_endpoints.py 的 fail-closed 约定(自定义服务不能复用官方账号的 token 或之前的成功读数)。现有测试 test_fallback_endpoint_overrides_cannot_reuse_official_cache 已覆盖 command = [..., "-c", 'model_provider="custom"'],却盖不到这个新字段。协调端可能按错误的官方配额路由或放过这个 worker。

建议改法: 在 _codex() 里,若 cfg.config_overrides 非空,直接返回与 command 内 -c 相同的拒绝理由(quota lookup is unsupported with Codex CLI configuration overrides)。fetch_quota 已经在读 cache 之前调 quota_block_reason,改 _codex 即可同时挡住 cache 与直接查询。补覆盖:非空 overrides 时缓存的官方 quota 必须被 forget,返回 unknown。



class EnvConfig(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion src/agent_bridge/quota_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def _codex(cfg: AgentConfig, env: Mapping[str, str]) -> str | None:
if any(arg.split("=", 1)[0] in ("--oss", "--local-provider") for arg in cfg.command):
return CUSTOM_ENDPOINT
override = _option(cfg.command, "-c", "--config")
if override is not None:
if override is not None or cfg.config_overrides:
# CLI overrides are arbitrary TOML paths. Do not build a second Codex
# configuration resolver merely to guess which account they select.
return "quota lookup is unsupported with Codex CLI configuration overrides"
Expand Down
60 changes: 60 additions & 0 deletions tests/test_codex_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,66 @@ def test_build_argv_resume_and_yolo():
assert cmd[-3:] == ["resume", "thread-1", "-"]


def test_build_argv_config_overrides_sit_before_task_flags():
overrides = [
'model_provider="my-gateway"',
'model_providers.my-gateway.base_url="https://example.com/v1"',
]
cmd = build_codex_exec_argv(
["codex.exe"],
cwd=r"E:\proj",
model="gpt-5.6-sol",
effort="high",
config_overrides=overrides,
)
assert "--ignore-user-config" in cmd
# Each override is its own `-c <item>` pair, in order, after `-C <cwd>`
# and before the task-level `-m`/effort so those still win.
first = cmd.index(overrides[0])
assert cmd[first - 1] == "-c"
assert cmd[cmd.index(overrides[1]) - 1] == "-c"
assert cmd.index("-C") < first < cmd.index(overrides[1]) < cmd.index("-m")
assert cmd.index(overrides[1]) < cmd.index('model_reasoning_effort="high"')
assert cmd[-1] == "-"


def test_build_argv_empty_overrides_unchanged():
base = build_codex_exec_argv(
["codex.exe"], cwd=r"E:\proj", model="gpt-5.6-sol", effort="high"
)
with_empty = build_codex_exec_argv(
["codex.exe"],
cwd=r"E:\proj",
model="gpt-5.6-sol",
effort="high",
config_overrides=[],
)
assert with_empty == base


def test_adapter_config_overrides_reach_argv(tmp_path: Path, monkeypatch):
monkeypatch.setattr(
"agent_bridge.adapters.codex.resolve_codex_command",
lambda *args, **kwargs: ["codex.exe"],
)
adapter = CodexAdapter(
AgentConfig(
name="codex",
protocol="codex",
command=["codex"],
config_overrides=['model_provider="my-gateway"'],
),
tmp_path,
)
session = Session(session_id="s1", agent="codex", cwd=str(tmp_path))
cmd = adapter._build_cmd(
session,
Task(task_id="t1", session_id="s1", agent="codex", message="hi", cwd=str(tmp_path)),
)
assert 'model_provider="my-gateway"' in cmd
assert cmd[cmd.index('model_provider="my-gateway"') - 1] == "-c"


def test_jsonl_takes_last_agent_message_and_requires_turn_completed():
state = CodexTurnState()
apply_codex_event(state, {"type": "thread.started", "thread_id": "t1"})
Expand Down
13 changes: 13 additions & 0 deletions tests/test_quota_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ async def test_fallback_endpoint_overrides_cannot_reuse_official_cache(name, fla
await cache.close()


def test_codex_agent_config_overrides_block_quota_lookup():
# config_overrides become `codex exec -c` pairs; like `-c` flags in
# command they can select a custom provider, so fail closed.
agent = AgentConfig(
name="codex",
protocol="codex",
command=["codex"],
config_overrides=['model_provider="my-gateway"'],
)
reason = quota_block_reason(agent, {})
assert reason is not None and "configuration overrides" in reason


@pytest.mark.parametrize("fallback", [False, True])
@pytest.mark.parametrize(("name", "folder", "flag", "text"), [
("codex", ".codex", "-pwork", '[profiles.work]\nmodel_provider="custom"'),
Expand Down
Loading