Skip to content

Commit dd3415b

Browse files
authored
Merge pull request #140 from QuantStrategyLab/codex/reconciliation-recovery-authority
fix: require human approval for broker recovery
2 parents c5b73ba + dbb7632 commit dd3415b

3 files changed

Lines changed: 45 additions & 2 deletions

File tree

docs/reconciliation_baseline_dual_review.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,16 @@
2828

2929
## 私有控制面职责
3030

31-
审计通过仍不等于恢复实盘。只有私有、受权限保护的控制面可以:
31+
审计通过仍不等于恢复实盘。恢复现有券商下单能力在自动化权限策略中属于高风险
32+
`broker_or_order_execution` 操作;审计输出会明确标记
33+
`requires_human_recovery_approval=true`。统一管理站点必须让操作者确认一次,且只有
34+
私有、受权限保护的控制面才可以:
3235

3336
1. 验证候选和每份来源收据来自受信任的运行时,且内容地址未变;
3437
2. 验证双审结果为通过,并绑定同一个候选摘要;
3538
3. 在同一 runtime target 上原子写入五个预期状态摘要并转换到 `ACTIVE_LKG`
3639
4. 保存不可变审计记录、操作人/服务身份、时间和目标版本,以便回滚和复核。
3740

3841
任一条件不满足,控制面必须保持 `RECONCILE_ONLY`。AIAuditBridge 不持有券商密钥,
39-
也不拥有下单或直接修改 Cloud Run runtime target 的权限。
42+
也不拥有下单或直接修改 Cloud Run runtime target 的权限;双 AI 审计不能绕过这条
43+
人工确认边界。

scripts/run_dual_review_pipeline.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
from pathlib import Path
1010
from typing import Any
1111

12+
from service.automation_authority import (
13+
CLASS_BROKER_OR_ORDER_EXECUTION,
14+
evaluate_automation_authority,
15+
)
16+
from service.autonomy import ACTION_ORDER
1217
from service.dual_review import VERDICT_DISAGREEMENT, VERDICT_FAIL, VERDICT_UNAVAILABLE
1318
from service.dual_review_dispatch import dispatch_dual_review_result
1419
from service.dual_review_orchestrator import orchestrate_from_payload
@@ -135,6 +140,16 @@ def run_pipeline(
135140
return {"ok": False, "error": "orchestration_failed", "payload": payload}
136141

137142
result = outcome.to_dict()
143+
if trigger == "reconciliation_baseline":
144+
# A matching, dual-reviewed candidate proves only that a legacy state
145+
# is ready for a human recovery decision. The authority policy treats
146+
# restoring broker execution as high risk and must never auto-apply it.
147+
result["recovery_authority"] = evaluate_automation_authority(
148+
["broker_reconciliation_baseline"],
149+
trusted_metadata={"change_class": CLASS_BROKER_OR_ORDER_EXECUTION},
150+
proposed_action=ACTION_ORDER,
151+
)
152+
result["requires_human_recovery_approval"] = True
138153
if outcome.outcome == VERDICT_UNAVAILABLE:
139154
result["skipped"] = ["reviewers_unavailable"]
140155
result["degraded"] = True

tests/test_dual_review_pipeline.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,30 @@ def test_all_reviewers_unavailable_degrades_with_durable_alert(self, mock_orches
8585
def test_disagreement_is_a_hard_block(self) -> None:
8686
self.assertEqual(_exit_code({"ok": True, "outcome": "disagreement"}), 2)
8787

88+
@patch("scripts.run_dual_review_pipeline.orchestrate_from_payload")
89+
def test_reconciliation_baseline_requires_human_recovery_approval(self, mock_orchestrate) -> None:
90+
from service.dual_review import DualReviewTrigger
91+
from service.dual_review_orchestrator import DualReviewResult
92+
93+
candidate_sha256 = "a" * 64
94+
mock_orchestrate.return_value = DualReviewResult(
95+
trigger=DualReviewTrigger.RECONCILIATION_BASELINE,
96+
strategy_profile="soxl_soxx_trend_income",
97+
primary_review={"verdict": "approve", "confidence": 0.95},
98+
outcome="pass",
99+
evidence_binding_sha256=candidate_sha256,
100+
)
101+
result = run_pipeline(
102+
trigger="reconciliation_baseline",
103+
strategy_profile="soxl_soxx_trend_income",
104+
context={"reconciliation_candidate_sha256": candidate_sha256},
105+
primary_review={"verdict": "approve", "confidence": 0.95},
106+
)
107+
108+
self.assertTrue(result["requires_human_recovery_approval"])
109+
self.assertTrue(result["recovery_authority"]["human_review_required"])
110+
self.assertEqual(result["recovery_authority"]["final_action"], "escalate")
111+
88112
@patch.dict("os.environ", {"DUAL_REVIEW_GATE_SKIP": "1"}, clear=False)
89113
def test_from_evidence_cli_without_trigger_flags(self) -> None:
90114
with tempfile.TemporaryDirectory() as tmp:

0 commit comments

Comments
 (0)