fix: audit batch 2 visibility (I-1 shadow dropped rows, I-2 partial policy failure, I-5 failure policy table)
- I-1: evaluate_shadow counts dropped rows by cause (unparseable / unknown_tier / explicit / duplicate) with the identity audit_rows_total == routed_task_n + dropped_total; the shadow endpoint passes created_at/id through and reports audit totals next to sample counts. - I-2 (option b): parse logs a warning naming each tier dropped for an invalid mode/requires; load attaches partial_failures to the policy; the worker records a task-level worker_routing_policy_load_failed audit for partial failures (same action as full failure, detail says failure=partial); the shadow report carries the policy fingerprint, dropped tiers and partial failures (no task context on that path, so no task-level audit there). - I-5: coordinator gate audit lookup failure now raises _CoordGateLookupFailed and the merge converts it to escalate (no requeue, no round consumed); persisted-model lookup failure and required-tools check failure each write a named audit row before falling through to dispatch/allow; audit-loop evaluate failure stays escalate (regression test). - Test fixtures: coordinator suites build audit_log via AuthStore as production's first real audit does, since their stubbed _audit never creates the table.
This commit is contained in:
1 parent
44e0f4f1d6
commit
a7f77c8a98
7 files changed
+648
-40
No files matched your search
+93
-13
@@ -472,6 +472,17 @@ def _task_from_row(row):
|
||||
}
|
||||
|
||||
|
||||
class _CoordGateLookupFailed(Exception):
|
||||
"""协调员门拒绝审计查询失败(Audit batch 2,I-5)。
|
||||
|
||||
由 _coord_gate_rejected_child_ids 在两次查询都失败时抛出,
|
||||
调用方(_coord_audit_loop_merge)捕获后转 escalate——分类
|
||||
依据缺失时绝不能回落到“空集 → 执行失败 → 推回”旧路。
|
||||
抛错而不是返回哨兵值:哨兵会被任何忘记检查的调用方
|
||||
当成集合误用,异常则无法被静默忽略。
|
||||
"""
|
||||
|
||||
|
||||
class Worker:
|
||||
"""单机 worker:一轮认领一个 queued 任务并执行到底。
|
||||
|
||||
@@ -555,6 +566,12 @@ class Worker:
|
||||
# 审计,让“策略已死”在运行期可见;models 侧的警告
|
||||
# 日志见 load_routing_policy。
|
||||
self.routing_policy_load_failed = False
|
||||
# Audit batch 2 (I-2): 部分失败标志——策略可用,但有
|
||||
# 来源失败或档被 fail-closed 弹出(见 models.py 的
|
||||
# partial_failures / dropped_tiers)。run_one 里与
|
||||
# 全失败同款 action 记任务级审计,detail 里区分
|
||||
# partial 与 full,不混为一谈。
|
||||
self.routing_policy_partial_failure = False
|
||||
if isinstance(routing_policy, RoutingPolicy):
|
||||
self.routing_policy = routing_policy
|
||||
elif isinstance(routing_policy, dict):
|
||||
@@ -573,6 +590,11 @@ class Worker:
|
||||
self.routing_policy_load_failed = True
|
||||
else:
|
||||
raise ValueError("routing_policy 必须是 RoutingPolicy 或 dict")
|
||||
if self.routing_policy is not None and (
|
||||
getattr(self.routing_policy, "partial_failures", None)
|
||||
or getattr(self.routing_policy, "dropped_tiers",
|
||||
None)):
|
||||
self.routing_policy_partial_failure = True
|
||||
# 真实 fallback 换模型列表:默认空 = 关闭,只记事件不换模型
|
||||
# (老行为,生产 worker 未配该参数时绝不突然多花模型钱)。
|
||||
# 归一化:去空白、去重保序、去掉与主模型相同的项(换成同一
|
||||
@@ -6139,8 +6161,15 @@ class Worker:
|
||||
IDENTITY——此前注释称“没有 id 列”是错的,真正无 id
|
||||
的只是测试手工建的表)。排序用 created_at 倒序、id
|
||||
作 tie-break;缺 id 列的库(如手工建表)会炸掉第一
|
||||
条查询,此时回落到仅按 created_at 排序再查一次,
|
||||
仍失败才记警告日志返回空集,绝不静默吞掉。
|
||||
条查询,此时回落到仅按 created_at 排序再查一次。
|
||||
Audit batch 2(I-5):两次查询都失败时不再返回空集
|
||||
——空集会被合流读成“没有门拒绝”,把门拒绝子任务
|
||||
重分类为执行失败推进回循环。此时抛
|
||||
_CoordGateLookupFailed,由合流捕获后转 escalate
|
||||
(分类依据缺失→升级而非推回);警告日志仍记。
|
||||
注意:生产里 audit_log 由 AuthStore 在首次审计时建
|
||||
好,合流时表一定存在;缺表只会出现在 _audit 被桩
|
||||
掉的测试里,那些夹具须按生产实际补建表(见测试)。
|
||||
查询取 5000 行封顶:超限时回落标记仍可兜底,绝不
|
||||
全表扫;封顶漏行只会导致执行失败走推回而非升级,
|
||||
是 fail-open 向推回,不是向放行。
|
||||
@@ -6174,7 +6203,7 @@ class Worker:
|
||||
"coord gate audit lookup failed: %s", exc)
|
||||
except Exception:
|
||||
pass
|
||||
return set()
|
||||
raise _CoordGateLookupFailed(str(exc)) from exc
|
||||
wanted = set(child_ids or [])
|
||||
started = attempt_started_at or {}
|
||||
found = set()
|
||||
@@ -6327,10 +6356,26 @@ class Worker:
|
||||
for child in children or []:
|
||||
child_names[child["id"]] = child["name"]
|
||||
rows = self._coord_child_rows(child_ids)
|
||||
gate_rejected_ids = self._coord_gate_rejected_child_ids(
|
||||
project_id, child_ids,
|
||||
{cid: (rows.get(cid) or {}).get("started_at")
|
||||
for cid in child_ids})
|
||||
# Audit batch 2 (I-5): 门拒绝审计查不到时不猜——
|
||||
# 分类依据缺失→升级而非推回。查询失败时清包、
|
||||
# 记 escalated 审计、返回升级决策(父任务由调用
|
||||
# 方置 failed,不重排、不耗轮次),与门拒绝升级
|
||||
# 同款终局,只是原因不同。
|
||||
try:
|
||||
gate_rejected_ids = self._coord_gate_rejected_child_ids(
|
||||
project_id, child_ids,
|
||||
{cid: (rows.get(cid) or {}).get("started_at")
|
||||
for cid in child_ids})
|
||||
except _CoordGateLookupFailed as exc:
|
||||
self._coord_clear_packets(task, child_ids)
|
||||
self._audit(project_id,
|
||||
"worker_coord_audit_loop_escalated",
|
||||
"%s gate_lookup_failed=%s" % (task_id, exc))
|
||||
return {
|
||||
"decision": "escalate",
|
||||
"error": "审计循环升级(门拒绝审计查询失败,分类依据"
|
||||
"缺失,不推回)",
|
||||
"gate_lookup_failed": True}
|
||||
gate_results = {}
|
||||
findings = []
|
||||
gate_rejected = []
|
||||
@@ -6856,6 +6901,8 @@ class Worker:
|
||||
一个注定缺工具的任务。租户目录准备失败时不由本
|
||||
方法定罪(返回 [],让 _docker_task_context 的
|
||||
租户准备失败路径处理),避免同一失败被两处各说一遍。
|
||||
Audit batch 2(I-5):返回 [] 放行时同时记一条审计行
|
||||
(worker_required_tools_check_failed),放行可见。
|
||||
"""
|
||||
tools = task.get("agent_tools") or []
|
||||
if not tools:
|
||||
@@ -6873,11 +6920,20 @@ class Worker:
|
||||
except Exception as exc:
|
||||
# 不由本方法定罪,但绝不静默吞掉:记日志,真正
|
||||
# 的租户准备失败由 _docker_task_context 报错。
|
||||
# Audit batch 2(I-5):放行仍是放行(返回 [] 不变),
|
||||
# 但同时记一条点名依据缺失的审计行,让“工具门
|
||||
# 因读不到租户配置而放行”在审计里可见;审计
|
||||
# 本身 best-effort,写失败不改放行决策。
|
||||
try:
|
||||
self.logger.warning(
|
||||
"required tools check skipped: %s", exc)
|
||||
except Exception:
|
||||
pass
|
||||
if task.get("project_id"):
|
||||
self._audit(task["project_id"],
|
||||
"worker_required_tools_check_failed",
|
||||
"%s error=%s" % (
|
||||
task.get("id") or "-", exc))
|
||||
return []
|
||||
return _missing_tools(tools, config_text)
|
||||
|
||||
@@ -6901,7 +6957,11 @@ class Worker:
|
||||
policy 行只可能来自该任务曾经模型为空的那次派发,
|
||||
也就是写回的来源。
|
||||
查找失败(DB 异常)时返回 False 并记警告日志:复核查
|
||||
不到依据就跳过,而不是拦任务。
|
||||
不到依据就跳过,而不是拦任务。Audit batch 2(I-5):
|
||||
跳过不再静默——同时记一条点名依据缺失的审计行
|
||||
(worker_persisted_model_lookup_failed),让“复核
|
||||
因查不到依据而放行”在审计里可见;审计本身
|
||||
best-effort,写失败不改放行决策。
|
||||
|
||||
残留缺口措辞(9.5-code,M1):fixes3 时期由 shadow /
|
||||
worker_default 决策写回的旧模型既无 persisted 行又
|
||||
@@ -6928,6 +6988,12 @@ class Worker:
|
||||
task_id=task_id, project_id=project_id,
|
||||
status="warning",
|
||||
message="persisted model lookup failed: %s" % exc)
|
||||
# Audit batch 2 (I-5): 依据缺失也要在审计里点名,
|
||||
# 不能只有日志——日志与审计是两条可见性通道。
|
||||
# _audit 本身 best-effort,写失败不改返回 False。
|
||||
self._audit(project_id,
|
||||
"worker_persisted_model_lookup_failed",
|
||||
"%s error=%s" % (task_id, exc))
|
||||
return False
|
||||
for row in rows:
|
||||
if row[0] != "worker_model_persisted":
|
||||
@@ -7100,14 +7166,28 @@ class Worker:
|
||||
task = self._apply_audit_loop_packet(task)
|
||||
self._audit(task["project_id"], "worker_claim",
|
||||
"%s executor=%s" % (task_id, self._executor_name()))
|
||||
# Audit fix 3(I3):策略已死时每任务记一条审计,
|
||||
# 否则运营方只能靠日志猜“策略为什么没生效”。
|
||||
# _audit 本身 best-effort,不影响派发。
|
||||
# Audit fix 3(I3)+ batch 2(I-2):策略已死或部分
|
||||
# 失败时每任务记一条审计,否则运营方只能靠日志猜
|
||||
# “策略为什么没生效”。全失败与部分失败用同款
|
||||
# action、detail 里区分 failure=full/partial 并附
|
||||
# 丢弃档与来源失败清单;_audit 本身 best-effort,
|
||||
# 不影响派发。
|
||||
if getattr(self, "routing_policy_load_failed", False):
|
||||
self._audit(task["project_id"],
|
||||
"worker_routing_policy_load_failed",
|
||||
"%s project=%s" % (task_id,
|
||||
task["project_id"]))
|
||||
"%s project=%s failure=full" % (
|
||||
task_id, task["project_id"]))
|
||||
elif getattr(self, "routing_policy_partial_failure", False):
|
||||
_policy = self.routing_policy
|
||||
self._audit(task["project_id"],
|
||||
"worker_routing_policy_load_failed",
|
||||
"%s project=%s failure=partial dropped_tiers=%s"
|
||||
" partial_failures=%s" % (
|
||||
task_id, task["project_id"],
|
||||
",".join(getattr(
|
||||
_policy, "dropped_tiers", []) or []),
|
||||
"; ".join(getattr(
|
||||
_policy, "partial_failures", []) or [])))
|
||||
# log_event 只接受 task_id/project_id/status/message 四个
|
||||
# 字段,executor 只进审计 detail,不进日志调用(否则
|
||||
# TypeError 会炸掉整个 run_one)。
|
||||
|
||||
Reference in new issue
Block a user