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

+88
View File
@@ -156,6 +156,7 @@ from cloud.db import connect as _db_connect
from cloud.goal_command import parse_goal_command as _parse_goal_command
from cloud.logging_config import get_logger, log_event, setup_logging
from cloud.models import ModelRouter, RoutingPolicy, decide_routing, \
check_model_capabilities, classify_task, \
is_fallbackable, load_routing_policy, parse_routing_policy, \
record_fallback, record_routing
from cloud.auto_memory import AutoMemoryStore
@@ -6019,10 +6020,12 @@ class Worker:
# xcmapi 模型名去撞 sub2api)。xcmapi 下 _ensure_gateway
# 直接返回 (None, None),不做任何事。注意:这里改的是
# 原 task dict,随后 task = dict(task) 会原样拷走。
_gateway_filled = False
try:
gateway_provider, gateway_env = self._ensure_gateway(task)
if not task.get("model") and gateway_provider == "sub2api":
task["model"] = self._sub2api_default_model(gateway_env)
_gateway_filled = True
except Exception as exc:
error = "网关准备失败: %s" % exc
self._set_status(task_id, "failed")
@@ -6044,6 +6047,78 @@ class Worker:
"stdout": "", "error": error, "events": [],
"cost_record_id": None}
# Audit fix:网关能力检查——此前路由决策只在模型
# 为空时跑,sub2api 网关先填默认模型后决策直接跳
# 过,能力门在生产路径上完全不生效。补齐:网关填
# 的模型同样按档需求检查,档模式与 v8 一致:shadow
# 只审计不拦,active 不满足则 fail-fast(同款工具
# 门形状,清占位、不调 runner)。没有策略、没有档
# 需求、档模式 off 时一律不查,旧行为不变。注意:
# 这里没有 worker 默认可回落(网关模型就是该任务
# 的网关内模型,换成 worker 默认会跑错网关),所以
# active 下不满足即拦,不存在“先试 worker 默认”
# 的中间态;运营方须先把网关模型写进能力目录,
# 否则 unknown 同样拦(fail-closed,与 v8 一致)。
if _gateway_filled and self.routing_policy is not None:
_gw_tier = classify_task(task)
_gw_requires = self.routing_policy.tier_requirements.get(
_gw_tier) or {}
if _gw_requires:
_gw_mode = self.routing_policy.tier_mode(_gw_tier)
if _gw_mode in ("active", "shadow"):
_gw_ok, _gw_missing, _gw_unknown = \
check_model_capabilities(
self.routing_policy.model_capabilities,
task.get("model"), _gw_requires)
if not _gw_ok:
_gw_note = ("gateway model %s %s capability:"
" %s" % (
task.get("model"),
"cannot verify"
if _gw_unknown else "lacks",
", ".join(_gw_missing)))
if _gw_mode == "shadow":
self._audit(
task["project_id"],
"worker_capability_gateway_shadow",
"%s tier=%s %s (shadow, not blocked)"
% (task_id, _gw_tier, _gw_note))
else:
_gw_error = (
"%s tier gateway model %s; refusing"
" to dispatch an incapable or"
" unverifiable model"
% (_gw_tier, _gw_note))
self._set_status(task_id, "failed")
self._write_execution_result(
task_id, error_text=_gw_error,
actual_model=self.model,
started_at=started_at,
finished_at=time.time(),
only_if_status="failed")
self._audit(
task["project_id"],
"worker_capability_gate_rejected",
"%s: %s" % (task_id, _gw_error))
log_event(
self.logger,
"worker_capability_gate_rejected",
task_id=task_id,
project_id=task["project_id"],
status="failed")
self._running_clear(task_id)
return {"dispatched": True,
"task_id": task_id,
"host_name":
self.executor._host_name(task)
if hasattr(self.executor,
"_host_name")
else task_id,
"status": "failed",
"executor": self._executor_name(),
"stdout": "", "error": _gw_error,
"events": [], "cost_record_id": None}
task = dict(task)
if not task.get("model"):
# 规则路由 v0(只在模型仍为空时跑一次,整个派发只算一次,
@@ -6103,6 +6178,19 @@ class Worker:
decision.applied,
decision.suggested_model or "-",
decision.chosen_model or "-"))
# Audit fix:能力回落以前不记任何审计(source 是
# worker_default,不在 policy/shadow 之列),运营
# 方看不到档模型被能力门否掉过。现在用 decision
# 上的 capability_fallback 标志单独记一条审计,
# 不改 source、不记 model_routing 事件(影子样本
# 只算 policy/shadow 决策,回落不是一次路由决策)。
if getattr(decision, "capability_fallback", False):
self._audit(task["project_id"],
"worker_capability_fallback",
"%s tier=%s chosen=%s reason=%s" % (
task_id, decision.tier,
decision.chosen_model or "-",
decision.reason))
task["model"] = decision.chosen_model or self.model
# agent 指令通道(v2):local/docker 由 sandbox 的
# --developer-instructions 派发,prompt 保持干净;通用