Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
bc120a7
fix(codex): preserve native agent model settings
danielmeppiel Oct 2, 2026
2ed7160
Merge remote-tracking branch 'origin/main' into danielmeppiel-issue-d…
danielmeppiel Oct 2, 2026
1266717
docs: record Codex model preservation fix
danielmeppiel Oct 2, 2026
e0b2582
fix(codex): bound dropped-fields diagnostic and derive leakage-guard …
danielmeppiel Oct 2, 2026
ff8bb12
Merge remote-tracking branch 'origin/main' into pr/3150/danielmeppiel…
danielmeppiel Oct 2, 2026
a0fcdfb
chore: resolve CHANGELOG.md merge with origin/main (v0.33.0 release)
danielmeppiel Oct 2, 2026
6f33b8b
fix(codex): bound individual dropped-field key display length
danielmeppiel Oct 2, 2026
3771702
spec: add req-tg-015 for Codex native model-settings + bounded diagno…
danielmeppiel Oct 3, 2026
fce1d9b
docs(spec): fold apm-spec-guardian round-1 findings into req-tg-015
danielmeppiel Oct 3, 2026
fc652ea
test(spec-conformance): exercise real non-Codex deployment for req-tg…
danielmeppiel Oct 3, 2026
a875a77
fix(apm-spec-guardian): repair Draft7 allOf/reserved_slot_anchor cont…
danielmeppiel Oct 5, 2026
4b7e411
chore(apm-spec-guardian): regenerate deployed schema mirror and lockf…
danielmeppiel Oct 5, 2026
fe455ab
Fold full-panel review nits: DRY field constants, tighter typing, bou…
danielmeppiel Oct 5, 2026
24f18c7
Correct _display_dropped_field_key type: revert to object
danielmeppiel Oct 5, 2026
d298a41
fix(codex): fold diagnostic and conformance review findings
danielmeppiel Oct 6, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ for...
| `name` | recommended | Display name; defaults to filename stem |
| `description` | yes | Used by Cascade and Copilot to decide when to surface the agent |
| `model` | optional | Pinned model the harness should switch to when invoked |
| `model_reasoning_effort` | optional | Native Codex reasoning effort, supplied as a string |
| `tools` | optional | Whitelist of tools the persona may call |
| `color` | optional | Display color for harnesses that render it (Copilot, Claude, OpenCode). OpenCode requires a `#rgb`/`#rrggbb` hex literal or one of its theme names; see "Common pitfalls" below |
| `handoffs` | optional | List of agent names (or VS Code structured handoff objects) this agent can hand off to |
Expand All @@ -182,9 +183,21 @@ of the supported capability tags (`read`, `write`, `shell`, `web`,
`*`). If any unsupported tag is present, the agent is not deployed at
all -- APM fails closed. Kiro may warn and fall back if the specified
`model` is unavailable; APM passes the value through without
validation. Codex translates only `name`, `description`, and the
Markdown body; APM does not yet generate the complete per-agent MCP
transport definitions needed to preserve `model` or `tools`. When
validation.

Codex receives `name`, `description`, and the Markdown body as
`developer_instructions`. Supplied string values for `model` and
`model_reasoning_effort` become top-level keys in the generated agent TOML,
as documented in the [Codex custom agent format](https://developers.openai.com/codex/subagents/#custom-agents).
Omitted fields remain absent; APM does not translate model names, choose
defaults, or validate which models and effort values your Codex supports.
Non-string values are dropped with a warning naming the field.

Other frontmatter fields are dropped with a warning that APM does not
translate them for Codex. This includes other native Codex settings and
any `codex:` block; there is no namespaced or arbitrary passthrough.
Remove unnecessary fields, or do not rely on their settings in the
generated agent. APM still cannot preserve `tools` restrictions. When
`tools` is present, `apm install` warns that the generated agent may
inherit every project or session MCP server. Remove `tools` if
unrestricted access is intentional; otherwise, do not use the
Expand Down Expand Up @@ -222,7 +235,7 @@ offending package and field so you can fix the source.
| grok-build | `.grok/agents/<name>.md` | verbatim |
| cursor | `.cursor/agents/<name>.md` | verbatim |
| opencode | `.opencode/agents/<name>.md` | verbatim |
| codex | `.codex/agents/<name>.toml` | `name` and `description` -> TOML; body becomes `developer_instructions`; unsupported `tools` emits a warning |
| codex | `.codex/agents/<name>.toml` | `name`, `description`, and supplied string `model` / `model_reasoning_effort` -> top-level TOML; body becomes `developer_instructions`; dropped metadata emits warnings |
| kiro | `.kiro/agents/<relative-stem>.md` | `description`, `model`, `tools` kept; `name` and unknown fields stripped; identity from path; fail closed on unsupported tools (ref: [kiro.dev/docs/custom-agents](https://kiro.dev/docs/custom-agents/), accessed 2026-08-03) |
| grok-build | `.grok/agents/<name>.md` | verbatim |
| windsurf | not deployed | Windsurf has no agents primitive -- author personas as skills (Cascade auto-invokes by description) |
Expand Down
28 changes: 28 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,34 @@ instructions: |
---
```

#### Codex target: native model settings

Codex agents deploy to `.codex/agents/<name>.toml`. APM writes `name`,
`description`, and the Markdown body as `developer_instructions`.
String `model` and `model_reasoning_effort` frontmatter values are
preserved as top-level TOML keys:

```markdown
---
name: reviewer
description: Reviews code
model: gpt-5.6-sol
model_reasoning_effort: high
---
Review the requested change.
```

Absent settings stay absent. APM does not translate model names, choose
defaults, or validate model availability or supported effort values.
Non-string values are dropped with a diagnostic. Other frontmatter,
including other native Codex settings and a `codex:` block, is dropped
with a diagnostic; APM provides no arbitrary or namespaced passthrough.
Remove unnecessary fields or do not rely on their settings in the
generated agent. `tools` restrictions still cannot be preserved: the
agent may inherit all project/session MCP servers. Remove `tools` only
if unrestricted access is intentional; otherwise do not use the generated
agent with Codex.

#### OpenCode target: frontmatter constraints

OpenCode (`target: opencode`, deploys to `.opencode/agents/`) parses
Expand Down
42 changes: 40 additions & 2 deletions src/apm_cli/integration/agent_integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,8 @@ def _write_codex_agent(
) -> None:
"""Transform an ``.agent.md`` file to Codex ``.toml`` format.

Parses YAML frontmatter for ``name`` and ``description``, uses
the markdown body as ``developer_instructions``.
Preserves ``name``, ``description``, and native model settings;
uses the markdown body as ``developer_instructions``.
"""
if source.is_symlink():
raise ValueError(f"Refusing to read symlink source: {source}")
Expand All @@ -477,6 +477,8 @@ def _write_codex_agent(
name = name[: -len(".agent")]
description = ""
body = content
model_fields = ("model", "model_reasoning_effort")
model_settings: dict[str, str] = {}

fm_match = AgentIntegrator._FRONTMATTER_RE.match(content)
if fm_match:
Expand All @@ -486,6 +488,41 @@ def _write_codex_agent(
if isinstance(fm, dict):
name = fm.get("name", name)
description = fm.get("description", description)
for field in model_fields:
if field not in fm:
continue
if isinstance(fm[field], str):
model_settings[field] = fm[field]
elif diagnostics is not None:
diagnostics.lossy_agent_compilation(
message=(
f"Codex agent {printable_ascii_text(source.name)}: frontmatter "
f"field '{field}' must be a string and was dropped."
),
package=printable_ascii_text(package_name),
detail=(
f"Fix: set '{field}' to a string in the source agent, "
"then rerun 'apm install'."
),
)
dropped_fields = [
f"'{printable_ascii_text(str(field))}'"
for field in fm
if field not in {"name", "description", "tools", *model_fields}
]
if dropped_fields and diagnostics is not None:
diagnostics.lossy_agent_compilation(
message=(
f"Codex agent {printable_ascii_text(source.name)}: frontmatter "
f"fields {', '.join(dropped_fields)} were dropped; "
"this metadata is not translated by APM for Codex."
),
package=printable_ascii_text(package_name),
detail=(
"Fix: remove these fields if unnecessary; otherwise do not rely on "
"their settings in the generated Codex agent."
),
)
Comment thread
danielmeppiel marked this conversation as resolved.
Outdated
else:
AgentIntegrator._warn_codex_unverified_scope(
diagnostics,
Expand Down Expand Up @@ -513,6 +550,7 @@ def _write_codex_agent(
doc = {
"name": name,
"description": description,
**model_settings,
"developer_instructions": body.strip(),
}
write_text_lf(target, _toml.dumps(doc))
Expand Down
41 changes: 40 additions & 1 deletion tests/integration/test_codex_agent_tool_scope_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
import tomllib

from apm_cli.utils.yaml_io import dump_yaml, load_yaml
from apm_cli.utils.yaml_io import dump_yaml, load_yaml, yaml_to_str
from tests.utils.apm_lifecycle_runner import ApmLifecycleRunner, CommandResult
from tests.utils.isolated_apm_environment import IsolatedApmEnvironment
from tests.utils.local_package import LocalPackageFactory
Expand Down Expand Up @@ -42,6 +42,7 @@ def _install_codex_agent(
*,
name: str,
tools: list[str] | None,
metadata: dict[str, str] | None = None,
) -> tuple[CommandResult, Path]:
"""Author and install one Codex project through the packaged CLI boundary."""
isolated = IsolatedApmEnvironment.create(root, base_env=dict(os.environ))
Expand All @@ -58,6 +59,8 @@ def _install_codex_agent(
]
if tools is not None:
frontmatter.append("tools: [read, search, 'allowed-demo/*']")
if metadata:
frontmatter.append(yaml_to_str(metadata).rstrip("\n"))
frontmatter.extend(("---", "", "Review the requested change.", ""))
factory.add_agent(project, name, "\n".join(frontmatter))

Expand Down Expand Up @@ -116,6 +119,42 @@ def test_codex_agent_tool_scope_is_never_silently_lost(
assert unscoped_agent["name"] == "plain-reviewer"
assert "tools" not in unscoped_agent
assert "mcp_servers" not in unscoped_agent
assert "model" not in unscoped_agent
assert "model_reasoning_effort" not in unscoped_agent
assert "[!]" not in unscoped_output
assert "lossy agent compilation" not in unscoped_output
assert "frontmatter field 'tools' was dropped" not in " ".join(unscoped_output.split())


def test_codex_agent_native_models_and_dropped_metadata_reach_cli_output(
tmp_path: Path,
apm_binary_path: Path,
) -> None:
"""Install preserves model settings and reports APM's remaining translation loss."""
result, project = _install_codex_agent(
tmp_path / "native-model",
apm_binary_path,
name="model-reviewer",
tools=None,
metadata={
"model": "gpt-5.6-sol",
"model_reasoning_effort": "high",
"model_verbosity": "low",
},
)
assert result.returncode == 0, result.stdout + result.stderr
assert _read_toml(project / ".codex" / "agents" / "model-reviewer.toml") == {
"name": "model-reviewer",
"description": "Codex agent model-reviewer",
"developer_instructions": "Review the requested change.",
"model": "gpt-5.6-sol",
"model_reasoning_effort": "high",
}
output = " ".join((result.stdout + result.stderr).split())
assert "[!]" in output
assert "1 lossy agent compilation warning" in output
assert "model-reviewer.agent.md" in output
assert "fields 'model_verbosity' were dropped" in output
assert "not translated by APM for Codex" in output
assert "otherwise do not rely on" in output
assert "field 'tools' was dropped" not in output
Loading
Loading