Harden Codex emergency profile configuration
Why: Normal execution roles must exclude Codex while a single enabled Codex adapter remains available for emergency routing and review. Evidence: Parser invalid-case tests were RED before validation; all 17 config tests and agent_exec validate now pass. Impact: Enables three attempts per normal profile, stable Cursor routing, and a validated emergency Codex profile. Refs: feature/agent-cli-enforcement
This commit is contained in:
+18
-7
@@ -7,11 +7,14 @@ parser accepts exactly one `agent-execution` fenced block.
|
||||
|
||||
U3's execution controller and U4's project wiring are integrated. This file is
|
||||
the execution-routing authority for implementation, repair, test, and review.
|
||||
Active routes are intentionally limited to Cursor, Antigravity (`agy`), and
|
||||
PI. `model: auto` lets each CLI use its configured default model; PI therefore
|
||||
uses the operator-selected MiniMax default. A provider that makes no successful
|
||||
stdin write or stdout/stderr read for `idle_timeout_seconds` is stopped before
|
||||
`timeout_seconds`.
|
||||
Normal implementation, repair, and test routes stay Cursor, Antigravity
|
||||
(`agy`), and PI. Each profile may be attempted up to
|
||||
`max_attempts_per_profile`. Only after those candidates safely exhaust their
|
||||
attempts does the enabled emergency `codex` profile run once. Codex remains out
|
||||
of the three normal role lists and may also appear in review. A configured
|
||||
emergency profile replaces host write fallback. Cursor uses
|
||||
`cursor-agent-stable`. `model: auto` lets each CLI use its configured default
|
||||
model; PI therefore uses the operator-selected MiniMax default.
|
||||
|
||||
```agent-execution
|
||||
schema: agent-execution/v1
|
||||
@@ -25,17 +28,19 @@ execution:
|
||||
idle_timeout_seconds: 600
|
||||
summary_bytes: 16384
|
||||
raw_output_bytes: 10485760
|
||||
max_attempts_per_profile: 3
|
||||
emergency_profile: codex
|
||||
|
||||
roles:
|
||||
implementation: [cursor, antigravity, pi]
|
||||
repair: [cursor, antigravity, pi]
|
||||
test: [pi, antigravity, cursor]
|
||||
review: [pi, antigravity, cursor]
|
||||
review: [pi, antigravity, cursor, codex]
|
||||
|
||||
profiles:
|
||||
cursor:
|
||||
adapter: cursor
|
||||
executable: cursor-agent
|
||||
executable: cursor-agent-stable
|
||||
model: auto
|
||||
enabled: true
|
||||
|
||||
@@ -50,4 +55,10 @@ profiles:
|
||||
executable: pi
|
||||
model: auto
|
||||
enabled: true
|
||||
|
||||
codex:
|
||||
adapter: codex
|
||||
executable: codex
|
||||
model: auto
|
||||
enabled: true
|
||||
```
|
||||
|
||||
@@ -343,6 +343,8 @@ def _build_config(raw: dict[str, Any]) -> ExecutionConfig:
|
||||
if profile.adapter not in custom_adapters:
|
||||
raise ConfigError(f"unknown adapter: {profile.adapter!r}")
|
||||
|
||||
_validate_emergency_profile(execution, profiles, roles)
|
||||
|
||||
return ExecutionConfig(
|
||||
schema=schema,
|
||||
execution=execution,
|
||||
@@ -428,6 +430,28 @@ def _build_execution(raw: Any) -> ExecutionPolicy:
|
||||
)
|
||||
|
||||
|
||||
def _validate_emergency_profile(
|
||||
execution: ExecutionPolicy,
|
||||
profiles: Mapping[str, Profile],
|
||||
roles: Mapping[str, tuple[str, ...]],
|
||||
) -> None:
|
||||
name = execution.emergency_profile
|
||||
if name is None:
|
||||
return
|
||||
profile = profiles.get(name)
|
||||
if profile is None:
|
||||
raise ConfigError(f"emergency_profile {name!r} does not exist")
|
||||
if not profile.enabled:
|
||||
raise ConfigError(f"emergency_profile {name!r} must be enabled")
|
||||
if profile.adapter != "codex":
|
||||
raise ConfigError(f"emergency_profile {name!r} must use adapter codex")
|
||||
for role in ("implementation", "repair", "test"):
|
||||
if name in roles[role]:
|
||||
raise ConfigError(
|
||||
f"emergency_profile {name!r} must not appear in role {role!r}"
|
||||
)
|
||||
|
||||
|
||||
def _positive_bounded(value: Any, name: str, lo: int, hi: int) -> int:
|
||||
if type(value) is not int or isinstance(value, bool):
|
||||
raise ConfigError(f"{name} must be an integer")
|
||||
|
||||
@@ -434,6 +434,80 @@ class AgentExecConfigTests(unittest.TestCase):
|
||||
self.assertEqual(cfg.execution.max_attempts_per_profile, 3)
|
||||
self.assertEqual(cfg.execution.emergency_profile, "codex")
|
||||
|
||||
def test_configured_emergency_profile_must_exist_be_enabled_and_use_adapter_codex(
|
||||
self,
|
||||
) -> None:
|
||||
write_repo(self.repo, _emergency_block(include_profile=False))
|
||||
with self.assertRaises(ConfigError) as ctx:
|
||||
load_config(self.repo)
|
||||
self.assertIn("does not exist", str(ctx.exception))
|
||||
|
||||
write_repo(self.repo, _emergency_block(enabled="false"))
|
||||
with self.assertRaises(ConfigError) as ctx:
|
||||
load_config(self.repo)
|
||||
self.assertIn("must be enabled", str(ctx.exception))
|
||||
|
||||
write_repo(self.repo, _emergency_block(adapter="pi"))
|
||||
with self.assertRaises(ConfigError) as ctx:
|
||||
load_config(self.repo)
|
||||
self.assertIn("adapter codex", str(ctx.exception))
|
||||
|
||||
def test_emergency_profile_is_excluded_from_normal_roles(self) -> None:
|
||||
for role, replacement in (
|
||||
("implementation", "[cursor, claude, codex]"),
|
||||
("repair", "[cursor, codex]"),
|
||||
("test", "[claude, codex]"),
|
||||
):
|
||||
write_repo(self.repo, _emergency_block(**{role: replacement}))
|
||||
with self.assertRaises(ConfigError) as ctx:
|
||||
load_config(self.repo)
|
||||
self.assertIn("must not appear", str(ctx.exception))
|
||||
self.assertIn(role, str(ctx.exception))
|
||||
|
||||
def test_review_may_include_emergency_profile(self) -> None:
|
||||
write_repo(self.repo, _emergency_block(review="[claude, codex]"))
|
||||
cfg = load_config(self.repo)
|
||||
self.assertEqual(cfg.execution.emergency_profile, "codex")
|
||||
self.assertTrue(cfg.profiles["codex"].enabled)
|
||||
self.assertEqual(cfg.profiles["codex"].adapter, "codex")
|
||||
self.assertEqual(cfg.roles["review"], ("claude", "codex"))
|
||||
self.assertNotIn("codex", cfg.roles["implementation"])
|
||||
self.assertNotIn("codex", cfg.roles["repair"])
|
||||
self.assertNotIn("codex", cfg.roles["test"])
|
||||
|
||||
|
||||
def _emergency_block(
|
||||
*,
|
||||
include_profile: bool = True,
|
||||
adapter: str = "codex",
|
||||
enabled: str = "true",
|
||||
implementation: str = "[cursor, claude]",
|
||||
repair: str = "[cursor]",
|
||||
test: str = "[claude]",
|
||||
review: str = "[claude]",
|
||||
) -> str:
|
||||
block = MINIMAL_BLOCK.replace(
|
||||
" raw_output_bytes: 10485760\n",
|
||||
" raw_output_bytes: 10485760\n emergency_profile: codex\n",
|
||||
)
|
||||
block = block.replace("implementation: [cursor, claude]", f"implementation: {implementation}")
|
||||
block = block.replace("repair: [cursor]", f"repair: {repair}")
|
||||
block = block.replace("test: [claude]", f"test: {test}")
|
||||
block = block.replace("review: [claude]", f"review: {review}")
|
||||
if not include_profile:
|
||||
return block
|
||||
return block.replace(
|
||||
" enabled: true\n",
|
||||
" enabled: true\n"
|
||||
"\n"
|
||||
" codex:\n"
|
||||
f" adapter: {adapter}\n"
|
||||
" executable: codex\n"
|
||||
" model: auto\n"
|
||||
f" enabled: {enabled}\n",
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user