fix: bounded audit loop (deterministic gates only, 3 pushbacks, escalate)

Audit loop engine (cloud/audit_loop.py): Finding/PushbackPacket/LoopState pure functions; deterministic gates only, subjective -> needs_review; packet completeness enforced; standards-weakening -> escalate; regression and missing-gate checks enforce full re-run; MAX_PUSHBACKS=3, round>=3 -> escalate. Persistence in new single-row task_audit_loop_state (attempts DDL pattern: module CREATE_TABLE_SQL + db.py POSTGRES_SCHEMA + migrations list + server init + worker ensure); task_attempts NOT reused (attempt_no PK collision) and task_followups NOT reused (user messages). Worker.audit_loop_evaluate is the explicit opt-in hook (task audit_loop field only, no env switch, never called by run_one); escalate audits worker_audit_loop_escalated; corrupted/missing count fail-closes to escalate.
This commit is contained in:
host committed 2026-11-04 18:02:21 +00:00
1 parent 1b8b6e525d
commit 37b1aadc02
6 files changed
+675

No files matched your search

+99
View File
@@ -143,6 +143,10 @@ from cloud.attempts import build_resume_preamble as _resume_preamble
from cloud.attempts import ensure_attempts_table as _ensure_attempts
from cloud.attempts import latest_attempt as _latest_attempt
from cloud.attempts import record_attempt as _record_attempt
from cloud.audit_loop import ensure_audit_loop_table as _ensure_audit_loop
from cloud.audit_loop import evaluate as _audit_loop_evaluate
from cloud.audit_loop import load_loop_state as _audit_loop_load
from cloud.audit_loop import save_loop_state as _audit_loop_save
from cloud.auth import AuthStore
from cloud.coordinator import build_report as _coord_build_report
from cloud.coordinator import DEFAULT_POLL_INTERVAL as _COORD_POLL_INTERVAL
@@ -1792,6 +1796,101 @@ class Worker:
except Exception:
pass
def audit_loop_evaluate(self, task, gate_results, findings=None,
fixed=None):
"""有界审计循环的显式入口(默认关闭,绝不由 run_one 自动调用)。
启用条件二选一中只选其一:任务字典含 audit_loop 真值。
没有 env 开关——全局 env 会让所有任务悄悄启用循环,本方法
刻意拒绝它。开关关闭时返回 enabled=False 且不碰数据库,
run_one、auto_resume、协调员、goal 断点行为逐字节不变。
本方法不改任务状态、不重排、不调 runner、不重新决策:
它只给决策——pass(放行)、pushback(调用方持 packet 重排
任务,packet 里有失败项、证据、复现、验收标准)、escalate
(调用方置任务 failed/needs_review,不再自动循环)、
needs_review(主观发现,走人工)、off(未启用)。
升级时记 worker_audit_loop_escalated 审计(best-effort,
审计失败不改决策)。轮次存在 task_audit_loop_state 单行表,
计数损坏时 fail-closed 到 escalate,绝不默认循环。
调用参数非法(空证据、空门集、非法门结果)时同样
fail-closed 到 escalate,不进循环。
gate_results 必须含本轮全部已跑的门;上一轮通过的门本轮
缺跑或变失败时,引擎按 gate_missing / regression 升级或推回。
"""
task = task if isinstance(task, dict) else {}
if not task.get("audit_loop"):
return {"enabled": False, "decision": "off", "packet": None,
"new_state": None, "reason": None}
task_id = task.get("id")
project_id = task.get("project_id")
try:
conn = _db_connect(self.db_path)
except Exception as exc:
# 连库都连不上:无法持久化轮次,fail-closed 到升级。
try:
self._audit(project_id, "worker_audit_loop_escalated",
"%s reason=state_unreadable error=%s"
% (task_id, exc))
except Exception:
pass
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)
if decision["new_state"] is not None:
_audit_loop_save(conn, decision["new_state"])
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"}
finally:
try:
conn.close()
except Exception:
pass
if decision["decision"] == "escalate":
try:
self._audit(
project_id, "worker_audit_loop_escalated",
"%s reason=%s round=%s"
% (task_id, decision["reason"], decision["round"]))
except Exception:
pass
result = dict(decision)
result["enabled"] = True
if result.get("packet") is not None:
result["packet"] = result["packet"].to_dict()
if result.get("new_state") is not None:
result["new_state"] = result["new_state"].to_dict()
return result
def _notify_task_finished(self, task_id, project_id, event_type,
error=None):
"""任务终态推送:best-effort,绝不因通知失败影响任务状态。