Audit batch 4: I-6 whitespace audit format, I-7 explicit non-sub2api capability gate, I-8 coord gate attribution, I-10 checked gates, I-11 gate-without-finding escalate, I-13 structured-first classification

This commit is contained in:
host committed 2026-11-04 18:02:23 +00:00
1 parent 06e4e16bbb
commit cdac8039b3
5 files changed
+959 -47

No files matched your search

+187 -31
View File
@@ -482,6 +482,45 @@ class _CoordGateLookupFailed(Exception):
"""
def _mark_gate_checked(task, gate):
"""在任务 dict 上记一笔“某门本轮确实跑过”(审计批 4,I-10)。
用 task["_gates_checked"] 列表承载,键名带下划线、私有、
内存态,与 _gateway_provider/_gateway_env 同款约定:
不进库、不进审计 detail、不随 prompt 落库。列表而非
集合,保证 task dict 仍是 JSON 安全的;run_one 内 task
经多次 dict(task) 浅拷贝,拷贝共享同一列表对象,因此
无论哪份拷贝打标记,终局块读到的都是同一份集合。
终局时 _audit_loop_gate_results 只列入被标记过的门,
不再把没跑过的门硬编码成通过。
"""
if not isinstance(task, dict):
return
checked = task.get("_gates_checked")
if not isinstance(checked, list):
checked = []
task["_gates_checked"] = checked
if gate not in checked:
checked.append(gate)
# 审计批 4(I-13):合流分类用的门拒绝子串标记表。
# 分类优先读结构化证据(_coord_gate_rejected_child_ids 的
# 审计行集合),子串只作最后回落——回落命中时合流记一条
# worker_coord_gate_classified_by_marker 审计行点名 marker,
# 让回落可见。生产方 error 文案本身不改措辞、不做全仓
# 常量化重构(那是另一批的事);分类侧的这份拷贝抽成
# 常量,文案改动时分类侧只有这一处要改。
_GATE_ERROR_MARKERS = (
"Missing environment variable:",
"网关准备失败",
"必需工具缺失",
"工具审批声明矛盾",
"refusing to dispatch an incapable or unverifiable model",
"fails capability",
)
class Worker:
"""单机 worker:一轮认领一个 queued 任务并执行到底。
@@ -1924,7 +1963,8 @@ class Worker:
result["new_state"] = result["new_state"].to_dict()
return result
def _audit_loop_gate_results(self, task, final, result):
def _audit_loop_gate_results(self, task, final, result,
gates_checked=None):
"""本轮已产生的确定性门结果(只取已有结果,不捏造判定)。
门的取值口径(核实结论):
@@ -1938,8 +1978,25 @@ class Worker:
- capability / required_tools / gateway:本方法只在终局
块被调用,而终局块的前提是尝试跑到了 runner——此前
的前置门若拒绝,run_one 早已在 runner 之前提前返回,
根本到不了这里;所以能到这里的尝试,这三门要么已
通过、要么不适用,记 True 是事实陈述,不是复查。
根本到不了这里;所以能到这里的尝试,已跑过的门
要么已通过、要么不适用。审计批 4(I-10)之前这里
把三门无条件记 True,显式任务(尤其 I-7 修复前的
非 sub2api 组合)能力门根本没跑过却同样记入
passed_gates 并持久化——“通过”一词在状态表里失真。
现在:调用方传 gates_checked(run_one 派发点用
_mark_gate_checked 在 task["_gates_checked"] 里记的
本轮实际跑过的门)时,只列入集合内的门;未传
(直接调用的旧调用方)时保持旧行为、三门全记 True。
gate_missing 语义核查:引擎只对“上一轮 passed_gates
里有、本轮缺失”的门升级;同一任务的跑过门集合在
两轮间是稳定的(显式任务无策略时两轮都不记
capability,政策任务两轮都记;门集合由任务/策略/
执行器决定,不由轮次决定),所以显式任务两轮不
会因“not_checked”误触发升级——两轮往返测试锁定。
唯一可预见的边界是部署时已在循环中的任务:旧状态
passed_gates 里含硬编码 True 的三门,新代码首轮
可能缺省其中几门 → gate_missing 升级一次,方向是
fail-closed 不是放行。
前置门拒绝的路径不接审计循环(见 _audit_loop_wire
的文档):那类任务在 agent 跑任何一轮之前就已失败,
推回包没有可投递的 agent 回合,且重排会确定性地
@@ -1970,9 +2027,15 @@ class Worker:
"re-run goal verification",
"goal verification passes",
kind="deterministic", gate="goal_verify"))
gate_results["capability"] = True
gate_results["required_tools"] = True
gate_results["gateway"] = True
if gates_checked is None:
gate_results["capability"] = True
gate_results["required_tools"] = True
gate_results["gateway"] = True
else:
checked = set(gates_checked)
for gate in ("capability", "required_tools", "gateway"):
if gate in checked:
gate_results[gate] = True
return gate_results, findings
def _audit_loop_wire(self, task, final, result, error):
@@ -2007,7 +2070,11 @@ class Worker:
if task.get("coordinator_parts"):
return final, error, "skipped"
gate_results, findings = self._audit_loop_gate_results(
task, final, result)
# 审计批 4(I-10):把派发点记的本轮实际跑过
# 的门透传进去;没记过的门不列入 gate_results,
# 不再硬编码成通过。未传(旧直接调用)时保持
# 旧行为,见 _audit_loop_gate_results 文档。
task, final, result, task.get("_gates_checked"))
try:
decision = self.audit_loop_evaluate(
task, gate_results, findings=findings)
@@ -6129,11 +6196,19 @@ class Worker:
attempt_started_at=None):
"""前置门拒绝的子任务 id 集(合流分类用)。
依据只有审计行:能力门、工具门、审批门的拒绝审计 detail
是 "<子任务 id>: <错误>" 格式,可按 id 前缀归属;缺 key
门(worker_missing_key)与仓库门(worker_repo_failed)
的 detail 只有错误文本、没有任务 id,审计行无法归属,
故不在此列——那两类只走下面的 error_text 强标记回落。
依据只有审计行:下列 6 个拒绝审计的 detail 均是
"<子任务 id>: <错误>" 格式,可按 id 前缀归属——
能力门、工具门、审批门(旧有 3 门),以及审计批 4
(I-8)补进的缺 key 门(worker_missing_key)、仓库门
(worker_repo_failed)、写回复核拒绝门
(worker_capability_recheck_rejected,其 detail 本就
是 "<id>: <错误>" 格式)。不补进集合的同类审计行:
worker_capability_gateway_explicit(含 blocked=false
的纯审计行,补进会把未被拦的显式任务误判为门拒绝)、
worker_capability_gateway_shadow(shadow 永不拦)、
worker_capability_fallback(回落不是拒绝);
worker_gateway_failed(网关准备失败)已有 error_text
标记覆盖,不补进集合。
审计是 best-effort,查不到不等于没拒绝,调用方不得把
“无审计行”当成“没被门拒绝”的证据(回落标记见分类)。
@@ -6175,7 +6250,10 @@ class Worker:
"""
actions = ("worker_capability_gate_rejected",
"worker_tool_gate_rejected",
"worker_tool_approval_gate_rejected")
"worker_tool_approval_gate_rejected",
"worker_missing_key",
"worker_repo_failed",
"worker_capability_recheck_rejected")
rows = None
try:
conn = _db_connect(self.db_path)
@@ -6183,17 +6261,21 @@ class Worker:
try:
rows = conn.execute(
"SELECT detail, created_at FROM audit_log"
" WHERE project_id = ? AND action IN (?, ?, ?)"
" WHERE project_id = ? AND action IN"
" (?, ?, ?, ?, ?, ?)"
" ORDER BY created_at DESC, id DESC LIMIT 5000",
(project_id, actions[0], actions[1],
actions[2])).fetchall()
actions[2], actions[3], actions[4],
actions[5])).fetchall()
except Exception:
rows = conn.execute(
"SELECT detail, created_at FROM audit_log"
" WHERE project_id = ? AND action IN (?, ?, ?)"
" WHERE project_id = ? AND action IN"
" (?, ?, ?, ?, ?, ?)"
" ORDER BY created_at DESC LIMIT 5000",
(project_id, actions[0], actions[1],
actions[2])).fetchall()
actions[2], actions[3], actions[4],
actions[5])).fetchall()
finally:
conn.close()
except Exception as exc:
@@ -6259,13 +6341,7 @@ class Worker:
if child_id in (gate_rejected_ids or set()):
return "gate_rejected", "gate_audit"
err = (row.get("error_text") or "")
for marker in ("Missing environment variable:",
"网关准备失败",
"必需工具缺失",
"工具审批声明矛盾",
"refusing to dispatch an incapable or"
" unverifiable model",
"fails capability"):
for marker in _GATE_ERROR_MARKERS:
if marker in err:
return "gate_rejected", "gate_error_marker"
if status == "failed" or status == "interrupted":
@@ -6392,6 +6468,19 @@ class Worker:
excluded_notes.append("%s:%s" % (name, reason))
elif kind == "gate_rejected":
gate_rejected.append(name)
if reason == "gate_error_marker":
# 审计批 4(I-13):子串回落命中必须可见,
# 不能静默——记一条点名 marker 的审计行,
# 让“分类依赖 error_text 回落”在审计里
# 可查;文案改动导致回落失效时,这里
# 的缺失本身就是信号。
fired = next(
(m for m in _GATE_ERROR_MARKERS
if m in (row.get("error_text") or "")),
"?")
self._audit(project_id,
"worker_coord_gate_classified_by_marker",
"%s marker=%s" % (cid, fired))
elif kind == "execution":
gate_results["child:" + name] = False
findings.append(_AuditFinding(
@@ -6910,6 +6999,11 @@ class Worker:
return []
if not isinstance(self.executor, DockerExecutor):
return []
# 审计批 4(I-10):走到这里说明工具门真正查了租户
# 配置(读失败也算跑过:返回 [] 的放行按 I-5 记审计,
# 且该任务随后在租户准备失败处提前返回、到不了
# 终局块,标记本身无实际影响)。
_mark_gate_checked(task, "required_tools")
try:
home = self._tenant_dirs(
task.get("project_id"))["codex_home"]
@@ -6994,12 +7088,22 @@ class Worker:
"worker_persisted_model_lookup_failed",
"%s error=%s" % (task_id, exc))
return False
# 审计批 4(I-6):persisted 行格式固定为
# "%s tier=%s model=%s",model 是最后一个字段,
# 按键提取而不是 split()[2] 定位置取。旧代码在
# 模型名含空白时永远匹配不上,于是写回模型被
# 默认当成“用户显式”而跳过复核——两处同时失效
# 的另一半(影子解析是 routing_shadow 侧)。
# 格式完全不符的行仍视为无证据:worker 写出的
# persisted 行格式固定,格式不符不可能是写回行,
# fail-closed 备选(按写回对待)在此不适用。
persisted_re = re.compile(
r"^%s\s+tier=\S+\s+model=(.*)$" % re.escape(task_id))
for row in rows:
if row[0] != "worker_model_persisted":
continue
parts = (row[1] or "").split()
if len(parts) >= 3 and parts[0] == task_id \
and parts[2] == "model=%s" % model:
match = persisted_re.match((row[1] or "").strip())
if match and match.group(1).strip() == model:
return True
for row in rows:
if row[0] != "worker_routing":
@@ -7033,6 +7137,9 @@ class Worker:
model = task.get("model")
verdict = evaluate_capability(
self.routing_policy.model_capabilities, model, requires)
# 审计批 4(I-10):走到这里说明复核真正算了
# verdict(无策略/无 requires/off 档已在上面返回)。
_mark_gate_checked(task, "capability")
if verdict.ok:
return None
known = verdict.known_missing
@@ -7206,7 +7313,12 @@ class Worker:
# 缺 key 门提前返回:同样要清暂存,否则与仓库解析
# 失败一路同款残留(见 _cleanup_task_seed)。
self._cleanup_task_seed(task)
self._audit(task["project_id"], "worker_missing_key", error)
# 审计批 4(I-8):缺 key 门审计 detail 改为
# "<任务 id>: <错误>",与能力/工具/审批门同款
# 可归属格式,协调员合流才能按 id 前缀认领;
# error_text 子串标记仍能命中(子串匹配)。
self._audit(task["project_id"], "worker_missing_key",
"%s: %s" % (task_id, error))
log_event(self.logger, "worker_missing_key", task_id=task_id,
project_id=task["project_id"], status="failed")
self._notify_task_finished(
@@ -7234,7 +7346,11 @@ class Worker:
# 仓库解析失败提前返回:够不到正常终局后的清理,
# 这里必须先清暂存,否则 bundle 目录永久残留。
self._cleanup_task_seed(task)
self._audit(task["project_id"], "worker_repo_failed", repo_error)
# 审计批 4(I-8):仓库门审计 detail 改为
# "<任务 id>: <错误>",与 3 门同款可归属格式;
# error_text 子串标记仍能命中(子串匹配)。
self._audit(task["project_id"], "worker_repo_failed",
"%s: %s" % (task_id, repo_error))
log_event(self.logger, "worker_repo_failed", task_id=task_id,
project_id=task["project_id"], status="failed")
self._notify_task_finished(
@@ -7419,6 +7535,12 @@ class Worker:
"stdout": "", "error": error, "events": [],
"cost_record_id": None}
if gateway_provider:
# 审计批 4(I-10):网关准备实际跑过且成功
# (失败已在上面提前返回),终局时 gateway
# 门可列入 gate_results。
_mark_gate_checked(task, "gateway")
# Audit fix:网关能力检查——此前路由决策只在模型
# 为空时跑,sub2api 网关先填默认模型后决策直接跳
# 过,能力门在生产路径上完全不生效。补齐:网关填
@@ -7435,8 +7557,29 @@ class Worker:
# 不满足才拦,unknown 只审计(见下),因为显式
# 优先保护的是用户选了哪个模型,不是选的模型
# 一定能跑;目录陈旧不应惩罚正确的显式选择。
if gateway_provider == "sub2api" \
and self.routing_policy is not None:
# 审计批 4(I-7):显式模型的 known/unknown 分档检查
# 与网关无关——网关分支只决定“待查模型是哪一个”
# (网关填充模型或显式模型),分档判定走第 3 批统一
# 的 evaluate_capability。进入条件因此是“有策略且
# (走了 sub2api 网关,或显式任务走了任一真实网关)”:
# 非显式 + 非 sub2api 的任务根本不进块(它们模型
# 为空、后续走 decide_routing,不存在误拦);显式
# + 无网关(provider 为 None)的任务也不进块——它
# 根本没走网关,显式语义不变(写回模型仍走后续
# 复核,用户显式仍不被复核,见 fixes5 契约);
# 显式 + sub2api 路径与原来逐字一致(同一审计
# action、同一 error 文案、同一 blocked 标志),
# 不会重复审计或重复拦;显式 + 非 sub2api 网关
# 在 active 下已知不满足时从“直接派发”变为“拦”,
# 这是有意的行为变更。
# 附带:已持久化的显式任务在非 sub2api 网关下会
# 同时走本门与后续写回复核(本门不拦时复核再按
# 分档审计/拦),两者审计 action 不同
# (gateway_explicit 与 recheck_*),不是重复拦。
if self.routing_policy is not None \
and (gateway_provider == "sub2api"
or (_explicit_model
and gateway_provider is not None)):
_gw_tier = classify_task(task)
_gw_requires = self.routing_policy.tier_requirements.get(
_gw_tier) or {}
@@ -7446,6 +7589,9 @@ class Worker:
_gw_verdict = evaluate_capability(
self.routing_policy.model_capabilities,
task.get("model"), _gw_requires)
# 审计批 4(I-10):走到这里说明网关
# 能力检查真正算了 verdict。
_mark_gate_checked(task, "capability")
if not _gw_verdict.ok:
_gw_note = ("gateway model %s %s capability:"
" %s" % (
@@ -7584,6 +7730,16 @@ class Worker:
# 断言 events[0] 就是 fallback 事件、无事件时为空),
# 而旧行为本身已由 task["model"] / actual_model 体现,
# 不需要再用一条事件重复说明。
# 审计批 4(I-10):decide_routing 对档模型算过
# capability 的决策才记 capability——policy/shadow
# 决策算过、capability_blocked 决策算过、
# capability_fallback 决策算过;worker_default 经
# 无档模型/off/无策略、explicit/worker_explicit
# 提前返回时均未算,不记。
if decision.source in ("policy", "shadow",
"capability_blocked") \
or getattr(decision, "capability_fallback", False):
_mark_gate_checked(task, "capability")
if decision.source in ("policy", "shadow"):
record_routing({"id": task_id, "events": events}, decision)
# 影子评估(v0b)需要建议模型:旧格式只有