fix: third-audit gate findings (explicit split, shadow explicit failures, N2-N5, N1 note)

Explicit gateway capability now splits known/unknown: known-missing
explicit models fail-fast under active (same shape as the gateway
default gate), unknown explicit models stay audit-only, shadow never
blocks; explicit priority now protects which model the user chose,
not that the chosen model must run. Shadow report reads
worker_capability_gateway_explicit rows as explicit_capability_failures
with a field note distinguishing the three explicit counts.
N2: coordinator requeue CAS rowcount=0 branch covered via a proxy
connection test (no refactor needed). N3: shadow endpoint test on
the create_server + real HTTP skeleton (capability, truncated, and
explicit-failure fields). N4: creation-layer rejection tests for
tools/instructions/tool_approvals on goal/coordinator tasks.
N5: model writeback failure logs a warning (best-effort kept).
N1: writeback capability-recheck trade-off documented in comment
only; recheck itself is a later batch.
This commit is contained in:
host committed 2026-11-04 18:02:21 +00:00
1 parent 621c08d10f
commit 18e3f1c82d
5 files changed
+660 -21

No files matched your search

+66 -14
View File
@@ -158,7 +158,8 @@ 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
record_fallback, record_routing, \
split_model_capability_missing
from cloud.auto_memory import AutoMemoryStore
from cloud.auto_memory import build_injection_prefix as _auto_prefix
from cloud.auto_memory import process_due_tasks as _auto_process_due
@@ -6121,8 +6122,10 @@ class Worker:
# Audit fix 3(B1):触发条件是“走了 sub2api 网关”,
# 不是“网关填了模型”。任务级显式模型此前因
# _gateway_filled=False 全程跳过能力检查,是生产
# 覆盖的一半缺口;显式优先原则不变——显式模型
# 不满足/unknown 时只审计、不拦任务(见下)。
# 覆盖的一半缺口。Audit fix 4(第三遍审计分档建议):
# 显式优先保护的是“用户选了哪个模型”,不是“选的
# 模型一定能跑”——显式模型已知不满足时同样拦,
# 只有无法验证时才只审计不拦(见下)。
_explicit_model = bool(task.get("model"))
_gateway_filled = False
gateway_provider = None
@@ -6167,6 +6170,10 @@ class Worker:
# active 下不满足即拦,不存在“先试 worker 默认”
# 的中间态;运营方须先把网关模型写进能力目录,
# 否则 unknown 同样拦(fail-closed,与 v8 一致)。
# 例外:任务级显式模型按 Audit fix 4 分档——已知
# 不满足才拦,unknown 只审计(见下),因为显式
# 优先保护的是用户选了哪个模型,不是选的模型
# 一定能跑;目录陈旧不应惩罚正确的显式选择。
if gateway_provider == "sub2api" \
and self.routing_policy is not None:
_gw_tier = classify_task(task)
@@ -6186,19 +6193,46 @@ class Worker:
"cannot verify"
if _gw_unknown else "lacks",
", ".join(_gw_missing)))
# Audit fix 4(第三遍审计分档建议):
# 显式模型按 known/unknown 分档。known
# 是目录明确写了不满足(布尔 False、
# 或 context 低于需求),与网关默认
# 模型同款 fail-fast;unknown 是目录
# 无该模型、或条目缺字段,目录陈旧
# 不应惩罚用户的正确选择,只审计不拦。
# shadow 档两种情况都只审计。
_gw_known, _gw_unk = \
split_model_capability_missing(
self.routing_policy.model_capabilities,
task.get("model"), _gw_requires)
_gw_block = False
_gw_error = None
if _explicit_model:
# 显式优先:不拦任务,但绝不能无声
# 豁免——记独立审计,含缺项/unknown,
# 影子解析器不认此 action,不污染样本。
_gw_blocked = (
_gw_mode == "active" and bool(_gw_known))
# 显式检查结果独立审计,含 blocked
# 标志:被拦的已知不满足也要进影子
# 报告的显式失败清单,否则报告会
# 漏掉恰恰最该被看到的一类。
self._audit(
task["project_id"],
"worker_capability_gateway_explicit",
"%s model=%s tier=%s mode=%s"
" missing=%s unknown=%s" % (
task_id, task.get("model"),
_gw_tier, _gw_mode,
",".join(_gw_missing) or "-",
_gw_unknown))
" missing=%s unknown=%s blocked=%s"
% (task_id, task.get("model"),
_gw_tier, _gw_mode,
",".join(_gw_missing) or "-",
_gw_unknown,
_gw_blocked))
if _gw_blocked:
_gw_block = True
_gw_error = (
"%s tier explicit model %s lacks"
" capability: %s; refusing to"
" dispatch a model known to lack"
" a required capability"
% (_gw_tier, task.get("model"),
", ".join(_gw_known)))
elif _gw_mode == "shadow":
self._audit(
task["project_id"],
@@ -6206,11 +6240,13 @@ class Worker:
"%s tier=%s %s (shadow, not blocked)"
% (task_id, _gw_tier, _gw_note))
else:
_gw_block = True
_gw_error = (
"%s tier gateway model %s; refusing"
" to dispatch an incapable or"
" unverifiable model"
% (_gw_tier, _gw_note))
if _gw_block:
self._set_status(task_id, "failed")
self._write_execution_result(
task_id, error_text=_gw_error,
@@ -6322,7 +6358,19 @@ class Worker:
# 策略一改同一任务中途就换模型。CAS 式写回:
# 只在库模型仍为空时写,绝不覆盖显式模型;
# 写失败 best-effort,不炸派发(本轮内存模型
# 照常生效,只是下次派发可能再次决策)。
# 照常生效,只是下次派发可能再次决策),但
# 必须记警告日志,不能静默吞掉(Audit fix 4
# N5:否则 I5 的保证悄悄失效还看不出来)。
# Audit fix 4(N1 trade-off,只写注释、不改
# 语义):写回是一把双刃剑——非网关路径下,
# 写回的模型使后续尝试走显式分支,decide_routing
# 的能力检查不再跑;两次尝试之间档 requires
# 一收紧,续跑任务将带着旧模型、不经复核直
# 接派发。网关路径不受影响:网关填模型的任务
# 跳过 decide_routing 与写回,每次尝试都重新
# 填网关模型并重新接受网关能力检查。复核方案
# (写回时记校验版本、或续跑时重查当前
# requires)是下一批的事,本轨不改写回语义。
if decision.chosen_model:
try:
_wb_conn = _db_connect(self.db_path)
@@ -6334,8 +6382,12 @@ class Worker:
_wb_conn.commit()
finally:
_wb_conn.close()
except Exception:
pass
except Exception as exc:
log_event(self.logger, "worker_model_writeback_failed",
task_id=task_id,
project_id=task["project_id"],
status="warning",
message="model writeback failed: %s" % exc)
task["model"] = decision.chosen_model or self.model
elif not _gateway_filled:
# Audit fix 3(I8):显式模型任务(含协调员子任务