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:
1 parent
06e4e16bbb
commit
cdac8039b3
5 files changed
+959
-47
No files matched your search
+187
-31
@@ -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)需要建议模型:旧格式只有
|
||||
|
||||
Reference in new issue
Block a user