diff --git a/src/autoskillit/core/types/_type_constants_env.py b/src/autoskillit/core/types/_type_constants_env.py index 893552cb9..47f629d27 100644 --- a/src/autoskillit/core/types/_type_constants_env.py +++ b/src/autoskillit/core/types/_type_constants_env.py @@ -158,6 +158,7 @@ CODEX_INTERACTIVE_REQUIRED_ENV: frozenset[str] = frozenset( { MCP_CLIENT_BACKEND_ENV_VAR, + AGENT_BACKEND_ENV_VAR, } ) diff --git a/src/autoskillit/execution/backends/claude.py b/src/autoskillit/execution/backends/claude.py index 33a98e412..7aa4a04dd 100644 --- a/src/autoskillit/execution/backends/claude.py +++ b/src/autoskillit/execution/backends/claude.py @@ -461,6 +461,8 @@ def build_interactive_cmd( for t in tools: builder.variadic_pair(ClaudeFlags.TOOLS, t) merged: dict[str, str] = dict(SHARED_BASELINE_ENV) + merged[AGENT_BACKEND_ENV_VAR] = AGENT_BACKEND_CLAUDE_CODE + merged[AGENT_BACKEND_DYNACONF_ENV_VAR] = AGENT_BACKEND_CLAUDE_CODE if env_extras: merged.update(env_extras) interactive_base = { @@ -500,6 +502,8 @@ def build_resume_cmd( case None: pass merged: dict[str, str] = dict(SHARED_BASELINE_ENV) + merged[AGENT_BACKEND_ENV_VAR] = AGENT_BACKEND_CLAUDE_CODE + merged[AGENT_BACKEND_DYNACONF_ENV_VAR] = AGENT_BACKEND_CLAUDE_CODE if env_extras: merged.update(env_extras) env = dict(build_agent_env(base={}, extras=merged)) diff --git a/src/autoskillit/execution/backends/codex.py b/src/autoskillit/execution/backends/codex.py index 85e45f98f..13631994e 100644 --- a/src/autoskillit/execution/backends/codex.py +++ b/src/autoskillit/execution/backends/codex.py @@ -888,6 +888,7 @@ def build_interactive_cmd( "AUTOSKILLIT_HEADLESS": "", "AUTOSKILLIT_HEADLESS_AUTO_GATE": "", "AUTOSKILLIT_SESSION_TYPE": "", + AGENT_BACKEND_ENV_VAR: AGENT_BACKEND_CODEX, AGENT_BACKEND_DYNACONF_ENV_VAR: AGENT_BACKEND_CODEX, MCP_CLIENT_BACKEND_ENV_VAR: AGENT_BACKEND_CODEX, FOOD_TRUCK_TOOL_TAGS_ENV_VAR: "", @@ -926,7 +927,9 @@ def build_resume_cmd( cmd.append(resume_session_id) cmd.append(prompt) filtered_base = {k: v for k, v in os.environ.items() if k not in _HEADLESS_EXCLUSIVE_VARS} - resume_extras = _codex_exec_extras(session_type="", include_session_baseline=True) + resume_extras = _codex_exec_extras( + session_type="", include_session_baseline=True, include_agent_backend_flat=True + ) if env_extras: resume_extras.update(env_extras) env = self.env_policy().build_env( diff --git a/tests/arch/test_env_symmetry.py b/tests/arch/test_env_symmetry.py index 42b9ac67d..c91a6f490 100644 --- a/tests/arch/test_env_symmetry.py +++ b/tests/arch/test_env_symmetry.py @@ -158,3 +158,72 @@ def test_dynaconf_and_flat_backend_values_match() -> None: food_truck_spec.env["AUTOSKILLIT_AGENT_BACKEND__BACKEND"] == food_truck_spec.env["AUTOSKILLIT_AGENT_BACKEND"] ), f"{name}: nested and flat AGENT_BACKEND values differ in build_food_truck_cmd" + + +_ALL_GUARD_BUILDERS: list[str] = [ + "build_skill_session_cmd", + "build_food_truck_cmd", + "build_interactive_cmd", + "build_resume_cmd", +] + + +def _call_builder(backend: object, builder_name: str) -> object: + from autoskillit.core.types._type_plugin_source import DirectInstall + + if builder_name == "build_skill_session_cmd": + return backend.build_skill_session_cmd( + "/autoskillit:investigate", "/repo", completion_marker="DONE" + ) + if builder_name == "build_food_truck_cmd": + return backend.build_food_truck_cmd( + orchestrator_prompt="test prompt", + plugin_source=DirectInstall(plugin_dir=Path("/plugins")), + cwd="/repo", + completion_marker="DONE", + ) + if builder_name == "build_interactive_cmd": + return backend.build_interactive_cmd() + if builder_name == "build_resume_cmd": + return backend.build_resume_cmd(resume_session_id="test-session", prompt="continue") + msg = f"Unknown builder: {builder_name}" + raise ValueError(msg) + + +@pytest.mark.parametrize("builder_name", _ALL_GUARD_BUILDERS) +def test_agent_backend_flat_env_var_in_all_guard_launch_builders(builder_name: str) -> None: + """AUTOSKILLIT_AGENT_BACKEND must appear in every builder's CmdSpec.env for every backend.""" + from autoskillit.core.types._type_backend import CmdSpec + from autoskillit.execution.backends import BACKEND_REGISTRY + + assert BACKEND_REGISTRY, "BACKEND_REGISTRY is empty — test provides no coverage" + for name, cls in BACKEND_REGISTRY.items(): + backend = cls() + if builder_name == "build_resume_cmd" and not backend.capabilities.session_resume_capable: + continue + spec = _call_builder(backend, builder_name) + assert isinstance(spec, CmdSpec), f"{name}: {builder_name} did not return CmdSpec" + assert "AUTOSKILLIT_AGENT_BACKEND" in spec.env, ( + f"{name}: AUTOSKILLIT_AGENT_BACKEND missing from {builder_name} env" + ) + + +def test_agent_backend_flat_and_dynaconf_values_match_in_interactive_and_resume() -> None: + """Nested and flat AGENT_BACKEND env vars must carry the same value in interactive and resume builders.""" # noqa: E501 + from autoskillit.execution.backends import BACKEND_REGISTRY + + assert BACKEND_REGISTRY, "BACKEND_REGISTRY is empty — test provides no coverage" + for name, cls in BACKEND_REGISTRY.items(): + backend = cls() + interactive_spec = backend.build_interactive_cmd() + assert ( + interactive_spec.env["AUTOSKILLIT_AGENT_BACKEND__BACKEND"] + == interactive_spec.env["AUTOSKILLIT_AGENT_BACKEND"] + ), f"{name}: nested and flat AGENT_BACKEND values differ in build_interactive_cmd" + if not backend.capabilities.session_resume_capable: + continue + resume_spec = backend.build_resume_cmd(resume_session_id="test-session", prompt="continue") + assert ( + resume_spec.env["AUTOSKILLIT_AGENT_BACKEND__BACKEND"] + == resume_spec.env["AUTOSKILLIT_AGENT_BACKEND"] + ), f"{name}: nested and flat AGENT_BACKEND values differ in build_resume_cmd" diff --git a/tests/arch/test_subpackage_isolation.py b/tests/arch/test_subpackage_isolation.py index 5da7a7f67..012b27170 100644 --- a/tests/arch/test_subpackage_isolation.py +++ b/tests/arch/test_subpackage_isolation.py @@ -982,7 +982,7 @@ def test_data_directories_are_not_python_packages() -> None: "dispatch gate add defense-in-depth checks", ), "execution/backends/codex.py": ( - 1062, + 1066, "REQ-CNST-010-E9: Codex backend — skill_sigil capability threading adds multi-line " "keyword args to _ensure_skill_prefix call sites and _has_prefix guard; " "write_guard_tool_names env injection adds 7 lines to _codex_exec_extras; " @@ -995,7 +995,10 @@ def test_data_directories_are_not_python_packages() -> None: "; session_log_path method added to CodexSessionLocator (+5 net lines)" "; process_idle_timeout_ms field wired through build_skill_session_cmd and " "build_food_truck_cmd CmdSpec constructors (+8 net lines)" - "; CapabilityNotSupportedError capability-gate in build_inspector_cmd (+1 net line)", + "; CapabilityNotSupportedError capability-gate in build_inspector_cmd (+1 net line); " + "AGENT_BACKEND_ENV_VAR injection in build_interactive_cmd merged_extras (+1 net line) " + "and build_resume_cmd _codex_exec_extras call expansion to multi-line (+2 net lines) " + "for T5-P4-A3-WP3 guard-hook backend dispatch", ), }