Skip to content

Commit e30abb5

Browse files
Trecekclaude
andcommitted
feat: gate all four content-serving surfaces on dispatch_feasible
Refuse DOA pipelines before any tool is revealed: - open_kitchen (both normal and deferred-recall): full gate disable + ctx.disable_components({kitchen}) + dispatch_infeasible envelope - get_recipe resource: lightweight JSON error envelope - load_recipe tool: dispatch_infeasible flag + user_visible_message After admission control, correctly-admitted pipelines never reach gate_backend_write with backend_capable="false". The step remains as defense-in-depth but should never fire for a correctly-admitted pipeline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a972bb6 commit e30abb5

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

src/autoskillit/server/tools/tools_kitchen.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,36 @@ def _recipe_validation_error_response(name: str, result: dict[str, Any]) -> str:
126126
)
127127

128128

129+
async def _dispatch_infeasible_response(
130+
result: dict[str, Any],
131+
backend: Any,
132+
gate: Any,
133+
ctx: Context,
134+
) -> str:
135+
"""Refuse a dead-on-arrival pipeline before the gate is enabled.
136+
137+
Used by the open_kitchen (both normal and deferred-recall) paths when
138+
load_and_validate reports dispatch_feasible=False.
139+
"""
140+
gate.disable()
141+
await ctx.disable_components(tags={"kitchen"})
142+
_infeasible = result.get("infeasible_steps", [])
143+
_backend_name = backend.name if backend is not None else "unknown"
144+
return json.dumps(
145+
{
146+
"success": False,
147+
"kitchen": "dispatch_infeasible",
148+
"infeasible_steps": _infeasible,
149+
"ingredients_table": result.get("ingredients_table"),
150+
"user_visible_message": (
151+
f"Cannot dispatch recipe: backend {_backend_name!r} "
152+
f"causes steps {_infeasible} to route to terminal failure. "
153+
f"Use a backend with git_metadata_writable=True (e.g. claude-code)."
154+
),
155+
}
156+
)
157+
158+
129159
class QuotaGuardHookPayload(TypedDict):
130160
cache_max_age: int
131161
cache_path: str
@@ -431,6 +461,14 @@ def get_recipe(name: str) -> str:
431461
"suggestions": result.get("suggestions", []),
432462
}
433463
)
464+
if not result.get("dispatch_feasible", True):
465+
return json.dumps(
466+
{
467+
"error": "Recipe is infeasible on current backend",
468+
"dispatch_feasible": False,
469+
"infeasible_steps": result.get("infeasible_steps", []),
470+
}
471+
)
434472
return result.get("content", json.dumps({"error": "Recipe composition failed."}))
435473

436474

@@ -646,6 +684,10 @@ async def open_kitchen(
646684
if not result.get("valid", False) or not result.get("content", ""):
647685
tool_ctx.gate.disable()
648686
return _recipe_validation_error_response(name, result)
687+
if not result.get("dispatch_feasible", True):
688+
return await _dispatch_infeasible_response(
689+
result, tool_ctx.backend, tool_ctx.gate, ctx
690+
)
649691
# Dispatch-feasibility preflight: verify the backend can enforce
650692
# all fix-required hooks for the recipe's run_skill steps.
651693
if tool_ctx.active_recipe_steps is not None:
@@ -751,6 +793,11 @@ async def open_kitchen(
751793
tool_ctx.gate.disable()
752794
return _recipe_validation_error_response(name, result)
753795

796+
if not result.get("dispatch_feasible", True):
797+
return await _dispatch_infeasible_response(
798+
result, tool_ctx.backend, tool_ctx.gate, ctx
799+
)
800+
754801
# Dispatch-feasibility preflight: verify the backend can enforce
755802
# all fix-required hooks for the recipe's run_skill steps.
756803
if tool_ctx.active_recipe_steps is not None:

src/autoskillit/server/tools/tools_recipe.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ async def load_recipe(
236236
result = await _apply_triage_gate(result, name, recipe_info=recipe_info)
237237
if not result.get("valid", False):
238238
result["validation_failed"] = True
239+
if not result.get("dispatch_feasible", True):
240+
result["dispatch_infeasible"] = True
241+
result["user_visible_message"] = (
242+
f"Recipe is infeasible on current backend: "
243+
f"steps {result.get('infeasible_steps', [])} route to terminal failure."
244+
)
239245
if ingredients_only:
240246
result = strip_ingredients_only_keys(result)
241247
return json.dumps(result)

0 commit comments

Comments
 (0)