From e2d26470094249a0c60406ab1e2cd31b95541b0b Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 12 Jun 2026 12:10:34 -0700 Subject: [PATCH 1/3] feat(backends): inject AGENT_BACKEND_ENV_VAR in interactive and resume builders Guard hooks launched in interactive and resume sessions can now dispatch on backend identity via the CmdSpec.env flat var, eliminating the implicit os.environ read that created a cross-layer contract. - claude.py build_interactive_cmd and build_resume_cmd: inject both AGENT_BACKEND_ENV_VAR and AGENT_BACKEND_DYNACONF_ENV_VAR into the merged extras dict before caller env_extras (callers can still override) - codex.py build_interactive_cmd: add AGENT_BACKEND_ENV_VAR to the merged_extras dict alongside the existing dynaconf var - codex.py build_resume_cmd: set include_agent_backend_flat=True on the _codex_exec_extras call so the flat var is injected - _type_constants_env.py: add AGENT_BACKEND_ENV_VAR to CODEX_INTERACTIVE_REQUIRED_ENV so CodexEnvPolicy.build_env() raises if a future regression drops the injection - test_subpackage_isolation.py: bump codex.py line-limit exemption from 1062 to 1066 to accommodate the new injections Co-Authored-By: Claude Fable 5 --- src/autoskillit/core/types/_type_constants_env.py | 1 + src/autoskillit/execution/backends/claude.py | 4 ++++ src/autoskillit/execution/backends/codex.py | 5 ++++- tests/arch/test_subpackage_isolation.py | 7 +++++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/autoskillit/core/types/_type_constants_env.py b/src/autoskillit/core/types/_type_constants_env.py index 893552cb93..47f629d279 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 33a98e412b..7aa4a04dd8 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 85e45f98f4..13631994ea 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_subpackage_isolation.py b/tests/arch/test_subpackage_isolation.py index 5da7a7f675..012b27170d 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", ), } From 4129a95be3131503ff4b657b641c13b8784ad65d Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 12 Jun 2026 12:10:41 -0700 Subject: [PATCH 2/3] test(arch): assert AGENT_BACKEND_ENV_VAR present in all four guard-launch builders Two new parametrized arch tests enforce the invariant that AUTOSKILLIT_AGENT_BACKEND appears in CmdSpec.env for every backend across all four guard-launch builders (build_skill_session_cmd, build_food_truck_cmd, build_interactive_cmd, build_resume_cmd), and that the flat and dynaconf forms carry matching values in the interactive and resume builders. Both tests rely on the existing _clean_env autouse fixture which strips AUTOSKILLIT_AGENT_BACKEND and AUTOSKILLIT_AGENT_BACKEND__BACKEND from os.environ to ensure the flat var is sourced from each builder's extras dict, not ambient env. Co-Authored-By: Claude Fable 5 --- tests/arch/test_env_symmetry.py | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/arch/test_env_symmetry.py b/tests/arch/test_env_symmetry.py index 42b9ac67d2..40bcdfc308 100644 --- a/tests/arch/test_env_symmetry.py +++ b/tests/arch/test_env_symmetry.py @@ -158,3 +158,70 @@ 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.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 "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" From f600652ab53708b9dbd2ffc708abd00cc89126f3 Mon Sep 17 00:00:00 2001 From: Trecek Date: Fri, 12 Jun 2026 12:35:30 -0700 Subject: [PATCH 3/3] fix(review): add isinstance guard for _call_builder return type in test Adds `from autoskillit.core.types._type_backend import CmdSpec` and `assert isinstance(spec, CmdSpec)` in `test_agent_backend_flat_env_var_in_all_guard_launch_builders` so a wrong-type return from `_call_builder` produces a clear assertion failure rather than an opaque AttributeError. Co-Authored-By: Claude Sonnet 4.6 --- tests/arch/test_env_symmetry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/arch/test_env_symmetry.py b/tests/arch/test_env_symmetry.py index 40bcdfc308..c91a6f490f 100644 --- a/tests/arch/test_env_symmetry.py +++ b/tests/arch/test_env_symmetry.py @@ -193,6 +193,7 @@ def _call_builder(backend: object, builder_name: str) -> object: @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" @@ -201,6 +202,7 @@ def test_agent_backend_flat_env_var_in_all_guard_launch_builders(builder_name: s 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" )