Skip to content

_is_vacuous_gate reachability blind spot bypasses dispatch_infeasible admission control for Codex backend #4161

Description

@Trecek

Investigation: Codex Pipeline DOA Bypass via _is_vacuous_gate Reachability Blind Spot

Date: 2026-06-30
Scope: Why Codex+implementation pipeline passes dispatch_infeasible admission control and runs 21 minutes of irreversible side effects before dying at gate_backend_write, despite PR #4094 and #4130 adding capability admission control.
Mode: Deep Analysis (2 exploration batches + adversarial challenge + post-report validation; 9 subagents)

Summary

The Codex+implementation pipeline bypasses admission control because _is_vacuous_gate (_recipe_composition.py:178-213, introduced by PR #4130 on June 27) incorrectly declares gate_backend_write as "vacuous" — concluding that since all guarded steps were pruned, the gate has nothing left to guard and should not trigger dispatch_infeasible. But _is_vacuous_gate only checks whether surviving steps use the capability ingredient in their with_args; it does not check whether the gate step itself remains reachable in the post-prune flow graph. After _prune_skipped_steps prunes implement, route-repair redirects create_impl_worktree.on_success from implement to gate_backend_write, making the gate directly reachable. The pipeline is dispatched, runs 11 side-effecting steps (~21 minutes), then gate_backend_write fires and routes to release_issue_failure — exactly the DOA outcome that admission control was designed to prevent.

This is the third oscillation of a pendulum pattern: PR #4094 blocks DOA pipelines → PR #4130 unblocks (vacuous-gate exemption goes too far) → today's failure. The structural cause is that admission control operates on heuristic inference (pattern-matching callables against registries) rather than author-declared intent (the purpose_critical step annotation proposed in PR #4094's REQ-SCHEMA-001, which was never implemented).

Root Cause

Primary (architectural): _is_vacuous_gate at _recipe_composition.py:178-213 has a reachability blind spot. It determines vacuity by checking two conditions: (1) at least one pre-prune step with a matching skip_when_false guard was pruned (pruned_for_capability), and (2) no surviving step (other than the gate itself) references the capability ingredient in with_args (live_uses_capability). Both conditions are met for Codex+implementation. But the function never checks whether the gate step is reachable from any surviving step via routing fields (on_success, on_failure, on_result, etc.). After route-repair, create_impl_worktree.on_success points directly to gate_backend_write. The gate is reachable, will execute, and will deterministically fail — but _is_vacuous_gate returns True. [SUPPORTED]

Proximate (why the reachability check is missing): PR #4130 was solving a real false-positive problem — #4094's admission control blocked all Codex+implementation runs even though the pruned recipe's gate was intended to be a defense-in-depth terminal. The vacuous-gate concept is architecturally correct (a gate CAN be genuinely unreachable), but the implementation checks the wrong predicate. It asks "does any surviving step consume the capability?" when it should also ask "is the gate step itself reachable from the entry point?" [SUPPORTED]

Why prior fixes did not address this: The fix chain follows a pendulum pattern:

  1. 100+ pre-[FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094 fixes — each patched the step where the previous run died (downstream relocation)
  2. PR [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094 (June 12) — admission control at load time (correct architecture, but over-blocked)
  3. PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130 (June 27) — vacuous-gate exemption (correct intent, but reachability gap re-opened the hole)
  4. Today (June 30) — pipeline passes admission and wastes 21 minutes before failing

The purpose_critical per-step annotation proposed in PR #4094's design (REQ-SCHEMA-001) was never implemented. The system uses inference-based heuristics layered on inference-based heuristics, each layer correcting the prior while introducing new failure surface. [SUPPORTED]

Affected Components

  • src/autoskillit/recipe/_recipe_composition.py:178-213_is_vacuous_gate(): missing reachability check [SUPPORTED]
  • src/autoskillit/recipe/_recipe_composition.py:109-175_compute_capability_feasibility(): calls _is_vacuous_gate and trusts its result [SUPPORTED]
  • src/autoskillit/recipe/_recipe_composition.py:401-524_prune_skipped_steps() (function); route-repair logic at lines 460-520 redirects create_impl_worktree.on_success to gate_backend_write [SUPPORTED]
  • src/autoskillit/recipe/_api.py:405-412load_and_validate(): calls _compute_capability_feasibility, propagates dispatch_feasible=True [SUPPORTED]
  • src/autoskillit/server/tools/tools_kitchen.py:844-847open_kitchen feasibility check: passes because dispatch_feasible=True [SUPPORTED]
  • src/autoskillit/recipes/implementation.yaml:298-365create_impl_worktree (no guard) → implement (guarded, line 328) → gate_backend_write (no guard) topology [SUPPORTED]
  • src/autoskillit/recipes/remediation.yaml:335-405 — Same topology (create_impl_worktree at 335, implement at 347, gate_backend_write at 396-405) [SUPPORTED]
  • src/autoskillit/recipes/implementation-groups.yaml:261-323 — Same topology [SUPPORTED]
  • src/autoskillit/recipe/_analysis_bfs.py:17,58bfs_reachable() and _build_step_graph() — existing reachability machinery not used by _is_vacuous_gate [SUPPORTED]
  • src/autoskillit/core/types/_type_constants.py:95-118BACKEND_CAPABILITY_INGREDIENTS, CAPABILITY_GATE_CALLABLES, CAPABILITY_INGREDIENT_TO_SKIP_GUARD registries [SUPPORTED]

Data Flow

  1. User runs AUTOSKILLIT_FEATURES__CODEX_BACKEND=true AUTOSKILLIT_AGENT_BACKEND__BACKEND=codex autoskillit order
  2. get_backend("codex")CodexBackendcapabilities.git_metadata_writable=False (codex.py:594)
  3. open_kitchen(name="implementation")_backend_capability_overrides(){"backend_supports_git_write": "false"} (_auto_overrides.py:27-28)
  4. _session_overrides.update(_backend_capability_overrides(...)) injects "false" (line 659); _promote_capability_keys(_config_layer, _session_overrides) copies it into config-authoritative layer so it wins the merge (line 661); merged at line 662
  5. load_and_validate("implementation", ingredient_overrides={"backend_supports_git_write": "false"})_prune_skipped_steps():
    • Prunes implement (line 328: skip_when_false: inputs.backend_supports_git_write)
    • Route-repair: create_impl_worktree.on_success redirected from implementgate_backend_write (lines 464-465, 484-485)
    • Prunes retry_worktree, fix, merge_gate_fix, rebase_conflict_fix, resolve_review, resolve_pre_review_conflicts — same guard
  6. _compute_capability_feasibility() evaluates surviving gate_backend_write:
    • tool=run_python ✓, callable in CAPABILITY_GATE_CALLABLES ✓, all_falsy=True ✓, on_exhausted="escalate" (schema default) ✓
    • Calls _is_vacuous_gate(): pruned_for_capability=True (implement pruned), live_uses_capability=False (no surviving step has backend_supports_git_write in with_args) → returns True
    • Gate NOT added to infeasibledispatch_feasible=True
  7. open_kitchen returns success=True → kitchen gate opens → orchestrator starts executing
  8. Side-effecting steps execute: bootstrap_clone (~3s), claim_and_resolve_issue (~3s), create_and_publish_branch (~3s), make-plan (~13min), review-approach (~5min), dry-walkthrough (~8min), create_impl_worktree (~1s)
  9. gate_backend_write(backend_supports_git_write="false"){"backend_capable": "false"} → routes to release_issue_failure
  10. Issue labeled fail, clone registered as error, worktree abandoned. ~21 minutes wasted.

Test Gap Analysis

Tests pin the bug as correct behavior — the same pattern as the June 12 investigation:

  • tests/server/test_backend_ingredient_injection.py:554-564 — asserts Codex + implementation open_kitchen returns success=True and dispatch_feasible=True (changed by PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130 from success=False, kitchen=dispatch_infeasible) [SUPPORTED]
  • tests/recipe/test_bundled_recipes_dispatch_ready.py:268-282 — function named test_codex_implementation_dispatch_infeasible but asserts dispatch_feasible is True (name-assertion mismatch from pendulum) [SUPPORTED]
  • tests/recipe/test_bundled_recipes_dispatch_ready.py:329-350test_codex_capability_gate_recipes uses disjunctive assertion accepting EITHER True OR False — cannot catch a new class of regression [SUPPORTED]
  • No end-to-end test simulates a full Codex+implementation pipeline (open_kitchen → execute steps → verify side-effect prevention). All tests are layer-local. [SUPPORTED]

Similar Patterns

  • _check_dispatch_feasibility (_preflight.py:34-114) solves the analogous problem for run_skill steps — iterates steps, checks per-step provider profile, skips steps routed to a capable backend. Same pattern needed for run_python capability gates. [SUPPORTED]
  • The recipe graph rule failure-verdict-bypass-reachable (Rectify: Failure-Verdict Bypass Immunity via Static Reachability Rule #3630) already performs terminal-classified reachability using bfs_reachable() and _build_step_graph(). The machinery for the correct check exists. [SUPPORTED]
  • The create_impl_worktree step executes at the orchestrator/host level (unsandboxed run_cmd), not inside a Codex session. All host-side git operations succeed on Codex — the irreducible gap is only implement committing inside the sandbox. [SUPPORTED]

Design Intent Findings

Historical Context

This is the third oscillation of a confirmed pendulum pattern:

The pendulum:

Fix chain (all verified by commit message + diff):
#3844#3880#3892#3902#3960#3973#3980#3981#3995#4068#4071#4075#4086#4087#4094#4130

Prior investigations directly on this topic: investigation_codex_gate_backend_write_recurrence_2026-06-12_083200.md (June 12, deep analysis, 9 subagents), investigation_codex_dispatch_infeasible_2026-06-12_151200.md (June 12, deep analysis). Both identified the root cause correctly but proposed fixes that were either not implemented as designed (purpose_critical) or introduced new gaps (vacuous-gate).

This is a recurring pattern — consider running /rectify for architectural immunity after resolving the immediate issue.

External Research

  • Codex CLI workspace-write sandbox protects .git/ as read-only by design (Linux landlock/bwrap). Headless codex exec cannot use --sandbox danger-full-access with approval_policy=never. This constraint is structural and unfixable from AutoSkillit's side. Source: openai.com/codex/concepts/sandboxing, openai/codex PR #19852. [SUPPORTED]
  • Aider performs all git operations host-side (LLM never touches .git/). AutoSkillit's static capability declaration is the most robust multi-backend pattern but is consumed only mid-pipeline, not at admission with correct reachability. [SUPPORTED]

Scope Boundary

Investigated: _is_vacuous_gate logic and its reachability blind spot; all three affected recipes (implementation, remediation, implementation-groups); _prune_skipped_steps route-repair mechanism; _compute_capability_feasibility full logic; all five admission-control enforcement surfaces (normal open_kitchen, deferred open_kitchen, get_recipe, load_recipe, dispatch_food_truck); 16-commit fix chain; installed vs. repo version parity; existing BFS reachability machinery; adversarial challenge of root cause (7 counterhypotheses tested, all refuted).

Not yet explored: Whether load_recipe soft enforcement (sets flag but doesn't early-return at tools_recipe.py:240-245) is a latent exploit path; whether purpose_critical annotation is still the correct long-term fix or whether reachability alone is sufficient; exact GitHub fail-label issue count attributable to this family; whether research/merge-prs recipes silently degrade on Codex (no gate step, so no DOA signal).

Recommendations

Single recommendation: Add post-prune reachability check to _is_vacuous_gate.

_is_vacuous_gate (_recipe_composition.py:178-213) must verify that the gate step is NOT reachable from any surviving step before declaring it vacuous. The check should use the existing _collect_all_route_targets helper (same file, line 25) which already covers all routing fields (on_success, on_failure, on_context_limit, on_rate_limit, on_exhausted, on_result conditions).

After the for cap_key in gate_input_keys loop completes (line 212 is inside the loop body), at the same indentation level as return True (line 213):

  • Scan post_prune_steps for any step (other than the gate itself) whose route targets include gate_step_name
  • If found, return False — the gate is reachable and therefore not vacuous

This leverages existing infrastructure (_collect_all_route_targets at line 25) and adds a single structural check to close the reachability gap. No schema changes, no new registries, no heuristic layers.

Complementary actions:

  1. Fix the misnamed test test_codex_implementation_dispatch_infeasible to assert dispatch_feasible=False (reversing PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130's flip back to [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094's correct contract)
  2. Add an end-to-end test that verifies Codex+implementation is rejected pre-claim (not just at load_and_validate level, but at open_kitchen/dispatch_food_truck level)
  3. Harden load_recipe enforcement at tools_recipe.py:240-245 from soft (flag-only) to hard (early-return refusal)

Killed alternatives:

  • Remove _is_vacuous_gate entirely — killed: re-introduces the false-positive from [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094 for any future recipe with genuinely unreachable gates
  • Add purpose_critical annotation (REQ-SCHEMA-001) — killed as immediate fix: correct long-term but scope is too large for the acute issue; reachability check is sufficient and composable with purpose_critical if added later
  • Add skip_when_false to create_impl_worktree — killed: create_impl_worktree correctly executes at host level (unsandboxed); blocking it blocks the worktree for non-gate uses
  • Hand-key backend_supports_git_write check in _check_dispatch_feasibility — killed: same rot class as prior fixes; does not generalize to next capability

Breakage Analysis

  • Recommendation: Add post-prune reachability check to _is_vacuous_gate
    • Breakage surface: Tests pinning dispatch_feasible=True for Codex+implementation: test_backend_ingredient_injection.py:554-564, test_bundled_recipes_dispatch_ready.py:268-282, test_capability_admission_e2e.py (vacuous-gate test). All must be updated to assert dispatch_feasible=False and infeasible_steps=["gate_backend_write"].
    • Prior reverts: None found on admission control or vacuous-gate logic (git log --grep="revert" -i)
    • Downstream contract violations: None — gate_backend_write step is retained as defense-in-depth; skip_when_false pruning unchanged; research/merge-prs recipes unaffected (no gate_backend_write step). Fleet dispatch gains earlier refusal. ingredients_only=true discovery flow is unaffected (infeasibility is on the recipe, not the ingredients).
    • Implementation sites easy to miss: The deferred-recall open_kitchen path (tools_kitchen.py:731-734) is a second enforcement site; load_recipe (tools_recipe.py:240-245) needs hard enforcement upgrade; fleet dispatch_food_truck (tools_fleet_dispatch.py:340-351) already checks dispatch_feasible.
    • Risk level: LOW — concentrated change in one function (_is_vacuous_gate), test inversions, and one soft→hard enforcement upgrade in load_recipe.

Confidence Levels

Finding Confidence
_is_vacuous_gate missing reachability check is the root cause SUPPORTED
Route-repair makes gate_backend_write reachable via create_impl_worktree SUPPORTED
All three gate recipes (implementation, remediation, implementation-groups) affected SUPPORTED
Existing BFS machinery can be reused for the fix SUPPORTED
No version skew — installed 0.10.831 == repo HEAD SUPPORTED
PR #4130 introduced the regression; PR #4094 was correct but over-blocked SUPPORTED
Pendulum pattern across the fix chain SUPPORTED
purpose_critical annotation (REQ-SCHEMA-001) never implemented SUPPORTED
Tests pin the bug as correct behavior SUPPORTED
Adversarial challenge: 7 counterhypotheses tested, all refuted SUPPORTED
load_recipe soft enforcement is a latent gap NEEDS-EVIDENCE (structural observation; no observed exploit)
Research-family recipe silent degradation on Codex NEEDS-EVIDENCE (no gate step = no DOA signal; no incident observed)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugExisting behavior is brokenrecipe:remediationRoute: investigate/decompose before implementationstagedImplementation staged and waiting for promotion to main

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions