fix: audit findings (fallback dead code, silent fallback, registry re-parse, shadow sort, gateway capability)

- RoutingPolicy.fallback_models: move the cleaning loop into
  __init__ (it was unreachable after tier_mode's return, so the
  attribute never existed); still parsed but not applied.
- Capability fallback: RoutingDecision gains capability_fallback;
  the worker audits worker_capability_fallback when it fires, so
  a tier-model rejection followed by a worker-default fallback
  is no longer invisible (source stays worker_default, no
  model_routing event, so shadow samples are unaffected).
- Registry: resolve_registry_agent checks skills directly
  against the loaded definition instead of re-parsing a
  hand-built dict, so future fields cannot be silently dropped
  the way tool_approvals was in v9.
- Shadow: evaluate_shadow sorts rows earliest-first by
  created_at (id tiebreak) when timestamps are present, so
  dedup no longer depends on caller row order; the server
  path (which strips created_at) is unchanged.
- Gateway: when sub2api fills the default model, the worker
  now checks it against the tier's capability requirements
  with the same mode semantics as v8 -- shadow only audits,
  active fails fast with worker_capability_gate_rejected and
  never calls the runner. No worker-default fallback here:
  the gateway model is the task's gateway model, falling back
  to a worker default would run against the wrong gateway.
This commit is contained in:
host committed 2026-11-04 18:02:21 +00:00
1 parent a51919f24b
commit f9a77577c5
5 files changed
+392 -34

No files matched your search

+24 -7
View File
@@ -336,6 +336,19 @@ class RoutingPolicy:
for tier, tier_mode in (tier_modes or {}).items():
if tier in self.tiers and tier_mode in ROUTING_MODES:
self.tier_modes[tier] = tier_mode
# Audit fix: fallback_models used to live after the
# return in tier_mode (unreachable dead code), so a
# RoutingPolicy never actually had the attribute and
# any future reader would get AttributeError. It is
# stored here, cleaned and deduplicated, and still
# deliberately NOT applied by the router (see class
# docstring): only worker CLI --fallback-models enables
# a real fallback retry.
self.fallback_models = []
for item in fallback_models or []:
model = _clean_model_name(item)
if model and model not in self.fallback_models:
self.fallback_models.append(model)
self.model_capabilities = parse_model_capabilities(
model_capabilities)
self.tier_requirements = {}
@@ -348,11 +361,6 @@ class RoutingPolicy:
def tier_mode(self, tier):
"""Effective mode for one tier: explicit tier mode, else global."""
return self.tier_modes.get(tier, self.mode)
self.fallback_models = []
for item in fallback_models or []:
model = _clean_model_name(item)
if model and model not in self.fallback_models:
self.fallback_models.append(model)
def parse_routing_policy(data):
@@ -466,7 +474,8 @@ class RoutingDecision:
"""One pre-dispatch model decision, kept auditable by construction."""
def __init__(self, task_id, tier, chosen_model, suggested_model,
source, policy_version, applied, reason, signals):
source, policy_version, applied, reason, signals,
capability_fallback=False):
self.task_id = task_id
self.tier = tier
self.chosen_model = chosen_model
@@ -476,6 +485,13 @@ class RoutingDecision:
self.applied = applied
self.reason = reason
self.signals = dict(signals or {})
# Audit fix: True only when an active tier model failed
# its capability check and the worker default was used
# instead. The source stays "worker_default" (existing
# callers rely on it), so the worker must check this
# flag to audit the fallback -- otherwise the fallback
# is invisible in the audit trail.
self.capability_fallback = bool(capability_fallback)
def to_event(self) -> dict:
return {
@@ -488,6 +504,7 @@ class RoutingDecision:
"policy_version": self.policy_version,
"applied": self.applied,
"reason": self.reason,
"capability_fallback": self.capability_fallback,
}
@@ -614,7 +631,7 @@ def decide_routing(task, worker_model, policy=None,
policy_version=policy.version, applied=True,
reason="%s tier model %s%s, worker default satisfies"
" requirements, falling back" % (tier, tier_model, cap_note),
signals=signals)
signals=signals, capability_fallback=True)
return RoutingDecision(
task_id=task.get("id"), tier=tier, chosen_model=None,
suggested_model=tier_model, source="capability_blocked",