diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 59f552d42..0fea41e16 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -124,6 +124,9 @@ from src.services.ai_agent_result_capture_no_write_readback import ( from src.services.ai_agent_result_capture_promotion_approval_gate import ( load_latest_ai_agent_result_capture_promotion_approval_gate, ) +from src.services.ai_agent_result_capture_write_gate_review import ( + load_latest_ai_agent_result_capture_write_gate_review, +) from src.services.ai_agent_runtime_readback_approval_package import ( load_latest_ai_agent_runtime_readback_approval_package, ) @@ -1688,6 +1691,35 @@ async def get_agent_owner_approved_result_capture_promotion_dry_run() -> dict[st ) from exc +@router.get( + "/agent-result-capture-write-gate-review", + response_model=dict[str, Any], + summary="取得 AI Agent result capture write gate review", + description=( + "讀取最新已提交的 P2-121 result capture write gate review;" + "此端點只回傳 writer gate、approval gate、post-write verifier、blocked live write " + "與 operator handoff,不寫 result capture、learning、PlayBook trust、reviewer queue、" + "Gateway queue,不送 Telegram、不呼叫 Bot API、不讀 canonical runtime target、不讀 secret。" + ), +) +async def get_agent_result_capture_write_gate_review() -> dict[str, Any]: + """Return the latest read-only result capture write gate review package.""" + try: + payload = await asyncio.to_thread(load_latest_ai_agent_result_capture_write_gate_review) + 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_write_gate_review_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="AI Agent result capture write gate review 無效", + ) from exc + + @router.get( "/agent-owner-approved-fixture-dry-run", response_model=dict[str, Any], diff --git a/apps/api/src/services/ai_agent_result_capture_write_gate_review.py b/apps/api/src/services/ai_agent_result_capture_write_gate_review.py new file mode 100644 index 000000000..708b1947d --- /dev/null +++ b/apps/api/src/services/ai_agent_result_capture_write_gate_review.py @@ -0,0 +1,381 @@ +""" +AI Agent result capture write gate review snapshot. + +Loads the latest committed P2-121 result capture write gate review package. +This module validates committed evidence only; it never writes result captures, +writes learning records, updates PlayBook trust, writes reviewer / Gateway +queues, sends Telegram messages, reads canonical runtime targets, reads secrets, +or performs destructive operations. +""" + +from __future__ import annotations + +import json +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_write_gate_review_*.json" +_SCHEMA_VERSION = "ai_agent_result_capture_write_gate_review_v1" +_RUNTIME_AUTHORITY = "result_capture_write_gate_review_only_no_live_write" +_TARGET_WRITE_GATE = "result_capture_write_gate_review" + + +def load_latest_ai_agent_result_capture_write_gate_review( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed result capture write gate review package.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError(f"no AI Agent result capture write gate review 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_write_gate_reviews(payload, label) + _require_approval_gates(payload, label) + _require_verifier_plan(payload, label) + _require_blocked_writes(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-121", + "next_task_id": "P2-122", + "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_owner_approved_promotion_dry_run") or {} + expected = { + "schema_version": "ai_agent_owner_approved_result_capture_promotion_dry_run_v1", + "promotion_dry_run_template_count": 5, + "owner_acceptance_fixture_count": 5, + "dry_run_verifier_check_count": 5, + "blocked_runtime_promotion_count": 5, + "operator_action_count": 5, + "owner_approval_received_count": 0, + "capture_promotion_approved_count": 0, + "dry_run_preview_generated_count": 0, + "result_capture_write_count": 0, + "learning_write_count": 0, + "playbook_trust_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, + } + mismatches = _mismatches(prior, expected) + if mismatches: + raise ValueError(f"{label}: prior_owner_approved_promotion_dry_run mismatch: {mismatches}") + if not prior.get("readiness_note"): + raise ValueError(f"{label}: prior_owner_approved_promotion_dry_run.readiness_note is required") + + +def _require_truth(payload: dict[str, Any], label: str) -> None: + truth = payload.get("write_gate_truth") or {} + required_true = { + "p2_120_dry_run_loaded", + "result_capture_writer_gate_review_ready", + "learning_writer_gate_review_ready", + "playbook_trust_writer_gate_review_ready", + "reviewer_queue_gate_review_ready", + "gateway_queue_gate_review_ready", + "dual_approval_required", + "dry_run_hash_required", + "post_write_verifier_required", + "rollback_required", + "redaction_required", + } + missing = sorted(field for field in required_true if truth.get(field) is not True) + if missing: + raise ValueError(f"{label}: write-gate ready flags must remain true: {missing}") + required_false = { + "result_capture_write_approved", + "learning_write_approved", + "playbook_trust_write_approved", + "reviewer_queue_write_approved", + "gateway_queue_write_approved", + "canonical_runtime_target_read_enabled", + "live_query_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}: live read/send/write flags must remain false: {unsafe}") + zero_counts = { + "owner_approval_received_count", + "dual_approval_received_count", + "dry_run_hash_verified_count", + "post_write_verifier_pass_count", + "rollback_plan_verified_count", + "canonical_runtime_target_read_count", + "live_query_count", + "reviewer_queue_write_count", + "gateway_queue_write_count", + "telegram_send_count", + "bot_api_call_count", + "report_receipt_write_count", + "result_capture_write_count", + "learning_write_count", + "playbook_trust_write_count", + "production_write_count", + "secret_read_count", + "destructive_operation_count", + } + non_zero = sorted(field for field in zero_counts if truth.get(field) != 0) + if non_zero: + raise ValueError(f"{label}: write-gate live counters must remain zero: {non_zero}") + if not truth.get("truth_note"): + raise ValueError(f"{label}: write_gate_truth.truth_note is required") + + +def _require_write_gate_reviews(payload: dict[str, Any], label: str) -> None: + reviews = payload.get("write_gate_reviews") or [] + required = { + "gate_result_capture_writer", + "gate_learning_writer", + "gate_playbook_trust_writer", + "gate_reviewer_queue_writer", + "gate_gateway_queue_writer", + } + review_ids = {review.get("gate_id") for review in reviews} + if review_ids != required: + raise ValueError(f"{label}: write gate reviews must match {sorted(required)}") + for review in reviews: + gate_id = review.get("gate_id") + if review.get("target_write_gate") != _TARGET_WRITE_GATE: + raise ValueError(f"{label}: review {gate_id} must target {_TARGET_WRITE_GATE}") + if review.get("runtime_write_enabled") is not False: + raise ValueError(f"{label}: review {gate_id} must not enable runtime write") + for field in {"dual_approval_required", "dry_run_hash_required", "post_write_verifier_required"}: + if review.get(field) is not True: + raise ValueError(f"{label}: review {gate_id} must require {field}") + if review.get("status") not in {"ready_for_owner_review", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: review {gate_id} status is invalid") + if not review.get("required_before_write") or not review.get("blocked_write_action"): + raise ValueError(f"{label}: review {gate_id} must include required_before_write and blocked_write_action") + if not _is_redacted_sha256(review.get("evidence_hash")): + raise ValueError(f"{label}: review {gate_id} must expose redacted evidence_hash") + + +def _require_approval_gates(payload: dict[str, Any], label: str) -> None: + gates = payload.get("approval_gates") or [] + required = { + "gate_owner_dual_approval", + "gate_dry_run_hash", + "gate_post_write_verifier", + "gate_rollback_reverify", + "gate_redaction_policy", + } + gate_ids = {gate.get("gate_id") for gate in gates} + if gate_ids != required: + raise ValueError(f"{label}: approval gates must match {sorted(required)}") + for gate in gates: + gate_id = gate.get("gate_id") + if gate.get("status") not in {"ready", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: approval gate {gate_id} status is invalid") + if gate.get("runtime_write_allowed") is not False: + raise ValueError(f"{label}: approval gate {gate_id} must not allow runtime write") + if not gate.get("required_evidence") or not gate.get("blocked_runtime_action"): + raise ValueError(f"{label}: approval gate {gate_id} must include required evidence and blocked action") + + +def _require_verifier_plan(payload: dict[str, Any], label: str) -> None: + checks = payload.get("post_write_verifier_plan") or [] + required = { + "verifier_result_capture_receipt", + "verifier_learning_write_boundary", + "verifier_playbook_trust_delta", + "verifier_gateway_queue_noop", + "verifier_rollback_reverify", + } + verifier_ids = {check.get("verifier_id") for check in checks} + if verifier_ids != required: + raise ValueError(f"{label}: post-write verifier plan must match {sorted(required)}") + for check in checks: + verifier_id = check.get("verifier_id") + if check.get("live_execution_enabled") is not False: + raise ValueError(f"{label}: verifier {verifier_id} must not enable live execution") + if check.get("status") not in {"ready", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: verifier {verifier_id} status is invalid") + if not check.get("verifies") or not check.get("failure_if_missing"): + raise ValueError(f"{label}: verifier {verifier_id} must include verifies and failure_if_missing") + if not _is_redacted_sha256(check.get("evidence_hash")): + raise ValueError(f"{label}: verifier {verifier_id} must expose redacted evidence_hash") + + +def _require_blocked_writes(payload: dict[str, Any], label: str) -> None: + blockers = payload.get("blocked_live_writes") or [] + required = { + "blocked_result_capture_write", + "blocked_learning_write", + "blocked_playbook_trust_write", + "blocked_reviewer_queue_write", + "blocked_gateway_queue_write", + "blocked_telegram_send", + } + blocker_ids = {blocker.get("blocker_id") for blocker in blockers} + if blocker_ids != required: + raise ValueError(f"{label}: blocked live writes must match {sorted(required)}") + for blocker in blockers: + blocker_id = blocker.get("blocker_id") + if blocker.get("status") not in {"approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: blocker {blocker_id} status is invalid") + if blocker.get("severity") not in {"high", "critical"}: + raise ValueError(f"{label}: blocker {blocker_id} severity is invalid") + if not blocker.get("blocked_action") or not blocker.get("blocked_until"): + raise ValueError(f"{label}: blocker {blocker_id} must include blocked_action and blocked_until") + if not _is_redacted_sha256(blocker.get("evidence_hash")): + raise ValueError(f"{label}: blocker {blocker_id} must expose redacted evidence_hash") + + +def _require_actions(payload: dict[str, Any], label: str) -> None: + actions = payload.get("operator_actions") or [] + required = { + "review_write_gate_packet", + "collect_dual_approval", + "verify_dry_run_hashes", + "confirm_post_write_verifier_plan", + "promote_to_p2_122", + } + action_ids = {action.get("action_id") for action in actions} + if action_ids != required: + raise ValueError(f"{label}: operator actions must match {sorted(required)}") + for action in actions: + action_id = action.get("action_id") + if action.get("runtime_write_allowed") is not False: + raise ValueError(f"{label}: action {action_id} must not allow runtime write") + if not action.get("operator_instruction"): + raise ValueError(f"{label}: action {action_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 {} + reviews = payload.get("write_gate_reviews") or [] + gates = payload.get("approval_gates") or [] + verifiers = payload.get("post_write_verifier_plan") or [] + blockers = payload.get("blocked_live_writes") or [] + actions = payload.get("operator_actions") or [] + expected = { + "write_gate_review_count": len(reviews), + "approval_gate_count": len(gates), + "post_write_verifier_plan_count": len(verifiers), + "blocked_live_write_count": len(blockers), + "operator_action_count": len(actions), + "approval_required_review_count": sum(1 for item in reviews if item.get("status") == "approval_required"), + "blocked_review_count": sum(1 for item in reviews if item.get("status") == "blocked_by_policy"), + "approval_required_gate_count": sum(1 for item in gates if item.get("status") == "approval_required"), + "blocked_gate_count": sum(1 for item in gates if item.get("status") == "blocked_by_policy"), + "approval_required_verifier_count": sum(1 for item in verifiers if item.get("status") == "approval_required"), + "critical_blocker_count": sum(1 for item in blockers if item.get("severity") == "critical"), + "owner_approval_received_count": 0, + "dual_approval_received_count": 0, + "dry_run_hash_verified_count": 0, + "post_write_verifier_pass_count": 0, + "rollback_plan_verified_count": 0, + "canonical_runtime_target_read_count": 0, + "live_query_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, + } + mismatches = _mismatches(rollups, expected) + if mismatches: + raise ValueError(f"{label}: rollup counts mismatch: {mismatches}") + + +def _require_no_forbidden_display_terms(payload: dict[str, Any], label: str) -> None: + serialized = json.dumps(payload, ensure_ascii=False) + forbidden = { + "work_window_transcript", + "session_id", + "browser_context", + "authorization_header", + "raw Telegram payload", + "private reasoning", + "raw prompt", + "chain-of-thought", + } + hits = sorted(term for term in forbidden if term in serialized) + if hits: + raise ValueError(f"{label}: forbidden display terms present: {hits}") + + +def _is_redacted_sha256(value: Any) -> bool: + if not isinstance(value, str) or not value.startswith("sha256:"): + return False + digest = value.removeprefix("sha256:") + return len(digest) == 64 and all(char in "0123456789abcdef" for char in digest) + + +def _mismatches(payload: dict[str, Any], expected: dict[str, Any]) -> dict[str, dict[str, Any]]: + return { + key: {"expected": value, "actual": payload.get(key)} + for key, value in expected.items() + if payload.get(key) != value + } diff --git a/apps/api/tests/test_ai_agent_result_capture_write_gate_review.py b/apps/api/tests/test_ai_agent_result_capture_write_gate_review.py new file mode 100644 index 000000000..3040441c2 --- /dev/null +++ b/apps/api/tests/test_ai_agent_result_capture_write_gate_review.py @@ -0,0 +1,114 @@ +import copy +import json +from pathlib import Path + +import pytest + +from src.services.ai_agent_result_capture_write_gate_review import ( + load_latest_ai_agent_result_capture_write_gate_review, +) + + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURE = REPO_ROOT / "docs/evaluations/ai_agent_result_capture_write_gate_review_2026-06-13.json" + + +def test_load_latest_ai_agent_result_capture_write_gate_review_snapshot() -> None: + data = load_latest_ai_agent_result_capture_write_gate_review() + + assert data["schema_version"] == "ai_agent_result_capture_write_gate_review_v1" + assert data["program_status"]["current_task_id"] == "P2-121" + assert data["program_status"]["next_task_id"] == "P2-122" + assert data["program_status"]["overall_completion_percent"] == 100 + assert data["program_status"]["read_only_mode"] is True + + rollups = data["rollups"] + assert rollups["write_gate_review_count"] == 5 + assert rollups["approval_gate_count"] == 5 + assert rollups["post_write_verifier_plan_count"] == 5 + assert rollups["blocked_live_write_count"] == 6 + assert rollups["operator_action_count"] == 5 + assert rollups["approval_required_review_count"] == 2 + assert rollups["blocked_review_count"] == 1 + assert rollups["approval_required_gate_count"] == 2 + assert rollups["blocked_gate_count"] == 1 + assert rollups["approval_required_verifier_count"] == 2 + assert rollups["critical_blocker_count"] == 3 + + assert {review["target_write_gate"] for review in data["write_gate_reviews"]} == { + "result_capture_write_gate_review" + } + assert {review["runtime_write_enabled"] for review in data["write_gate_reviews"]} == {False} + + zero_fields = [ + "owner_approval_received_count", + "dual_approval_received_count", + "dry_run_hash_verified_count", + "post_write_verifier_pass_count", + "rollback_plan_verified_count", + "canonical_runtime_target_read_count", + "live_query_count", + "reviewer_queue_write_count", + "gateway_queue_write_count", + "telegram_send_count", + "bot_api_call_count", + "report_receipt_write_count", + "result_capture_write_count", + "learning_write_count", + "playbook_trust_write_count", + "production_write_count", + "secret_read_count", + "destructive_operation_count", + ] + for field in zero_fields: + assert rollups[field] == 0 + + +def test_result_capture_write_gate_review_rejects_runtime_write_enabled(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["write_gate_reviews"][0]["runtime_write_enabled"] = True + target = tmp_path / "ai_agent_result_capture_write_gate_review_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="must not enable runtime write"): + load_latest_ai_agent_result_capture_write_gate_review(tmp_path) + + +def test_result_capture_write_gate_review_rejects_truth_write_flag(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["write_gate_truth"]["result_capture_write_enabled"] = True + target = tmp_path / "ai_agent_result_capture_write_gate_review_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="live read/send/write flags"): + load_latest_ai_agent_result_capture_write_gate_review(tmp_path) + + +def test_result_capture_write_gate_review_rejects_target_drift(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["write_gate_reviews"][0]["target_write_gate"] = "live_result_capture_writer" + target = tmp_path / "ai_agent_result_capture_write_gate_review_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="must target result_capture_write_gate_review"): + load_latest_ai_agent_result_capture_write_gate_review(tmp_path) + + +def test_result_capture_write_gate_review_rejects_rollup_drift(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["rollups"]["write_gate_review_count"] = 4 + target = tmp_path / "ai_agent_result_capture_write_gate_review_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="rollup counts mismatch"): + load_latest_ai_agent_result_capture_write_gate_review(tmp_path) + + +def test_result_capture_write_gate_review_rejects_forbidden_display_terms(tmp_path: Path) -> None: + source = copy.deepcopy(json.loads(FIXTURE.read_text(encoding="utf-8"))) + source["operator_actions"][0]["operator_instruction"] = "do not expose session_id" + target = tmp_path / "ai_agent_result_capture_write_gate_review_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="forbidden display terms"): + load_latest_ai_agent_result_capture_write_gate_review(tmp_path) diff --git a/apps/api/tests/test_ai_agent_result_capture_write_gate_review_api.py b/apps/api/tests/test_ai_agent_result_capture_write_gate_review_api.py new file mode 100644 index 000000000..6f412d4ee --- /dev/null +++ b/apps/api/tests/test_ai_agent_result_capture_write_gate_review_api.py @@ -0,0 +1,41 @@ +import pytest +from httpx import ASGITransport, AsyncClient + +from src.main import app + + +@pytest.mark.asyncio +async def test_get_agent_result_capture_write_gate_review_api() -> None: + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/api/v1/agents/agent-result-capture-write-gate-review") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "ai_agent_result_capture_write_gate_review_v1" + assert data["program_status"]["current_task_id"] == "P2-121" + assert data["program_status"]["next_task_id"] == "P2-122" + assert data["program_status"]["overall_completion_percent"] == 100 + + rollups = data["rollups"] + assert rollups["write_gate_review_count"] == 5 + assert rollups["approval_gate_count"] == 5 + assert rollups["post_write_verifier_plan_count"] == 5 + assert rollups["blocked_live_write_count"] == 6 + assert rollups["operator_action_count"] == 5 + assert rollups["critical_blocker_count"] == 3 + assert rollups["owner_approval_received_count"] == 0 + assert rollups["dual_approval_received_count"] == 0 + assert rollups["dry_run_hash_verified_count"] == 0 + assert rollups["post_write_verifier_pass_count"] == 0 + assert rollups["result_capture_write_count"] == 0 + assert rollups["learning_write_count"] == 0 + assert rollups["playbook_trust_write_count"] == 0 + assert rollups["gateway_queue_write_count"] == 0 + assert rollups["telegram_send_count"] == 0 + assert rollups["production_write_count"] == 0 + + assert {review["target_write_gate"] for review in data["write_gate_reviews"]} == { + "result_capture_write_gate_review" + } + assert {review["runtime_write_enabled"] for review in data["write_gate_reviews"]} == {False} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 6cfa1ff1e..f56ebea2a 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -5709,6 +5709,39 @@ "blockedAction": "blocked:{value}", "runtimeWriteAllowed": "runtime write={value}" } + }, + "resultCaptureWriteGateReview": { + "title": "P2-121 result capture write gate review", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 owner-approved promotion dry-run", + "truthTitle": "Result capture write gate truth", + "metrics": { + "overall": "完成度", + "reviews": "Writer gates", + "approvals": "Approval gates", + "verifiers": "Post-write verifiers", + "blockers": "Blocked writes", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "hashVerified": "Hash verified", + "liveWrites": "Live writer / send" + }, + "flags": { + "dryRunLoaded": "dry-run loaded={value}", + "dualApproval": "dual approval required={value}", + "verifierRequired": "post-write verifier required={value}" + }, + "labels": { + "targetGate": "目標 gate:{value}", + "resultWrites": "result write={value}", + "learningWrites": "learning write={value}", + "trustWrites": "trust write={value}", + "writer": "writer:{value}", + "runtimeWriteEnabled": "runtime write={value}", + "blockedAction": "blocked:{value}", + "runtimeWriteAllowed": "runtime write={value}" + } } } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 6cfa1ff1e..f56ebea2a 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -5709,6 +5709,39 @@ "blockedAction": "blocked:{value}", "runtimeWriteAllowed": "runtime write={value}" } + }, + "resultCaptureWriteGateReview": { + "title": "P2-121 result capture write gate review", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 owner-approved promotion dry-run", + "truthTitle": "Result capture write gate truth", + "metrics": { + "overall": "完成度", + "reviews": "Writer gates", + "approvals": "Approval gates", + "verifiers": "Post-write verifiers", + "blockers": "Blocked writes", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "hashVerified": "Hash verified", + "liveWrites": "Live writer / send" + }, + "flags": { + "dryRunLoaded": "dry-run loaded={value}", + "dualApproval": "dual approval required={value}", + "verifierRequired": "post-write verifier required={value}" + }, + "labels": { + "targetGate": "目標 gate:{value}", + "resultWrites": "result write={value}", + "learningWrites": "learning write={value}", + "trustWrites": "trust write={value}", + "writer": "writer:{value}", + "runtimeWriteEnabled": "runtime write={value}", + "blockedAction": "blocked:{value}", + "runtimeWriteAllowed": "runtime write={value}" + } } } }, diff --git a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx index ab97a5197..0207fdfe3 100644 --- a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx +++ b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx @@ -68,6 +68,7 @@ import { type AiAgentResultCaptureNoWriteReadbackSnapshot, type AiAgentOwnerApprovedResultCapturePromotionDryRunSnapshot, type AiAgentResultCapturePromotionApprovalGateSnapshot, + type AiAgentResultCaptureWriteGateReviewSnapshot, type AiAgentOwnerApprovedFixturePromotionGateSnapshot, type AiAgentRuntimeWorkerShadowGateSnapshot, type AiAgentRuntimeVerifierEvidenceReviewSnapshot, @@ -458,6 +459,7 @@ export function AutomationInventoryTab() { const [resultCaptureNoWriteReadback, setResultCaptureNoWriteReadback] = useState(null) const [resultCapturePromotionApprovalGate, setResultCapturePromotionApprovalGate] = useState(null) const [ownerApprovedResultCapturePromotionDryRun, setOwnerApprovedResultCapturePromotionDryRun] = useState(null) + const [resultCaptureWriteGateReview, setResultCaptureWriteGateReview] = useState(null) const [reportTruthActionabilityReview, setReportTruthActionabilityReview] = useState(null) const [ownerDryRunPackage, setOwnerDryRunPackage] = useState(null) const [hostStatefulInventory, setHostStatefulInventory] = useState(null) @@ -515,6 +517,7 @@ export function AutomationInventoryTab() { apiClient.getAiAgentResultCaptureNoWriteReadback(), apiClient.getAiAgentResultCapturePromotionApprovalGate(), apiClient.getAiAgentOwnerApprovedResultCapturePromotionDryRun(), + apiClient.getAiAgentResultCaptureWriteGateReview(), apiClient.getAiAgentReportTruthActionabilityReview(), apiClient.getAiAgentOwnerApprovedFixtureDryRun(), apiClient.getAiAgentHostStatefulVersionInventory(), @@ -571,6 +574,7 @@ export function AutomationInventoryTab() { resultCaptureNoWriteReadbackResult, resultCapturePromotionApprovalGateResult, ownerApprovedResultCapturePromotionDryRunResult, + resultCaptureWriteGateReviewResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -624,6 +628,7 @@ export function AutomationInventoryTab() { setResultCaptureNoWriteReadback(resultCaptureNoWriteReadbackResult.status === 'fulfilled' ? resultCaptureNoWriteReadbackResult.value : null) setResultCapturePromotionApprovalGate(resultCapturePromotionApprovalGateResult.status === 'fulfilled' ? resultCapturePromotionApprovalGateResult.value : null) setOwnerApprovedResultCapturePromotionDryRun(ownerApprovedResultCapturePromotionDryRunResult.status === 'fulfilled' ? ownerApprovedResultCapturePromotionDryRunResult.value : null) + setResultCaptureWriteGateReview(resultCaptureWriteGateReviewResult.status === 'fulfilled' ? resultCaptureWriteGateReviewResult.value : null) setReportTruthActionabilityReview(reportTruthActionabilityReviewResult.status === 'fulfilled' ? reportTruthActionabilityReviewResult.value : null) setOwnerDryRunPackage(ownerDryRunPackageResult.status === 'fulfilled' ? ownerDryRunPackageResult.value : null) setHostStatefulInventory(hostStatefulInventoryResult.status === 'fulfilled' ? hostStatefulInventoryResult.value : null) @@ -675,6 +680,7 @@ export function AutomationInventoryTab() { resultCaptureNoWriteReadbackResult, resultCapturePromotionApprovalGateResult, ownerApprovedResultCapturePromotionDryRunResult, + resultCaptureWriteGateReviewResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -1917,7 +1923,7 @@ export function AutomationInventoryTab() { ) } - if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !proactiveOperations || !interactionLearningProof || !liveReadModelGate || !redisDryRunGate || !learningWritebackPackage || !telegramReceiptPackage || !ownerApprovedLearningDryRun || !runtimeWriteGateReview || !postWriteVerifierPackage || !runtimeVerifierEvidenceReview || !reportAutomationReview || !reportStatusBoard || !reportRuntimeReadiness || !reportRuntimeDryRun || !reportRuntimeFixtureReadback || !runtimeWorkerShadowGate || !operationPermissionModel || !candidateOperationDryRunEvidence || !taskResultAuditTrail || !matchedPlaybookLearningGap || !criticReviewerResultCapture || !ownerApprovedResultCaptureDryRun || !ownerApprovedResultCaptureReadback || !runtimeReadbackApprovalPackage || !runtimeReadbackImplementationReview || !reportLiveDeliveryApprovalPackage || !runtimeReadbackFixtureApproval || !runtimeReadbackPromotionGate || !ownerApprovedFixturePromotionGate || !canonicalRuntimeReadbackOwnerAcceptance || !failureReceiptNoSendReplay || !reviewerQueueNoWriteReadback || !resultCaptureNoWriteReadback || !resultCapturePromotionApprovalGate || !ownerApprovedResultCapturePromotionDryRun || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { + if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !proactiveOperations || !interactionLearningProof || !liveReadModelGate || !redisDryRunGate || !learningWritebackPackage || !telegramReceiptPackage || !ownerApprovedLearningDryRun || !runtimeWriteGateReview || !postWriteVerifierPackage || !runtimeVerifierEvidenceReview || !reportAutomationReview || !reportStatusBoard || !reportRuntimeReadiness || !reportRuntimeDryRun || !reportRuntimeFixtureReadback || !runtimeWorkerShadowGate || !operationPermissionModel || !candidateOperationDryRunEvidence || !taskResultAuditTrail || !matchedPlaybookLearningGap || !criticReviewerResultCapture || !ownerApprovedResultCaptureDryRun || !ownerApprovedResultCaptureReadback || !runtimeReadbackApprovalPackage || !runtimeReadbackImplementationReview || !reportLiveDeliveryApprovalPackage || !runtimeReadbackFixtureApproval || !runtimeReadbackPromotionGate || !ownerApprovedFixturePromotionGate || !canonicalRuntimeReadbackOwnerAcceptance || !failureReceiptNoSendReplay || !reviewerQueueNoWriteReadback || !resultCaptureNoWriteReadback || !resultCapturePromotionApprovalGate || !ownerApprovedResultCapturePromotionDryRun || !resultCaptureWriteGateReview || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { return (
@@ -2631,6 +2637,35 @@ export function AutomationInventoryTab() { + ownerApprovedResultCapturePromotionDryRun.rollups.playbook_trust_write_count + ownerApprovedResultCapturePromotionDryRun.rollups.production_write_count ) + const resultCaptureWriteGateOverall = resultCaptureWriteGateReview.program_status.overall_completion_percent + const resultCaptureWriteGateReviews = resultCaptureWriteGateReview.rollups.write_gate_review_count + const resultCaptureWriteGateApprovals = resultCaptureWriteGateReview.rollups.approval_gate_count + const resultCaptureWriteGateVerifiers = resultCaptureWriteGateReview.rollups.post_write_verifier_plan_count + const resultCaptureWriteGateBlockers = resultCaptureWriteGateReview.rollups.blocked_live_write_count + const resultCaptureWriteGateActions = resultCaptureWriteGateReview.rollups.operator_action_count + const resultCaptureWriteGateApprovalRequired = ( + resultCaptureWriteGateReview.rollups.approval_required_review_count + + resultCaptureWriteGateReview.rollups.approval_required_gate_count + + resultCaptureWriteGateReview.rollups.approval_required_verifier_count + ) + const resultCaptureWriteGateBlocked = ( + resultCaptureWriteGateReview.rollups.blocked_review_count + + resultCaptureWriteGateReview.rollups.blocked_gate_count + + resultCaptureWriteGateReview.rollups.critical_blocker_count + ) + const resultCaptureWriteGateLiveWrites = ( + resultCaptureWriteGateReview.rollups.canonical_runtime_target_read_count + + resultCaptureWriteGateReview.rollups.live_query_count + + resultCaptureWriteGateReview.rollups.reviewer_queue_write_count + + resultCaptureWriteGateReview.rollups.gateway_queue_write_count + + resultCaptureWriteGateReview.rollups.telegram_send_count + + resultCaptureWriteGateReview.rollups.bot_api_call_count + + resultCaptureWriteGateReview.rollups.report_receipt_write_count + + resultCaptureWriteGateReview.rollups.result_capture_write_count + + resultCaptureWriteGateReview.rollups.learning_write_count + + resultCaptureWriteGateReview.rollups.playbook_trust_write_count + + resultCaptureWriteGateReview.rollups.production_write_count + ) const reportTruthOverall = reportTruthActionabilityReview.program_status.overall_completion_percent const reportTruthFindings = reportTruthActionabilityReview.rollups.zero_signal_finding_count const reportTruthCritical = reportTruthActionabilityReview.rollups.critical_finding_count @@ -6916,6 +6951,147 @@ export function AutomationInventoryTab() {
+
+
+
+ + + {t('resultCaptureWriteGateReview.title')} + +
+ +
+ +
+ } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
+ +
+
+ {t('resultCaptureWriteGateReview.priorTitle')} + + {resultCaptureWriteGateReview.prior_owner_approved_promotion_dry_run.readiness_note} + +
+ + + +
+
+ +
+ {t('resultCaptureWriteGateReview.truthTitle')} + + {resultCaptureWriteGateReview.write_gate_truth.truth_note} + +
+ + + + + + + +
+
+
+ +
+ {resultCaptureWriteGateReview.write_gate_reviews.slice(0, 5).map(review => ( +
+
+ + {review.display_name} + + +
+ + {review.required_before_write} + +
+ + + +
+
+ ))} +
+ +
+ {resultCaptureWriteGateReview.approval_gates.slice(0, 5).map(gate => ( +
+
+ + {gate.display_name} + + +
+ + {gate.required_evidence} + +
+ + + +
+
+ ))} +
+ +
+ {resultCaptureWriteGateReview.blocked_live_writes.slice(0, 3).map(blocker => ( +
+
+ + {blocker.display_name} + + +
+ + {blocker.blocked_until} + +
+ + +
+
+ ))} +
+ +
+ {resultCaptureWriteGateReview.operator_actions.slice(0, 3).map(action => ( +
+
+ + {action.display_name} + + +
+ + {action.operator_instruction} + + +
+ ))} +
+
+
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 32f30cc66..c49a35d22 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -500,6 +500,11 @@ export const apiClient = { return handleResponse(res) }, + async getAiAgentResultCaptureWriteGateReview() { + const res = await fetch(`${API_BASE_URL}/agents/agent-result-capture-write-gate-review`) + return handleResponse(res) + }, + async getAiAgentOwnerApprovedFixtureDryRun() { const res = await fetch(`${API_BASE_URL}/agents/agent-owner-approved-fixture-dry-run`) return handleResponse(res) @@ -5628,6 +5633,181 @@ export interface AiAgentOwnerApprovedResultCapturePromotionDryRunSnapshot { } } +export interface AiAgentResultCaptureWriteGateReviewSnapshot { + schema_version: 'ai_agent_result_capture_write_gate_review_v1' + generated_at: string + program_status: { + overall_completion_percent: number + current_priority: 'P0' | 'P1' | 'P2' | 'P3' + current_task_id: 'P2-121' + next_task_id: 'P2-122' + read_only_mode: true + runtime_authority: 'result_capture_write_gate_review_only_no_live_write' + status_note: string + } + source_refs: string[] + prior_owner_approved_promotion_dry_run: { + schema_version: string + promotion_dry_run_template_count: number + owner_acceptance_fixture_count: number + dry_run_verifier_check_count: number + blocked_runtime_promotion_count: number + operator_action_count: number + owner_approval_received_count: number + capture_promotion_approved_count: number + dry_run_preview_generated_count: number + result_capture_write_count: number + learning_write_count: number + playbook_trust_write_count: number + reviewer_queue_write_count: number + gateway_queue_write_count: number + telegram_send_count: number + bot_api_call_count: number + report_receipt_write_count: number + readiness_note: string + } + write_gate_truth: { + p2_120_dry_run_loaded: boolean + result_capture_writer_gate_review_ready: boolean + learning_writer_gate_review_ready: boolean + playbook_trust_writer_gate_review_ready: boolean + reviewer_queue_gate_review_ready: boolean + gateway_queue_gate_review_ready: boolean + dual_approval_required: boolean + dry_run_hash_required: boolean + post_write_verifier_required: boolean + rollback_required: boolean + redaction_required: boolean + result_capture_write_approved: boolean + learning_write_approved: boolean + playbook_trust_write_approved: boolean + reviewer_queue_write_approved: boolean + gateway_queue_write_approved: boolean + canonical_runtime_target_read_enabled: boolean + live_query_enabled: boolean + reviewer_queue_write_enabled: boolean + gateway_queue_write_enabled: boolean + telegram_send_enabled: boolean + bot_api_call_enabled: boolean + report_receipt_write_enabled: boolean + result_capture_write_enabled: boolean + learning_write_enabled: boolean + playbook_trust_write_enabled: boolean + production_write_enabled: boolean + secret_read_enabled: boolean + destructive_operation_enabled: boolean + owner_approval_received_count: number + dual_approval_received_count: number + dry_run_hash_verified_count: number + post_write_verifier_pass_count: number + rollback_plan_verified_count: number + canonical_runtime_target_read_count: number + live_query_count: number + reviewer_queue_write_count: number + gateway_queue_write_count: number + telegram_send_count: number + bot_api_call_count: number + report_receipt_write_count: number + result_capture_write_count: number + learning_write_count: number + playbook_trust_write_count: number + production_write_count: number + secret_read_count: number + destructive_operation_count: number + truth_note: string + } + write_gate_reviews: Array<{ + gate_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + target_writer: string + target_write_gate: string + status: 'ready_for_owner_review' | 'approval_required' | 'blocked_by_policy' + required_before_write: string + blocked_write_action: string + dual_approval_required: true + dry_run_hash_required: true + post_write_verifier_required: true + runtime_write_enabled: false + evidence_hash: string + }> + approval_gates: Array<{ + gate_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready' | 'approval_required' | 'blocked_by_policy' + required_evidence: string + blocked_runtime_action: string + runtime_write_allowed: false + }> + post_write_verifier_plan: Array<{ + verifier_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready' | 'approval_required' | 'blocked_by_policy' + verifies: string + failure_if_missing: string + live_execution_enabled: false + evidence_hash: string + }> + blocked_live_writes: Array<{ + blocker_id: string + display_name: string + severity: 'high' | 'critical' + status: 'approval_required' | 'blocked_by_policy' + blocked_action: string + blocked_until: string + evidence_hash: string + }> + operator_actions: Array<{ + action_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + operator_instruction: string + runtime_write_allowed: false + }> + display_redaction_contract: { + 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 + frontend_display_policy: string + } + rollups: { + write_gate_review_count: number + approval_gate_count: number + post_write_verifier_plan_count: number + blocked_live_write_count: number + operator_action_count: number + approval_required_review_count: number + blocked_review_count: number + approval_required_gate_count: number + blocked_gate_count: number + approval_required_verifier_count: number + critical_blocker_count: number + owner_approval_received_count: number + dual_approval_received_count: number + dry_run_hash_verified_count: number + post_write_verifier_pass_count: number + rollback_plan_verified_count: number + canonical_runtime_target_read_count: number + live_query_count: number + reviewer_queue_write_count: number + gateway_queue_write_count: number + telegram_send_count: number + bot_api_call_count: number + report_receipt_write_count: number + result_capture_write_count: number + learning_write_count: number + playbook_trust_write_count: number + production_write_count: number + secret_read_count: number + destructive_operation_count: number + } +} + export interface AiAgentOwnerApprovedFixtureDryRunSnapshot { schema_version: 'ai_agent_owner_approved_fixture_dry_run_v1' generated_at: string diff --git a/docs/evaluations/ai_agent_result_capture_write_gate_review_2026-06-13.json b/docs/evaluations/ai_agent_result_capture_write_gate_review_2026-06-13.json new file mode 100644 index 000000000..1edb0bd23 --- /dev/null +++ b/docs/evaluations/ai_agent_result_capture_write_gate_review_2026-06-13.json @@ -0,0 +1,398 @@ +{ + "schema_version": "ai_agent_result_capture_write_gate_review_v1", + "generated_at": "2026-06-13T23:45:00+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P2", + "current_task_id": "P2-121", + "next_task_id": "P2-122", + "read_only_mode": true, + "runtime_authority": "result_capture_write_gate_review_only_no_live_write", + "status_note": "P2-121 只建立 result capture write gate review;不得寫 result capture、learning、PlayBook trust、Gateway queue、Telegram 或 production。" + }, + "source_refs": [ + "docs/evaluations/ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json", + "docs/evaluations/ai_agent_result_capture_promotion_approval_gate_2026-06-13.json", + "docs/evaluations/ai_agent_result_capture_no_write_readback_2026-06-13.json", + "docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md#41-建立-owner-approved-result-capture-promotion-dry-run" + ], + "prior_owner_approved_promotion_dry_run": { + "schema_version": "ai_agent_owner_approved_result_capture_promotion_dry_run_v1", + "promotion_dry_run_template_count": 5, + "owner_acceptance_fixture_count": 5, + "dry_run_verifier_check_count": 5, + "blocked_runtime_promotion_count": 5, + "operator_action_count": 5, + "owner_approval_received_count": 0, + "capture_promotion_approved_count": 0, + "dry_run_preview_generated_count": 0, + "result_capture_write_count": 0, + "learning_write_count": 0, + "playbook_trust_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, + "readiness_note": "P2-120 已正式驗證 owner-approved promotion dry-run;P2-121 只整理真正寫入前的 gate review,仍不開任何 writer。" + }, + "write_gate_truth": { + "p2_120_dry_run_loaded": true, + "result_capture_writer_gate_review_ready": true, + "learning_writer_gate_review_ready": true, + "playbook_trust_writer_gate_review_ready": true, + "reviewer_queue_gate_review_ready": true, + "gateway_queue_gate_review_ready": true, + "dual_approval_required": true, + "dry_run_hash_required": true, + "post_write_verifier_required": true, + "rollback_required": true, + "redaction_required": true, + "result_capture_write_approved": false, + "learning_write_approved": false, + "playbook_trust_write_approved": false, + "reviewer_queue_write_approved": false, + "gateway_queue_write_approved": false, + "canonical_runtime_target_read_enabled": false, + "live_query_enabled": false, + "reviewer_queue_write_enabled": false, + "gateway_queue_write_enabled": false, + "telegram_send_enabled": false, + "bot_api_call_enabled": false, + "report_receipt_write_enabled": false, + "result_capture_write_enabled": false, + "learning_write_enabled": false, + "playbook_trust_write_enabled": false, + "production_write_enabled": false, + "secret_read_enabled": false, + "destructive_operation_enabled": false, + "owner_approval_received_count": 0, + "dual_approval_received_count": 0, + "dry_run_hash_verified_count": 0, + "post_write_verifier_pass_count": 0, + "rollback_plan_verified_count": 0, + "canonical_runtime_target_read_count": 0, + "live_query_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, + "truth_note": "真正寫入前需要雙重批准、dry-run hash、post-write verifier、rollback 與 redaction;目前所有 live writer 與 send 仍為 0。" + }, + "write_gate_reviews": [ + { + "gate_id": "gate_result_capture_writer", + "display_name": "Result capture writer gate", + "owner_agent": "openclaw", + "target_writer": "result_capture_writer", + "target_write_gate": "result_capture_write_gate_review", + "status": "ready_for_owner_review", + "required_before_write": "owner decision、decision reason、incident scope、dry-run hash、post-write verifier 與 rollback owner 必須齊備。", + "blocked_write_action": "result_capture_write", + "dual_approval_required": true, + "dry_run_hash_required": true, + "post_write_verifier_required": true, + "runtime_write_enabled": false, + "evidence_hash": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + }, + { + "gate_id": "gate_learning_writer", + "display_name": "Learning writer gate", + "owner_agent": "hermes", + "target_writer": "learning_writer", + "target_write_gate": "result_capture_write_gate_review", + "status": "approval_required", + "required_before_write": "KM owner 必須確認 learning 範圍、來源引用、stale 風險與撤回條件。", + "blocked_write_action": "learning_write", + "dual_approval_required": true, + "dry_run_hash_required": true, + "post_write_verifier_required": true, + "runtime_write_enabled": false, + "evidence_hash": "sha256:a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2" + }, + { + "gate_id": "gate_playbook_trust_writer", + "display_name": "PlayBook trust writer gate", + "owner_agent": "nemotron", + "target_writer": "playbook_trust_writer", + "target_write_gate": "result_capture_write_gate_review", + "status": "blocked_by_policy", + "required_before_write": "PlayBook 成功條件、失敗回寫、版本 owner 與 verifier 成功證據必須存在。", + "blocked_write_action": "playbook_trust_write", + "dual_approval_required": true, + "dry_run_hash_required": true, + "post_write_verifier_required": true, + "runtime_write_enabled": false, + "evidence_hash": "sha256:a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3" + }, + { + "gate_id": "gate_reviewer_queue_writer", + "display_name": "Reviewer queue writer gate", + "owner_agent": "openclaw", + "target_writer": "reviewer_queue_writer", + "target_write_gate": "result_capture_write_gate_review", + "status": "approval_required", + "required_before_write": "reviewer queue target、dedupe key、owner route 與 SRE 戰情室通知邊界必須先審查。", + "blocked_write_action": "reviewer_queue_write", + "dual_approval_required": true, + "dry_run_hash_required": true, + "post_write_verifier_required": true, + "runtime_write_enabled": false, + "evidence_hash": "sha256:a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4" + }, + { + "gate_id": "gate_gateway_queue_writer", + "display_name": "Gateway queue writer gate", + "owner_agent": "nemotron", + "target_writer": "gateway_queue_writer", + "target_write_gate": "result_capture_write_gate_review", + "status": "ready_for_owner_review", + "required_before_write": "Gateway queue envelope、idempotency key、retry / dead-letter 與 no-secret policy 必須完成。", + "blocked_write_action": "gateway_queue_write", + "dual_approval_required": true, + "dry_run_hash_required": true, + "post_write_verifier_required": true, + "runtime_write_enabled": false, + "evidence_hash": "sha256:a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5" + } + ], + "approval_gates": [ + { + "gate_id": "gate_owner_dual_approval", + "display_name": "Owner dual approval", + "owner_agent": "openclaw", + "status": "approval_required", + "required_evidence": "result owner 與 runtime owner 兩方批准紀錄。", + "blocked_runtime_action": "all_writer_enablement", + "runtime_write_allowed": false + }, + { + "gate_id": "gate_dry_run_hash", + "display_name": "Dry-run hash verification", + "owner_agent": "nemotron", + "status": "ready", + "required_evidence": "dry-run preview hash 與 committed snapshot hash 對齊。", + "blocked_runtime_action": "result_capture_write", + "runtime_write_allowed": false + }, + { + "gate_id": "gate_post_write_verifier", + "display_name": "Post-write verifier plan", + "owner_agent": "nemotron", + "status": "approval_required", + "required_evidence": "寫入後 verifier、失敗升級、rollback 準則。", + "blocked_runtime_action": "learning_and_trust_write", + "runtime_write_allowed": false + }, + { + "gate_id": "gate_rollback_reverify", + "display_name": "Rollback reverify owner", + "owner_agent": "hermes", + "status": "blocked_by_policy", + "required_evidence": "rollback owner、reverify window 與手動接手 SOP。", + "blocked_runtime_action": "playbook_trust_write", + "runtime_write_allowed": false + }, + { + "gate_id": "gate_redaction_policy", + "display_name": "Redaction policy", + "owner_agent": "hermes", + "status": "ready", + "required_evidence": "前端只顯示紅線後欄位,不顯示 raw payload 或內部協作內容。", + "blocked_runtime_action": "telegram_send", + "runtime_write_allowed": false + } + ], + "post_write_verifier_plan": [ + { + "verifier_id": "verifier_result_capture_receipt", + "display_name": "Result capture receipt verifier", + "owner_agent": "openclaw", + "status": "ready", + "verifies": "result capture receipt、incident link、owner decision 與 no-duplicate key。", + "failure_if_missing": "缺 receipt 時不得將 result capture 視為成功。", + "live_execution_enabled": false, + "evidence_hash": "sha256:b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1" + }, + { + "verifier_id": "verifier_learning_write_boundary", + "display_name": "Learning write boundary verifier", + "owner_agent": "hermes", + "status": "approval_required", + "verifies": "learning write 只接收 owner-reviewed 條目,不寫高影響 KM。", + "failure_if_missing": "缺 owner review 時必須退回 KM task。", + "live_execution_enabled": false, + "evidence_hash": "sha256:b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2" + }, + { + "verifier_id": "verifier_playbook_trust_delta", + "display_name": "PlayBook trust delta verifier", + "owner_agent": "nemotron", + "status": "blocked_by_policy", + "verifies": "PlayBook trust delta 有成功/失敗依據與 rollback 條件。", + "failure_if_missing": "缺 trust delta 證據時不得更新 trust score。", + "live_execution_enabled": false, + "evidence_hash": "sha256:b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3" + }, + { + "verifier_id": "verifier_gateway_queue_noop", + "display_name": "Gateway queue no-op verifier", + "owner_agent": "openclaw", + "status": "approval_required", + "verifies": "Gateway queue write 維持 0;只顯示 no-op queue envelope。", + "failure_if_missing": "若 queue write 不為 0,必須立即標記 unauthorized write。", + "live_execution_enabled": false, + "evidence_hash": "sha256:b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4" + }, + { + "verifier_id": "verifier_rollback_reverify", + "display_name": "Rollback reverify verifier", + "owner_agent": "nemotron", + "status": "ready", + "verifies": "rollback / reverify plan 存在且可由人工接手。", + "failure_if_missing": "缺 rollback 計畫時不得 promote writer。", + "live_execution_enabled": false, + "evidence_hash": "sha256:b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5" + } + ], + "blocked_live_writes": [ + { + "blocker_id": "blocked_result_capture_write", + "display_name": "Result capture write blocked", + "severity": "high", + "status": "approval_required", + "blocked_action": "result_capture_write", + "blocked_until": "dual approval and post-write verifier pass exist", + "evidence_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1" + }, + { + "blocker_id": "blocked_learning_write", + "display_name": "Learning write blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "learning_write", + "blocked_until": "KM owner accepts scoped learning draft", + "evidence_hash": "sha256:c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2" + }, + { + "blocker_id": "blocked_playbook_trust_write", + "display_name": "PlayBook trust write blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "playbook_trust_write", + "blocked_until": "verifier success and rollback criteria exist", + "evidence_hash": "sha256:c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3" + }, + { + "blocker_id": "blocked_reviewer_queue_write", + "display_name": "Reviewer queue write blocked", + "severity": "high", + "status": "approval_required", + "blocked_action": "reviewer_queue_write", + "blocked_until": "dedupe and owner route are approved", + "evidence_hash": "sha256:c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4" + }, + { + "blocker_id": "blocked_gateway_queue_write", + "display_name": "Gateway queue write blocked", + "severity": "high", + "status": "approval_required", + "blocked_action": "gateway_queue_write", + "blocked_until": "idempotency and dead-letter policy are approved", + "evidence_hash": "sha256:c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5" + }, + { + "blocker_id": "blocked_telegram_send", + "display_name": "Telegram send blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "telegram_send", + "blocked_until": "AwoooI SRE 戰情室 delivery receipt gate is approved", + "evidence_hash": "sha256:c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6" + } + ], + "operator_actions": [ + { + "action_id": "review_write_gate_packet", + "display_name": "Review write gate packet", + "owner_agent": "openclaw", + "operator_instruction": "審查 5 個 writer gate 是否具備 owner、scope、dry-run hash、verifier 與 rollback 條件。", + "runtime_write_allowed": false + }, + { + "action_id": "collect_dual_approval", + "display_name": "Collect dual approval", + "owner_agent": "hermes", + "operator_instruction": "收齊 result owner 與 runtime owner 的批准紀錄;未收齊不得啟用 writer。", + "runtime_write_allowed": false + }, + { + "action_id": "verify_dry_run_hashes", + "display_name": "Verify dry-run hashes", + "owner_agent": "nemotron", + "operator_instruction": "比對 committed snapshot hash 與 dry-run preview hash;不一致即退回 P2-120。", + "runtime_write_allowed": false + }, + { + "action_id": "confirm_post_write_verifier_plan", + "display_name": "Confirm post-write verifier plan", + "owner_agent": "nemotron", + "operator_instruction": "確認寫入後 verifier 與 rollback owner 存在,並列出失敗時人工接手路徑。", + "runtime_write_allowed": false + }, + { + "action_id": "promote_to_p2_122", + "display_name": "Promote to P2-122", + "owner_agent": "openclaw", + "operator_instruction": "只在 P2-121 gate review 全部通過後,推進到 P2-122;本步不執行 live write。", + "runtime_write_allowed": false + } + ], + "display_redaction_contract": { + "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, + "frontend_display_policy": "前端只顯示 gate、owner、狀態、阻擋動作與紅線後 evidence hash;不得顯示內部協作內容、secret、原始 payload 或私有推理。" + }, + "rollups": { + "write_gate_review_count": 5, + "approval_gate_count": 5, + "post_write_verifier_plan_count": 5, + "blocked_live_write_count": 6, + "operator_action_count": 5, + "approval_required_review_count": 2, + "blocked_review_count": 1, + "approval_required_gate_count": 2, + "blocked_gate_count": 1, + "approval_required_verifier_count": 2, + "critical_blocker_count": 3, + "owner_approval_received_count": 0, + "dual_approval_received_count": 0, + "dry_run_hash_verified_count": 0, + "post_write_verifier_pass_count": 0, + "rollback_plan_verified_count": 0, + "canonical_runtime_target_read_count": 0, + "live_query_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 + } +} diff --git a/docs/schemas/ai_agent_result_capture_write_gate_review_v1.schema.json b/docs/schemas/ai_agent_result_capture_write_gate_review_v1.schema.json new file mode 100644 index 000000000..3cc398de6 --- /dev/null +++ b/docs/schemas/ai_agent_result_capture_write_gate_review_v1.schema.json @@ -0,0 +1,188 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.local/schemas/ai_agent_result_capture_write_gate_review_v1.schema.json", + "title": "AI Agent Result Capture Write Gate Review", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "generated_at", + "program_status", + "prior_owner_approved_promotion_dry_run", + "write_gate_truth", + "write_gate_reviews", + "approval_gates", + "post_write_verifier_plan", + "blocked_live_writes", + "operator_actions", + "display_redaction_contract", + "rollups" + ], + "properties": { + "schema_version": { + "const": "ai_agent_result_capture_write_gate_review_v1" + }, + "program_status": { + "type": "object", + "required": [ + "overall_completion_percent", + "current_priority", + "current_task_id", + "next_task_id", + "read_only_mode", + "runtime_authority", + "status_note" + ], + "properties": { + "overall_completion_percent": { + "const": 100 + }, + "current_priority": { + "const": "P2" + }, + "current_task_id": { + "const": "P2-121" + }, + "next_task_id": { + "const": "P2-122" + }, + "read_only_mode": { + "const": true + }, + "runtime_authority": { + "const": "result_capture_write_gate_review_only_no_live_write" + } + } + }, + "write_gate_truth": { + "type": "object", + "required": [ + "dual_approval_required", + "dry_run_hash_required", + "post_write_verifier_required", + "result_capture_write_enabled", + "learning_write_enabled", + "playbook_trust_write_enabled", + "gateway_queue_write_enabled" + ], + "properties": { + "dual_approval_required": { + "const": true + }, + "dry_run_hash_required": { + "const": true + }, + "post_write_verifier_required": { + "const": true + }, + "result_capture_write_enabled": { + "const": false + }, + "learning_write_enabled": { + "const": false + }, + "playbook_trust_write_enabled": { + "const": false + }, + "gateway_queue_write_enabled": { + "const": false + }, + "telegram_send_enabled": { + "const": false + }, + "production_write_enabled": { + "const": false + } + } + }, + "write_gate_reviews": { + "type": "array", + "minItems": 5, + "maxItems": 5 + }, + "approval_gates": { + "type": "array", + "minItems": 5, + "maxItems": 5 + }, + "post_write_verifier_plan": { + "type": "array", + "minItems": 5, + "maxItems": 5 + }, + "blocked_live_writes": { + "type": "array", + "minItems": 6, + "maxItems": 6 + }, + "operator_actions": { + "type": "array", + "minItems": 5, + "maxItems": 5 + }, + "display_redaction_contract": { + "type": "object", + "properties": { + "redaction_required": { + "const": true + }, + "secret_value_display_allowed": { + "const": false + }, + "internal_collaboration_content_display_allowed": { + "const": false + } + } + }, + "rollups": { + "type": "object", + "required": [ + "write_gate_review_count", + "approval_gate_count", + "post_write_verifier_plan_count", + "blocked_live_write_count", + "operator_action_count", + "result_capture_write_count", + "learning_write_count", + "playbook_trust_write_count", + "gateway_queue_write_count", + "telegram_send_count" + ], + "properties": { + "write_gate_review_count": { + "const": 5 + }, + "approval_gate_count": { + "const": 5 + }, + "post_write_verifier_plan_count": { + "const": 5 + }, + "blocked_live_write_count": { + "const": 6 + }, + "operator_action_count": { + "const": 5 + }, + "result_capture_write_count": { + "const": 0 + }, + "learning_write_count": { + "const": 0 + }, + "playbook_trust_write_count": { + "const": 0 + }, + "gateway_queue_write_count": { + "const": 0 + }, + "telegram_send_count": { + "const": 0 + }, + "production_write_count": { + "const": 0 + } + } + } + } +}