code-to10 v6 A3: enforce signer separation on audit-verdict submission

process_audit_verdict now calls validate_signer_separation
before any state save or ledger write (previously the function
had no production caller on this path: it only ran inside
collect_failed_checks via decide_pushback, which itself has no
production caller). Signer is bound to the authenticated
caller -- a body signer differing from the caller is rejected
-- and executor/coordinator_approver/qa_tester must be
declared and distinct from the caller; missing field,
self-audit, or forged signer raises ValueError, mapped by
_loop_run to HTTP 400 with no side effects. No new route.
Honest limits are in the helper docstring: declared-identity
enforcement only (task rows store no authoritative executor /
coordinator / QA identity; task executor is a backend, not a
person), and audit-fix reaudit verdicts are out of scope
(the fix poster is not necessarily the re-auditor, so binding
signer to the fix poster would be a mis-binding).

(cherry picked from commit 56dce5165f4d08f38fe1730b9eea3b78f90cb97d)

Reconcile-port: handler_lifecycle.py docstring hunk redirected into cloud/server.py audit-verdict handler (target keeps 6-mixin split; enforcement itself is in audit_loop_wiring.py and applied cleanly).
This commit is contained in:
host committed 2026-11-03 02:49:26 +00:00
1 parent 5db89d1b06
commit 19b9767478
4 files changed
+160 -3

No files matched your search

+26
View File
@@ -64,6 +64,11 @@ def fail_body(source="audit"):
"failures": [{"check": "ci_green", "detail": "x"}],
"passed_items": ["a", "b", "c"],
"contract_version": VERSION,
# A3: signer is the HTTP caller; the other three
# roles must be declared and distinct from it.
"executor": "executor-1",
"coordinator_approver": "coordinator-1",
"qa_tester": "qa-1",
},
}
@@ -79,6 +84,10 @@ def pass_body():
"failures": [],
"passed_items": ["a", "b", "c"],
"contract_version": VERSION,
# A3: see fail_body.
"executor": "executor-1",
"coordinator_approver": "coordinator-1",
"qa_tester": "qa-1",
},
}
@@ -452,6 +461,23 @@ class AuditLoopServerTestCase(unittest.TestCase):
fail_body(), self.contrib_key)
self.assertEqual(status, 403)
def test_verdict_self_audit_rejected_http(self):
# A3 HTTP 层:调用方(owner)自审(声明的 executor 即
# 调用方本人)回 400,且循环状态未落盘(audit-loop 404)。
task = self._completed_task()
body = fail_body()
body["verdict"]["executor"] = self.owner_id
status, _ = self._req(
"POST", "/tasks/%s/audit-verdict" % task["id"],
body, self.owner_key)
self.assertEqual(status, 400)
status, _ = self._req(
"GET", "/tasks/%s/audit-loop?module_id=%s"
"&contract_version=%s"
% (task["id"], MODULE, VERSION),
key=self.owner_key)
self.assertEqual(status, 404)
if __name__ == "__main__":
unittest.main()