From 1e16dc26815b457493506eb5287bef8e5ee692e2 Mon Sep 17 00:00:00 2001 From: FeiZhuLulu <279467036+FeiZhuLulu@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:10:13 +0800 Subject: [PATCH 1/2] =?UTF-8?q?Codex=20=E6=94=AF=E6=8C=81=20agents.toml=20?= =?UTF-8?q?config=5Foverrides=EF=BC=88exec=20-c=20=E9=80=8F=E4=BC=A0?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SETUP.md | 16 +++++++- src/agent_bridge/adapters/codex.py | 1 + src/agent_bridge/codex_exec.py | 4 ++ src/agent_bridge/config.py | 4 ++ tests/test_codex_exec.py | 60 ++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/SETUP.md b/SETUP.md index 1268d6f..290ed69 100644 --- a/SETUP.md +++ b/SETUP.md @@ -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\\\\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 ` 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). 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. diff --git a/src/agent_bridge/adapters/codex.py b/src/agent_bridge/adapters/codex.py index 8d5ff0b..bca1947 100644 --- a/src/agent_bridge/adapters/codex.py +++ b/src/agent_bridge/adapters/codex.py @@ -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, ) async def _drain_stderr(self, proc: asyncio.subprocess.Process, session_id: str) -> str: diff --git a/src/agent_bridge/codex_exec.py b/src/agent_bridge/codex_exec.py index 17c4ebd..980f2b7 100644 --- a/src/agent_bridge/codex_exec.py +++ b/src/agent_bridge/codex_exec.py @@ -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, @@ -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) diff --git a/src/agent_bridge/config.py b/src/agent_bridge/config.py index 3d79a4f..78219b3 100644 --- a/src/agent_bridge/config.py +++ b/src/agent_bridge/config.py @@ -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 ` (raw TOML + # key=value). No key validation, no filtering, user config.toml is + # never read. + config_overrides: list[str] = Field(default_factory=list) class EnvConfig(BaseModel): diff --git a/tests/test_codex_exec.py b/tests/test_codex_exec.py index 121c196..bc32aa9 100644 --- a/tests/test_codex_exec.py +++ b/tests/test_codex_exec.py @@ -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 ` pair, in order, after `-C ` + # 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"}) From 46a4e7e0bb24e5d43276983bab374408b18e2998 Mon Sep 17 00:00:00 2001 From: FeiZhuLulu <279467036+FeiZhuLulu@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:19:38 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E9=85=8D=E9=A2=9D=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E8=A6=86=E7=9B=96=20codex=20config=5Foverrides=20=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=20env=5Fkey=20=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SETUP.md | 2 +- src/agent_bridge/quota_endpoints.py | 2 +- tests/test_quota_endpoints.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/SETUP.md b/SETUP.md index 290ed69..eddc7d9 100644 --- a/SETUP.md +++ b/SETUP.md @@ -505,7 +505,7 @@ config_overrides = [ ] ``` -Keep secrets out of overrides: a key can only be referenced via `env_key` (command-line arguments are visible in the process list). Task-level `model`/`effort` are appended after overrides, so they keep priority. +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. diff --git a/src/agent_bridge/quota_endpoints.py b/src/agent_bridge/quota_endpoints.py index c16eaa8..de3817d 100644 --- a/src/agent_bridge/quota_endpoints.py +++ b/src/agent_bridge/quota_endpoints.py @@ -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" diff --git a/tests/test_quota_endpoints.py b/tests/test_quota_endpoints.py index 302be77..c70af08 100644 --- a/tests/test_quota_endpoints.py +++ b/tests/test_quota_endpoints.py @@ -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"'),