feat(governance): 新增 P2-143 owner response 預檢
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m28s
CD Pipeline / build-and-deploy (push) Successful in 4m46s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s

This commit is contained in:
Your Name
2026-06-14 14:37:04 +08:00
parent 1a2c9e36c7
commit 755b0a8d30
12 changed files with 1366 additions and 7 deletions

View File

@@ -136,6 +136,9 @@ from src.services.ai_agent_result_capture_release_decision_next_handoff import (
from src.services.ai_agent_result_capture_release_decision_input_prep import (
load_latest_ai_agent_result_capture_release_decision_input_prep,
)
from src.services.ai_agent_result_capture_release_decision_owner_response_preflight import (
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight,
)
from src.services.ai_agent_result_capture_owner_promotion_review import (
load_latest_ai_agent_result_capture_owner_promotion_review,
)
@@ -2424,6 +2427,36 @@ async def get_agent_result_capture_release_decision_input_prep() -> dict[str, An
) from exc
@router.get(
"/agent-result-capture-release-decision-owner-response-preflight",
response_model=dict[str, Any],
summary="取得 AI Agent result capture release decision owner response preflight",
description=(
"讀取最新已提交的 P2-143 release decision owner response preflight"
"此端點只把 P2-141 決策輸入準備包轉成 owner response 預檢與拒收邊界,"
"不把預檢視為正式收件或批准、不核發 release authorization、"
"不寫 reviewer queue、Gateway queue、receipt、result capture、learning 或 PlayBook trust"
"不送 Telegram、不呼叫 Bot API、不讀 secret、不執行 production 寫入。"
),
)
async def get_agent_result_capture_release_decision_owner_response_preflight() -> dict[str, Any]:
"""Return the latest read-only release decision owner-response preflight."""
try:
payload = await asyncio.to_thread(load_latest_ai_agent_result_capture_release_decision_owner_response_preflight)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(exc),
) from exc
except (json.JSONDecodeError, ValueError) as exc:
logger.error("ai_agent_result_capture_release_decision_owner_response_preflight_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AI Agent result capture release decision owner response preflight 無效",
) from exc
@router.get(
"/agent-owner-approved-fixture-dry-run",
response_model=dict[str, Any],

View File

@@ -0,0 +1,376 @@
"""
AI Agent result capture release decision owner-response preflight snapshot.
Loads the latest committed P2-143 owner-response preflight package. This module
only turns P2-141 decision input prep into read-only intake checks and rejection
guards; it never treats preflight as receipt, approval, reviewer queue write,
Gateway queue write, Telegram send, Bot API call, result capture, learning,
PlayBook trust, secret read, production write, or destructive operation.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
from src.services.snapshot_paths import default_evaluations_dir
_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__))
_SNAPSHOT_PATTERN = "ai_agent_result_capture_release_decision_owner_response_preflight_*.json"
_SCHEMA_VERSION = "ai_agent_result_capture_release_decision_owner_response_preflight_v1"
_RUNTIME_AUTHORITY = "result_capture_release_decision_owner_response_preflight_only_no_live_write"
def load_latest_ai_agent_result_capture_release_decision_owner_response_preflight(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed release decision owner-response preflight."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(
f"no AI Agent result capture release decision owner response preflight snapshots found in {directory}"
)
latest = candidates[-1]
with latest.open(encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError(f"{latest}: expected JSON object")
label = str(latest)
_require_schema(payload, label)
_require_prior(payload, label)
_require_truth(payload, label)
_require_response_intake_lanes(payload, label)
_require_validation_checks(payload, label)
_require_rejection_guards(payload, label)
_require_actions(payload, label)
_require_display_redaction(payload, label)
_require_no_forbidden_display_terms(payload, label)
_require_rollup_consistency(payload, label)
return payload
def _require_schema(payload: dict[str, Any], label: str) -> None:
if payload.get("schema_version") != _SCHEMA_VERSION:
raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}")
status = payload.get("program_status") or {}
expected = {
"current_priority": "P2",
"current_task_id": "P2-143",
"next_task_id": "P2-144",
"read_only_mode": True,
"runtime_authority": _RUNTIME_AUTHORITY,
"overall_completion_percent": 100,
}
mismatches = _mismatches(status, expected)
if mismatches:
raise ValueError(f"{label}: program_status mismatch: {mismatches}")
if not status.get("status_note"):
raise ValueError(f"{label}: program_status.status_note is required")
def _require_prior(payload: dict[str, Any], label: str) -> None:
prior = payload.get("prior_decision_input_prep") or {}
expected = {
"schema_version": "ai_agent_result_capture_release_decision_input_prep_v1",
"current_task_id": "P2-141",
"next_task_id": "P2-142",
"decision_input_packet_count": 5,
"missing_input_field_count": 18,
"blocked_input_transition_count": 6,
"operator_action_count": 5,
"approval_required_subtotal": 12,
"blocked_and_critical_subtotal": 12,
}
expected.update(_zero_count_expectations())
mismatches = _mismatches(prior, expected)
if mismatches:
raise ValueError(f"{label}: prior_decision_input_prep mismatch: {mismatches}")
if not prior.get("readiness_note"):
raise ValueError(f"{label}: prior_decision_input_prep.readiness_note is required")
def _require_truth(payload: dict[str, Any], label: str) -> None:
truth = payload.get("owner_response_preflight_truth") or {}
required_true = {
"p2_141_input_prep_loaded",
"p2_142_war_room_baseline_preserved",
"required_owner_fields_declared",
"required_verifier_fields_declared",
"required_rollback_fields_declared",
"required_maintenance_window_fields_declared",
"required_live_apply_fields_declared",
"redaction_contract_loaded",
"preflight_only_mode",
"no_raw_payload_required",
"rejection_policy_active",
}
missing = sorted(field for field in required_true if truth.get(field) is not True)
if missing:
raise ValueError(f"{label}: owner response preflight flags must remain true: {missing}")
required_false = {
"owner_response_received",
"owner_response_accepted",
"owner_response_rejected",
"redacted_payload_ingested",
"owner_release_authorized",
"owner_release_approved",
"owner_review_approved",
"owner_decision_approved",
"verifier_decision_approved",
"maintenance_window_approved",
"rollback_owner_confirmed",
"post_release_verifier_ready",
"final_release_candidate_approved",
"final_release_candidate_passed",
"release_decision_passed",
"release_authorization_granted",
"release_authorization_passed",
"rollback_release_passed",
"live_apply_release_passed",
"writer_apply_enabled",
"execution_apply_enabled",
"receipt_write_enabled",
"reviewer_queue_write_enabled",
"gateway_queue_write_enabled",
"telegram_send_enabled",
"bot_api_call_enabled",
"report_receipt_write_enabled",
"result_capture_write_enabled",
"learning_write_enabled",
"playbook_trust_write_enabled",
"production_write_enabled",
"secret_read_enabled",
"destructive_operation_enabled",
}
unsafe = sorted(field for field in required_false if truth.get(field) is not False)
if unsafe:
raise ValueError(f"{label}: owner response live/send/write flags must remain false: {unsafe}")
non_zero = sorted(field for field in _owner_response_zero_counts() if truth.get(field) != 0)
if non_zero:
raise ValueError(f"{label}: owner response live counters must remain zero: {non_zero}")
if not truth.get("truth_note"):
raise ValueError(f"{label}: owner_response_preflight_truth.truth_note is required")
def _require_response_intake_lanes(payload: dict[str, Any], label: str) -> None:
items = payload.get("response_intake_lanes") or []
_require_ids(
items,
"lane_id",
{
"owner_release_response_preflight",
"verifier_response_preflight",
"rollback_owner_response_preflight",
"maintenance_window_response_preflight",
"live_apply_response_preflight",
},
label,
"response intake lanes",
)
for item in items:
item_id = item.get("lane_id")
expected = {
"current_task_id": "P2-143",
"target_next_task_id": "P2-144",
"intake_status": "waiting_for_external_owner_response",
"accepted": False,
"redacted_payload_ingested": False,
"runtime_write_allowed": False,
"telegram_send_allowed": False,
}
mismatches = _mismatches(item, expected)
if mismatches:
raise ValueError(f"{label}: response intake lane {item_id} mismatch: {mismatches}")
required_fields = item.get("required_fields") or []
if not isinstance(required_fields, list) or not required_fields:
raise ValueError(f"{label}: response intake lane {item_id}.required_fields is required")
if not item.get("source_packet_id"):
raise ValueError(f"{label}: response intake lane {item_id}.source_packet_id is required")
if not item.get("preflight_summary"):
raise ValueError(f"{label}: response intake lane {item_id}.preflight_summary is required")
if not _is_redacted_sha256(item.get("evidence_hash")):
raise ValueError(f"{label}: response intake lane {item_id}.evidence_hash must be redacted sha256")
def _require_validation_checks(payload: dict[str, Any], label: str) -> None:
items = payload.get("intake_validation_checks") or []
_require_count(items, 6, label, "intake validation checks")
for item in items:
item_id = item.get("check_id")
if item.get("status") not in {"waiting_for_owner_response", "not_satisfied", "blocked_by_policy"}:
raise ValueError(f"{label}: intake validation check {item_id} status is invalid")
if item.get("runtime_write_allowed") is not False:
raise ValueError(f"{label}: intake validation check {item_id} must keep runtime_write_allowed=false")
if not item.get("requirement"):
raise ValueError(f"{label}: intake validation check {item_id}.requirement is required")
def _require_rejection_guards(payload: dict[str, Any], label: str) -> None:
items = payload.get("rejection_guards") or []
_require_count(items, 6, label, "rejection guards")
for item in items:
item_id = item.get("guard_id")
if item.get("status") != "active":
raise ValueError(f"{label}: rejection guard {item_id} status must remain active")
if item.get("runtime_write_allowed") is not False:
raise ValueError(f"{label}: rejection guard {item_id} must keep runtime_write_allowed=false")
if not item.get("reason"):
raise ValueError(f"{label}: rejection guard {item_id}.reason is required")
def _require_actions(payload: dict[str, Any], label: str) -> None:
items = payload.get("operator_actions") or []
_require_count(items, 5, label, "operator actions")
for item in items:
item_id = str(item.get("action_id"))
if "p2_139" in item_id or "p2_140" in item_id or "p2_141" in item_id:
raise ValueError(f"{label}: operator action {item_id} must not point back to P2-139/P2-140/P2-141")
if item.get("runtime_write_allowed") is not False:
raise ValueError(f"{label}: operator action {item_id} must keep runtime_write_allowed=false")
if item.get("status") not in {"ready_for_operator_review", "approval_required"}:
raise ValueError(f"{label}: operator action {item_id} status is invalid")
if not item.get("operator_instruction"):
raise ValueError(f"{label}: operator action {item_id}.operator_instruction is required")
def _require_display_redaction(payload: dict[str, Any], label: str) -> None:
contract = payload.get("display_redaction_contract") or {}
expected = {
"redaction_required": True,
"raw_prompt_display_allowed": False,
"private_reasoning_display_allowed": False,
"secret_value_display_allowed": False,
"raw_runtime_payload_display_allowed": False,
"internal_collaboration_content_display_allowed": False,
}
mismatches = _mismatches(contract, expected)
if mismatches:
raise ValueError(f"{label}: display_redaction_contract mismatch: {mismatches}")
if not contract.get("frontend_display_policy"):
raise ValueError(f"{label}: display_redaction_contract.frontend_display_policy is required")
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
rollups = payload.get("rollups") or {}
required_owner_field_count = sum(len(item.get("required_fields") or []) for item in payload.get("response_intake_lanes") or [])
expected = {
"response_intake_lane_count": len(payload.get("response_intake_lanes") or []),
"required_owner_field_count": required_owner_field_count,
"intake_validation_check_count": len(payload.get("intake_validation_checks") or []),
"rejection_guard_count": len(payload.get("rejection_guards") or []),
"operator_action_count": len(payload.get("operator_actions") or []),
"waiting_external_response_count": len(payload.get("response_intake_lanes") or []),
"approval_required_subtotal": 12,
"blocked_and_critical_subtotal": 12,
}
expected.update(_owner_response_zero_counts())
mismatches = _mismatches(rollups, expected)
if mismatches:
raise ValueError(f"{label}: rollups mismatch: {mismatches}")
def _require_no_forbidden_display_terms(payload: Any, label: str) -> None:
forbidden = {
"work_window_transcript",
"internal_collaboration_transcript",
"raw_prompt",
"private_reasoning",
"chain_of_thought",
"authorization_header",
"secret_value",
"工作視窗",
"內部協作",
"原始提示詞",
"私有推理",
"原始 runtime payload",
"raw runtime payload",
"authorization header",
}
found = sorted(term for term in forbidden if _contains_term(payload, term))
if found:
raise ValueError(f"{label}: forbidden display terms leaked: {found}")
def _contains_term(value: Any, term: str) -> bool:
if isinstance(value, str):
return term.lower() in value.lower()
if isinstance(value, dict):
return any(_contains_term(child, term) for child in value.values())
if isinstance(value, list):
return any(_contains_term(child, term) for child in value)
return False
def _owner_response_zero_counts() -> dict[str, int]:
return {
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"owner_response_rejected_count": 0,
"redacted_payload_ingested_count": 0,
**_zero_count_expectations(),
}
def _zero_count_expectations() -> dict[str, int]:
return {
"owner_release_authorized_count": 0,
"owner_release_approved_count": 0,
"owner_review_approved_count": 0,
"owner_decision_approved_count": 0,
"verifier_decision_approved_count": 0,
"maintenance_window_approved_count": 0,
"rollback_owner_confirmed_count": 0,
"post_release_verifier_ready_count": 0,
"final_release_candidate_approved_count": 0,
"final_release_candidate_pass_count": 0,
"release_decision_pass_count": 0,
"release_authorization_granted_count": 0,
"release_authorization_pass_count": 0,
"rollback_release_pass_count": 0,
"live_apply_release_pass_count": 0,
"writer_apply_count": 0,
"execution_apply_count": 0,
"receipt_write_count": 0,
"reviewer_queue_write_count": 0,
"gateway_queue_write_count": 0,
"telegram_send_count": 0,
"bot_api_call_count": 0,
"report_receipt_write_count": 0,
"result_capture_write_count": 0,
"learning_write_count": 0,
"playbook_trust_write_count": 0,
"production_write_count": 0,
"secret_read_count": 0,
"destructive_operation_count": 0,
}
def _require_ids(items: list[dict[str, Any]], field: str, expected_ids: set[str], label: str, group_name: str) -> None:
actual_ids = {str(item.get(field)) for item in items}
if actual_ids != expected_ids:
raise ValueError(f"{label}: {group_name} ids mismatch: expected={sorted(expected_ids)} actual={sorted(actual_ids)}")
def _require_count(items: list[Any], expected: int, label: str, group_name: str) -> None:
if len(items) != expected:
raise ValueError(f"{label}: expected {expected} {group_name}, got {len(items)}")
def _is_redacted_sha256(value: Any) -> bool:
return isinstance(value, str) and re.fullmatch(r"sha256:[0-9a-f]{64}", value) is not None
def _mismatches(payload: dict[str, Any], expected: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {
key: {"expected": expected_value, "actual": payload.get(key)}
for key, expected_value in expected.items()
if payload.get(key) != expected_value
}

View File

@@ -0,0 +1,110 @@
from __future__ import annotations
import copy
import json
from pathlib import Path
import pytest
from src.services.ai_agent_result_capture_release_decision_owner_response_preflight import (
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight,
)
def test_load_latest_release_decision_owner_response_preflight_snapshot() -> None:
snapshot = load_latest_ai_agent_result_capture_release_decision_owner_response_preflight()
assert snapshot["schema_version"] == "ai_agent_result_capture_release_decision_owner_response_preflight_v1"
assert snapshot["program_status"]["current_task_id"] == "P2-143"
assert snapshot["program_status"]["next_task_id"] == "P2-144"
assert snapshot["program_status"]["overall_completion_percent"] == 100
assert snapshot["program_status"]["runtime_authority"] == (
"result_capture_release_decision_owner_response_preflight_only_no_live_write"
)
rollups = snapshot["rollups"]
assert rollups["response_intake_lane_count"] == 5
assert rollups["required_owner_field_count"] == 18
assert rollups["intake_validation_check_count"] == 6
assert rollups["rejection_guard_count"] == 6
assert rollups["operator_action_count"] == 5
assert rollups["waiting_external_response_count"] == 5
assert rollups["owner_response_received_count"] == 0
assert rollups["owner_response_accepted_count"] == 0
assert rollups["redacted_payload_ingested_count"] == 0
assert rollups["reviewer_queue_write_count"] == 0
assert rollups["gateway_queue_write_count"] == 0
assert rollups["telegram_send_count"] == 0
assert rollups["bot_api_call_count"] == 0
assert rollups["production_write_count"] == 0
def test_owner_response_preflight_truth_keeps_all_runtime_gates_closed() -> None:
snapshot = load_latest_ai_agent_result_capture_release_decision_owner_response_preflight()
truth = snapshot["owner_response_preflight_truth"]
assert truth["p2_141_input_prep_loaded"] is True
assert truth["p2_142_war_room_baseline_preserved"] is True
assert truth["preflight_only_mode"] is True
assert truth["rejection_policy_active"] is True
assert truth["owner_response_received"] is False
assert truth["owner_response_accepted"] is False
assert truth["redacted_payload_ingested"] is False
assert truth["release_authorization_granted"] is False
assert truth["reviewer_queue_write_enabled"] is False
assert truth["gateway_queue_write_enabled"] is False
assert truth["telegram_send_enabled"] is False
assert truth["production_write_enabled"] is False
assert truth["secret_read_enabled"] is False
assert truth["destructive_operation_enabled"] is False
def test_rejects_missing_response_intake_lane(tmp_path: Path) -> None:
snapshot = copy.deepcopy(load_latest_ai_agent_result_capture_release_decision_owner_response_preflight())
snapshot["response_intake_lanes"] = snapshot["response_intake_lanes"][:-1]
snapshot["rollups"]["response_intake_lane_count"] = 4
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="response intake lanes ids mismatch"):
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight(tmp_path)
def test_rejects_owner_response_received_drift(tmp_path: Path) -> None:
snapshot = copy.deepcopy(load_latest_ai_agent_result_capture_release_decision_owner_response_preflight())
snapshot["owner_response_preflight_truth"]["owner_response_received"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="flags must remain false"):
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight(tmp_path)
def test_rejects_gateway_write_drift(tmp_path: Path) -> None:
snapshot = copy.deepcopy(load_latest_ai_agent_result_capture_release_decision_owner_response_preflight())
snapshot["rollups"]["gateway_queue_write_count"] = 1
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="rollups mismatch"):
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight(tmp_path)
def test_rejects_forbidden_display_terms(tmp_path: Path) -> None:
snapshot = copy.deepcopy(load_latest_ai_agent_result_capture_release_decision_owner_response_preflight())
snapshot["operator_actions"][0]["operator_instruction"] = "不要顯示 raw_prompt"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="forbidden display terms leaked"):
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight(tmp_path)
def test_rejects_chinese_forbidden_display_terms(tmp_path: Path) -> None:
snapshot = copy.deepcopy(load_latest_ai_agent_result_capture_release_decision_owner_response_preflight())
snapshot["operator_actions"][0]["operator_instruction"] = "不要顯示工作視窗內容"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="forbidden display terms leaked"):
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight(tmp_path)
def _write_snapshot(directory: Path, payload: dict) -> None:
path = directory / "ai_agent_result_capture_release_decision_owner_response_preflight_2099-01-01.json"
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")

View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from fastapi.testclient import TestClient
from src.main import app
def test_release_decision_owner_response_preflight_endpoint() -> None:
client = TestClient(app)
response = client.get("/api/v1/agents/agent-result-capture-release-decision-owner-response-preflight")
assert response.status_code == 200
payload = response.json()
assert payload["schema_version"] == "ai_agent_result_capture_release_decision_owner_response_preflight_v1"
assert payload["program_status"]["current_task_id"] == "P2-143"
assert payload["program_status"]["next_task_id"] == "P2-144"
assert payload["program_status"]["overall_completion_percent"] == 100
assert payload["program_status"]["runtime_authority"] == (
"result_capture_release_decision_owner_response_preflight_only_no_live_write"
)
assert payload["prior_decision_input_prep"]["schema_version"] == (
"ai_agent_result_capture_release_decision_input_prep_v1"
)
assert payload["prior_decision_input_prep"]["next_task_id"] == "P2-142"
assert payload["rollups"]["response_intake_lane_count"] == 5
assert payload["rollups"]["required_owner_field_count"] == 18
assert payload["rollups"]["intake_validation_check_count"] == 6
assert payload["rollups"]["rejection_guard_count"] == 6
assert payload["rollups"]["operator_action_count"] == 5
assert payload["rollups"]["owner_response_received_count"] == 0
assert payload["rollups"]["owner_response_accepted_count"] == 0
assert payload["rollups"]["redacted_payload_ingested_count"] == 0
assert payload["rollups"]["reviewer_queue_write_count"] == 0
assert payload["rollups"]["gateway_queue_write_count"] == 0
assert payload["rollups"]["telegram_send_count"] == 0
assert payload["rollups"]["bot_api_call_count"] == 0
assert payload["rollups"]["production_write_count"] == 0
owner_lane = payload["response_intake_lanes"][0]
assert owner_lane["lane_id"] == "owner_release_response_preflight"
assert owner_lane["required_fields"] == [
"owner_role_team",
"owner_decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"followup_owner",
]