Audit batch 5: I-9, I-12, I-14, N-1..N-12, single-task wire single-transaction

This commit is contained in:
host committed 2026-11-04 18:02:23 +00:00
1 parent cdac8039b3
commit 2adc605373
7 files changed
+1274 -365

No files matched your search

+11
View File
@@ -216,6 +216,17 @@ def parse_agent_tools_column(raw):
parse logic therefore changes no observable behaviour on
either side; the old server/worker defaults (None vs []) were
only different spellings of the same dispatch outcome.
Audit batch 5 (N-9, deliberate choice: document, do not filter):
the parser guarantees list shape only, NOT item types — a dirty
row like ["a", 1] is returned as-is. Downstream tolerates
non-string items, which is why filtering here would be churn,
not a fix: the worker required-tools gate only formats items
with "%s" (missing_required_tools) and compares them with ==
(deny check), the server only JSON-dumps the list, and no use
site calls a str method on an item. Filtering at parse time
would instead change serialization of old dirty rows (the
server today echoes them exactly), so the loose contract stays
and is locked by test.
"""
if raw is None:
return None
+39 -38
View File
@@ -224,6 +224,28 @@ def _validate_gate_results(gate_results):
raise ValueError("gate_results must map gate name to bool")
def _escalate(task_id, prev_round, passed_gates, criteria, reason,
regression=(), missing_gates=(), weakened=()):
"""升级决策 dict(审计批 5,N-8:纯结构整理,行为不变)。
evaluate() 内四处 escalate 返回原来各写一遍同样的 dict
骨架(new_state + decision/round/packet/reason + 三个清单),
只有 passed_gates / criteria / reason / 清单内容不同;抽成
助手后各处只传不同项。new_state 的 round 恒为 prev_round
(升级不推进轮次)、last_decision 恒为 escalate。
"""
new_state = LoopState(
task_id, round=prev_round, passed_gates=sorted(passed_gates),
criteria=criteria, last_decision="escalate")
return {
"decision": "escalate", "round": prev_round, "packet": None,
"new_state": new_state, "reason": reason,
"regression": list(regression),
"missing_gates": list(missing_gates),
"weakened": list(weakened),
}
def evaluate(state, task_id, gate_results, findings=None, fixed=None):
"""纯函数:一轮审计的决策。
@@ -269,17 +291,10 @@ def evaluate(state, task_id, gate_results, findings=None, fixed=None):
# 全量回归:上一轮通过的门本轮必须重跑,缺跑即升级。
missing_gates = sorted(g for g in prev_passed if g not in gate_results)
if missing_gates:
new_state = LoopState(
task_id, round=prev_round,
passed_gates=sorted(
g for g, v in gate_results.items() if v),
criteria=prev_criteria, last_decision="escalate")
return {
"decision": "escalate", "round": prev_round, "packet": None,
"new_state": new_state, "reason": "gate_missing",
"regression": [], "missing_gates": missing_gates,
"weakened": [],
}
return _escalate(
task_id, prev_round,
(g for g, v in gate_results.items() if v),
prev_criteria, "gate_missing", missing_gates=missing_gates)
passed_now = {g for g, v in gate_results.items() if v}
failed_now = {g for g, v in gate_results.items() if not v}
@@ -310,15 +325,11 @@ def evaluate(state, task_id, gate_results, findings=None, fixed=None):
else:
weakened.append(fid)
if weakened:
new_state = LoopState(
task_id, round=prev_round, passed_gates=sorted(passed_now),
criteria=prev_criteria, last_decision="escalate")
return {
"decision": "escalate", "round": prev_round, "packet": None,
"new_state": new_state, "reason": "standards_weakened",
"regression": [f.to_dict() for f in regression],
"missing_gates": [], "weakened": sorted(weakened),
}
return _escalate(
task_id, prev_round, passed_now, prev_criteria,
"standards_weakened",
regression=[f.to_dict() for f in regression],
weakened=sorted(weakened))
# 新标准 = 未解决的旧标准 + 本轮新 finding(含回归)。
for finding in all_findings:
@@ -348,26 +359,16 @@ def evaluate(state, task_id, gate_results, findings=None, fixed=None):
# 转 escalate),新行为后看到的是 gate_failed_without_
# finding 升级——原因更精确,不是行为倒退。
if failed_now and not all_findings:
new_state = LoopState(
task_id, round=prev_round, passed_gates=sorted(passed_now),
criteria=new_criteria, last_decision="escalate")
return {
"decision": "escalate", "round": prev_round, "packet": None,
"new_state": new_state, "reason": "gate_failed_without_finding",
"regression": [f.to_dict() for f in regression],
"missing_gates": [], "weakened": [],
}
return _escalate(
task_id, prev_round, passed_now, new_criteria,
"gate_failed_without_finding",
regression=[f.to_dict() for f in regression])
if prev_round >= MAX_PUSHBACKS:
new_state = LoopState(
task_id, round=prev_round, passed_gates=sorted(passed_now),
criteria=new_criteria, last_decision="escalate")
return {
"decision": "escalate", "round": prev_round, "packet": None,
"new_state": new_state, "reason": "max_rounds_exceeded",
"regression": [f.to_dict() for f in regression],
"missing_gates": [], "weakened": [],
}
return _escalate(
task_id, prev_round, passed_now, new_criteria,
"max_rounds_exceeded",
regression=[f.to_dict() for f in regression])
new_round = prev_round + 1
packet = PushbackPacket(task_id, new_round, all_findings, new_criteria)
+24 -19
View File
@@ -98,6 +98,25 @@ def _tier_dropped_reason(raw_entry):
return "unrecognised tier entry (expected string or dict)"
def _empty_report(warnings, tiers, dropped, unknown, gateway):
"""无效策略的空报告(审计批 5,N-10:纯结构整理,行为不变)。
dry_run_policy_document 的两处早期返回(非对象文档、解析
出空策略)原来各写一遍同样的 dict 骨架,只有 warnings 不同;
抽成助手后各处只传不同项。传入的列表按引用进报告(与原来
直接写字面量时同一语义),不拷贝。
"""
return {
"valid": False, "blocked": True, "version": None,
"content_fingerprint": None,
"global_mode": None, "global_mode_source": None,
"tiers": tiers, "dropped_tiers": dropped,
"unknown_tiers": unknown, "warnings": warnings,
"would_activate": [], "would_shadow": [], "would_off": [],
"gateway": gateway,
}
def dry_run_policy_document(data, env_mode_override=None,
gateway_model=None):
"""Dry-run one merged policy document (pure, no IO).
@@ -118,17 +137,10 @@ def dry_run_policy_document(data, env_mode_override=None,
if gateway_model else "env_unset"),
"tiers": {}}
if not isinstance(data, dict):
return {
"valid": False, "blocked": True, "version": None,
"content_fingerprint": None,
"global_mode": None, "global_mode_source": None,
"tiers": tiers_report, "dropped_tiers": dropped,
"unknown_tiers": unknown,
"warnings": ["policy document is not an object; "
return _empty_report(
["policy document is not an object; "
"the loader would ignore it entirely"],
"would_activate": [], "would_shadow": [], "would_off": [],
"gateway": _gw,
}
tiers_report, dropped, unknown, _gw)
raw_tiers = data.get("tiers")
if isinstance(raw_tiers, dict):
for name in sorted(raw_tiers):
@@ -142,15 +154,8 @@ def dry_run_policy_document(data, env_mode_override=None,
"policy is unusable (no usable tiers, or global mode "
"outside off/shadow/active); the loader would ignore it "
"entirely and workers would keep the worker default")
return {
"valid": False, "blocked": True, "version": None,
"content_fingerprint": None,
"global_mode": None, "global_mode_source": None,
"tiers": tiers_report, "dropped_tiers": dropped,
"unknown_tiers": unknown, "warnings": warnings,
"would_activate": [], "would_shadow": [], "would_off": [],
"gateway": _gw,
}
return _empty_report(
warnings, tiers_report, dropped, unknown, _gw)
global_mode = policy.mode
global_source = "policy"
if env_mode_override:
+82 -39
View File
@@ -4200,7 +4200,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
# cost_records 表只在 CostStore 首次初始化时建:新鲜
# 库上直接查会报 no such table,报告端点不应因此 500。
# 先建表(幂等),再读;建表失败时成本按空算、报告
# 里 token/usd 为 0,不假装成本已计。
# 里 token/usd 为 0,不假装成本已计。审计批 5(N-1):
# 下面成本 SELECT 同样加 except 兜底(此前只有 finally、
# 没有 except,建表失败会直接抛到 handler 层),失败时
# 成本按空算并在报告里标 cost_unavailable。
try:
_cost_store = CostStore(self.server.db_path)
try:
@@ -4238,11 +4241,19 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
" WHERE project_id = ?",
(project_id,),
).fetchall()
cost_unavailable = False
try:
cost_rows = conn.execute(
"SELECT task_id, total_tokens, usd_micros"
" FROM cost_records WHERE project_id = ?",
(project_id,),
).fetchall()
except Exception:
# 审计批 5(N-1):成本查询失败(通常是建表失败后
# 表仍不存在)时成本按空算,不抛到 handler 层;
# 报告里的 cost_unavailable 标明成本不可用。
cost_rows = []
cost_unavailable = True
finally:
conn.close()
tasks_by_id = {}
@@ -4275,6 +4286,18 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
tasks_by_id, costs_by_task, explicit_n=explicit_n)
report["project_id"] = project_id
report["truncated"] = truncated
report["cost_unavailable"] = cost_unavailable
# 审计批 5(N-5):截断取最早 10000 行(ORDER BY
# created_at ASC),与协调员门查询取最新 5000 行方向
# 相反——调用方须知样本是哪一段:同时返回样本时间窗
# (保留样本的最早/最晚 created_at;无样本时为 None)。
if audit_rows:
report["sample_window"] = {
"earliest_created_at": audit_rows[0]["created_at"],
"latest_created_at": audit_rows[-1]["created_at"]}
else:
report["sample_window"] = {
"earliest_created_at": None, "latest_created_at": None}
# 显式失败清单:解析 worker_capability_gateway_explicit
# 行(格式:task_id model=.. tier=.. mode=.. missing=..
# unknown=.. blocked=..;fix4 之前的行无 blocked 项,
@@ -4570,6 +4593,57 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
for path, size in saved],
})
@staticmethod
def _resolve_create_agent_definition(body):
"""建任务的 agent 定义解析(审计批 5,I-9③抽取,行为逐字不变)。
原 _create_task 内 agent/agent_name 解析段平移:返回
(definition, error)——error 非空时调用方按原样发 400 且
不建任务;definition 为 None 且 error 为空表示未给定义。
副作用:无库写、无响应发送(400 由调用方发,与原地一致)。
"""
if body.get("agent") is not None \
and body.get("agent_name") is not None:
return None, "agent and agent_name cannot both be used"
if body.get("agent") is not None \
and body.get("agent_version") is not None:
return None, ("agent_version is only for agent_name;"
" an inline agent carries its own version")
definition = None
if body.get("agent") is not None:
if not isinstance(body.get("agent"), dict):
return None, "agent must be an object"
try:
definition = _parse_agent_definition(
body.get("agent"),
known_skills=_agent_known_skills())
except ValueError as exc:
return None, str(exc)
if body.get("agent_name") is not None:
try:
definition = _resolve_agent(
body.get("agent_name"),
version=body.get("agent_version"),
agents_dir=_agent_default_dir(),
known_skills=_agent_known_skills())
except ValueError as exc:
return None, str(exc)
return definition, None
@staticmethod
def _validate_create_audit_loop(body):
"""建任务的 audit_loop 开关校验(审计批 5,I-9③抽取,行为逐字不变)。
原 _create_task 内审计循环接线校验段平移:只认 True/1
(其它真值一律错,不猜)。返回 (audit_loop_val, error)——
error 非空时调用方按原样发 400 且不建任务。
"""
if body.get("audit_loop") is None:
return 0, None
if body.get("audit_loop") not in (True, 1):
return None, "audit_loop must be true or 1"
return 1, None
def _create_task(self, user_id):
body, err = self._read_json()
if err:
@@ -4606,38 +4680,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
# 优先于定义;解析出的字段落进已有列,定义身份与
# 指令落进 agent_name/agent_version/agent_instructions
# 列(v2),prompt 不再带头。
definition = None
if body.get("agent") is not None \
and body.get("agent_name") is not None:
self._send_json(400, {
"error": "agent and agent_name cannot both be used"})
return
if body.get("agent") is not None \
and body.get("agent_version") is not None:
self._send_json(400, {
"error": "agent_version is only for agent_name;"
" an inline agent carries its own version"})
return
if body.get("agent") is not None:
if not isinstance(body.get("agent"), dict):
self._send_json(400, {"error": "agent must be an object"})
return
try:
definition = _parse_agent_definition(
body.get("agent"),
known_skills=_agent_known_skills())
except ValueError as exc:
self._send_json(400, {"error": str(exc)})
return
if body.get("agent_name") is not None:
try:
definition = _resolve_agent(
body.get("agent_name"),
version=body.get("agent_version"),
agents_dir=_agent_default_dir(),
known_skills=_agent_known_skills())
except ValueError as exc:
self._send_json(400, {"error": str(exc)})
definition, _agent_err = \
self._resolve_create_agent_definition(body)
if _agent_err:
self._send_json(400, {"error": _agent_err})
return
depends_on = body.get("depends_on") or []
if isinstance(depends_on, str):
@@ -4976,13 +5022,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
# 就不写 audit_loop 列(默认 0),父任务的循环只记在
# 父任务上,子任务包只是前言载体。goal 任务允许(goal
# 校验门是单任务接线要消费的确定性门之一)。
audit_loop_val = 0
if body.get("audit_loop") is not None:
if body.get("audit_loop") not in (True, 1):
self._send_json(400, {
"error": "audit_loop must be true or 1"})
audit_loop_val, _al_err = self._validate_create_audit_loop(body)
if _al_err:
self._send_json(400, {"error": _al_err})
return
audit_loop_val = 1
agent_name_val = definition.name if definition else None
agent_version_val = definition.version if definition else None
agent_instructions_val = (
+593
View File
@@ -0,0 +1,593 @@
"""审计批 5(I-9、I-14、N-1~N-12、I-12、事务分离)测试。
只用注入的假 runner 与 init_db 的本地库,不跑真实模型回合、
不联网。按任务书:新测试 + 相关旧套件确认无回归;本文件
只覆盖本批的新行为与新契约,旧行为由旧套件锁定。
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
try:
from cloud.agent_def import (
missing_required_tools,
parse_agent_tools_column,
)
from cloud.audit_loop import Finding
from cloud.auth import AuthStore
from cloud.models import parse_routing_policy
from cloud.routing_dryrun import dry_run_policy_document
from cloud.server import CloudControlPlaneHandler, create_server, init_db
from cloud.worker import Worker
import cloud.worker as worker_module
import cloud.server as server_module
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.agent_def import (
missing_required_tools,
parse_agent_tools_column,
)
from cloud.audit_loop import Finding
from cloud.auth import AuthStore
from cloud.models import parse_routing_policy
from cloud.routing_dryrun import dry_run_policy_document
from cloud.server import CloudControlPlaneHandler, create_server, init_db
from cloud.worker import Worker
import cloud.worker as worker_module
import cloud.server as server_module
def _completed_runner(cmd):
name = cmd[cmd.index("--name") + 1] if "dispatch" in cmd else "t"
return SimpleNamespace(
returncode=0,
stdout=json.dumps({"task": name, "status": "completed",
"text": "done"}),
stderr="")
def _insert_task(db_path, task_id, status="queued", audit_loop=0,
goal_mode=0, goal=None, verify_command=None,
goal_max_rounds=None, model=None, packet=None):
now = time.time()
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat, audit_loop,"
" goal_mode, goal, verify_command, goal_max_rounds, model,"
" audit_loop_packet)"
" VALUES(?, 'p1', 'do work', ?, NULL, '[]', 0, 'tester',"
" ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)",
(task_id, status, now, now, audit_loop, goal_mode, goal,
verify_command, goal_max_rounds, model, packet))
conn.commit()
finally:
conn.close()
def _set_status(db_path, task_id, status):
conn = sqlite3.connect(db_path)
try:
conn.execute("UPDATE tasks SET status = ? WHERE id = ?",
(status, task_id))
conn.commit()
finally:
conn.close()
def _row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT status, audit_loop_packet, goal, verify_command,"
" goal_max_rounds, model FROM tasks WHERE id = ?",
(task_id,)).fetchone()
finally:
conn.close()
def _state_row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT round, last_decision FROM task_audit_loop_state"
" WHERE task_id = ?", (task_id,)).fetchone()
finally:
conn.close()
def _seed_state(db_path, task_id, round_no, last_decision):
conn = sqlite3.connect(db_path)
try:
conn.execute(
"CREATE TABLE IF NOT EXISTS task_audit_loop_state("
"task_id TEXT PRIMARY KEY, round INTEGER,"
" passed_gates TEXT, criteria TEXT, last_decision TEXT,"
" updated_at DOUBLE PRECISION)")
conn.execute(
"INSERT OR REPLACE INTO task_audit_loop_state"
"(task_id, round, passed_gates, criteria, last_decision,"
" updated_at) VALUES(?, ?, ?, ?, ?, ?)",
(task_id, round_no, '["execution"]', '{}', last_decision,
time.time()))
conn.commit()
finally:
conn.close()
class _FailingExecuteConn:
"""注入失败的连接包装:指定 SQL 片段一律抛错,其余透传。"""
def __init__(self, real, needle):
self._real = real
self._needle = needle
def execute(self, sql, params=()):
if self._needle in sql:
raise RuntimeError("injected store failure")
return self._real.execute(sql, params)
def __getattr__(self, name):
return getattr(self._real, name)
class _WorkerCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
self.audits = []
def tearDown(self):
self.tmp.cleanup()
def _worker(self, **kwargs):
kwargs.setdefault("env", {"XCM_API_KEY": "test-placeholder"})
kwargs.setdefault("max_retries", 0)
kwargs.setdefault("retry_backoff_seconds", 0)
kwargs.setdefault("runner", _completed_runner)
worker = Worker(self.db_path, "/fake/host.py", **kwargs)
worker._audit = lambda p, a, d="": self.audits.append((p, a, d))
return worker
def _actions(self):
return [a for _, a, _ in self.audits]
class FingerprintTestCase(unittest.TestCase):
def test_fallback_models_change_fingerprint(self):
# I-14①:指纹宣称含 fallback_models,变化必须敏感。
base = {"mode": "shadow", "tiers": {"default": "m1"},
"model_capabilities": {"m1": {"tool_calling": True}}}
a = parse_routing_policy(base)
b = parse_routing_policy(
dict(base, fallback_models=["m2"]))
self.assertNotEqual(a.content_fingerprint(),
b.content_fingerprint())
c = parse_routing_policy(dict(base, fallback_models=["m2"]))
self.assertEqual(b.content_fingerprint(),
c.content_fingerprint())
class WireTxTestCase(_WorkerCase):
def _patched_worker(self, needle):
real_connect = worker_module._db_connect
def _connect(path):
return _FailingExecuteConn(real_connect(path), needle)
patcher = mock.patch.object(worker_module, "_db_connect",
_connect)
patcher.start()
self.addCleanup(patcher.stop)
return self._worker()
def test_pushback_store_failure_rolls_back_state(self):
# 事务分离:推回 UPDATE 失败时状态保存一并回滚,任务不
# 以“已推回状态 + failed 任务行”的半截态落地。
_insert_task(self.db_path, "TP", status="failed", audit_loop=1)
worker = self._patched_worker("SET status = 'queued'")
task = worker.get_task("TP")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "error")
self.assertEqual(final, "failed")
row = _row(self.db_path, "TP")
self.assertEqual(row[0], "failed")
self.assertIsNone(row[1])
self.assertIsNone(_state_row(self.db_path, "TP"))
self.assertNotIn("worker_audit_loop_pushback", self._actions())
def test_escalate_store_failure_claims_nothing(self):
# I-14②(改动后新行为):升级 UPDATE 失败时接线返回
# error、任务保持原终局、状态行不动,且不记“已升级”
# 审计——升级审计只在提交后记,绝不谎称已升级。
_insert_task(self.db_path, "TE", status="failed", audit_loop=1)
_seed_state(self.db_path, "TE", 3, "pushback")
worker = self._patched_worker("SET status = 'failed'")
task = worker.get_task("TE")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "error")
self.assertEqual(final, "failed")
self.assertEqual(_row(self.db_path, "TE")[0], "failed")
state = _state_row(self.db_path, "TE")
self.assertEqual(state[0], 3)
self.assertEqual(state[1], "pushback")
self.assertNotIn("worker_audit_loop_escalated", self._actions())
def test_escalate_cas_miss_audits_skipped(self):
# N-2:升级 CAS 未命中(状态已被改动)时记
# escalate_skipped 审计、不谎称已升级,任务与状态原样不动。
_insert_task(self.db_path, "TC", status="failed", audit_loop=1)
_seed_state(self.db_path, "TC", 3, "pushback")
_set_status(self.db_path, "TC", "queued")
worker = self._worker()
task = worker.get_task("TC")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "error")
self.assertEqual(final, "failed")
self.assertEqual(_row(self.db_path, "TC")[0], "queued")
self.assertEqual(_state_row(self.db_path, "TC")[0], 3)
self.assertIn("worker_audit_loop_escalate_skipped",
self._actions())
self.assertNotIn("worker_audit_loop_escalated", self._actions())
class CoordClearTestCase(_WorkerCase):
def test_clear_packets_failure_is_best_effort(self):
# I-14③:清包失败只记警告、不炸调用方、不改已定结局。
_insert_task(self.db_path, "PP", status="failed", audit_loop=1,
packet='{"round": 1}')
_insert_task(self.db_path, "CC", status="failed", audit_loop=0,
packet='{"round": 1}')
worker = self._worker()
worker._coord_clear_packets(
{"id": "PP", "project_id": "p1"}, ["CC"])
self.assertIsNone(_row(self.db_path, "PP")[1])
self.assertIsNone(_row(self.db_path, "CC")[1])
def _boom(path):
raise RuntimeError("injected connect failure")
with mock.patch.object(worker_module, "_db_connect", _boom):
# 不抛错即通过:清失败绝不改变已定的父任务结局。
worker._coord_clear_packets(
{"id": "PP", "project_id": "p1"}, ["CC"])
class ClassifyAndSplitTestCase(_WorkerCase):
def test_merge_failed_is_gate_rejected(self):
# N-3:merge-failed 归 gate_rejected,不再是
# excluded/non_terminal;容忍声明仍优先。
worker = self._worker()
kind, reason = worker._coord_classify_child(
None, {"id": "C1", "status": "merge-failed"}, set())
self.assertEqual((kind, reason), ("gate_rejected", "merge_failed"))
kind, _ = worker._coord_classify_child(
{"on_failure": "tolerate"},
{"id": "C2", "status": "merge-failed"}, set())
self.assertEqual(kind, "excluded")
kind, _ = worker._coord_classify_child(
None, {"id": "C3", "status": "merged"}, set())
self.assertEqual(kind, "completed")
def test_wire_splits_subjective_before_evaluate(self):
# N-4:单任务接线先拆分再评估——确定性照常推回,主观
# 只记审计,不吞推回。
_insert_task(self.db_path, "TS", status="failed", audit_loop=1)
worker = self._worker()
det = Finding("execution_failed", "high", "boom", "re-run",
"task completes", kind="deterministic",
gate="execution")
subj = Finding("design_note", "low", "looks odd", "review",
"human ok", kind="subjective", gate=None)
worker._audit_loop_gate_results = (
lambda task, final, result, gates_checked=None: (
{"execution": False}, [det, subj]))
task = worker.get_task("TS")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "pushback")
self.assertEqual(final, "queued")
needs = [d for _, a, d in self.audits
if a == "worker_audit_loop_needs_review"]
self.assertTrue(any("design_note" in d for d in needs))
class TerminalSqlTestCase(unittest.TestCase):
def test_terminal_sql_matches_old_literal_set(self):
# N-6:生成的终态串集合与旧手写字面量逐字等价。
from cloud.worker import _terminal_status_sql
sql = _terminal_status_sql()
inner = sql.strip()[1:-1]
got = {part.strip().strip("'") for part in inner.split(",")}
self.assertEqual(
got,
{"completed", "failed", "interrupted", "merged",
"merge-failed"})
class GoalWireTestCase(_WorkerCase):
def test_goal_pushback_leaves_goal_columns_and_bounds_rounds(self):
# N-12 定性:推回不改 goal 列;包贴进内存 goal 与 prompt;
# 审计轮次 3 轮后必升级(无无界循环)。
_insert_task(self.db_path, "TG", status="failed", audit_loop=1,
goal_mode=1, goal="reach the goal",
verify_command="true", goal_max_rounds=3)
worker = self._worker()
task = worker.get_task("TG")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"goal_met": False, "goal_error": "nope"},
"goal unmet")
self.assertEqual(decision, "pushback")
self.assertEqual(final, "queued")
row = _row(self.db_path, "TG")
self.assertEqual(row[2], "reach the goal")
self.assertEqual(row[3], "true")
self.assertEqual(row[4], 3)
packet = row[1]
self.assertIsNotNone(packet)
injected = worker._apply_audit_loop_packet({
"id": "TG", "project_id": "p1", "prompt": "do work",
"goal": "reach the goal", "goal_mode": 1,
"audit_loop_packet": packet})
self.assertTrue(injected["prompt"].startswith("[审计推回包"))
self.assertTrue(injected["goal"].startswith("[审计推回包"))
# 库里的原 prompt/goal 不动。
self.assertEqual(_row(self.db_path, "TG")[2], "reach the goal")
# 继续推回直至升级:轮次有界。
decisions = []
for _ in range(3):
_set_status(self.db_path, "TG", "failed")
task = worker.get_task("TG")
_, _, decision = worker._audit_loop_wire(
task, "failed",
{"goal_met": False, "goal_error": "nope"}, "goal unmet")
decisions.append(decision)
if decision == "escalate":
break
self.assertEqual(decisions[-1], "escalate")
self.assertEqual(_row(self.db_path, "TG")[0], "failed")
self.assertIsNone(_row(self.db_path, "TG")[1])
class ToolApprovalEffectiveTestCase(_WorkerCase):
def _insert_approval_task(self, task_id, approvals):
_insert_task(self.db_path, task_id, status="queued")
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET agent_tools = ?,"
" agent_tool_approvals = ? WHERE id = ?",
(json.dumps(["websearch"]), json.dumps(approvals),
task_id))
conn.commit()
finally:
conn.close()
def test_ask_is_audited_as_decline_under_host_deny(self):
# I-12:派发时真的调 effective,ask 在行里折算成
# decline,ask_honoured 恒为 False。
self._insert_approval_task("TA", {"websearch": "ask"})
worker = self._worker()
out = worker.run_one()
self.assertEqual(out["status"], "completed")
details = [d for _, a, d in self.audits
if a == "worker_tool_approvals_declared"]
self.assertTrue(details)
self.assertIn('"websearch"', details[0])
self.assertIn('"declared": "ask"', details[0])
self.assertIn('"effective": "decline"', details[0])
self.assertIn('"ask_honoured": false', details[0])
def test_auto_accepts_only_when_host_config_is_auto(self):
# host approval 取自传给 host 的 --config 文件:auto 时
# auto 声明 effective 为 accept;默认 deny 时为 decline。
self._insert_approval_task("TB", {"websearch": "auto"})
cfg = os.path.join(self.tmp.name, "host-config.json")
with open(cfg, "w", encoding="utf-8") as fh:
json.dump({"approval": "auto"}, fh)
worker = self._worker(config_path=cfg)
out = worker.run_one()
self.assertEqual(out["status"], "completed")
details = [d for _, a, d in self.audits
if a == "worker_tool_approvals_declared"]
self.assertIn('"effective": "accept"', details[0])
class GatewayExtractTestCase(_WorkerCase):
def _policy(self, capable):
return {"mode": "active",
"tiers": {"default": {
"model": "m1",
"requires": {"tool_calling": True}}},
"model_capabilities": {"m1": {"tool_calling": capable}}}
def test_blocked_and_pass_through(self):
# I-9①:抽取后行为逐字不变——被拦时早返回 dict +
# 门审计 + 标记点;满足时返回 None 继续派发。
_insert_task(self.db_path, "TG1", status="queued")
worker = self._worker(routing_policy=self._policy(False))
task = {"id": "TG1", "project_id": "p1", "model": "m1"}
blocked = worker._gateway_capability_check(
task, "sub2api", False, time.time())
self.assertIsNotNone(blocked)
self.assertEqual(blocked["status"], "failed")
self.assertIn("worker_capability_gate_rejected", self._actions())
self.assertIn("capability", task.get("_gates_checked", []))
self.audits.clear()
_insert_task(self.db_path, "TG2", status="queued")
worker2 = self._worker(routing_policy=self._policy(True))
task2 = {"id": "TG2", "project_id": "p1", "model": "m1"}
self.assertIsNone(worker2._gateway_capability_check(
task2, "sub2api", False, time.time()))
self.assertNotIn("worker_capability_gate_rejected",
self._actions())
class PersistExtractTestCase(_WorkerCase):
def test_writeback_only_for_applied_policy(self):
# I-9②:只持久化 source=policy 且 applied 的决策。
_insert_task(self.db_path, "TW", status="queued", model=None)
worker = self._worker()
task = {"id": "TW", "project_id": "p1"}
worker._persist_policy_model(
task, SimpleNamespace(chosen_model="m9", source="policy",
applied=True, tier="default"))
self.assertEqual(_row(self.db_path, "TW")[5], "m9")
self.assertIn("worker_model_persisted", self._actions())
self.audits.clear()
_insert_task(self.db_path, "TW2", status="queued", model=None)
worker._persist_policy_model(
{"id": "TW2", "project_id": "p1"},
SimpleNamespace(chosen_model="m9", source="shadow",
applied=False, tier="default"))
self.assertIsNone(_row(self.db_path, "TW2")[5])
self.assertNotIn("worker_model_persisted", self._actions())
class ServerHelperTestCase(unittest.TestCase):
def test_agent_definition_and_audit_loop_helpers(self):
# I-9③:两个小函数与原地行为一致(错误文案逐字)。
resolve = CloudControlPlaneHandler._resolve_create_agent_definition
_, err = resolve({"agent": {"a": 1}, "agent_name": "x"})
self.assertEqual(err, "agent and agent_name cannot both be used")
_, err = resolve({"agent": "not-a-dict"})
self.assertEqual(err, "agent must be an object")
validate = CloudControlPlaneHandler._validate_create_audit_loop
self.assertEqual(validate({}), (0, None))
self.assertEqual(validate({"audit_loop": True}), (1, None))
self.assertEqual(validate({"audit_loop": 1}), (1, None))
_, err = validate({"audit_loop": 2})
self.assertEqual(err, "audit_loop must be true or 1")
class ParseToleranceTestCase(unittest.TestCase):
def test_non_string_items_survive_and_are_tolerated(self):
# N-9(文档选项):解析层只保 list 形状,下游容忍非
# 字符串项——不过滤、不改旧脏行序列化。
self.assertEqual(parse_agent_tools_column('["a", 1]'),
["a", 1])
self.assertEqual(missing_required_tools(["a", 1], ""),
["a", 1])
class DryRunEmptyTestCase(unittest.TestCase):
def test_early_returns_share_shape(self):
# N-10:两处早期返回经 _empty_report 归一,键集合一致。
a = dry_run_policy_document("not-a-dict")
b = dry_run_policy_document({"mode": "bogus"})
self.assertFalse(a["valid"])
self.assertFalse(b["valid"])
self.assertEqual(set(a.keys()), set(b.keys()))
class ShadowEndpointTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _req(self, method, path, body=None, key=None):
conn = http.client.HTTPConnection("127.0.0.1", self.port,
timeout=10)
headers = {"Content-Type": "application/json"}
if key:
headers["X-API-Key"] = key
conn.request(method, path,
json.dumps(body) if body is not None else None,
headers)
resp = conn.getresponse()
raw = resp.read()
conn.close()
try:
data = json.loads(raw)
except ValueError:
data = None
return resp.status, data
def _shadow(self):
status, report = self._req(
"GET", "/routing-shadow?project_id=" + self.project["id"],
key=self.owner_key)
self.assertEqual(status, 200, report)
return report
def test_cost_query_failure_flagged_not_500(self):
# N-1 + I-14④:成本查询失败时报告仍 200、成本按空算、
# cost_unavailable 标明,不抛到 handler 层。
conn = sqlite3.connect(self.db_path)
try:
conn.execute("DROP TABLE IF EXISTS cost_records")
conn.commit()
finally:
conn.close()
class _BoomCostStore:
def __init__(self, *args, **kwargs):
raise RuntimeError("injected cost store failure")
with mock.patch.object(server_module, "CostStore",
_BoomCostStore):
report = self._shadow()
self.assertTrue(report["cost_unavailable"])
def test_sample_window_and_normal_cost_flag(self):
# N-5:截断样本(最早 10000 行)附带最早/最晚时间窗;
# 正常路径 cost_unavailable 为 False。
conn = sqlite3.connect(self.db_path)
try:
for task_id, created in (("t1", 10.0), ("t2", 20.0)):
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at) VALUES(?, ?, ?, ?, ?)",
("system", self.project["id"], "worker_routing",
"%s tier=plan source=shadow applied=False"
" suggested=mx" % task_id, created))
conn.commit()
finally:
conn.close()
report = self._shadow()
self.assertFalse(report["cost_unavailable"])
self.assertEqual(report["sample_window"],
{"earliest_created_at": 10.0,
"latest_created_at": 20.0})
self.assertFalse(report["truncated"])
if __name__ == "__main__":
unittest.main()
+6 -2
View File
@@ -232,9 +232,13 @@ class WireTestCase(unittest.TestCase):
def test_needs_review_leaves_outcome_untouched(self):
_insert_task(self.db_path, "T6", status="failed", audit_loop=1)
worker = self._worker()
worker.audit_loop_evaluate = lambda t, g, findings=None, fixed=None: {
# 审计批 5:接线改走 _audit_loop_evaluate_on_conn(同连接
# 单事务),桩点随之移到新内核;意图不变——防御分支仍在,
# needs_review 时终局、包列一律不动、只记审计。
worker._audit_loop_evaluate_on_conn = (
lambda conn, t, g, findings=None, fixed=None: {
"enabled": True, "decision": "needs_review", "packet": None,
"new_state": None, "reason": "subjective"}
"new_state": None, "reason": "subjective"})
task = worker.get_task("T6")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
+519 -267
View File
@@ -135,6 +135,7 @@ import uuid
from types import SimpleNamespace
from cloud.agent_def import AgentDefinition as _AgentDefinition
from cloud.agent_def import effective_tool_approvals as _effective_tool_approvals
from cloud.agent_def import format_instructions_header as _agent_header
from cloud.agent_def import missing_required_tools as _missing_tools
from cloud.agent_def import parse_agent_tool_approvals_column as _parse_tool_approvals_col
@@ -227,6 +228,20 @@ from cloud.workers import heartbeat_worker, register_worker
# 与 server.TERMINAL_STATUSES 对齐:这些状态的任务不再被认领或取消。
TERMINAL_STATUSES = frozenset(
{"completed", "failed", "interrupted", "merged", "merge-failed"})
def _terminal_status_sql():
"""终态集合的 SQL IN 串(审计批 5,N-6)。
worker 内多处“不在终态才改状态”的 SQL 原先各自手写同一串
字面量,改一处漏一处就会口径分裂;现在统一从
TERMINAL_STATUSES 生成。排序只为输出稳定,SQL 的 IN 是
集合语义,顺序不影响结果;测试断言生成串解析出的集合与
旧字面量集合逐字等价。其它模块(server、coordinator)各
自的 TERMINAL_STATUSES 常量本批不动,只统一 worker 内 SQL。
"""
return "(%s)" % ", ".join(
"'%s'" % status for status in sorted(TERMINAL_STATUSES))
COMPLETED_STATUSES = frozenset({"completed", "merged"})
DEFAULT_ENV_KEY = "XCM_API_KEY"
# 默认模型(2026-10-27 起为 gpt-5.6-terra):此前默认的
@@ -962,8 +977,9 @@ class Worker:
(已被别的 worker 抢走)时,只清掉本次新建的占位,绝
不动已在跑任务的条目。占位条目在执行路径的 finally
里清(本地 _execute_via_process / Docker 同步路径),
认领后提前返回的三道门(缺 key / 仓库 / 配额)在
run_one 里显式清占位;CAS 语义本身一句未改。
认领后提前返回的门(缺 key、仓库、配额、goal 轮次上限、
网关准备、网关能力、路由能力、工具审批、必需工具、
写回复核等)在 run_one 里显式清占位;CAS 语义本身一句未改。
P1 签名门:占位之前先比对多文件签名,签名缺失或已变
时直接返回 False(fail-closed,不抢任务、不占槽),
并记日志;不要只在 run_loop 轮次边界查——并行池线程
@@ -1868,11 +1884,55 @@ class Worker:
except Exception:
pass
def _audit_loop_evaluate_on_conn(self, conn, task, gate_results,
findings=None, fixed=None):
"""在调用方给定的连接上跑评估并写状态(不提交、不关连接)。
审计批 5(事务分离):本方法是 audit_loop_evaluate 与
_audit_loop_wire 共用的内核——前者自带连接、写完提交,
后者用同一连接把“状态保存、包写、终局更新”一次提交,
不再出现“状态已提交为 escalate、终局更新却没落库”的
自相矛盾。返回的决策 dict 与引擎原样一致(packet /
new_state 仍为对象,未转 dict);失败类决策另带内部键
“_error”(损坏/评估失败的错误文本),调用方在提交后
用它写升级审计,公开返回前必须 strip 掉(见
audit_loop_evaluate)。评估域内的异常一律收敛为
evaluate_failed 升级决策(与旧 audit_loop_evaluate 的
except 分支同款),不向调用方抛;连接级异常(提交失败
等)仍由调用方处理。
"""
task_id = task.get("id")
if not task.get("audit_loop"):
return {"enabled": False, "decision": "off", "packet": None,
"new_state": None, "reason": None}
try:
_ensure_audit_loop(conn)
state, corrupted, error = _audit_loop_load(conn, task_id)
if corrupted:
return {"enabled": True, "decision": "escalate",
"packet": None, "new_state": None,
"reason": "count_corrupted", "_error": error}
decision = _audit_loop_evaluate(
state, task_id, gate_results, findings=findings,
fixed=fixed)
if decision["new_state"] is not None:
_audit_loop_save(conn, decision["new_state"])
return decision
except Exception as exc:
# 评估或写状态失败:轮次不可信,fail-closed 到升级。
try:
conn.rollback()
except Exception:
pass
return {"enabled": True, "decision": "escalate",
"packet": None, "new_state": None,
"reason": "evaluate_failed", "_error": str(exc)}
def audit_loop_evaluate(self, task, gate_results, findings=None,
fixed=None):
"""有界审计循环的显式入口(默认关闭,绝不由 run_one 自动调用)。
启用条件二选一中只选其一:任务字典含 audit_loop 真值。
启用条件只有一个:任务字典含 audit_loop 真值。
没有 env 开关——全局 env 会让所有任务悄悄启用循环,本方法
刻意拒绝它。开关关闭时返回 enabled=False 且不碰数据库,
run_one、auto_resume、协调员、goal 断点行为逐字节不变。
@@ -1889,6 +1949,11 @@ class Worker:
fail-closed 到 escalate,不进循环。
gate_results 必须含本轮全部已跑的门;上一轮通过的门本轮
缺跑或变失败时,引擎按 gate_missing / regression 升级或推回。
调用方契约(审计批 5,N-4):主观发现绝不直接进本方法——
调用方须先用 _coord_split_findings 拆分,确定性进循环,
主观只记审计;协调员合流与单任务接线均已如此。
内部实现走 _audit_loop_evaluate_on_conn(自带连接、写完
提交),与单任务接线共用同一内核。
"""
task = task if isinstance(task, dict) else {}
if not task.get("audit_loop"):
@@ -1909,45 +1974,41 @@ class Worker:
return {"enabled": True, "decision": "escalate", "packet": None,
"new_state": None, "reason": "state_unreadable"}
try:
_ensure_audit_loop(conn)
state, corrupted, error = _audit_loop_load(conn, task_id)
if corrupted:
try:
self._audit(
project_id, "worker_audit_loop_escalated",
"%s reason=count_corrupted error=%s"
% (task_id, error))
except Exception:
pass
return {"enabled": True, "decision": "escalate",
"packet": None, "new_state": None,
"reason": "count_corrupted"}
decision = _audit_loop_evaluate(
state, task_id, gate_results, findings=findings,
fixed=fixed)
decision = self._audit_loop_evaluate_on_conn(
conn, task, gate_results, findings=findings, fixed=fixed)
if decision["new_state"] is not None:
_audit_loop_save(conn, decision["new_state"])
try:
conn.commit()
except Exception as exc:
# 评估或写状态失败:轮次不可信,fail-closed 到升级。
try:
conn.rollback()
except Exception:
pass
try:
self._audit(project_id, "worker_audit_loop_escalated",
"%s reason=evaluate_failed error=%s"
% (task_id, exc))
except Exception:
pass
return {"enabled": True, "decision": "escalate", "packet": None,
"new_state": None, "reason": "evaluate_failed"}
decision = {"enabled": True, "decision": "escalate",
"packet": None, "new_state": None,
"reason": "evaluate_failed",
"_error": str(exc)}
finally:
try:
conn.close()
except Exception:
pass
if decision["decision"] == "escalate":
if decision["reason"] == "count_corrupted":
try:
self._audit(
project_id, "worker_audit_loop_escalated",
"%s reason=count_corrupted error=%s"
% (task_id, decision.get("_error")))
except Exception:
pass
elif decision["reason"] == "evaluate_failed":
try:
self._audit(project_id, "worker_audit_loop_escalated",
"%s reason=evaluate_failed error=%s"
% (task_id, decision.get("_error")))
except Exception:
pass
elif decision["decision"] == "escalate":
try:
self._audit(
project_id, "worker_audit_loop_escalated",
@@ -1955,7 +2016,8 @@ class Worker:
% (task_id, decision["reason"], decision["round"]))
except Exception:
pass
result = dict(decision)
result = {k: v for k, v in decision.items()
if not k.startswith("_")}
result["enabled"] = True
if result.get("packet") is not None:
result["packet"] = result["packet"].to_dict()
@@ -2054,14 +2116,48 @@ class Worker:
- escalate:清掉推回包列,状态置 failed(合法终态;
VALID_STATUSES 里没有 needs_review,绝不发明它),
error_text 写明“审计循环升级”+ 轮次 + 原因 + 上一轮
错误摘要,之后不再自动重排。引擎已记
worker_audit_loop_escalated 审计,这里不重复记。
- needs_review:防御分支(接线从不产生主观发现,正常
流程到不了这里):只记 worker_audit_loop_needs_review
错误摘要,之后不再自动重排。升级审计在事务提交后记,
与旧接线“引擎已记、这里不重复记”的审计行一模一样,
只是时序从“评估提交后”挪到“接线提交后”。
- needs_review:防御分支(接线只产生确定性发现,正常
流程到不了这里;审计批 5(N-4)起接线先拆分主观发现
只记审计再评确定性,所以 needs_review 更到不了):
只记 worker_audit_loop_needs_review
审计,终局、包列一律不动,主观审计永不自动循环。
- off / skipped:未启用或协调员父任务,不碰库。
包列写失败、CAS 失败时 fail-closed 回原终局并记警告,
绝不谎称已推回或已升级。
审计批 5(事务分离 + N-2):状态保存、包写、终局更新在
同一连接、同一事务里一次提交——评估内核走
_audit_loop_evaluate_on_conn(只写不提交),分支写完
才 commit;事务内任何一步失败(包括升级/推回的 CAS
未命中)即整体回滚,任务不以“已升级状态 + completed
任务行”的矛盾状态落地。pass 的清包与 escalate 的置
failed 同样带 AND status=<终局> CAS(旧版 pass 清包与
escalate 更新无 CAS);CAS 未命中时记警告/审计(升级
未命中另记 worker_audit_loop_escalate_skipped 审计),
fail-closed 回原终局,绝不谎称已推回或已升级。
例外:接线连库都连不上时直接返回 error、任务保持原
终局(旧版评估连库失败会返回 state_unreadable 升级、
接线另开连接置 failed;单事务下无连接即无决策可提交,
不谎称升级)。升级成功后结果文本写回(_write_execution_
result,自带连接)仍是提交后的独立写:它失败时只记
警告、不再把已 failed 的任务谎报成 error(旧版写回
失败会返回 error,尽管状态已是 failed)。
goal 任务(审计批 5,N-12 定性,只文档化、不改行为):
推回只改 tasks.status 与包列(+ 轮次状态表),不碰
goal / verify_command / goal_max_rounds 列,也不碰宿主
goal 状态文件——goal_rounds_done 根本不在任务表里,
接线既无权重置它、也不由接线延续它。下一轮正常认领
时包前言贴进内存 prompt 与 goal(_apply_audit_loop_
packet 对 goal 任务同步贴两者),但 goal 任务下一轮
实际会撞宿主任务名已存在、经 run_one 的已存在回落走
断点续跑(goal-resume 不传 --goal-text,宿主用存储的
goal 与 goal_rounds_done+1 继续),所以包前言贴了却
不进续跑循环——这是审计推回对 goal 任务的已知效果
缺口,不是轮次会计错误,本批不改行为、留待后续批
定夺。轮次会计本身有界:审计轮次硬上限 3(MAX_
PUSHBACKS),3 轮后必升级;goal 旧形态总回合数受
goal_max_rounds 约束(续跑不重置计数),新形态受配额
约束;两组计数器独立、不叠加成无界循环。
"""
task_id = task.get("id")
project_id = task.get("project_id")
@@ -2075,18 +2171,29 @@ class Worker:
# 不再硬编码成通过。未传(旧直接调用)时保持
# 旧行为,见 _audit_loop_gate_results 文档。
task, final, result, task.get("_gates_checked"))
# 审计批 5(N-4):先拆分再评估,与协调员合流对称。
# 单任务接线目前只产生确定性发现,拆分在生产路径上
# 不改变决策;主观发现只记审计,不进循环、不吞推回。
deterministic, subjective = self._coord_split_findings(findings)
if subjective:
self._audit(project_id, "worker_audit_loop_needs_review",
"%s subjective=%s" % (
task_id,
",".join(f.id for f in subjective)))
try:
decision = self.audit_loop_evaluate(
task, gate_results, findings=findings)
conn = _db_connect(self.db_path)
except Exception as exc:
try:
log_event(self.logger, "worker_audit_loop_wire_failed",
task_id=task_id, project_id=project_id,
status="warning",
message="audit loop evaluate failed: %s" % exc)
message="audit loop wire connect failed: %s" % exc)
except Exception:
pass
return final, error, "error"
try:
decision = self._audit_loop_evaluate_on_conn(
conn, task, gate_results, findings=deterministic)
kind = decision.get("decision")
if kind == "off":
return final, error, "off"
@@ -2095,57 +2202,51 @@ class Worker:
"%s" % task_id)
return final, error, "needs_review"
if kind == "pass":
try:
conn = _db_connect(self.db_path)
try:
self._ensure_result_columns(conn)
conn.execute(
cur = conn.execute(
"UPDATE tasks SET audit_loop_packet = NULL,"
" updated_at = ? WHERE id = ?",
(time.time(), task_id))
conn.commit()
finally:
conn.close()
except Exception as exc:
" updated_at = ? WHERE id = ? AND status = ?",
(time.time(), task_id, final))
if cur.rowcount != 1:
# 审计批 5(N-2):清包同样带终局 CAS——状态
# 已被改动时不清包(可能已是新一轮的包),
# 整个事务回滚(状态保存亦不落库),不谎称 pass。
conn.rollback()
try:
log_event(self.logger,
"worker_audit_loop_packet_clear_failed",
log_event(
self.logger,
"worker_audit_loop_packet_clear_skipped",
task_id=task_id, project_id=project_id,
status="warning",
message="clear packet failed: %s" % exc)
message="task status changed before packet clear")
except Exception:
pass
return final, error, "error"
conn.commit()
self._audit(project_id, "worker_audit_loop_passed",
"%s round=%s" % (task_id, decision.get("round")))
return final, error, "pass"
if kind == "pushback":
packet = decision.get("packet") or {}
try:
conn = _db_connect(self.db_path)
try:
packet = decision.get("packet")
if packet is None:
packet_dict = {}
elif hasattr(packet, "to_dict"):
packet_dict = packet.to_dict()
else:
packet_dict = packet
self._ensure_result_columns(conn)
cur = conn.execute(
"UPDATE tasks SET status = 'queued',"
" audit_loop_packet = ?, updated_at = ?"
" WHERE id = ? AND status = ?",
(json.dumps(packet), time.time(), task_id, final))
conn.commit()
moved = cur.rowcount == 1
finally:
conn.close()
except Exception as exc:
(json.dumps(packet_dict), time.time(), task_id, final))
if cur.rowcount != 1:
# CAS 未命中即整个事务回滚:状态保存亦不落库,
# 不出现“状态已推回、任务仍是终局”的半截态。
conn.rollback()
try:
log_event(self.logger,
"worker_audit_loop_pushback_failed",
task_id=task_id, project_id=project_id,
status="warning",
message="pushback store failed: %s" % exc)
except Exception:
pass
return final, error, "error"
if not moved:
try:
log_event(self.logger,
log_event(
self.logger,
"worker_audit_loop_pushback_skipped",
task_id=task_id, project_id=project_id,
status="warning",
@@ -2153,46 +2254,101 @@ class Worker:
except Exception:
pass
return final, error, "error"
conn.commit()
self._audit(project_id, "worker_audit_loop_pushback",
"%s round=%s findings=%s" % (
task_id, decision.get("round"),
",".join(
f["id"]
for f in packet.get("findings", []))))
for f in packet_dict.get("findings", []))))
return "queued", error, "pushback"
if kind == "escalate":
esc_error = (
"审计循环升级(第 %s 轮,原因 %s):%s"
% (decision.get("round"), decision.get("reason"),
(error or "未通过确定性门")[:500]))
try:
conn = _db_connect(self.db_path)
try:
self._ensure_result_columns(conn)
conn.execute(
cur = conn.execute(
"UPDATE tasks SET status = 'failed',"
" audit_loop_packet = NULL, updated_at = ?"
" WHERE id = ?", (time.time(), task_id))
" WHERE id = ? AND status = ?",
(time.time(), task_id, final))
if cur.rowcount != 1:
# 审计批 5(N-2):升级同样带终局 CAS;未命中
# 时记审计、不谎称已升级,整个事务回滚。
conn.rollback()
self._audit(
project_id, "worker_audit_loop_escalate_skipped",
"%s status changed before escalate" % task_id)
try:
log_event(
self.logger,
"worker_audit_loop_escalate_skipped",
task_id=task_id, project_id=project_id,
status="warning",
message="task status changed before escalate")
except Exception:
pass
return final, error, "error"
conn.commit()
finally:
conn.close()
# 升级审计在提交后记(与旧接线“引擎已记”的行同款,
# 只是时序从评估提交后挪到接线提交后,绝不出现
# “审计写着已升级、任务行却仍是终局”的矛盾)。
if decision.get("reason") == "count_corrupted":
self._audit(
project_id, "worker_audit_loop_escalated",
"%s reason=count_corrupted error=%s"
% (task_id, decision.get("_error")))
elif decision.get("reason") == "evaluate_failed":
self._audit(
project_id, "worker_audit_loop_escalated",
"%s reason=evaluate_failed error=%s"
% (task_id, decision.get("_error")))
else:
self._audit(
project_id, "worker_audit_loop_escalated",
"%s reason=%s round=%s"
% (task_id, decision.get("reason"),
decision.get("round")))
try:
self._write_execution_result(
task_id, result_text=(result or {}).get("stdout", ""),
error_text=esc_error, actual_model=None,
started_at=None, finished_at=time.time(),
only_if_status="failed")
except Exception as exc:
# 结果文本写回是提交后的独立写:失败只记警告,
# 任务已是 failed,不再谎报成 error。
try:
log_event(self.logger,
"worker_audit_loop_escalate_failed",
log_event(
self.logger,
"worker_audit_loop_escalate_result_failed",
task_id=task_id, project_id=project_id,
status="warning",
message="escalate store failed: %s" % exc)
message="escalate result write failed: %s" % exc)
except Exception:
pass
return "failed", esc_error, "escalate"
conn.rollback()
return final, error, "error"
except Exception as exc:
try:
conn.rollback()
except Exception:
pass
try:
log_event(self.logger, "worker_audit_loop_wire_failed",
task_id=task_id, project_id=project_id,
status="warning",
message="audit loop wire store failed: %s" % exc)
except Exception:
pass
return final, error, "error"
return "failed", esc_error, "escalate"
return final, error, "error"
finally:
try:
conn.close()
except Exception:
pass
def _notify_task_finished(self, task_id, project_id, event_type,
error=None):
@@ -2940,8 +3096,7 @@ class Worker:
cur = conn.execute(
"UPDATE tasks SET status = 'interrupted', updated_at = ?"
" WHERE id = ? AND status NOT IN "
" ('completed', 'failed', 'interrupted', 'merged',"
" 'merge-failed')",
+ _terminal_status_sql(),
(time.time(), task_id),
)
conn.commit()
@@ -5947,8 +6102,7 @@ class Worker:
"UPDATE tasks SET status = 'interrupted',"
" interrupted_reason = 'coordinator_orphan',"
" updated_at = ? WHERE id = ? AND status NOT IN "
" ('completed', 'failed', 'interrupted', 'merged',"
" 'merge-failed')",
+ _terminal_status_sql(),
(time.time(), ch["id"]))
if cur.rowcount == 1:
marked.append(ch["id"])
@@ -6097,8 +6251,7 @@ class Worker:
rows = conn.execute(
"SELECT id, parent_task_id, project_id FROM tasks"
" WHERE parent_task_id IS NOT NULL AND status NOT IN "
" ('completed', 'failed', 'interrupted', 'merged',"
" 'merge-failed')").fetchall()
+ _terminal_status_sql()).fetchall()
parents = {}
for row in rows:
try:
@@ -6129,8 +6282,7 @@ class Worker:
"UPDATE tasks SET status = 'interrupted',"
" interrupted_reason = 'coordinator_orphan',"
" updated_at = ? WHERE id = ? AND status NOT IN "
" ('completed', 'failed', 'interrupted', 'merged',"
" 'merge-failed')",
+ _terminal_status_sql(),
(time.time(), cid))
if cur.rowcount == 1:
marked.append((pid, cid))
@@ -6155,6 +6307,10 @@ class Worker:
再调 evaluate:确定性进循环,主观只记审计。返回
(deterministic, subjective)。本方法本身不产生发现,
合流代码产生的发现全是确定性的,拆分是防御层。
审计批 5(N-4):单任务接线 _audit_loop_wire 同样先拆分
再调 evaluate(对称;单任务接线目前只产生确定性发现,
拆分在生产路径上不改变决策,只防未来某处混入主观发现
时把确定性推回吞掉)。
"""
deterministic = []
subjective = []
@@ -6326,6 +6482,8 @@ class Worker:
completed:用户取消等 excluded 情形下,审计 pass
只表示“无可重做的确定性失败”,父任务终局仍由
overall 定(取消的子任务仍会使 overall 为 failed)。
审计批 5(N-3):merge-failed 归 gate_rejected(见
分类体内的注释),不再落入 excluded/non_terminal。
"""
status = (row or {}).get("status")
if status == "completed" or status == "merged":
@@ -6337,6 +6495,21 @@ class Worker:
return "excluded", "user_cancelled"
if row.get("interrupted_reason") == "coordinator_orphan":
return "excluded", "orphaned"
# 审计批 5(N-3):merge-failed 明确归 gate_rejected,
# 不再是 excluded/non_terminal。理由:①它是确定性终态——
# agent 的工作已做完,卡在合入环节(冲突/目标分支状态),
# 重跑 agent 解决不了合入问题,修复方在运营方(手动解
# 冲突、手动 /pr 重试——手动重排 SQL 本就另接受
# merge-failed,与审计推回是两条路);②审计推回的重排
# SQL 只接受 ('failed', 'interrupted'),即使归 execution
# 也会被重排步静默跳过(rowcount 0 → pushback_skipped),
# 父任务却已回 queued,等于一次无子任务重排的空推回;
# 归 gate_rejected 则父任务直接升级,不烧审计轮次。
# 位置在门拒绝审计/标记检查之前:merge-failed 本身已是
# 足够强的确定性证据,不必再靠审计行或 error 文案佐证;
# 容忍/取消/孤儿仍优先(运营方已声明的处置意图更强)。
if status == "merge-failed":
return "gate_rejected", "merge_failed"
child_id = row.get("id")
if child_id in (gate_rejected_ids or set()):
return "gate_rejected", "gate_audit"
@@ -7160,6 +7333,227 @@ class Worker:
" required capability"
% (tier, model, ", ".join(known)))
def _gateway_capability_check(self, task, gateway_provider,
_explicit_model, started_at):
"""网关能力检查门(审计批 5,I-9①抽取,行为逐字不变)。
原 run_one 内整段平移:判定本身(evaluate_capability)
已是纯函数,本方法是 worker 内唯一的一处接线调用——
这就是立约的新门形状:纯判定函数 + 一处接线调用;
coord_store 抽取是中长期项,本批只在注释里写明不做。
返回:被拦时返回 run_one 同款早返回 dict(dispatched
True、status failed、error 为门错误文本),否则返回
None(调用方继续派发)。副作用点逐项核对(与原代码
一致,抽取后无增减):
- 审计:显式模型记 worker_capability_gateway_explicit
(含 blocked 标志);shadow 档记
worker_capability_gateway_shadow;被拦时记
worker_capability_gate_rejected(detail 为
任务 id 加错误格式)。
- 标记:verdict 实际算出时 _mark_gate_checked(task,
“capability”)(审计批 4,I-10 标记点,位置不变)。
- 状态:被拦时 _set_status(task_id, “failed”)。
- 结果写回:被拦时 _write_execution_result(
only_if_status 为“failed”)。
- 清理:被拦时 _cleanup_task_seed + _running_clear。
- 不改 task 的 model,不碰事件列表;无策略、无档需求、
档模式 off 时一律不查(旧行为不变)。
"""
task_id = task.get("id")
# Audit fix:网关能力检查——此前路由决策只在模型
# 为空时跑,sub2api 网关先填默认模型后决策直接跳
# 过,能力门在生产路径上完全不生效。补齐:网关填
# 的模型同样按档需求检查,档模式与 v8 一致:shadow
# 只审计不拦,active 不满足则 fail-fast(同款工具
# 门形状,清占位、不调 runner)。没有策略、没有档
# 需求、档模式 off 时一律不查,旧行为不变。注意:
# 这里没有 worker 默认可回落(网关模型就是该任务
# 的网关内模型,换成 worker 默认会跑错网关),所以
# active 下不满足即拦,不存在“先试 worker 默认”
# 的中间态;运营方须先把网关模型写进能力目录,
# 否则 unknown 同样拦(fail-closed,与 v8 一致)。
# 例外:任务级显式模型按 Audit fix 4 分档——已知
# 不满足才拦,unknown 只审计(见下),因为显式
# 优先保护的是用户选了哪个模型,不是选的模型
# 一定能跑;目录陈旧不应惩罚正确的显式选择。
# 审计批 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 {}
if _gw_requires:
_gw_mode = self.routing_policy.tier_mode(_gw_tier)
if _gw_mode in ("active", "shadow"):
_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" % (
task.get("model"),
"cannot verify"
if _gw_verdict.unknown
else "lacks",
", ".join(_gw_verdict.missing)))
# Audit fix 4(第三遍审计分档建议):
# 显式模型按 known/unknown 分档。known
# 是目录明确写了不满足(布尔 False、
# 或 context 低于需求),与网关默认
# 模型同款 fail-fast;unknown 是目录
# 无该模型、或条目缺字段,目录陈旧
# 不应惩罚用户的正确选择,只审计不拦。
# shadow 档两种情况都只审计。
_gw_known = _gw_verdict.known_missing
_gw_block = False
_gw_error = None
if _explicit_model:
_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 blocked=%s"
% (task_id, task.get("model"),
_gw_tier, _gw_mode,
",".join(_gw_verdict.missing) or "-",
_gw_verdict.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"],
"worker_capability_gateway_shadow",
"%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,
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")
# Audit fix 3(M5):网关门拒绝
# 早返回也要清暂存,同缺 key 门。
self._cleanup_task_seed(task)
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}
return None
def _persist_policy_model(self, task, decision):
"""策略写回(审计批 5,I-9②抽取,行为逐字不变)。
原 run_one 内 decide_routing 后的写回段平移:只持久化
source=policy 且 applied 的决策(shadow / worker_default /
worker_explicit 一律不写回,见原注释)。副作用点逐项核对
(与原代码一致,抽取后无增减):
- 库写:CAS 式 UPDATE tasks SET model(仅当库模型仍为
NULL/空时写,绝不覆盖显式模型),自带连接、写完提交。
- 日志:写失败时记 worker_model_writeback_failed 警告,
不炸派发(本轮内存模型照常生效)。
- 审计:仅写成功时记 worker_model_persisted(身份标记,
后续尝试据此区分写回模型与用户显式模型)。
- 不改任务状态、不清占位、不记 gates_checked 标记、不碰
task["model"](内存模型的赋值仍在调用方原地)。
新门形状规则同 _gateway_capability_check:纯判定
(decide_routing)+ worker 内一处接线调用。
"""
task_id = task.get("id")
_persisted = False
if decision.chosen_model and decision.source == "policy" \
and decision.applied:
try:
_wb_conn = _db_connect(self.db_path)
try:
_wb_conn.execute(
"UPDATE tasks SET model = ? WHERE id = ?"
" AND (model IS NULL OR model = '')",
(decision.chosen_model, task_id))
_wb_conn.commit()
finally:
_wb_conn.close()
_persisted = True
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)
if _persisted:
# 写回模型的身份标记:后续尝试据此区分
# 写回模型与用户显式模型(见
# _is_persisted_model)。审计本身 best-effort,
# 丢了也不影响写回,旧模型靠 policy 行兜底。
self._audit(task["project_id"],
"worker_model_persisted",
"%s tier=%s model=%s" % (
task_id, decision.tier,
decision.chosen_model))
def run_one(self, task=None):
"""认领并执行一个任务,返回结果 dict(见模块 docstring)。
@@ -7541,148 +7935,13 @@ class Worker:
# 门可列入 gate_results。
_mark_gate_checked(task, "gateway")
# Audit fix:网关能力检查——此前路由决策只在模型
# 为空时跑,sub2api 网关先填默认模型后决策直接跳
# 过,能力门在生产路径上完全不生效。补齐:网关填
# 的模型同样按档需求检查,档模式与 v8 一致:shadow
# 只审计不拦,active 不满足则 fail-fast(同款工具
# 门形状,清占位、不调 runner)。没有策略、没有档
# 需求、档模式 off 时一律不查,旧行为不变。注意:
# 这里没有 worker 默认可回落(网关模型就是该任务
# 的网关内模型,换成 worker 默认会跑错网关),所以
# active 下不满足即拦,不存在“先试 worker 默认”
# 的中间态;运营方须先把网关模型写进能力目录,
# 否则 unknown 同样拦(fail-closed,与 v8 一致)。
# 例外:任务级显式模型按 Audit fix 4 分档——已知
# 不满足才拦,unknown 只审计(见下),因为显式
# 优先保护的是用户选了哪个模型,不是选的模型
# 一定能跑;目录陈旧不应惩罚正确的显式选择。
# 审计批 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 {}
if _gw_requires:
_gw_mode = self.routing_policy.tier_mode(_gw_tier)
if _gw_mode in ("active", "shadow"):
_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" % (
task.get("model"),
"cannot verify"
if _gw_verdict.unknown
else "lacks",
", ".join(_gw_verdict.missing)))
# Audit fix 4(第三遍审计分档建议):
# 显式模型按 known/unknown 分档。known
# 是目录明确写了不满足(布尔 False、
# 或 context 低于需求),与网关默认
# 模型同款 fail-fast;unknown 是目录
# 无该模型、或条目缺字段,目录陈旧
# 不应惩罚用户的正确选择,只审计不拦。
# shadow 档两种情况都只审计。
_gw_known = _gw_verdict.known_missing
_gw_block = False
_gw_error = None
if _explicit_model:
_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 blocked=%s"
% (task_id, task.get("model"),
_gw_tier, _gw_mode,
",".join(_gw_verdict.missing) or "-",
_gw_verdict.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"],
"worker_capability_gateway_shadow",
"%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,
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")
# Audit fix 3(M5):网关门拒绝
# 早返回也要清暂存,同缺 key 门。
self._cleanup_task_seed(task)
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}
# 审计批 5(I-9①):网关能力检查整段已抽成
# _gateway_capability_check(纯平移,行为逐字不变,
# 副作用清单见该方法文档串)。
_gw_blocked = self._gateway_capability_check(
task, gateway_provider, _explicit_model, started_at)
if _gw_blocked is not None:
return _gw_blocked
task = dict(task)
if not task.get("model"):
@@ -7793,36 +8052,7 @@ class Worker:
# 网关路径不受影响:网关填模型的任务跳过
# decide_routing 与写回,每次尝试都重新填网关
# 模型并重新接受网关能力检查。
_persisted = False
if decision.chosen_model and decision.source == "policy" \
and decision.applied:
try:
_wb_conn = _db_connect(self.db_path)
try:
_wb_conn.execute(
"UPDATE tasks SET model = ? WHERE id = ?"
" AND (model IS NULL OR model = '')",
(decision.chosen_model, task_id))
_wb_conn.commit()
finally:
_wb_conn.close()
_persisted = True
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)
if _persisted:
# 写回模型的身份标记:后续尝试据此区分
# 写回模型与用户显式模型(见
# _is_persisted_model)。审计本身 best-effort,
# 丢了也不影响写回,旧模型靠 policy 行兜底。
self._audit(task["project_id"],
"worker_model_persisted",
"%s tier=%s model=%s" % (
task_id, decision.tier,
decision.chosen_model))
self._persist_policy_model(task, decision)
task["model"] = decision.chosen_model or self.model
elif not _gateway_filled:
# Audit fix 3(I8):显式模型任务(含协调员子任务
@@ -7896,6 +8126,13 @@ class Worker:
# 宣称运行时按工具拦截:host 的 elicitation 回答是
# 全局开关(auto 接受、其余拒绝),没有按工具分流,
# ask 在今天没有询问通道,见 agent_def.py 的 v9 边界。
# 审计批 5(I-12):派发时真的调 effective_tool_approvals
# 并把 effective 映射写进同一审计行——此前模块文档称
# “派发时审计其有效结果”,实际只记了声明本身。host 的
# 全局开关取自传给 host 的 --config 文件里的 approval
# (读不到/未配置时按 host 自身的默认 deny 计,与
# host.py 的 args.approval 回落同款);ask_honoured 恒为
# False,ask 在行里明确折算成 decline,不谎称“会问”。
_tool_approvals = task.get("agent_tool_approvals") or {}
if _tool_approvals:
_denied_required = [
@@ -7928,11 +8165,26 @@ class Worker:
"executor": self._executor_name(),
"stdout": "", "error": _appr_error, "events": [],
"cost_record_id": None}
_host_approval = "deny"
_cfg_path = getattr(self.executor, "config_path", None)
if _cfg_path:
try:
with open(_cfg_path, encoding="utf-8") as _fh:
_host_approval = (
json.load(_fh).get("approval") or "deny")
except Exception:
_host_approval = "deny"
_effective_approvals = _effective_tool_approvals(
_tool_approvals, _host_approval)
self._audit(task["project_id"],
"worker_tool_approvals_declared",
"%s: %s (per-tool interception is not enforced"
" at runtime; host answers elicitation globally)"
" effective=%s"
% (task_id, json.dumps(_tool_approvals,
ensure_ascii=False,
sort_keys=True),
json.dumps(_effective_approvals,
ensure_ascii=False,
sort_keys=True)))
# agent 必需工具门(v3):协调员父任务已在前面返回,