code-to10 v6 A1: delete server merge-gate modes, hard only

Server POST /tasks/{id}/merge no longer has record/enforce/off:
hard_mode_from_env and HARD_MODES deleted (not re-defaulted),
handler always evaluates the hard gate, env CLOUD_MERGE_GATE_MODE
and body merge_gate_mode are ignored server-side. The record
'no-evidence passes' and 'unprotected+confirm passes with
merge_gate_risk' server paths are gone with the dead risk audit;
no production caller justified keeping them (only production
caller of the non-hard evaluator was the merge handler itself;
audit_loop_wiring already used the hard gate). cloud/merge_gate
pure functions keep their #209 semantics but are documented as
non-server: they are no longer wired to any endpoint.
Server-level record/enforce/off tests rewritten to the new
behavior (renamed, not deleted silently); pure-function tests
unchanged.

(cherry picked from commit 08e75edbd6d04701965c5db951fa054cfbb75bb0)

Reconcile-port: handler_lifecycle.py hunks redirected into cloud/server.py _merge_task (target keeps 6-mixin split; mixin file does not exist here); handler_support.py kept at HEAD (its A1 hunk is a docstring of _validate_hard_gate_body, ported to server.py copy); test mock paths cloud.handler_lifecycle.* rewritten to cloud.server.*.
This commit is contained in:
host committed 2026-11-04 21:03:32 +00:00
1 parent 593197a4a2
commit e6a1346420
8 files changed
+200 -183

No files matched your search

+33 -50
View File
@@ -99,7 +99,6 @@ from cloud.marker_tools import parse_spawn_defaults as _parse_spawn_defaults
from cloud.evidence_binding import EvidenceNonceStore as _EvidenceNonceStore
from cloud.evidence_binding import binding_presence as _evidence_presence
from cloud.evidence_binding import validate_bound_evidence as _validate_evidence
from cloud.merge_gate import evaluate_merge_gate as _merge_gate_evaluate
from cloud.merge_gate import protection_from_env as _merge_gate_protection
from cloud.audit_wiring import pushback_block_for_task as _pushback_block
from cloud.audit_loop_wiring import build_quota_check as _loop_quota_check
@@ -114,7 +113,6 @@ from cloud.audit_loop_wiring import load_loop_state as _loop_load_state
from cloud.ledger import LedgerStore as _LedgerStore
from cloud.merge_gate_hard import blocked_audit_detail as _gate_blocked_detail
from cloud.merge_gate_hard import evaluate_hard_merge_gate as _merge_gate_hard
from cloud.merge_gate_hard import hard_mode_from_env as _merge_gate_hard_mode
from cloud.tree import DEFAULT_MAX_DEPTH as _TREE_DEFAULT_DEPTH
from cloud.tree import DEFAULT_MAX_ENTRIES as _TREE_DEFAULT_ENTRIES
from cloud.tree import build_tree as _build_tree
@@ -1828,11 +1826,10 @@ def sweep_stale_tasks(db_path, threshold_seconds=_STALE_SWEEP):
def _validate_hard_gate_body(body):
"""硬模式请求体类型校验:畸形字段回 400,不进门禁判定。
"""硬门禁请求体类型校验:畸形字段回 400,不进门禁判定。
只在 hard 模式调用;record/enforce/off 下这些字段按 #209 语义
被忽略,不因此 400。缺失字段不是畸形,是无证据,交门禁判 409。
返回错误串,无错返回 None。
服务端只有 hard 模式,该校验一律适用。缺失字段不是畸形,
是无证据,交门禁判 409。返回错误串,无错返回 None。
"""
for field in ("ci_checks", "required_checks", "approved_exceptions"):
value = body.get(field)
@@ -6847,12 +6844,16 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
必须是 completed(其它状态一律 409,含已 merged 的重复
调用,避免二次合并混淆)。成功后写审计 merge_task。
合并门禁(cloud/merge_gate + cloud/merge_gate_hard,补 v0.3
合并门禁(cloud/merge_gate_hard,唯一服务端门禁,补 v0.3
技能文本检查在服务端缺失的缺口;v0.3 之前本接口只改状态,
不查目录、不查保护):
模式只由环境变量 CLOUD_MERGE_GATE_MODE 决定,请求体里的
merge_gate_mode 在服务端被忽略(调用方不能自行降级):
- hard(默认:环境变量未设或非法值都落到 hard):五项全查,
服务端只有 hard 一种模式:环境变量 CLOUD_MERGE_GATE_MODE
与请求体里的 merge_gate_mode 在服务端一律被忽略(旧值
record/enforce/off 自 A1 起不再是服务端可选项,设了也等同
hard;此前 record 下“无证据放行”、record 下“未保护 + 确认
放行并记 merge_gate_risk”、off 下“只记风险不阻断”三条
服务端路径已随删模式一并消失,不存在受控降级口)。
hard:五项全查,
缺一 409 且不改状态,并写 merge_gate_blocked 审计。五项:
① CI 全绿(ci_status=success + ci_checks 全 success +
required_checks 非空且全 success);② 目录隔离(v0.3 口径,
@@ -6864,12 +6865,7 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
另:branch_protection 必须为 protected;unprotected 即使带
merge_gate_confirm 也阻断,unknown 同样阻断(unknown
不等于 protected)。无证据不放行(fail-closed)。
- record / enforce / off(必须显式设置,属过渡/显式降级):
原样沿用 #209 语义:record 下无证据调用仍按旧接口放行
(只在响应里附带门禁结果),未保护 + 无确认阻断、+ 确认
放行并记 merge_gate_risk;enforce 下未保护一律阻断;
off 下只记风险不阻断。确认只覆盖保护风险,绝不覆盖目录违规。
门禁阻断(任一模式)都写 merge_gate_blocked 审计后才回 409,
门禁阻断都写 merge_gate_blocked 审计后才回 409,
这是第 3 轨自动推回的阻塞依据:推回包取响应 merge_gate 的
blocks 码,不要解析审计文本;本接口不建推回任务、不改状态机。
审计推回接线(cloud/audit_wiring,第 1 项接线缺口):五项
@@ -6878,8 +6874,8 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
merge_gate.blocks 里追加 audit_pushback_blocked 码、响应
里附 audit_pushback 对象(merge_blocked / release_blocked
/ blocked_by="audit_pushback" / round / failed_checks)。
推回阻塞与门禁模式无关:record 模式的显式确认只覆盖
保护风险,绝不覆盖推回阻塞;链读不到(audit_ledger 表
推回阻塞与门禁无关:显式确认在 hard 下不覆盖任何一项,
绝不覆盖推回阻塞;链读不到(audit_ledger 表
不存在)视为无推回状态,其它读链失败一律 fail-closed
409,不放行。注意:本接口没有独立的发布端点,
release_blocked 随响应返回、由未来的发布端点用同一
@@ -6921,36 +6917,27 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
body.get("files_changed"), list):
self._send_json(400, {"error": "files_changed must be a list"})
return
mode = _merge_gate_hard_mode()
protection = body.get("branch_protection") \
or _merge_gate_protection()
if mode == "hard":
hard_err = _validate_hard_gate_body(body)
if hard_err:
self._send_json(400, {"error": hard_err})
return
gate = _merge_gate_hard(
files_changed=body.get("files_changed"),
owned_dir=body.get("owned_dir"),
approved_exceptions=body.get("approved_exceptions") or (),
protection=protection,
ci_status=body.get("ci_status"),
ci_checks=body.get("ci_checks"),
required_checks=body.get("required_checks"),
contract_version=body.get("contract_version"),
contract_frozen=body.get("contract_frozen", False),
contract_expected_version=body.get(
"contract_expected_version"),
coordinator_approved=body.get("coordinator_approved"),
unresolved_review_comments=body.get(
"unresolved_review_comments"))
else:
gate = _merge_gate_evaluate(
files_changed=body.get("files_changed"),
owned_dir=body.get("owned_dir"),
approved_exceptions=body.get("approved_exceptions") or (),
protection=protection, mode=mode,
explicit_confirm=bool(body.get("merge_gate_confirm")))
hard_err = _validate_hard_gate_body(body)
if hard_err:
self._send_json(400, {"error": hard_err})
return
gate = _merge_gate_hard(
files_changed=body.get("files_changed"),
owned_dir=body.get("owned_dir"),
approved_exceptions=body.get("approved_exceptions") or (),
protection=protection,
ci_status=body.get("ci_status"),
ci_checks=body.get("ci_checks"),
required_checks=body.get("required_checks"),
contract_version=body.get("contract_version"),
contract_frozen=body.get("contract_frozen", False),
contract_expected_version=body.get(
"contract_expected_version"),
coordinator_approved=body.get("coordinator_approved"),
unresolved_review_comments=body.get(
"unresolved_review_comments"))
# 审计推回接线:链上阻塞与门禁五项为“与”关系,且与模式
# 无关。读链失败(表不存在除外,见 audit_wiring)时
# fail-closed:不证明无阻塞就不放行。
@@ -7019,10 +7006,6 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
conn.close()
with _auth_store(self.server.db_path) as store:
store.audit(user_id, task["project_id"], "merge_task", task_id)
if gate["protection"] == "unprotected":
store.audit(user_id, task["project_id"],
"merge_gate_risk",
"%s: %s" % (task_id, "; ".join(gate["risks"])))
task["merge_gate"] = gate
self._send_json(200, task)