From cdc6fe873781228bd8d5255e6cb67ae2f0e6dd28 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 14 Jun 2026 00:39:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(governance):=20=E6=96=B0=E5=A2=9E=20result?= =?UTF-8?q?=20capture=20writer=20dry-run=20readback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/api/v1/agents.py | 33 ++ ..._result_capture_writer_dry_run_readback.py | 390 +++++++++++++++++ ..._result_capture_writer_dry_run_readback.py | 121 ++++++ ...ult_capture_writer_dry_run_readback_api.py | 49 +++ apps/web/messages/en.json | 33 ++ apps/web/messages/zh-TW.json | 33 ++ .../tabs/automation-inventory-tab.tsx | 146 ++++++- apps/web/src/lib/api-client.ts | 185 ++++++++ ...re_writer_dry_run_readback_2026-06-14.json | 394 ++++++++++++++++++ ...ure_writer_dry_run_readback_v1.schema.json | 222 ++++++++++ 10 files changed, 1605 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/services/ai_agent_result_capture_writer_dry_run_readback.py create mode 100644 apps/api/tests/test_ai_agent_result_capture_writer_dry_run_readback.py create mode 100644 apps/api/tests/test_ai_agent_result_capture_writer_dry_run_readback_api.py create mode 100644 docs/evaluations/ai_agent_result_capture_writer_dry_run_readback_2026-06-14.json create mode 100644 docs/schemas/ai_agent_result_capture_writer_dry_run_readback_v1.schema.json diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 7d1be964..3ed2b017 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -133,6 +133,9 @@ from src.services.ai_agent_result_capture_writer_implementation_review import ( from src.services.ai_agent_result_capture_writer_dry_run_fixture import ( load_latest_ai_agent_result_capture_writer_dry_run_fixture, ) +from src.services.ai_agent_result_capture_writer_dry_run_readback import ( + load_latest_ai_agent_result_capture_writer_dry_run_readback, +) from src.services.ai_agent_runtime_readback_approval_package import ( load_latest_ai_agent_runtime_readback_approval_package, ) @@ -1785,6 +1788,36 @@ async def get_agent_result_capture_writer_dry_run_fixture() -> dict[str, Any]: ) from exc +@router.get( + "/agent-result-capture-writer-dry-run-readback", + response_model=dict[str, Any], + summary="取得 AI Agent result capture writer dry-run readback", + description=( + "讀取最新已提交的 P2-124 result capture writer dry-run readback;" + "此端點只回傳 dry-run readback、receipt verifier、promotion readiness、" + "blocked promotion write 與 operator handoff,不套用 writer、不執行 dry-run、" + "不寫 receipt、不寫 result capture、learning、PlayBook trust、reviewer queue、" + "Gateway queue,不送 Telegram、不呼叫 Bot API、不讀 secret。" + ), +) +async def get_agent_result_capture_writer_dry_run_readback() -> dict[str, Any]: + """Return the latest read-only result capture writer dry-run readback package.""" + try: + payload = await asyncio.to_thread(load_latest_ai_agent_result_capture_writer_dry_run_readback) + 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_writer_dry_run_readback_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="AI Agent result capture writer dry-run readback 無效", + ) 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_writer_dry_run_readback.py b/apps/api/src/services/ai_agent_result_capture_writer_dry_run_readback.py new file mode 100644 index 00000000..aef3a1a7 --- /dev/null +++ b/apps/api/src/services/ai_agent_result_capture_writer_dry_run_readback.py @@ -0,0 +1,390 @@ +""" +AI Agent result capture writer dry-run readback snapshot. + +Loads the latest committed P2-124 dry-run readback / promotion readiness package. +This module validates committed evidence only; it never applies writers, executes +dry-runs, writes receipts, 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_writer_dry_run_readback_*.json" +_SCHEMA_VERSION = "ai_agent_result_capture_writer_dry_run_readback_v1" +_RUNTIME_AUTHORITY = "result_capture_writer_dry_run_readback_only_no_live_write" + + +def load_latest_ai_agent_result_capture_writer_dry_run_readback( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed result capture writer dry-run readback 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 writer dry-run readback 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_readbacks(payload, label) + _require_receipt_verifiers(payload, label) + _require_promotion_gates(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-124", + "next_task_id": "P2-125", + "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_result_capture_writer_dry_run_fixture") or {} + expected = { + "schema_version": "ai_agent_result_capture_writer_dry_run_fixture_v1", + "writer_dry_run_fixture_count": 5, + "receipt_preview_count": 5, + "idempotency_replay_check_count": 5, + "rollback_rehearsal_count": 5, + "blocked_runtime_write_count": 6, + "operator_action_count": 5, + "writer_apply_count": 0, + "dry_run_execution_count": 0, + "receipt_write_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_result_capture_writer_dry_run_fixture mismatch: {mismatches}") + if not prior.get("readiness_note"): + raise ValueError(f"{label}: prior_result_capture_writer_dry_run_fixture.readiness_note is required") + + +def _require_truth(payload: dict[str, Any], label: str) -> None: + truth = payload.get("readback_truth") or {} + required_true = { + "p2_123_fixture_loaded", + "dry_run_readback_ready", + "receipt_verifier_ready", + "promotion_readiness_ready", + "fixture_only_mode", + "owner_approval_required", + "dual_approval_required", + "dry_run_hash_required", + "receipt_verifier_required", + "promotion_owner_required", + "rollback_owner_required", + } + missing = sorted(field for field in required_true if truth.get(field) is not True) + if missing: + raise ValueError(f"{label}: readback ready flags must remain true: {missing}") + required_false = { + "writer_apply_enabled", + "dry_run_execution_enabled", + "receipt_write_enabled", + "promotion_apply_enabled", + "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", + "receipt_verifier_pass_count", + "writer_apply_count", + "dry_run_execution_count", + "receipt_write_count", + "promotion_apply_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}: dry-run readback live counters must remain zero: {non_zero}") + if not truth.get("truth_note"): + raise ValueError(f"{label}: readback_truth.truth_note is required") + + +def _require_readbacks(payload: dict[str, Any], label: str) -> None: + cards = payload.get("dry_run_readback_cards") or [] + required = { + "readback_result_capture_writer", + "readback_learning_writer", + "readback_playbook_trust_writer", + "readback_reviewer_queue_writer", + "readback_gateway_queue_writer", + } + ids = {card.get("readback_id") for card in cards} + if ids != required: + raise ValueError(f"{label}: dry-run readback cards must match {sorted(required)}") + for card in cards: + readback_id = card.get("readback_id") + if card.get("readback_only") is not True: + raise ValueError(f"{label}: readback {readback_id} must remain readback-only") + if card.get("runtime_write_enabled") is not False: + raise ValueError(f"{label}: readback {readback_id} must not enable runtime write") + if card.get("dry_run_execution_enabled") is not False: + raise ValueError(f"{label}: readback {readback_id} must not enable dry-run execution") + if card.get("status") not in {"ready_for_owner_review", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: readback {readback_id} status is invalid") + for field in {"source_fixture", "receipt_preview", "promotion_candidate", "operator_note"}: + if not card.get(field): + raise ValueError(f"{label}: readback {readback_id} missing {field}") + if not _is_redacted_sha256(card.get("evidence_hash")): + raise ValueError(f"{label}: readback {readback_id} must expose redacted evidence_hash") + + +def _require_receipt_verifiers(payload: dict[str, Any], label: str) -> None: + checks = payload.get("receipt_verifier_checks") or [] + required = { + "verify_result_capture_receipt", + "verify_learning_receipt", + "verify_playbook_trust_receipt", + "verify_reviewer_queue_receipt", + "verify_gateway_queue_receipt", + } + ids = {check.get("verifier_id") for check in checks} + if ids != required: + raise ValueError(f"{label}: receipt verifier checks must match {sorted(required)}") + for check in checks: + verifier_id = check.get("verifier_id") + if check.get("verifier_mode") != "no_write_receipt_verifier": + raise ValueError(f"{label}: verifier {verifier_id} must remain no-write") + if check.get("live_receipt_enabled") is not False: + raise ValueError(f"{label}: verifier {verifier_id} must not enable live receipt") + if check.get("required_before_promotion") is not True: + raise ValueError(f"{label}: verifier {verifier_id} must be required before promotion") + 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("failure_if_missing"): + raise ValueError(f"{label}: verifier {verifier_id} failure_if_missing is required") + + +def _require_promotion_gates(payload: dict[str, Any], label: str) -> None: + gates = payload.get("promotion_readiness_gates") or [] + required = { + "gate_writer_apply_owner_approval", + "gate_dry_run_hash", + "gate_receipt_verifier", + "gate_rollback_owner", + "gate_production_write_block", + } + ids = {gate.get("gate_id") for gate in gates} + if ids != required: + raise ValueError(f"{label}: promotion readiness gates must match {sorted(required)}") + for gate in gates: + gate_id = gate.get("gate_id") + if gate.get("promotion_allowed") is not False: + raise ValueError(f"{label}: promotion gate {gate_id} must not allow promotion") + if gate.get("status") not in {"ready_for_owner_review", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: promotion gate {gate_id} status is invalid") + if not gate.get("required_evidence"): + raise ValueError(f"{label}: promotion gate {gate_id} required_evidence is required") + + +def _require_blocked_writes(payload: dict[str, Any], label: str) -> None: + blockers = payload.get("blocked_promotion_writes") or [] + required = { + "blocked_writer_apply", + "blocked_dry_run_execution", + "blocked_receipt_write", + "blocked_result_capture_write", + "blocked_gateway_queue_write", + "blocked_telegram_send", + } + ids = {blocker.get("blocker_id") for blocker in blockers} + if ids != required: + raise ValueError(f"{label}: blocked promotion 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_dry_run_readback", + "verify_receipt_verifier", + "score_promotion_readiness", + "prepare_p2_125_owner_packet", + "hold_runtime_write_gate", + } + ids = {action.get("action_id") for action in actions} + if 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 {} + readbacks = payload.get("dry_run_readback_cards") or [] + verifiers = payload.get("receipt_verifier_checks") or [] + gates = payload.get("promotion_readiness_gates") or [] + blockers = payload.get("blocked_promotion_writes") or [] + actions = payload.get("operator_actions") or [] + expected = { + "dry_run_readback_card_count": len(readbacks), + "receipt_verifier_check_count": len(verifiers), + "promotion_readiness_gate_count": len(gates), + "blocked_promotion_write_count": len(blockers), + "operator_action_count": len(actions), + "approval_required_readback_count": sum(1 for item in readbacks if item.get("status") == "approval_required"), + "blocked_readback_count": sum(1 for item in readbacks if item.get("status") == "blocked_by_policy"), + "approval_required_verifier_count": sum(1 for item in verifiers if item.get("status") == "approval_required"), + "blocked_verifier_count": sum(1 for item in verifiers 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"), + "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, + "receipt_verifier_pass_count": 0, + "writer_apply_count": 0, + "dry_run_execution_count": 0, + "receipt_write_count": 0, + "promotion_apply_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_writer_dry_run_readback.py b/apps/api/tests/test_ai_agent_result_capture_writer_dry_run_readback.py new file mode 100644 index 00000000..7957045d --- /dev/null +++ b/apps/api/tests/test_ai_agent_result_capture_writer_dry_run_readback.py @@ -0,0 +1,121 @@ +import copy +import json +import os +from pathlib import Path + +import pytest + +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") + +from src.services.ai_agent_result_capture_writer_dry_run_readback import ( + load_latest_ai_agent_result_capture_writer_dry_run_readback, +) + + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURE = REPO_ROOT / "docs/evaluations/ai_agent_result_capture_writer_dry_run_readback_2026-06-14.json" + + +def test_load_latest_ai_agent_result_capture_writer_dry_run_readback_snapshot() -> None: + data = load_latest_ai_agent_result_capture_writer_dry_run_readback() + + assert data["schema_version"] == "ai_agent_result_capture_writer_dry_run_readback_v1" + assert data["program_status"]["current_task_id"] == "P2-124" + assert data["program_status"]["next_task_id"] == "P2-125" + assert data["program_status"]["overall_completion_percent"] == 100 + assert data["program_status"]["read_only_mode"] is True + + rollups = data["rollups"] + assert rollups["dry_run_readback_card_count"] == 5 + assert rollups["receipt_verifier_check_count"] == 5 + assert rollups["promotion_readiness_gate_count"] == 5 + assert rollups["blocked_promotion_write_count"] == 6 + assert rollups["operator_action_count"] == 5 + assert rollups["approval_required_readback_count"] == 2 + assert rollups["blocked_readback_count"] == 1 + assert rollups["approval_required_verifier_count"] == 2 + assert rollups["blocked_verifier_count"] == 1 + assert rollups["approval_required_gate_count"] == 2 + assert rollups["blocked_gate_count"] == 1 + assert rollups["critical_blocker_count"] == 4 + + assert {card["readback_only"] for card in data["dry_run_readback_cards"]} == {True} + assert {card["runtime_write_enabled"] for card in data["dry_run_readback_cards"]} == {False} + assert {card["dry_run_execution_enabled"] for card in data["dry_run_readback_cards"]} == {False} + assert {gate["promotion_allowed"] for gate in data["promotion_readiness_gates"]} == {False} + + zero_fields = [ + "owner_approval_received_count", + "dual_approval_received_count", + "dry_run_hash_verified_count", + "receipt_verifier_pass_count", + "writer_apply_count", + "dry_run_execution_count", + "receipt_write_count", + "promotion_apply_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_writer_dry_run_readback_rejects_runtime_write_enabled(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["dry_run_readback_cards"][0]["runtime_write_enabled"] = True + target = tmp_path / "ai_agent_result_capture_writer_dry_run_readback_2026-06-14.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_writer_dry_run_readback(tmp_path) + + +def test_result_capture_writer_dry_run_readback_rejects_truth_write_flag(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["readback_truth"]["result_capture_write_enabled"] = True + target = tmp_path / "ai_agent_result_capture_writer_dry_run_readback_2026-06-14.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_writer_dry_run_readback(tmp_path) + + +def test_result_capture_writer_dry_run_readback_rejects_promotion_allowed(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["promotion_readiness_gates"][0]["promotion_allowed"] = True + target = tmp_path / "ai_agent_result_capture_writer_dry_run_readback_2026-06-14.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="must not allow promotion"): + load_latest_ai_agent_result_capture_writer_dry_run_readback(tmp_path) + + +def test_result_capture_writer_dry_run_readback_rejects_rollup_drift(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["rollups"]["dry_run_readback_card_count"] = 4 + target = tmp_path / "ai_agent_result_capture_writer_dry_run_readback_2026-06-14.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="rollup counts mismatch"): + load_latest_ai_agent_result_capture_writer_dry_run_readback(tmp_path) + + +def test_result_capture_writer_dry_run_readback_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_writer_dry_run_readback_2026-06-14.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="forbidden display terms"): + load_latest_ai_agent_result_capture_writer_dry_run_readback(tmp_path) diff --git a/apps/api/tests/test_ai_agent_result_capture_writer_dry_run_readback_api.py b/apps/api/tests/test_ai_agent_result_capture_writer_dry_run_readback_api.py new file mode 100644 index 00000000..39ecd20c --- /dev/null +++ b/apps/api/tests/test_ai_agent_result_capture_writer_dry_run_readback_api.py @@ -0,0 +1,49 @@ +import os + +import pytest +from httpx import ASGITransport, AsyncClient + +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") + +from src.main import app + + +@pytest.mark.asyncio +async def test_get_agent_result_capture_writer_dry_run_readback_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-writer-dry-run-readback") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "ai_agent_result_capture_writer_dry_run_readback_v1" + assert data["program_status"]["current_task_id"] == "P2-124" + assert data["program_status"]["next_task_id"] == "P2-125" + assert data["program_status"]["overall_completion_percent"] == 100 + + rollups = data["rollups"] + assert rollups["dry_run_readback_card_count"] == 5 + assert rollups["receipt_verifier_check_count"] == 5 + assert rollups["promotion_readiness_gate_count"] == 5 + assert rollups["blocked_promotion_write_count"] == 6 + assert rollups["operator_action_count"] == 5 + assert rollups["critical_blocker_count"] == 4 + assert rollups["owner_approval_received_count"] == 0 + assert rollups["dual_approval_received_count"] == 0 + assert rollups["dry_run_hash_verified_count"] == 0 + assert rollups["receipt_verifier_pass_count"] == 0 + assert rollups["writer_apply_count"] == 0 + assert rollups["dry_run_execution_count"] == 0 + assert rollups["receipt_write_count"] == 0 + assert rollups["promotion_apply_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 {card["readback_only"] for card in data["dry_run_readback_cards"]} == {True} + assert {card["runtime_write_enabled"] for card in data["dry_run_readback_cards"]} == {False} + assert {card["dry_run_execution_enabled"] for card in data["dry_run_readback_cards"]} == {False} + assert {gate["promotion_allowed"] for gate in data["promotion_readiness_gates"]} == {False} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 64fb233f..b9ea5ffb 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -5779,6 +5779,39 @@ "liveExecutionEnabled": "正式執行={value}" } }, + "resultCaptureWriterDryRunReadback": { + "title": "P2-124 結果寫入器 dry-run readback", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 dry-run fixture", + "truthTitle": "dry-run readback 真相", + "metrics": { + "overall": "完成度", + "readbacks": "Dry-run readbacks", + "verifiers": "Receipt verifiers", + "promotionGates": "Promotion gates", + "blockers": "已阻擋提升", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "verifierPass": "Verifier 通過", + "liveWrites": "正式寫入 / 發送" + }, + "flags": { + "fixtureLoaded": "已載入 fixture={value}", + "receiptVerifier": "需要 receipt verifier={value}", + "promotionOwner": "需要提升 owner={value}" + }, + "labels": { + "writerApply": "writer 套用={value}", + "dryRunExecution": "dry-run 執行={value}", + "receiptWrite": "receipt 寫入={value}", + "gatewayWrites": "Gateway 寫入={value}", + "promotionCandidate": "提升候選:{value}", + "readbackOnly": "readback-only={value}", + "runtimeWriteEnabled": "正式寫入={value}", + "promotionAllowed": "允許提升={value}" + } + }, "resultCaptureWriteGateReview": { "title": "P2-121 result capture write gate review", "source": "產生 {generated};目前 {current};下一步 {next}", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 64fb233f..b9ea5ffb 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -5779,6 +5779,39 @@ "liveExecutionEnabled": "正式執行={value}" } }, + "resultCaptureWriterDryRunReadback": { + "title": "P2-124 結果寫入器 dry-run readback", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 dry-run fixture", + "truthTitle": "dry-run readback 真相", + "metrics": { + "overall": "完成度", + "readbacks": "Dry-run readbacks", + "verifiers": "Receipt verifiers", + "promotionGates": "Promotion gates", + "blockers": "已阻擋提升", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "verifierPass": "Verifier 通過", + "liveWrites": "正式寫入 / 發送" + }, + "flags": { + "fixtureLoaded": "已載入 fixture={value}", + "receiptVerifier": "需要 receipt verifier={value}", + "promotionOwner": "需要提升 owner={value}" + }, + "labels": { + "writerApply": "writer 套用={value}", + "dryRunExecution": "dry-run 執行={value}", + "receiptWrite": "receipt 寫入={value}", + "gatewayWrites": "Gateway 寫入={value}", + "promotionCandidate": "提升候選:{value}", + "readbackOnly": "readback-only={value}", + "runtimeWriteEnabled": "正式寫入={value}", + "promotionAllowed": "允許提升={value}" + } + }, "resultCaptureWriteGateReview": { "title": "P2-121 result capture write gate review", "source": "產生 {generated};目前 {current};下一步 {next}", 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 714f6e44..d4110e84 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 @@ -71,6 +71,7 @@ import { type AiAgentResultCapturePromotionApprovalGateSnapshot, type AiAgentResultCaptureWriteGateReviewSnapshot, type AiAgentResultCaptureWriterDryRunFixtureSnapshot, + type AiAgentResultCaptureWriterDryRunReadbackSnapshot, type AiAgentResultCaptureWriterImplementationReviewSnapshot, type AiAgentOwnerApprovedFixturePromotionGateSnapshot, type AiAgentRuntimeWorkerShadowGateSnapshot, @@ -465,6 +466,7 @@ export function AutomationInventoryTab() { const [resultCaptureWriteGateReview, setResultCaptureWriteGateReview] = useState(null) const [resultCaptureWriterImplementationReview, setResultCaptureWriterImplementationReview] = useState(null) const [resultCaptureWriterDryRunFixture, setResultCaptureWriterDryRunFixture] = useState(null) + const [resultCaptureWriterDryRunReadback, setResultCaptureWriterDryRunReadback] = useState(null) const [reportTruthActionabilityReview, setReportTruthActionabilityReview] = useState(null) const [ownerDryRunPackage, setOwnerDryRunPackage] = useState(null) const [hostStatefulInventory, setHostStatefulInventory] = useState(null) @@ -525,6 +527,7 @@ export function AutomationInventoryTab() { apiClient.getAiAgentResultCaptureWriteGateReview(), apiClient.getAiAgentResultCaptureWriterImplementationReview(), apiClient.getAiAgentResultCaptureWriterDryRunFixture(), + apiClient.getAiAgentResultCaptureWriterDryRunReadback(), apiClient.getAiAgentReportTruthActionabilityReview(), apiClient.getAiAgentOwnerApprovedFixtureDryRun(), apiClient.getAiAgentHostStatefulVersionInventory(), @@ -584,6 +587,7 @@ export function AutomationInventoryTab() { resultCaptureWriteGateReviewResult, resultCaptureWriterImplementationReviewResult, resultCaptureWriterDryRunFixtureResult, + resultCaptureWriterDryRunReadbackResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -640,6 +644,7 @@ export function AutomationInventoryTab() { setResultCaptureWriteGateReview(resultCaptureWriteGateReviewResult.status === 'fulfilled' ? resultCaptureWriteGateReviewResult.value : null) setResultCaptureWriterImplementationReview(resultCaptureWriterImplementationReviewResult.status === 'fulfilled' ? resultCaptureWriterImplementationReviewResult.value : null) setResultCaptureWriterDryRunFixture(resultCaptureWriterDryRunFixtureResult.status === 'fulfilled' ? resultCaptureWriterDryRunFixtureResult.value : null) + setResultCaptureWriterDryRunReadback(resultCaptureWriterDryRunReadbackResult.status === 'fulfilled' ? resultCaptureWriterDryRunReadbackResult.value : null) setReportTruthActionabilityReview(reportTruthActionabilityReviewResult.status === 'fulfilled' ? reportTruthActionabilityReviewResult.value : null) setOwnerDryRunPackage(ownerDryRunPackageResult.status === 'fulfilled' ? ownerDryRunPackageResult.value : null) setHostStatefulInventory(hostStatefulInventoryResult.status === 'fulfilled' ? hostStatefulInventoryResult.value : null) @@ -694,6 +699,7 @@ export function AutomationInventoryTab() { resultCaptureWriteGateReviewResult, resultCaptureWriterImplementationReviewResult, resultCaptureWriterDryRunFixtureResult, + resultCaptureWriterDryRunReadbackResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -1936,7 +1942,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 || !resultCaptureWriteGateReview || !resultCaptureWriterImplementationReview || !resultCaptureWriterDryRunFixture || !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 || !resultCaptureWriterImplementationReview || !resultCaptureWriterDryRunFixture || !resultCaptureWriterDryRunReadback || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { return (
@@ -2745,6 +2751,40 @@ export function AutomationInventoryTab() { + resultCaptureWriterDryRunFixture.rollups.playbook_trust_write_count + resultCaptureWriterDryRunFixture.rollups.production_write_count ) + const resultCaptureWriterReadbackOverall = resultCaptureWriterDryRunReadback.program_status.overall_completion_percent + const resultCaptureWriterReadbackCards = resultCaptureWriterDryRunReadback.rollups.dry_run_readback_card_count + const resultCaptureWriterReadbackVerifiers = resultCaptureWriterDryRunReadback.rollups.receipt_verifier_check_count + const resultCaptureWriterReadbackPromotionGates = resultCaptureWriterDryRunReadback.rollups.promotion_readiness_gate_count + const resultCaptureWriterReadbackBlockers = resultCaptureWriterDryRunReadback.rollups.blocked_promotion_write_count + const resultCaptureWriterReadbackActions = resultCaptureWriterDryRunReadback.rollups.operator_action_count + const resultCaptureWriterReadbackApprovalRequired = ( + resultCaptureWriterDryRunReadback.rollups.approval_required_readback_count + + resultCaptureWriterDryRunReadback.rollups.approval_required_verifier_count + + resultCaptureWriterDryRunReadback.rollups.approval_required_gate_count + ) + const resultCaptureWriterReadbackBlocked = ( + resultCaptureWriterDryRunReadback.rollups.blocked_readback_count + + resultCaptureWriterDryRunReadback.rollups.blocked_verifier_count + + resultCaptureWriterDryRunReadback.rollups.blocked_gate_count + + resultCaptureWriterDryRunReadback.rollups.critical_blocker_count + ) + const resultCaptureWriterReadbackLiveWrites = ( + resultCaptureWriterDryRunReadback.rollups.writer_apply_count + + resultCaptureWriterDryRunReadback.rollups.dry_run_execution_count + + resultCaptureWriterDryRunReadback.rollups.receipt_write_count + + resultCaptureWriterDryRunReadback.rollups.promotion_apply_count + + resultCaptureWriterDryRunReadback.rollups.canonical_runtime_target_read_count + + resultCaptureWriterDryRunReadback.rollups.live_query_count + + resultCaptureWriterDryRunReadback.rollups.reviewer_queue_write_count + + resultCaptureWriterDryRunReadback.rollups.gateway_queue_write_count + + resultCaptureWriterDryRunReadback.rollups.telegram_send_count + + resultCaptureWriterDryRunReadback.rollups.bot_api_call_count + + resultCaptureWriterDryRunReadback.rollups.report_receipt_write_count + + resultCaptureWriterDryRunReadback.rollups.result_capture_write_count + + resultCaptureWriterDryRunReadback.rollups.learning_write_count + + resultCaptureWriterDryRunReadback.rollups.playbook_trust_write_count + + resultCaptureWriterDryRunReadback.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 @@ -7294,6 +7334,110 @@ export function AutomationInventoryTab() {
+
+
+
+ + + {t('resultCaptureWriterDryRunReadback.title')} + +
+ +
+ +
+ } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
+ +
+
+ {t('resultCaptureWriterDryRunReadback.priorTitle')} + + {resultCaptureWriterDryRunReadback.prior_result_capture_writer_dry_run_fixture.readiness_note} + +
+ + + +
+
+ +
+ {t('resultCaptureWriterDryRunReadback.truthTitle')} + + {resultCaptureWriterDryRunReadback.readback_truth.truth_note} + +
+ + + + + + + +
+
+
+ +
+ {resultCaptureWriterDryRunReadback.dry_run_readback_cards.slice(0, 5).map(card => ( +
+
+ + {card.display_name} + + +
+ + {card.source_fixture} → {card.receipt_preview} + +
+ + + + +
+
+ ))} +
+ +
+ {resultCaptureWriterDryRunReadback.promotion_readiness_gates.slice(0, 5).map(gate => ( +
+
+ + {gate.display_name} + + +
+ + {gate.required_evidence} + +
+ + +
+
+ ))} +
+
+
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index ce3a49f5..946ae5bf 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -515,6 +515,11 @@ export const apiClient = { return handleResponse(res) }, + async getAiAgentResultCaptureWriterDryRunReadback() { + const res = await fetch(`${API_BASE_URL}/agents/agent-result-capture-writer-dry-run-readback`) + return handleResponse(res) + }, + async getAiAgentOwnerApprovedFixtureDryRun() { const res = await fetch(`${API_BASE_URL}/agents/agent-owner-approved-fixture-dry-run`) return handleResponse(res) @@ -6190,6 +6195,186 @@ export interface AiAgentResultCaptureWriterDryRunFixtureSnapshot { } } +export interface AiAgentResultCaptureWriterDryRunReadbackSnapshot { + schema_version: 'ai_agent_result_capture_writer_dry_run_readback_v1' + generated_at: string + program_status: { + overall_completion_percent: number + current_priority: 'P0' | 'P1' | 'P2' | 'P3' + current_task_id: 'P2-124' + next_task_id: 'P2-125' + read_only_mode: true + runtime_authority: 'result_capture_writer_dry_run_readback_only_no_live_write' + status_note: string + } + source_refs: string[] + prior_result_capture_writer_dry_run_fixture: { + schema_version: string + writer_dry_run_fixture_count: number + receipt_preview_count: number + idempotency_replay_check_count: number + rollback_rehearsal_count: number + blocked_runtime_write_count: number + operator_action_count: number + writer_apply_count: number + dry_run_execution_count: number + receipt_write_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 + } + readback_truth: { + p2_123_fixture_loaded: boolean + dry_run_readback_ready: boolean + receipt_verifier_ready: boolean + promotion_readiness_ready: boolean + fixture_only_mode: boolean + owner_approval_required: boolean + dual_approval_required: boolean + dry_run_hash_required: boolean + receipt_verifier_required: boolean + promotion_owner_required: boolean + rollback_owner_required: boolean + writer_apply_enabled: boolean + dry_run_execution_enabled: boolean + receipt_write_enabled: boolean + promotion_apply_enabled: 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 + receipt_verifier_pass_count: number + writer_apply_count: number + dry_run_execution_count: number + receipt_write_count: number + promotion_apply_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 + } + dry_run_readback_cards: Array<{ + readback_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + source_fixture: string + receipt_preview: string + status: 'ready_for_owner_review' | 'approval_required' | 'blocked_by_policy' + readback_only: true + runtime_write_enabled: false + dry_run_execution_enabled: false + promotion_candidate: string + operator_note: string + evidence_hash: string + }> + receipt_verifier_checks: Array<{ + verifier_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready' | 'approval_required' | 'blocked_by_policy' + verifier_mode: 'no_write_receipt_verifier' + required_before_promotion: true + live_receipt_enabled: false + failure_if_missing: string + }> + promotion_readiness_gates: Array<{ + gate_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready_for_owner_review' | 'approval_required' | 'blocked_by_policy' + required_evidence: string + promotion_allowed: false + }> + blocked_promotion_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: { + dry_run_readback_card_count: number + receipt_verifier_check_count: number + promotion_readiness_gate_count: number + blocked_promotion_write_count: number + operator_action_count: number + approval_required_readback_count: number + blocked_readback_count: number + approval_required_verifier_count: number + blocked_verifier_count: number + approval_required_gate_count: number + blocked_gate_count: number + critical_blocker_count: number + owner_approval_received_count: number + dual_approval_received_count: number + dry_run_hash_verified_count: number + receipt_verifier_pass_count: number + writer_apply_count: number + dry_run_execution_count: number + receipt_write_count: number + promotion_apply_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_writer_dry_run_readback_2026-06-14.json b/docs/evaluations/ai_agent_result_capture_writer_dry_run_readback_2026-06-14.json new file mode 100644 index 00000000..7fc2fc37 --- /dev/null +++ b/docs/evaluations/ai_agent_result_capture_writer_dry_run_readback_2026-06-14.json @@ -0,0 +1,394 @@ +{ + "schema_version": "ai_agent_result_capture_writer_dry_run_readback_v1", + "generated_at": "2026-06-14T00:50:00+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P2", + "current_task_id": "P2-124", + "next_task_id": "P2-125", + "read_only_mode": true, + "runtime_authority": "result_capture_writer_dry_run_readback_only_no_live_write", + "status_note": "P2-124 只建立 result capture writer dry-run readback、receipt verifier 與 promotion readiness;不得套用 writer、執行 dry-run、寫 receipt 或寫入 production。" + }, + "source_refs": [ + "docs/evaluations/ai_agent_result_capture_writer_dry_run_fixture_2026-06-14.json", + "docs/evaluations/ai_agent_result_capture_writer_implementation_review_2026-06-13.json", + "docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md#44-建立-result-capture-writer-dry-run-fixture" + ], + "prior_result_capture_writer_dry_run_fixture": { + "schema_version": "ai_agent_result_capture_writer_dry_run_fixture_v1", + "writer_dry_run_fixture_count": 5, + "receipt_preview_count": 5, + "idempotency_replay_check_count": 5, + "rollback_rehearsal_count": 5, + "blocked_runtime_write_count": 6, + "operator_action_count": 5, + "writer_apply_count": 0, + "dry_run_execution_count": 0, + "receipt_write_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-123 已提供 fixture-only dry-run、receipt preview、idempotency replay 與 rollback rehearsal;P2-124 只讀回這些輸出並建立 promotion readiness。" + }, + "readback_truth": { + "p2_123_fixture_loaded": true, + "dry_run_readback_ready": true, + "receipt_verifier_ready": true, + "promotion_readiness_ready": true, + "fixture_only_mode": true, + "owner_approval_required": true, + "dual_approval_required": true, + "dry_run_hash_required": true, + "receipt_verifier_required": true, + "promotion_owner_required": true, + "rollback_owner_required": true, + "writer_apply_enabled": false, + "dry_run_execution_enabled": false, + "receipt_write_enabled": false, + "promotion_apply_enabled": 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, + "receipt_verifier_pass_count": 0, + "writer_apply_count": 0, + "dry_run_execution_count": 0, + "receipt_write_count": 0, + "promotion_apply_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": "P2-124 把 P2-123 fixture-only dry-run 讀回成 promotion readiness。所有 live writer、queue、Telegram、Bot API、receipt 與 production write 仍為 0。" + }, + "dry_run_readback_cards": [ + { + "readback_id": "readback_result_capture_writer", + "display_name": "Result capture writer dry-run readback", + "owner_agent": "openclaw", + "source_fixture": "fixture_result_capture_writer", + "receipt_preview": "receipt_result_capture_preview", + "status": "ready_for_owner_review", + "readback_only": true, + "runtime_write_enabled": false, + "dry_run_execution_enabled": false, + "promotion_candidate": "result_capture_writer_apply", + "operator_note": "先確認 result capture receipt preview 與 idempotency key,再進 P2-125 owner promotion review。", + "evidence_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1" + }, + { + "readback_id": "readback_learning_writer", + "display_name": "Learning writer dry-run readback", + "owner_agent": "hermes", + "source_fixture": "fixture_learning_writer", + "receipt_preview": "receipt_learning_preview", + "status": "approval_required", + "readback_only": true, + "runtime_write_enabled": false, + "dry_run_execution_enabled": false, + "promotion_candidate": "learning_writer_apply", + "operator_note": "高影響 KM 必須 owner review 後才能進 promotion readiness。", + "evidence_hash": "sha256:c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2" + }, + { + "readback_id": "readback_playbook_trust_writer", + "display_name": "PlayBook trust writer dry-run readback", + "owner_agent": "nemotron", + "source_fixture": "fixture_playbook_trust_writer", + "receipt_preview": "receipt_playbook_trust_preview", + "status": "blocked_by_policy", + "readback_only": true, + "runtime_write_enabled": false, + "dry_run_execution_enabled": false, + "promotion_candidate": "playbook_trust_writer_apply", + "operator_note": "信任分數更新需 verifier success、negative reinforcement 與 rollback owner 同時成立。", + "evidence_hash": "sha256:c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3" + }, + { + "readback_id": "readback_reviewer_queue_writer", + "display_name": "Reviewer queue dry-run readback", + "owner_agent": "openclaw", + "source_fixture": "fixture_reviewer_queue_writer", + "receipt_preview": "receipt_reviewer_queue_preview", + "status": "approval_required", + "readback_only": true, + "runtime_write_enabled": false, + "dry_run_execution_enabled": false, + "promotion_candidate": "reviewer_queue_apply", + "operator_note": "reviewer queue 寫入前需確認 owner route、dedupe key 與 SLA。", + "evidence_hash": "sha256:c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4" + }, + { + "readback_id": "readback_gateway_queue_writer", + "display_name": "Gateway queue dry-run readback", + "owner_agent": "nemotron", + "source_fixture": "fixture_gateway_queue_writer", + "receipt_preview": "receipt_gateway_queue_preview", + "status": "ready_for_owner_review", + "readback_only": true, + "runtime_write_enabled": false, + "dry_run_execution_enabled": false, + "promotion_candidate": "gateway_queue_apply", + "operator_note": "只能顯示 AwoooI SRE 戰情室邏輯路由,不顯示 chat id、token 或 Bot API payload。", + "evidence_hash": "sha256:c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5" + } + ], + "receipt_verifier_checks": [ + { + "verifier_id": "verify_result_capture_receipt", + "display_name": "Result capture receipt verifier", + "owner_agent": "openclaw", + "status": "ready", + "verifier_mode": "no_write_receipt_verifier", + "required_before_promotion": true, + "live_receipt_enabled": false, + "failure_if_missing": "缺 result capture receipt verifier 時不得進 writer promotion。" + }, + { + "verifier_id": "verify_learning_receipt", + "display_name": "Learning receipt verifier", + "owner_agent": "hermes", + "status": "approval_required", + "verifier_mode": "no_write_receipt_verifier", + "required_before_promotion": true, + "live_receipt_enabled": false, + "failure_if_missing": "缺 owner-reviewed learning receipt verifier 時不得學習寫入。" + }, + { + "verifier_id": "verify_playbook_trust_receipt", + "display_name": "PlayBook trust receipt verifier", + "owner_agent": "nemotron", + "status": "blocked_by_policy", + "verifier_mode": "no_write_receipt_verifier", + "required_before_promotion": true, + "live_receipt_enabled": false, + "failure_if_missing": "缺 trust delta verifier 時不得更新 PlayBook trust。" + }, + { + "verifier_id": "verify_reviewer_queue_receipt", + "display_name": "Reviewer queue receipt verifier", + "owner_agent": "openclaw", + "status": "approval_required", + "verifier_mode": "no_write_receipt_verifier", + "required_before_promotion": true, + "live_receipt_enabled": false, + "failure_if_missing": "缺 reviewer queue verifier 時不得寫 reviewer queue。" + }, + { + "verifier_id": "verify_gateway_queue_receipt", + "display_name": "Gateway queue receipt verifier", + "owner_agent": "nemotron", + "status": "ready", + "verifier_mode": "no_write_receipt_verifier", + "required_before_promotion": true, + "live_receipt_enabled": false, + "failure_if_missing": "缺 Gateway queue verifier 時不得送 Telegram。" + } + ], + "promotion_readiness_gates": [ + { + "gate_id": "gate_writer_apply_owner_approval", + "display_name": "Writer apply owner approval", + "owner_agent": "openclaw", + "status": "approval_required", + "required_evidence": "owner approval、dual approval、dry-run hash", + "promotion_allowed": false + }, + { + "gate_id": "gate_dry_run_hash", + "display_name": "Dry-run hash verifier", + "owner_agent": "nemotron", + "status": "ready_for_owner_review", + "required_evidence": "fixture hash、receipt preview hash、idempotency key", + "promotion_allowed": false + }, + { + "gate_id": "gate_receipt_verifier", + "display_name": "Receipt verifier gate", + "owner_agent": "hermes", + "status": "approval_required", + "required_evidence": "no-write receipt verifier result", + "promotion_allowed": false + }, + { + "gate_id": "gate_rollback_owner", + "display_name": "Rollback owner gate", + "owner_agent": "openclaw", + "status": "ready_for_owner_review", + "required_evidence": "rollback owner、rollback rehearsal、manual escalation route", + "promotion_allowed": false + }, + { + "gate_id": "gate_production_write_block", + "display_name": "Production write block", + "owner_agent": "nemotron", + "status": "blocked_by_policy", + "required_evidence": "explicit production write approval and maintenance window", + "promotion_allowed": false + } + ], + "blocked_promotion_writes": [ + { + "blocker_id": "blocked_writer_apply", + "display_name": "Writer apply blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "writer_apply", + "blocked_until": "P2-125 owner promotion review and explicit write approval", + "evidence_hash": "sha256:d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1" + }, + { + "blocker_id": "blocked_dry_run_execution", + "display_name": "Dry-run execution blocked", + "severity": "high", + "status": "approval_required", + "blocked_action": "dry_run_execution", + "blocked_until": "receipt verifier and rollback owner are accepted", + "evidence_hash": "sha256:d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2" + }, + { + "blocker_id": "blocked_receipt_write", + "display_name": "Receipt write blocked", + "severity": "high", + "status": "approval_required", + "blocked_action": "receipt_write", + "blocked_until": "no-write receipt verifier passes and owner approves", + "evidence_hash": "sha256:d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3" + }, + { + "blocker_id": "blocked_result_capture_write", + "display_name": "Result capture write blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "result_capture_write", + "blocked_until": "result capture writer promotion is accepted", + "evidence_hash": "sha256:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4" + }, + { + "blocker_id": "blocked_gateway_queue_write", + "display_name": "Gateway queue write blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "gateway_queue_write", + "blocked_until": "SRE war room route and receipt verifier are accepted", + "evidence_hash": "sha256:d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5" + }, + { + "blocker_id": "blocked_telegram_send", + "display_name": "Telegram send blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "telegram_send", + "blocked_until": "Gateway queue write is approved and no-send verifier passes", + "evidence_hash": "sha256:d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6" + } + ], + "operator_actions": [ + { + "action_id": "review_dry_run_readback", + "display_name": "Review dry-run readback", + "owner_agent": "openclaw", + "operator_instruction": "比對 readback card、receipt preview 與 idempotency key,確認沒有 live write 欄位。", + "runtime_write_allowed": false + }, + { + "action_id": "verify_receipt_verifier", + "display_name": "Verify receipt verifier", + "owner_agent": "hermes", + "operator_instruction": "逐一確認 no-write receipt verifier 的 failure_if_missing 與 redaction policy。", + "runtime_write_allowed": false + }, + { + "action_id": "score_promotion_readiness", + "display_name": "Score promotion readiness", + "owner_agent": "nemotron", + "operator_instruction": "檢查 promotion readiness gate,不把 ready_for_owner_review 當正式批准。", + "runtime_write_allowed": false + }, + { + "action_id": "prepare_p2_125_owner_packet", + "display_name": "Prepare P2-125 owner packet", + "owner_agent": "openclaw", + "operator_instruction": "只產生下一步 owner promotion review 所需欄位,不寫 reviewer queue 或 Gateway queue。", + "runtime_write_allowed": false + }, + { + "action_id": "hold_runtime_write_gate", + "display_name": "Hold runtime write gate", + "owner_agent": "nemotron", + "operator_instruction": "持續確認 writer apply、receipt write、Telegram send、production write 全部維持 0。", + "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": "只顯示 readback、receipt verifier、promotion gate 與 blocked action 摘要;不顯示提示詞原文、推理原文、機敏值、chat id、token、原始 payload 或內部協作內容。" + }, + "rollups": { + "dry_run_readback_card_count": 5, + "receipt_verifier_check_count": 5, + "promotion_readiness_gate_count": 5, + "blocked_promotion_write_count": 6, + "operator_action_count": 5, + "approval_required_readback_count": 2, + "blocked_readback_count": 1, + "approval_required_verifier_count": 2, + "blocked_verifier_count": 1, + "approval_required_gate_count": 2, + "blocked_gate_count": 1, + "critical_blocker_count": 4, + "owner_approval_received_count": 0, + "dual_approval_received_count": 0, + "dry_run_hash_verified_count": 0, + "receipt_verifier_pass_count": 0, + "writer_apply_count": 0, + "dry_run_execution_count": 0, + "receipt_write_count": 0, + "promotion_apply_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_writer_dry_run_readback_v1.schema.json b/docs/schemas/ai_agent_result_capture_writer_dry_run_readback_v1.schema.json new file mode 100644 index 00000000..b420a75f --- /dev/null +++ b/docs/schemas/ai_agent_result_capture_writer_dry_run_readback_v1.schema.json @@ -0,0 +1,222 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.local/schemas/ai_agent_result_capture_writer_dry_run_readback_v1.schema.json", + "title": "AI Agent Result Capture Writer Dry Run Readback", + "type": "object", + "additionalProperties": true, + "required": [ + "schema_version", + "generated_at", + "program_status", + "prior_result_capture_writer_dry_run_fixture", + "readback_truth", + "dry_run_readback_cards", + "receipt_verifier_checks", + "promotion_readiness_gates", + "blocked_promotion_writes", + "operator_actions", + "display_redaction_contract", + "rollups" + ], + "properties": { + "schema_version": { + "const": "ai_agent_result_capture_writer_dry_run_readback_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-124" + }, + "next_task_id": { + "const": "P2-125" + }, + "read_only_mode": { + "const": true + }, + "runtime_authority": { + "const": "result_capture_writer_dry_run_readback_only_no_live_write" + } + } + }, + "readback_truth": { + "type": "object", + "required": [ + "p2_123_fixture_loaded", + "dry_run_readback_ready", + "receipt_verifier_ready", + "promotion_readiness_ready", + "writer_apply_enabled", + "dry_run_execution_enabled", + "receipt_write_enabled", + "promotion_apply_enabled", + "result_capture_write_enabled", + "learning_write_enabled", + "playbook_trust_write_enabled", + "gateway_queue_write_enabled", + "telegram_send_enabled" + ], + "properties": { + "p2_123_fixture_loaded": { + "const": true + }, + "dry_run_readback_ready": { + "const": true + }, + "receipt_verifier_ready": { + "const": true + }, + "promotion_readiness_ready": { + "const": true + }, + "writer_apply_enabled": { + "const": false + }, + "dry_run_execution_enabled": { + "const": false + }, + "receipt_write_enabled": { + "const": false + }, + "promotion_apply_enabled": { + "const": false + }, + "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 + } + } + }, + "dry_run_readback_cards": { + "type": "array", + "minItems": 5, + "maxItems": 5 + }, + "receipt_verifier_checks": { + "type": "array", + "minItems": 5, + "maxItems": 5 + }, + "promotion_readiness_gates": { + "type": "array", + "minItems": 5, + "maxItems": 5 + }, + "blocked_promotion_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": [ + "dry_run_readback_card_count", + "receipt_verifier_check_count", + "promotion_readiness_gate_count", + "blocked_promotion_write_count", + "operator_action_count", + "writer_apply_count", + "dry_run_execution_count", + "receipt_write_count", + "promotion_apply_count", + "result_capture_write_count", + "learning_write_count", + "playbook_trust_write_count", + "gateway_queue_write_count", + "telegram_send_count" + ], + "properties": { + "dry_run_readback_card_count": { + "const": 5 + }, + "receipt_verifier_check_count": { + "const": 5 + }, + "promotion_readiness_gate_count": { + "const": 5 + }, + "blocked_promotion_write_count": { + "const": 6 + }, + "operator_action_count": { + "const": 5 + }, + "writer_apply_count": { + "const": 0 + }, + "dry_run_execution_count": { + "const": 0 + }, + "receipt_write_count": { + "const": 0 + }, + "promotion_apply_count": { + "const": 0 + }, + "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 + } + } + } + } +}