feat(codex): pass per-agent config overrides via codexConfig#278
feat(codex): pass per-agent config overrides via codexConfig#278vprudnikoff wants to merge 3 commits into
Conversation
Add an optional `codexConfig` map to the agent profile. The codex provider serializes each entry to a TOML scalar and passes it as `-c key=value` at launch, in both the default --yolo path and the --profile <codexProfile> path. Keys may be dotted config paths (e.g. features.fast_mode); strings are quoted, bools/numbers emitted bare. This lets a profile set per-agent Codex knobs — reasoning effort, service tier, fast mode, etc. — without editing the global ~/.codex/config.toml or maintaining named profile files, mirroring how the mcpServers overrides already work.
294f3dc to
0b5e33c
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #278 +/- ##
=======================================
Coverage ? 92.16%
=======================================
Files ? 70
Lines ? 7119
Branches ? 0
=======================================
Hits ? 6561
Misses ? 558
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds per-agent Codex configuration overrides via a new optional codexConfig map on agent profiles, allowing CAO to launch Codex with -c key=value flags so settings (e.g., reasoning effort, service tier, fast mode) can be scoped to a single agent/session without mutating the user’s global ~/.codex/config.toml or requiring named Codex profiles.
Changes:
- Introduces
codexConfig: Optional[Dict[str, Any]]onAgentProfileand parses it from profile frontmatter. - Updates the Codex provider to serialize
codexConfigvalues and append-coverrides to the launch command (both--yoloand--profilepaths). - Adds documentation and unit tests covering parsing, serialization, and command composition.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/utils/test_agent_profiles.py | Adds tests ensuring codexConfig frontmatter parses into AgentProfile. |
| test/providers/test_codex_provider_unit.py | Adds unit tests for _toml_scalar and for codexConfig -c emission/composition. |
| src/cli_agent_orchestrator/providers/codex.py | Adds _toml_scalar and appends codexConfig overrides to the Codex command. |
| src/cli_agent_orchestrator/models/agent_profile.py | Adds the new codexConfig field to the profile model. |
| docs/codex-cli.md | Documents inline Codex config overrides via codexConfig. |
| docs/agent-profile.md | Documents codexProfile and codexConfig in the profile field reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _toml_scalar(value: Any) -> str: | ||
| """Serialize a Python scalar to a TOML literal for a ``-c key=<value>`` override. | ||
|
|
||
| Strings become quoted TOML basic strings (backslash/quote/newline escaped so | ||
| tmux ``send_keys`` keeps the launch command on one line); bools become | ||
| ``true``/``false``; ints and floats are emitted bare. ``bool`` is checked | ||
| before ``int`` because ``bool`` is a subclass of ``int`` in Python, so the | ||
| order here is load-bearing — a flipped order would render ``True`` as ``1``. | ||
| """ | ||
| if isinstance(value, bool): | ||
| return "true" if value else "false" | ||
| if isinstance(value, (int, float)): | ||
| return str(value) | ||
| escaped = str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") | ||
| return f'"{escaped}"' |
There was a problem hiding this comment.
Partially agreed. The valuable part is real: silently accepting non-scalars (dict/list/None) would serialize a Python repr and fail confusingly in Codex, so _toml_scalar now raises TypeError for non-scalar values so a misconfigured profile fails fast. I also added \t/\r escaping for completeness.
That said, in practice codexConfig values are author-controlled config scalars (effort/service-tier strings, bool flags) rather than untrusted input, so the control-character path is more defensive than a real vector here.
_toml_scalar now raises TypeError for non-scalar values (dict/list/None) so a misconfigured profile fails with a clear message instead of silently serializing a Python repr that Codex cannot parse. Tab and carriage return are escaped too, keeping the emitted TOML basic string valid.
| if profile.codexConfig: | ||
| for key, value in profile.codexConfig.items(): | ||
| command_parts.extend(["-c", f"{key}={_toml_scalar(value)}"]) |
There was a problem hiding this comment.
Addressed in 9f66945. Two parts here:
- Values were already escaped and validated by
_toml_scalar(backslash/quote/\t/\r/\nescaped, non-scalars rejected withTypeError), and the whole command goes throughshlex.join, so a value can't split the tmux payload or inject. - Keys were the gap — they were interpolated raw. Added
_toml_override(), which allowlists keys to a safe dotted-path syntax ([A-Za-z0-9_.-]) and rejects spaces /=/ quotes / control chars with a clearValueErrornaming the key, so a misconfigured profile fails fast instead of producing a malformed-coverride. Value-serialization errors are also re-raised with the offending key for context.
| # Codex-only. Inline Codex config overrides passed as `-c key=value` at | ||
| # launch (e.g. {"model_reasoning_effort": "xhigh", "service_tier": "fast", | ||
| # "features.fast_mode": true}). Keys may be dotted paths into Codex's | ||
| # config.toml schema; values are serialized to TOML scalars (strings are |
There was a problem hiding this comment.
Fixed in 9f66945 — the example is Python (a dict literal), so it now uses True instead of the YAML-style true.
codexConfig keys were interpolated raw into the `-c key=value` override. Add _toml_override(), which rejects keys outside a safe dotted-path syntax ([A-Za-z0-9_.-]) so spaces, `=`, quotes, or control characters fail fast instead of producing a malformed override or a multi-line tmux payload, and re-raises value-serialization errors with the offending key. Values were already escaped/validated by _toml_scalar. Also fix the AgentProfile.codexConfig doc example to use Python `True` instead of YAML `true`.
Summary
There is no way to set a per-agent Codex setting — reasoning effort, service tier, fast mode, model, and so on — from an agent profile. Today you must either edit the global
~/.codex/config.toml(which affects every agent and the user's own Codex) or maintain named profile files referenced bycodexProfile. This PR adds an optionalcodexConfigmap to the agent profile whose entries are passed straight to Codex as-c key=valueoverrides at launch — the same mechanism CAO already uses fordeveloper_instructionsandmcpServers.What changed
models/agent_profile— new optionalcodexConfig: Dict[str, Any].providers/codex—_toml_scalarserializes each value to a TOML literal (strings quoted; bools/ints/floats bare;boolis checked beforeint, sinceboolsubclassesint), and_build_codex_commandemits-c <key>=<value>for every entry, in both the default--yolopath and the--profile <codexProfile>path. Keys may be dotted config paths (e.g.features.fast_mode); overrides are emitted last so they win on key conflicts.codex-cli.md(new "Inline Codex Config Overrides" section) andagent-profile.md(field reference)._toml_scalarserialization,codexConfigcommand building (yolo path,--profilecomposition, model/MCP composition), and frontmatter parsing.Example
launches:
so the effort / fast-mode settings apply to that agent only — no global
~/.codex/config.tomledit and no named profile file.Why
This mirrors the existing per-session, no-global-mutation philosophy of the MCP overrides. It composes with
codexProfile(which governs sandbox/approvals) rather than replacing it, and unblocks per-agent reasoning effort without forcing the agent into a restricted-tools--profilelaunch.Testing
pytest— full suite green.black/isort— clean.codex exec --strict-config -c model_reasoning_effort=xhigh -c service_tier=fast -c features.fast_mode=truestarts clean and the session banner reportsreasoning effort: xhigh(--strict-configproves every key is recognized by this Codex version).