diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index a7c48f33b..3825a0b1d 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_owner_promotion_review import ( load_latest_ai_agent_result_capture_owner_promotion_review, ) +from src.services.ai_agent_result_capture_owner_approved_execution_rehearsal import ( + load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal, +) from src.services.ai_agent_result_capture_promotion_approval_gate import ( load_latest_ai_agent_result_capture_promotion_approval_gate, ) @@ -1851,6 +1854,36 @@ async def get_agent_result_capture_owner_promotion_review() -> dict[str, Any]: ) from exc +@router.get( + "/agent-result-capture-owner-approved-execution-rehearsal", + response_model=dict[str, Any], + summary="取得 AI Agent result capture owner-approved execution rehearsal", + description=( + "讀取最新已提交的 P2-126 owner-approved execution rehearsal;" + "此端點只回傳 no-write execution rehearsal、apply gate、verifier rehearsal、" + "rollback drill、blocked live apply 與 operator handoff,不套用 writer、不執行 live apply、" + "不寫 receipt、不寫 result capture、learning、PlayBook trust、reviewer queue、" + "Gateway queue,不送 Telegram、不呼叫 Bot API、不讀 secret。" + ), +) +async def get_agent_result_capture_owner_approved_execution_rehearsal() -> dict[str, Any]: + """Return the latest read-only owner-approved execution rehearsal package.""" + try: + payload = await asyncio.to_thread(load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal) + 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_owner_approved_execution_rehearsal_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="AI Agent result capture owner-approved execution rehearsal 無效", + ) 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_owner_approved_execution_rehearsal.py b/apps/api/src/services/ai_agent_result_capture_owner_approved_execution_rehearsal.py new file mode 100644 index 000000000..254bd16c7 --- /dev/null +++ b/apps/api/src/services/ai_agent_result_capture_owner_approved_execution_rehearsal.py @@ -0,0 +1,407 @@ +""" +AI Agent result capture owner-approved execution rehearsal snapshot. + +Loads the latest committed P2-126 owner-approved execution rehearsal package. +This module validates committed evidence only; it never applies writers, executes +live applies, 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_owner_approved_execution_rehearsal_*.json" +_SCHEMA_VERSION = "ai_agent_result_capture_owner_approved_execution_rehearsal_v1" +_RUNTIME_AUTHORITY = "result_capture_owner_approved_execution_rehearsal_only_no_live_write" + + +def load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed owner-approved execution rehearsal 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 owner-approved execution rehearsal 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_execution_rehearsals(payload, label) + _require_apply_checks(payload, label) + _require_verifier_rehearsals(payload, label) + _require_rollback_drills(payload, label) + _require_blocked_applies(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-126", + "next_task_id": "P2-127", + "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_owner_promotion_review") or {} + expected = { + "schema_version": "ai_agent_result_capture_owner_promotion_review_v1", + "owner_promotion_packet_count": 5, + "execution_gate_check_count": 5, + "rollback_owner_review_count": 5, + "blocked_execution_write_count": 6, + "operator_action_count": 5, + "owner_promotion_approved_count": 0, + "execution_gate_pass_count": 0, + "rollback_owner_confirmed_count": 0, + "post_write_verifier_ready_count": 0, + "writer_apply_count": 0, + "execution_apply_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_owner_promotion_review mismatch: {mismatches}") + if not prior.get("readiness_note"): + raise ValueError(f"{label}: prior_result_capture_owner_promotion_review.readiness_note is required") + + +def _require_truth(payload: dict[str, Any], label: str) -> None: + truth = payload.get("execution_rehearsal_truth") or {} + required_true = { + "p2_125_owner_promotion_loaded", + "owner_approval_record_required", + "no_write_apply_rehearsal_ready", + "post_write_verifier_rehearsal_ready", + "rollback_drill_ready", + "idempotency_replay_required", + "redaction_review_required", + "execution_rehearsal_only", + } + missing = sorted(field for field in required_true if truth.get(field) is not True) + if missing: + raise ValueError(f"{label}: rehearsal ready flags must remain true: {missing}") + required_false = { + "owner_approval_record_received", + "no_write_apply_executed", + "post_write_verifier_passed", + "rollback_drill_confirmed", + "writer_apply_enabled", + "execution_apply_enabled", + "receipt_write_enabled", + "reviewer_queue_write_enabled", + "gateway_queue_write_enabled", + "telegram_send_enabled", + "bot_api_call_enabled", + "report_receipt_write_enabled", + "result_capture_write_enabled", + "learning_write_enabled", + "playbook_trust_write_enabled", + "production_write_enabled", + "secret_read_enabled", + "destructive_operation_enabled", + } + unsafe = sorted(field for field in required_false if truth.get(field) is not False) + if unsafe: + raise ValueError(f"{label}: live execution/send/write flags must remain false: {unsafe}") + zero_counts = { + "owner_approval_record_received_count", + "no_write_apply_execution_count", + "post_write_verifier_pass_count", + "rollback_drill_confirmed_count", + "writer_apply_count", + "execution_apply_count", + "receipt_write_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}: rehearsal live counters must remain zero: {non_zero}") + if not truth.get("truth_note"): + raise ValueError(f"{label}: execution_rehearsal_truth.truth_note is required") + + +def _require_execution_rehearsals(payload: dict[str, Any], label: str) -> None: + items = payload.get("execution_rehearsals") or [] + required = { + "rehearsal_result_capture_writer", + "rehearsal_learning_writer", + "rehearsal_playbook_trust_writer", + "rehearsal_reviewer_queue_writer", + "rehearsal_gateway_queue_writer", + } + ids = {item.get("rehearsal_id") for item in items} + if ids != required: + raise ValueError(f"{label}: execution rehearsals must match {sorted(required)}") + for item in items: + item_id = item.get("rehearsal_id") + if item.get("rehearsal_mode") != "no_write_apply_rehearsal": + raise ValueError(f"{label}: rehearsal {item_id} must remain no-write apply rehearsal") + if item.get("no_write_only") is not True or item.get("live_apply_enabled") is not False: + raise ValueError(f"{label}: rehearsal {item_id} must not enable live apply") + if item.get("status") not in {"ready_for_rehearsal_review", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: rehearsal {item_id} status is invalid") + if not item.get("expected_output") or not _is_redacted_sha256(item.get("evidence_hash")): + raise ValueError(f"{label}: rehearsal {item_id} must include expected output and redacted evidence") + + +def _require_apply_checks(payload: dict[str, Any], label: str) -> None: + items = payload.get("no_write_apply_checks") or [] + required = { + "check_owner_approval_record", + "check_idempotency_replay", + "check_payload_redaction", + "check_post_write_verifier", + "check_production_write_block", + } + ids = {item.get("check_id") for item in items} + if ids != required: + raise ValueError(f"{label}: no-write apply checks must match {sorted(required)}") + for item in items: + item_id = item.get("check_id") + if item.get("check_mode") != "no_write_apply_gate": + raise ValueError(f"{label}: check {item_id} must remain no-write apply gate") + if item.get("required_before_apply") is not True or item.get("live_apply_enabled") is not False: + raise ValueError(f"{label}: check {item_id} must not enable live apply") + if item.get("status") not in {"ready_for_rehearsal_review", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: check {item_id} status is invalid") + if not item.get("failure_if_missing"): + raise ValueError(f"{label}: check {item_id} failure_if_missing is required") + + +def _require_verifier_rehearsals(payload: dict[str, Any], label: str) -> None: + items = payload.get("post_write_verifier_rehearsals") or [] + required = { + "verifier_result_capture", + "verifier_learning", + "verifier_playbook_trust", + "verifier_reviewer_queue", + "verifier_gateway_queue", + } + ids = {item.get("verifier_id") for item in items} + if ids != required: + raise ValueError(f"{label}: verifier rehearsals must match {sorted(required)}") + for item in items: + item_id = item.get("verifier_id") + if item.get("verifier_mode") != "fixture_only_no_live_read": + raise ValueError(f"{label}: verifier {item_id} must remain fixture-only") + if item.get("verifier_ready") is not False or item.get("live_read_enabled") is not False: + raise ValueError(f"{label}: verifier {item_id} must not enable live read") + if item.get("status") not in {"ready_for_rehearsal_review", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: verifier {item_id} status is invalid") + if not item.get("expected_signal"): + raise ValueError(f"{label}: verifier {item_id} expected_signal is required") + + +def _require_rollback_drills(payload: dict[str, Any], label: str) -> None: + items = payload.get("rollback_drills") or [] + required = { + "rollback_drill_result_capture", + "rollback_drill_learning", + "rollback_drill_playbook_trust", + "rollback_drill_reviewer_queue", + "rollback_drill_gateway_queue", + } + ids = {item.get("drill_id") for item in items} + if ids != required: + raise ValueError(f"{label}: rollback drills must match {sorted(required)}") + for item in items: + item_id = item.get("drill_id") + if item.get("rollback_owner_required") is not True or item.get("rollback_drill_confirmed") is not False: + raise ValueError(f"{label}: rollback drill {item_id} must require unconfirmed owner") + if item.get("status") not in {"ready_for_rehearsal_review", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: rollback drill {item_id} status is invalid") + if not item.get("drill_note"): + raise ValueError(f"{label}: rollback drill {item_id} drill_note is required") + + +def _require_blocked_applies(payload: dict[str, Any], label: str) -> None: + items = payload.get("blocked_live_applies") or [] + required = { + "blocked_writer_apply", + "blocked_execution_apply", + "blocked_receipt_write", + "blocked_result_capture_write", + "blocked_gateway_queue_write", + "blocked_telegram_send", + } + ids = {item.get("blocker_id") for item in items} + if ids != required: + raise ValueError(f"{label}: blocked live applies must match {sorted(required)}") + for item in items: + item_id = item.get("blocker_id") + if item.get("status") not in {"approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: blocker {item_id} status is invalid") + if item.get("severity") not in {"high", "critical"}: + raise ValueError(f"{label}: blocker {item_id} severity is invalid") + if not item.get("blocked_action") or not item.get("blocked_until"): + raise ValueError(f"{label}: blocker {item_id} must include blocked_action and blocked_until") + if not _is_redacted_sha256(item.get("evidence_hash")): + raise ValueError(f"{label}: blocker {item_id} must expose redacted evidence_hash") + + +def _require_actions(payload: dict[str, Any], label: str) -> None: + actions = payload.get("operator_actions") or [] + required = { + "review_execution_rehearsals", + "verify_no_write_apply_checks", + "inspect_post_write_verifier_rehearsal", + "confirm_rollback_drills", + "hold_live_apply_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 {} + rehearsals = payload.get("execution_rehearsals") or [] + checks = payload.get("no_write_apply_checks") or [] + verifiers = payload.get("post_write_verifier_rehearsals") or [] + drills = payload.get("rollback_drills") or [] + blockers = payload.get("blocked_live_applies") or [] + actions = payload.get("operator_actions") or [] + expected = { + "execution_rehearsal_count": len(rehearsals), + "no_write_apply_check_count": len(checks), + "post_write_verifier_rehearsal_count": len(verifiers), + "rollback_drill_count": len(drills), + "blocked_live_apply_count": len(blockers), + "operator_action_count": len(actions), + "approval_required_rehearsal_count": sum(1 for item in rehearsals if item.get("status") == "approval_required"), + "blocked_rehearsal_count": sum(1 for item in rehearsals if item.get("status") == "blocked_by_policy"), + "approval_required_check_count": sum(1 for item in checks if item.get("status") == "approval_required"), + "blocked_check_count": sum(1 for item in checks 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_rollback_count": sum(1 for item in drills if item.get("status") == "approval_required"), + "blocked_rollback_count": sum(1 for item in drills if item.get("status") == "blocked_by_policy"), + "critical_blocker_count": sum(1 for item in blockers if item.get("severity") == "critical"), + "owner_approval_record_received_count": 0, + "no_write_apply_execution_count": 0, + "post_write_verifier_pass_count": 0, + "rollback_drill_confirmed_count": 0, + "writer_apply_count": 0, + "execution_apply_count": 0, + "receipt_write_count": 0, + "reviewer_queue_write_count": 0, + "gateway_queue_write_count": 0, + "telegram_send_count": 0, + "bot_api_call_count": 0, + "report_receipt_write_count": 0, + "result_capture_write_count": 0, + "learning_write_count": 0, + "playbook_trust_write_count": 0, + "production_write_count": 0, + "secret_read_count": 0, + "destructive_operation_count": 0, + } + 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_owner_approved_execution_rehearsal.py b/apps/api/tests/test_ai_agent_result_capture_owner_approved_execution_rehearsal.py new file mode 100644 index 000000000..f275df1b1 --- /dev/null +++ b/apps/api/tests/test_ai_agent_result_capture_owner_approved_execution_rehearsal.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from src.services.ai_agent_result_capture_owner_approved_execution_rehearsal import ( + load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURE = REPO_ROOT / "docs/evaluations/ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json" + + +def test_load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal_snapshot() -> None: + data = load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal() + + assert data["schema_version"] == "ai_agent_result_capture_owner_approved_execution_rehearsal_v1" + assert data["program_status"]["current_task_id"] == "P2-126" + assert data["program_status"]["next_task_id"] == "P2-127" + assert data["program_status"]["runtime_authority"] == "result_capture_owner_approved_execution_rehearsal_only_no_live_write" + + rollups = data["rollups"] + assert rollups["execution_rehearsal_count"] == 5 + assert rollups["no_write_apply_check_count"] == 5 + assert rollups["post_write_verifier_rehearsal_count"] == 5 + assert rollups["rollback_drill_count"] == 5 + assert rollups["blocked_live_apply_count"] == 6 + assert rollups["operator_action_count"] == 5 + assert rollups["critical_blocker_count"] == 5 + assert rollups["owner_approval_record_received_count"] == 0 + assert rollups["no_write_apply_execution_count"] == 0 + assert rollups["post_write_verifier_pass_count"] == 0 + assert rollups["rollback_drill_confirmed_count"] == 0 + assert rollups["writer_apply_count"] == 0 + assert rollups["execution_apply_count"] == 0 + assert rollups["receipt_write_count"] == 0 + assert rollups["gateway_queue_write_count"] == 0 + assert rollups["telegram_send_count"] == 0 + assert rollups["bot_api_call_count"] == 0 + assert rollups["result_capture_write_count"] == 0 + assert rollups["learning_write_count"] == 0 + assert rollups["playbook_trust_write_count"] == 0 + assert rollups["production_write_count"] == 0 + assert {item["no_write_only"] for item in data["execution_rehearsals"]} == {True} + assert {item["live_apply_enabled"] for item in data["execution_rehearsals"]} == {False} + assert {item["live_read_enabled"] for item in data["post_write_verifier_rehearsals"]} == {False} + assert {item["rollback_drill_confirmed"] for item in data["rollback_drills"]} == {False} + + +def _copy_fixture(tmp_path: Path) -> dict: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + target = tmp_path / "ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json" + target.write_text(json.dumps(source, ensure_ascii=False), encoding="utf-8") + return source + + +def test_result_capture_owner_approved_execution_rehearsal_rejects_live_apply_enabled(tmp_path: Path) -> None: + source = _copy_fixture(tmp_path) + source["execution_rehearsals"][0]["live_apply_enabled"] = True + target = tmp_path / "ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json" + target.write_text(json.dumps(source, ensure_ascii=False), encoding="utf-8") + + with pytest.raises(ValueError, match="must not enable live apply"): + load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal(tmp_path) + + +def test_result_capture_owner_approved_execution_rehearsal_rejects_truth_write_flag(tmp_path: Path) -> None: + source = _copy_fixture(tmp_path) + source["execution_rehearsal_truth"]["result_capture_write_enabled"] = True + target = tmp_path / "ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json" + target.write_text(json.dumps(source, ensure_ascii=False), encoding="utf-8") + + with pytest.raises(ValueError, match="live execution/send/write flags"): + load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal(tmp_path) + + +def test_result_capture_owner_approved_execution_rehearsal_rejects_rollup_drift(tmp_path: Path) -> None: + source = _copy_fixture(tmp_path) + source["rollups"]["execution_rehearsal_count"] = 4 + target = tmp_path / "ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json" + target.write_text(json.dumps(source, ensure_ascii=False), encoding="utf-8") + + with pytest.raises(ValueError, match="rollup counts mismatch"): + load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal(tmp_path) + + +def test_result_capture_owner_approved_execution_rehearsal_rejects_forbidden_display_terms(tmp_path: Path) -> None: + source = _copy_fixture(tmp_path) + source["operator_actions"][0]["operator_instruction"] = "work_window_transcript must never show" + target = tmp_path / "ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json" + target.write_text(json.dumps(source, ensure_ascii=False), encoding="utf-8") + + with pytest.raises(ValueError, match="forbidden display terms"): + load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal(tmp_path) diff --git a/apps/api/tests/test_ai_agent_result_capture_owner_approved_execution_rehearsal_api.py b/apps/api/tests/test_ai_agent_result_capture_owner_approved_execution_rehearsal_api.py new file mode 100644 index 000000000..3281f00f0 --- /dev/null +++ b/apps/api/tests/test_ai_agent_result_capture_owner_approved_execution_rehearsal_api.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import pytest +from httpx import ASGITransport, AsyncClient + +from src.main import app + + +@pytest.mark.asyncio +async def test_get_agent_result_capture_owner_approved_execution_rehearsal_api() -> None: + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.get("/api/v1/agents/agent-result-capture-owner-approved-execution-rehearsal") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "ai_agent_result_capture_owner_approved_execution_rehearsal_v1" + assert data["program_status"]["current_task_id"] == "P2-126" + assert data["program_status"]["next_task_id"] == "P2-127" + + rollups = data["rollups"] + assert rollups["execution_rehearsal_count"] == 5 + assert rollups["no_write_apply_check_count"] == 5 + assert rollups["post_write_verifier_rehearsal_count"] == 5 + assert rollups["rollback_drill_count"] == 5 + assert rollups["blocked_live_apply_count"] == 6 + assert rollups["operator_action_count"] == 5 + assert rollups["writer_apply_count"] == 0 + assert rollups["execution_apply_count"] == 0 + assert rollups["receipt_write_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["bot_api_call_count"] == 0 + assert rollups["production_write_count"] == 0 + assert {item["no_write_only"] for item in data["execution_rehearsals"]} == {True} + assert {item["live_apply_enabled"] for item in data["execution_rehearsals"]} == {False} + assert {item["live_read_enabled"] for item in data["post_write_verifier_rehearsals"]} == {False} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index cd3dc7846..287cac3c2 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -5846,6 +5846,38 @@ "rollbackConfirmed": "rollback 已確認={value}" } }, + "resultCaptureOwnerApprovedExecutionRehearsal": { + "title": "P2-126 owner-approved execution rehearsal / no-write apply gate", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 owner promotion review", + "truthTitle": "execution rehearsal 真相", + "metrics": { + "overall": "完成度", + "rehearsals": "Execution rehearsals", + "applyChecks": "No-write apply checks", + "verifiers": "Verifier rehearsals", + "rollbackDrills": "Rollback drills", + "blockers": "已阻擋 live apply", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "liveWrites": "正式寫入 / 發送" + }, + "flags": { + "ownerPromotionLoaded": "已載入 owner promotion={value}", + "noWriteReady": "no-write rehearsal ready={value}", + "rollbackReady": "rollback drill ready={value}" + }, + "labels": { + "writerApply": "writer 套用={value}", + "executionApply": "execution 套用={value}", + "gatewayWrites": "Gateway 寫入={value}", + "noWriteOnly": "no-write only={value}", + "liveApply": "live apply={value}", + "rollbackRequired": "需要 rollback owner={value}", + "rollbackConfirmed": "rollback 已確認={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 cd3dc7846..287cac3c2 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -5846,6 +5846,38 @@ "rollbackConfirmed": "rollback 已確認={value}" } }, + "resultCaptureOwnerApprovedExecutionRehearsal": { + "title": "P2-126 owner-approved execution rehearsal / no-write apply gate", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 owner promotion review", + "truthTitle": "execution rehearsal 真相", + "metrics": { + "overall": "完成度", + "rehearsals": "Execution rehearsals", + "applyChecks": "No-write apply checks", + "verifiers": "Verifier rehearsals", + "rollbackDrills": "Rollback drills", + "blockers": "已阻擋 live apply", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "liveWrites": "正式寫入 / 發送" + }, + "flags": { + "ownerPromotionLoaded": "已載入 owner promotion={value}", + "noWriteReady": "no-write rehearsal ready={value}", + "rollbackReady": "rollback drill ready={value}" + }, + "labels": { + "writerApply": "writer 套用={value}", + "executionApply": "execution 套用={value}", + "gatewayWrites": "Gateway 寫入={value}", + "noWriteOnly": "no-write only={value}", + "liveApply": "live apply={value}", + "rollbackRequired": "需要 rollback owner={value}", + "rollbackConfirmed": "rollback 已確認={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 e84b9d69f..7d48dc049 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 @@ -73,6 +73,7 @@ import { type AiAgentResultCaptureWriterDryRunFixtureSnapshot, type AiAgentResultCaptureWriterDryRunReadbackSnapshot, type AiAgentResultCaptureOwnerPromotionReviewSnapshot, + type AiAgentResultCaptureOwnerApprovedExecutionRehearsalSnapshot, type AiAgentResultCaptureWriterImplementationReviewSnapshot, type AiAgentOwnerApprovedFixturePromotionGateSnapshot, type AiAgentRuntimeWorkerShadowGateSnapshot, @@ -469,6 +470,7 @@ export function AutomationInventoryTab() { const [resultCaptureWriterDryRunFixture, setResultCaptureWriterDryRunFixture] = useState(null) const [resultCaptureWriterDryRunReadback, setResultCaptureWriterDryRunReadback] = useState(null) const [resultCaptureOwnerPromotionReview, setResultCaptureOwnerPromotionReview] = useState(null) + const [resultCaptureOwnerApprovedExecutionRehearsal, setResultCaptureOwnerApprovedExecutionRehearsal] = useState(null) const [reportTruthActionabilityReview, setReportTruthActionabilityReview] = useState(null) const [ownerDryRunPackage, setOwnerDryRunPackage] = useState(null) const [hostStatefulInventory, setHostStatefulInventory] = useState(null) @@ -531,6 +533,7 @@ export function AutomationInventoryTab() { apiClient.getAiAgentResultCaptureWriterDryRunFixture(), apiClient.getAiAgentResultCaptureWriterDryRunReadback(), apiClient.getAiAgentResultCaptureOwnerPromotionReview(), + apiClient.getAiAgentResultCaptureOwnerApprovedExecutionRehearsal(), apiClient.getAiAgentReportTruthActionabilityReview(), apiClient.getAiAgentOwnerApprovedFixtureDryRun(), apiClient.getAiAgentHostStatefulVersionInventory(), @@ -592,6 +595,7 @@ export function AutomationInventoryTab() { resultCaptureWriterDryRunFixtureResult, resultCaptureWriterDryRunReadbackResult, resultCaptureOwnerPromotionReviewResult, + resultCaptureOwnerApprovedExecutionRehearsalResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -650,6 +654,7 @@ export function AutomationInventoryTab() { setResultCaptureWriterDryRunFixture(resultCaptureWriterDryRunFixtureResult.status === 'fulfilled' ? resultCaptureWriterDryRunFixtureResult.value : null) setResultCaptureWriterDryRunReadback(resultCaptureWriterDryRunReadbackResult.status === 'fulfilled' ? resultCaptureWriterDryRunReadbackResult.value : null) setResultCaptureOwnerPromotionReview(resultCaptureOwnerPromotionReviewResult.status === 'fulfilled' ? resultCaptureOwnerPromotionReviewResult.value : null) + setResultCaptureOwnerApprovedExecutionRehearsal(resultCaptureOwnerApprovedExecutionRehearsalResult.status === 'fulfilled' ? resultCaptureOwnerApprovedExecutionRehearsalResult.value : null) setReportTruthActionabilityReview(reportTruthActionabilityReviewResult.status === 'fulfilled' ? reportTruthActionabilityReviewResult.value : null) setOwnerDryRunPackage(ownerDryRunPackageResult.status === 'fulfilled' ? ownerDryRunPackageResult.value : null) setHostStatefulInventory(hostStatefulInventoryResult.status === 'fulfilled' ? hostStatefulInventoryResult.value : null) @@ -706,6 +711,7 @@ export function AutomationInventoryTab() { resultCaptureWriterDryRunFixtureResult, resultCaptureWriterDryRunReadbackResult, resultCaptureOwnerPromotionReviewResult, + resultCaptureOwnerApprovedExecutionRehearsalResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -1948,7 +1954,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 || !resultCaptureWriterDryRunReadback || !resultCaptureOwnerPromotionReview || !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 || !resultCaptureOwnerPromotionReview || !resultCaptureOwnerApprovedExecutionRehearsal || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { return (
@@ -2822,6 +2828,40 @@ export function AutomationInventoryTab() { + resultCaptureOwnerPromotionReview.rollups.playbook_trust_write_count + resultCaptureOwnerPromotionReview.rollups.production_write_count ) + const resultCaptureExecutionRehearsalOverall = resultCaptureOwnerApprovedExecutionRehearsal.program_status.overall_completion_percent + const resultCaptureExecutionRehearsals = resultCaptureOwnerApprovedExecutionRehearsal.rollups.execution_rehearsal_count + const resultCaptureExecutionApplyChecks = resultCaptureOwnerApprovedExecutionRehearsal.rollups.no_write_apply_check_count + const resultCaptureExecutionVerifiers = resultCaptureOwnerApprovedExecutionRehearsal.rollups.post_write_verifier_rehearsal_count + const resultCaptureExecutionRollbacks = resultCaptureOwnerApprovedExecutionRehearsal.rollups.rollback_drill_count + const resultCaptureExecutionBlockers = resultCaptureOwnerApprovedExecutionRehearsal.rollups.blocked_live_apply_count + const resultCaptureExecutionActions = resultCaptureOwnerApprovedExecutionRehearsal.rollups.operator_action_count + const resultCaptureExecutionApprovalRequired = ( + resultCaptureOwnerApprovedExecutionRehearsal.rollups.approval_required_rehearsal_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.approval_required_check_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.approval_required_verifier_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.approval_required_rollback_count + ) + const resultCaptureExecutionBlocked = ( + resultCaptureOwnerApprovedExecutionRehearsal.rollups.blocked_rehearsal_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.blocked_check_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.blocked_verifier_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.blocked_rollback_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.critical_blocker_count + ) + const resultCaptureExecutionLiveWrites = ( + resultCaptureOwnerApprovedExecutionRehearsal.rollups.writer_apply_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.execution_apply_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.receipt_write_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.reviewer_queue_write_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.gateway_queue_write_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.telegram_send_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.bot_api_call_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.report_receipt_write_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.result_capture_write_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.learning_write_count + + resultCaptureOwnerApprovedExecutionRehearsal.rollups.playbook_trust_write_count + + resultCaptureOwnerApprovedExecutionRehearsal.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 @@ -7371,6 +7411,127 @@ export function AutomationInventoryTab() {
+
+
+
+ + + {t('resultCaptureOwnerApprovedExecutionRehearsal.title')} + +
+ +
+ +
+ } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
+ +
+
+ {t('resultCaptureOwnerApprovedExecutionRehearsal.priorTitle')} + + {resultCaptureOwnerApprovedExecutionRehearsal.prior_result_capture_owner_promotion_review.readiness_note} + +
+ + + +
+
+ +
+ {t('resultCaptureOwnerApprovedExecutionRehearsal.truthTitle')} + + {resultCaptureOwnerApprovedExecutionRehearsal.execution_rehearsal_truth.truth_note} + +
+ + + + + + +
+
+
+ +
+ {resultCaptureOwnerApprovedExecutionRehearsal.execution_rehearsals.slice(0, 5).map(rehearsal => ( +
+
+ + {rehearsal.display_name} + + +
+ + {rehearsal.source_packet} → {rehearsal.expected_output} + +
+ + + + +
+
+ ))} +
+ +
+ {resultCaptureOwnerApprovedExecutionRehearsal.no_write_apply_checks.slice(0, 5).map(check => ( +
+
+ + {check.display_name} + + +
+ + {check.failure_if_missing} + + +
+ ))} +
+ +
+ {resultCaptureOwnerApprovedExecutionRehearsal.rollback_drills.slice(0, 5).map(drill => ( +
+
+ + {drill.display_name} + + +
+ + {drill.drill_note} + +
+ + + +
+
+ ))} +
+
+
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 702a05d8f..db2f3bb00 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -525,6 +525,11 @@ export const apiClient = { return handleResponse(res) }, + async getAiAgentResultCaptureOwnerApprovedExecutionRehearsal() { + const res = await fetch(`${API_BASE_URL}/agents/agent-result-capture-owner-approved-execution-rehearsal`) + return handleResponse(res) + }, + async getAiAgentOwnerApprovedFixtureDryRun() { const res = await fetch(`${API_BASE_URL}/agents/agent-owner-approved-fixture-dry-run`) return handleResponse(res) @@ -6549,6 +6554,193 @@ export interface AiAgentResultCaptureOwnerPromotionReviewSnapshot { } } +export interface AiAgentResultCaptureOwnerApprovedExecutionRehearsalSnapshot { + schema_version: 'ai_agent_result_capture_owner_approved_execution_rehearsal_v1' + generated_at: string + program_status: { + overall_completion_percent: number + current_priority: 'P0' | 'P1' | 'P2' | 'P3' + current_task_id: 'P2-126' + next_task_id: 'P2-127' + read_only_mode: true + runtime_authority: 'result_capture_owner_approved_execution_rehearsal_only_no_live_write' + status_note: string + } + source_refs: string[] + prior_result_capture_owner_promotion_review: { + schema_version: string + owner_promotion_packet_count: number + execution_gate_check_count: number + rollback_owner_review_count: number + blocked_execution_write_count: number + operator_action_count: number + owner_promotion_approved_count: number + execution_gate_pass_count: number + rollback_owner_confirmed_count: number + post_write_verifier_ready_count: number + writer_apply_count: number + execution_apply_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 + } + execution_rehearsal_truth: { + p2_125_owner_promotion_loaded: boolean + owner_approval_record_required: boolean + no_write_apply_rehearsal_ready: boolean + post_write_verifier_rehearsal_ready: boolean + rollback_drill_ready: boolean + idempotency_replay_required: boolean + redaction_review_required: boolean + execution_rehearsal_only: boolean + owner_approval_record_received: boolean + no_write_apply_executed: boolean + post_write_verifier_passed: boolean + rollback_drill_confirmed: boolean + writer_apply_enabled: boolean + execution_apply_enabled: boolean + receipt_write_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_record_received_count: number + no_write_apply_execution_count: number + post_write_verifier_pass_count: number + rollback_drill_confirmed_count: number + writer_apply_count: number + execution_apply_count: number + receipt_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 + 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 + } + execution_rehearsals: Array<{ + rehearsal_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + source_packet: string + status: 'ready_for_rehearsal_review' | 'approval_required' | 'blocked_by_policy' + rehearsal_mode: 'no_write_apply_rehearsal' + no_write_only: true + live_apply_enabled: false + expected_output: string + evidence_hash: string + }> + no_write_apply_checks: Array<{ + check_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready_for_rehearsal_review' | 'approval_required' | 'blocked_by_policy' + check_mode: 'no_write_apply_gate' + required_before_apply: true + live_apply_enabled: false + failure_if_missing: string + }> + post_write_verifier_rehearsals: Array<{ + verifier_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready_for_rehearsal_review' | 'approval_required' | 'blocked_by_policy' + verifier_mode: 'fixture_only_no_live_read' + verifier_ready: false + live_read_enabled: false + expected_signal: string + }> + rollback_drills: Array<{ + drill_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready_for_rehearsal_review' | 'approval_required' | 'blocked_by_policy' + rollback_owner_required: true + rollback_drill_confirmed: false + drill_note: string + }> + blocked_live_applies: 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: { + execution_rehearsal_count: number + no_write_apply_check_count: number + post_write_verifier_rehearsal_count: number + rollback_drill_count: number + blocked_live_apply_count: number + operator_action_count: number + approval_required_rehearsal_count: number + blocked_rehearsal_count: number + approval_required_check_count: number + blocked_check_count: number + approval_required_verifier_count: number + blocked_verifier_count: number + approval_required_rollback_count: number + blocked_rollback_count: number + critical_blocker_count: number + owner_approval_record_received_count: number + no_write_apply_execution_count: number + post_write_verifier_pass_count: number + rollback_drill_confirmed_count: number + writer_apply_count: number + execution_apply_count: number + receipt_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 + 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_owner_approved_execution_rehearsal_2026-06-14.json b/docs/evaluations/ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json new file mode 100644 index 000000000..8c811e9de --- /dev/null +++ b/docs/evaluations/ai_agent_result_capture_owner_approved_execution_rehearsal_2026-06-14.json @@ -0,0 +1,439 @@ +{ + "schema_version": "ai_agent_result_capture_owner_approved_execution_rehearsal_v1", + "generated_at": "2026-06-14T01:55:00+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P2", + "current_task_id": "P2-126", + "next_task_id": "P2-127", + "read_only_mode": true, + "runtime_authority": "result_capture_owner_approved_execution_rehearsal_only_no_live_write", + "status_note": "P2-126 只建立 owner-approved execution rehearsal、no-write apply gate、post-write verifier rehearsal 與 rollback drill;不得套用 writer、寫 receipt、寫 result capture 或寫入 production。" + }, + "source_refs": [ + "docs/evaluations/ai_agent_result_capture_owner_promotion_review_2026-06-14.json", + "docs/evaluations/ai_agent_result_capture_writer_dry_run_readback_2026-06-14.json", + "docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md#46-建立-result-capture-owner-promotion-review--execution-gate" + ], + "prior_result_capture_owner_promotion_review": { + "schema_version": "ai_agent_result_capture_owner_promotion_review_v1", + "owner_promotion_packet_count": 5, + "execution_gate_check_count": 5, + "rollback_owner_review_count": 5, + "blocked_execution_write_count": 6, + "operator_action_count": 5, + "owner_promotion_approved_count": 0, + "execution_gate_pass_count": 0, + "rollback_owner_confirmed_count": 0, + "post_write_verifier_ready_count": 0, + "writer_apply_count": 0, + "execution_apply_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-125 已提供 owner promotion review、execution gate 與 rollback owner review;P2-126 只讀回這些輸出並建立 no-write execution rehearsal。" + }, + "execution_rehearsal_truth": { + "p2_125_owner_promotion_loaded": true, + "owner_approval_record_required": true, + "no_write_apply_rehearsal_ready": true, + "post_write_verifier_rehearsal_ready": true, + "rollback_drill_ready": true, + "idempotency_replay_required": true, + "redaction_review_required": true, + "execution_rehearsal_only": true, + "owner_approval_record_received": false, + "no_write_apply_executed": false, + "post_write_verifier_passed": false, + "rollback_drill_confirmed": false, + "writer_apply_enabled": false, + "execution_apply_enabled": false, + "receipt_write_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_record_received_count": 0, + "no_write_apply_execution_count": 0, + "post_write_verifier_pass_count": 0, + "rollback_drill_confirmed_count": 0, + "writer_apply_count": 0, + "execution_apply_count": 0, + "receipt_write_count": 0, + "reviewer_queue_write_count": 0, + "gateway_queue_write_count": 0, + "telegram_send_count": 0, + "bot_api_call_count": 0, + "report_receipt_write_count": 0, + "result_capture_write_count": 0, + "learning_write_count": 0, + "playbook_trust_write_count": 0, + "production_write_count": 0, + "secret_read_count": 0, + "destructive_operation_count": 0, + "truth_note": "P2-126 只演練 owner-approved execution path,不套用任何 writer、不寫 queue、不發 Telegram、不呼叫 Bot API、不寫 production。" + }, + "execution_rehearsals": [ + { + "rehearsal_id": "rehearsal_result_capture_writer", + "display_name": "Result capture writer no-write rehearsal", + "owner_agent": "openclaw", + "source_packet": "packet_result_capture_writer", + "status": "ready_for_rehearsal_review", + "rehearsal_mode": "no_write_apply_rehearsal", + "no_write_only": true, + "live_apply_enabled": false, + "expected_output": "產生 result capture writer input preview、idempotency key 與 verifier rehearsal,不寫 result_capture。", + "evidence_hash": "sha256:e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1" + }, + { + "rehearsal_id": "rehearsal_learning_writer", + "display_name": "Learning writer no-write rehearsal", + "owner_agent": "hermes", + "source_packet": "packet_learning_writer", + "status": "approval_required", + "rehearsal_mode": "no_write_apply_rehearsal", + "no_write_only": true, + "live_apply_enabled": false, + "expected_output": "產生 KM / learning write preview、stale guard 與 owner review handoff,不寫 learning。", + "evidence_hash": "sha256:e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2" + }, + { + "rehearsal_id": "rehearsal_playbook_trust_writer", + "display_name": "PlayBook trust no-write rehearsal", + "owner_agent": "nemotron", + "source_packet": "packet_playbook_trust_writer", + "status": "blocked_by_policy", + "rehearsal_mode": "no_write_apply_rehearsal", + "no_write_only": true, + "live_apply_enabled": false, + "expected_output": "產生 trust delta preview、negative reinforcement check 與 rollback rehearsal,不寫 PlayBook trust。", + "evidence_hash": "sha256:e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3" + }, + { + "rehearsal_id": "rehearsal_reviewer_queue_writer", + "display_name": "Reviewer queue no-write rehearsal", + "owner_agent": "openclaw", + "source_packet": "packet_reviewer_queue_writer", + "status": "approval_required", + "rehearsal_mode": "no_write_apply_rehearsal", + "no_write_only": true, + "live_apply_enabled": false, + "expected_output": "產生 reviewer queue preview、dedupe key 與 SLA handoff,不寫 reviewer queue。", + "evidence_hash": "sha256:e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4" + }, + { + "rehearsal_id": "rehearsal_gateway_queue_writer", + "display_name": "Gateway queue no-write rehearsal", + "owner_agent": "nemotron", + "source_packet": "packet_gateway_queue_writer", + "status": "ready_for_rehearsal_review", + "rehearsal_mode": "no_write_apply_rehearsal", + "no_write_only": true, + "live_apply_enabled": false, + "expected_output": "產生 AwoooI SRE 戰情室 queue preview、route lock 與 redaction check,不寫 Gateway queue。", + "evidence_hash": "sha256:e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5" + } + ], + "no_write_apply_checks": [ + { + "check_id": "check_owner_approval_record", + "display_name": "Owner approval record check", + "owner_agent": "openclaw", + "status": "approval_required", + "check_mode": "no_write_apply_gate", + "required_before_apply": true, + "live_apply_enabled": false, + "failure_if_missing": "缺 owner approval record 時不得進 no-write apply rehearsal。" + }, + { + "check_id": "check_idempotency_replay", + "display_name": "Idempotency replay check", + "owner_agent": "nemotron", + "status": "ready_for_rehearsal_review", + "check_mode": "no_write_apply_gate", + "required_before_apply": true, + "live_apply_enabled": false, + "failure_if_missing": "缺 idempotency replay 時不得提升 writer。" + }, + { + "check_id": "check_payload_redaction", + "display_name": "Payload redaction check", + "owner_agent": "hermes", + "status": "ready_for_rehearsal_review", + "check_mode": "no_write_apply_gate", + "required_before_apply": true, + "live_apply_enabled": false, + "failure_if_missing": "缺 redaction review 時不得產生 queue / Telegram payload。" + }, + { + "check_id": "check_post_write_verifier", + "display_name": "Post-write verifier rehearsal check", + "owner_agent": "openclaw", + "status": "approval_required", + "check_mode": "no_write_apply_gate", + "required_before_apply": true, + "live_apply_enabled": false, + "failure_if_missing": "缺 post-write verifier rehearsal 時不得寫 result capture。" + }, + { + "check_id": "check_production_write_block", + "display_name": "Production write block check", + "owner_agent": "nemotron", + "status": "blocked_by_policy", + "check_mode": "no_write_apply_gate", + "required_before_apply": true, + "live_apply_enabled": false, + "failure_if_missing": "未有正式維護窗口與 rollback owner 前不得寫 production。" + } + ], + "post_write_verifier_rehearsals": [ + { + "verifier_id": "verifier_result_capture", + "display_name": "Result capture verifier rehearsal", + "owner_agent": "openclaw", + "status": "ready_for_rehearsal_review", + "verifier_mode": "fixture_only_no_live_read", + "verifier_ready": false, + "live_read_enabled": false, + "expected_signal": "result_capture preview hash 與 audit trail preview 對齊。" + }, + { + "verifier_id": "verifier_learning", + "display_name": "Learning verifier rehearsal", + "owner_agent": "hermes", + "status": "approval_required", + "verifier_mode": "fixture_only_no_live_read", + "verifier_ready": false, + "live_read_enabled": false, + "expected_signal": "KM draft / stale guard preview 對齊 owner scope。" + }, + { + "verifier_id": "verifier_playbook_trust", + "display_name": "PlayBook trust verifier rehearsal", + "owner_agent": "nemotron", + "status": "blocked_by_policy", + "verifier_mode": "fixture_only_no_live_read", + "verifier_ready": false, + "live_read_enabled": false, + "expected_signal": "trust delta preview 與 negative reinforcement rule 對齊。" + }, + { + "verifier_id": "verifier_reviewer_queue", + "display_name": "Reviewer queue verifier rehearsal", + "owner_agent": "openclaw", + "status": "approval_required", + "verifier_mode": "fixture_only_no_live_read", + "verifier_ready": false, + "live_read_enabled": false, + "expected_signal": "reviewer queue preview、dedupe key 與 SLA route 對齊。" + }, + { + "verifier_id": "verifier_gateway_queue", + "display_name": "Gateway queue verifier rehearsal", + "owner_agent": "nemotron", + "status": "ready_for_rehearsal_review", + "verifier_mode": "fixture_only_no_live_read", + "verifier_ready": false, + "live_read_enabled": false, + "expected_signal": "AwoooI SRE 戰情室 route lock 與 redaction policy 對齊。" + } + ], + "rollback_drills": [ + { + "drill_id": "rollback_drill_result_capture", + "display_name": "Result capture rollback drill", + "owner_agent": "openclaw", + "status": "ready_for_rehearsal_review", + "rollback_owner_required": true, + "rollback_drill_confirmed": false, + "drill_note": "演練撤回 result capture preview 與 audit trail append,不動 production。" + }, + { + "drill_id": "rollback_drill_learning", + "display_name": "Learning rollback drill", + "owner_agent": "hermes", + "status": "approval_required", + "rollback_owner_required": true, + "rollback_drill_confirmed": false, + "drill_note": "演練撤回 KM / learning preview 與 stale guard,不寫 canonical KM。" + }, + { + "drill_id": "rollback_drill_playbook_trust", + "display_name": "PlayBook trust rollback drill", + "owner_agent": "nemotron", + "status": "blocked_by_policy", + "rollback_owner_required": true, + "rollback_drill_confirmed": false, + "drill_note": "演練撤回 trust delta preview 與 failure reinforcement,不寫 trust history。" + }, + { + "drill_id": "rollback_drill_reviewer_queue", + "display_name": "Reviewer queue rollback drill", + "owner_agent": "openclaw", + "status": "approval_required", + "rollback_owner_required": true, + "rollback_drill_confirmed": false, + "drill_note": "演練撤回 reviewer queue preview 與 dedupe key,不寫 queue。" + }, + { + "drill_id": "rollback_drill_gateway_queue", + "display_name": "Gateway queue rollback drill", + "owner_agent": "nemotron", + "status": "ready_for_rehearsal_review", + "rollback_owner_required": true, + "rollback_drill_confirmed": false, + "drill_note": "演練撤回 Gateway queue preview 與 route lock,不發 Telegram。" + } + ], + "blocked_live_applies": [ + { + "blocker_id": "blocked_writer_apply", + "display_name": "Writer apply blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "writer_apply", + "blocked_until": "P2-127 owner acceptance + maintenance window + rollback owner", + "evidence_hash": "sha256:f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1" + }, + { + "blocker_id": "blocked_execution_apply", + "display_name": "Execution apply blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "execution_apply", + "blocked_until": "P2-127 execution acceptance", + "evidence_hash": "sha256:f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2" + }, + { + "blocker_id": "blocked_receipt_write", + "display_name": "Receipt write blocked", + "severity": "high", + "status": "approval_required", + "blocked_action": "receipt_write", + "blocked_until": "post-write verifier rehearsal passes", + "evidence_hash": "sha256:f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3" + }, + { + "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": "dual approval + owner acceptance", + "evidence_hash": "sha256:f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4" + }, + { + "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": "AwoooI SRE route owner accepts queue write", + "evidence_hash": "sha256:f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5" + }, + { + "blocker_id": "blocked_telegram_send", + "display_name": "Telegram send blocked", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "telegram_send", + "blocked_until": "Gateway queue E2E receipt + SRE route lock", + "evidence_hash": "sha256:f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6" + } + ], + "operator_actions": [ + { + "action_id": "review_execution_rehearsals", + "display_name": "審查 execution rehearsal", + "owner_agent": "openclaw", + "operator_instruction": "確認每個 rehearsal 仍是 no-write,並核對 owner approval record 是否完整。", + "runtime_write_allowed": false + }, + { + "action_id": "verify_no_write_apply_checks", + "display_name": "檢查 no-write apply gate", + "owner_agent": "hermes", + "operator_instruction": "逐項核對 owner approval、idempotency replay、payload redaction 與 post-write verifier rehearsal。", + "runtime_write_allowed": false + }, + { + "action_id": "inspect_post_write_verifier_rehearsal", + "display_name": "檢查 verifier rehearsal", + "owner_agent": "nemotron", + "operator_instruction": "確認 verifier rehearsal 只看 fixture / preview,不讀 canonical runtime target。", + "runtime_write_allowed": false + }, + { + "action_id": "confirm_rollback_drills", + "display_name": "確認 rollback drill", + "owner_agent": "openclaw", + "operator_instruction": "確認每個 rollback drill 有 owner、撤回路線與不寫 production 的證據。", + "runtime_write_allowed": false + }, + { + "action_id": "hold_live_apply_gate", + "display_name": "維持 live apply gate 關閉", + "owner_agent": "hermes", + "operator_instruction": "P2-126 完成前不得開 writer apply、queue write、Telegram send 或 production 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": "只顯示 rehearsal id、owner、status、redacted hash 與 operator instruction;不顯示 prompt、私有推理、secret、raw payload、chat id 或內部協作內容。" + }, + "rollups": { + "execution_rehearsal_count": 5, + "no_write_apply_check_count": 5, + "post_write_verifier_rehearsal_count": 5, + "rollback_drill_count": 5, + "blocked_live_apply_count": 6, + "operator_action_count": 5, + "approval_required_rehearsal_count": 2, + "blocked_rehearsal_count": 1, + "approval_required_check_count": 2, + "blocked_check_count": 1, + "approval_required_verifier_count": 2, + "blocked_verifier_count": 1, + "approval_required_rollback_count": 2, + "blocked_rollback_count": 1, + "critical_blocker_count": 5, + "owner_approval_record_received_count": 0, + "no_write_apply_execution_count": 0, + "post_write_verifier_pass_count": 0, + "rollback_drill_confirmed_count": 0, + "writer_apply_count": 0, + "execution_apply_count": 0, + "receipt_write_count": 0, + "reviewer_queue_write_count": 0, + "gateway_queue_write_count": 0, + "telegram_send_count": 0, + "bot_api_call_count": 0, + "report_receipt_write_count": 0, + "result_capture_write_count": 0, + "learning_write_count": 0, + "playbook_trust_write_count": 0, + "production_write_count": 0, + "secret_read_count": 0, + "destructive_operation_count": 0 + } +} diff --git a/docs/schemas/ai_agent_result_capture_owner_approved_execution_rehearsal_v1.schema.json b/docs/schemas/ai_agent_result_capture_owner_approved_execution_rehearsal_v1.schema.json new file mode 100644 index 000000000..c058da78a --- /dev/null +++ b/docs/schemas/ai_agent_result_capture_owner_approved_execution_rehearsal_v1.schema.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.wooo.work/schemas/ai_agent_result_capture_owner_approved_execution_rehearsal_v1.schema.json", + "title": "AI Agent result capture owner-approved execution rehearsal v1", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "program_status", + "source_refs", + "prior_result_capture_owner_promotion_review", + "execution_rehearsal_truth", + "execution_rehearsals", + "no_write_apply_checks", + "post_write_verifier_rehearsals", + "rollback_drills", + "blocked_live_applies", + "operator_actions", + "display_redaction_contract", + "rollups" + ], + "properties": { + "schema_version": { + "const": "ai_agent_result_capture_owner_approved_execution_rehearsal_v1" + }, + "generated_at": { + "type": "string" + }, + "program_status": { + "type": "object" + }, + "source_refs": { + "type": "array", + "items": { + "type": "string" + } + }, + "prior_result_capture_owner_promotion_review": { + "type": "object" + }, + "execution_rehearsal_truth": { + "type": "object" + }, + "execution_rehearsals": { + "type": "array", + "minItems": 5, + "maxItems": 5, + "items": { + "type": "object" + } + }, + "no_write_apply_checks": { + "type": "array", + "minItems": 5, + "maxItems": 5, + "items": { + "type": "object" + } + }, + "post_write_verifier_rehearsals": { + "type": "array", + "minItems": 5, + "maxItems": 5, + "items": { + "type": "object" + } + }, + "rollback_drills": { + "type": "array", + "minItems": 5, + "maxItems": 5, + "items": { + "type": "object" + } + }, + "blocked_live_applies": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "items": { + "type": "object" + } + }, + "operator_actions": { + "type": "array", + "minItems": 5, + "maxItems": 5, + "items": { + "type": "object" + } + }, + "display_redaction_contract": { + "type": "object" + }, + "rollups": { + "type": "object" + } + }, + "additionalProperties": false +}