From f3c3dc84205e93062ae3512cdb2af67e40f9c8d0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 13 Jun 2026 22:00:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(governance):=20=E6=96=B0=E5=A2=9E=20result?= =?UTF-8?q?=20capture=20promotion=20dry-run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/api/v1/agents.py | 32 ++ ...proved_result_capture_promotion_dry_run.py | 370 ++++++++++++++++++ ...proved_result_capture_promotion_dry_run.py | 112 ++++++ ...ed_result_capture_promotion_dry_run_api.py | 40 ++ apps/web/messages/en.json | 34 ++ apps/web/messages/zh-TW.json | 34 ++ .../tabs/automation-inventory-tab.tsx | 175 ++++++++- apps/web/src/lib/api-client.ts | 165 ++++++++ ..._capture_promotion_dry_run_2026-06-13.json | 370 ++++++++++++++++++ ...t_capture_promotion_dry_run_v1.schema.json | 357 +++++++++++++++++ 10 files changed, 1688 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/services/ai_agent_owner_approved_result_capture_promotion_dry_run.py create mode 100644 apps/api/tests/test_ai_agent_owner_approved_result_capture_promotion_dry_run.py create mode 100644 apps/api/tests/test_ai_agent_owner_approved_result_capture_promotion_dry_run_api.py create mode 100644 docs/evaluations/ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json create mode 100644 docs/schemas/ai_agent_owner_approved_result_capture_promotion_dry_run_v1.schema.json diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index a0c1acf77..59f552d42 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -97,6 +97,9 @@ from src.services.ai_agent_owner_approved_learning_dry_run import ( from src.services.ai_agent_owner_approved_result_capture_dry_run import ( load_latest_ai_agent_owner_approved_result_capture_dry_run, ) +from src.services.ai_agent_owner_approved_result_capture_promotion_dry_run import ( + load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run, +) from src.services.ai_agent_owner_approved_result_capture_readback import ( load_latest_ai_agent_owner_approved_result_capture_readback, ) @@ -1656,6 +1659,35 @@ async def get_agent_result_capture_promotion_approval_gate() -> dict[str, Any]: ) from exc +@router.get( + "/agent-owner-approved-result-capture-promotion-dry-run", + response_model=dict[str, Any], + summary="取得 AI Agent owner-approved result capture promotion dry-run", + description=( + "讀取最新已提交的 P2-120 owner-approved result capture promotion dry-run;" + "此端點只回傳 dry-run template、owner acceptance fixture、verifier、blocked runtime promotion " + "與 operator handoff,不寫 result capture、learning、PlayBook trust、Gateway queue," + "不送 Telegram、不呼叫 Bot API、不讀 canonical runtime target、不讀 secret。" + ), +) +async def get_agent_owner_approved_result_capture_promotion_dry_run() -> dict[str, Any]: + """Return the latest read-only owner-approved result capture promotion dry-run package.""" + try: + payload = await asyncio.to_thread(load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run) + 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_owner_approved_result_capture_promotion_dry_run_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="AI Agent owner-approved result capture promotion dry-run 無效", + ) from exc + + @router.get( "/agent-owner-approved-fixture-dry-run", response_model=dict[str, Any], diff --git a/apps/api/src/services/ai_agent_owner_approved_result_capture_promotion_dry_run.py b/apps/api/src/services/ai_agent_owner_approved_result_capture_promotion_dry_run.py new file mode 100644 index 000000000..929aad3f2 --- /dev/null +++ b/apps/api/src/services/ai_agent_owner_approved_result_capture_promotion_dry_run.py @@ -0,0 +1,370 @@ +""" +AI Agent owner-approved result capture promotion dry-run snapshot. + +Loads the latest committed P2-120 owner-approved result capture promotion dry-run +package. This module validates committed evidence only; it never writes result +captures, writes learning records, updates PlayBook trust, writes 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_owner_approved_result_capture_promotion_dry_run_*.json" +_SCHEMA_VERSION = "ai_agent_owner_approved_result_capture_promotion_dry_run_v1" +_RUNTIME_AUTHORITY = "owner_approved_result_capture_promotion_dry_run_only_no_live_write" +_TARGET_DRY_RUN = "result_capture_promotion_dry_run_preview" + + +def load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed owner-approved result capture promotion dry-run package.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError(f"no AI Agent owner-approved result capture promotion dry-run 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_templates(payload, label) + _require_fixtures(payload, label) + _require_verifier_checks(payload, label) + _require_blockers(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-120", + "next_task_id": "P2-121", + "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_promotion_gate") or {} + expected = { + "schema_version": "ai_agent_result_capture_promotion_approval_gate_v1", + "promotion_approval_packet_count": 5, + "acceptance_gate_template_count": 5, + "promotion_verifier_check_count": 5, + "blocked_promotion_write_count": 5, + "operator_action_count": 5, + "owner_approval_received_count": 0, + "capture_promotion_approved_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_promotion_gate mismatch: {mismatches}") + if not prior.get("readiness_note"): + raise ValueError(f"{label}: prior_result_capture_promotion_gate.readiness_note is required") + + +def _require_truth(payload: dict[str, Any], label: str) -> None: + truth = payload.get("dry_run_truth") or {} + required_true = { + "p2_119_promotion_gate_loaded", + "owner_approval_required", + "dry_run_preview_allowed", + "promotion_packet_ready", + "acceptance_template_ready", + "verifier_dry_run_ready", + "operator_handoff_ready", + } + missing = sorted(field for field in required_true if truth.get(field) is not True) + if missing: + raise ValueError(f"{label}: dry-run ready flags must remain true: {missing}") + for field in {"owner_approval_received", "capture_promotion_approved"}: + if truth.get(field) is not False: + raise ValueError(f"{label}: {field} must remain false before live write") + required_false = { + "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", + "capture_promotion_approved_count", + "dry_run_preview_generated_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", + } + non_zero = sorted(field for field in zero_counts if truth.get(field) != 0) + if non_zero: + raise ValueError(f"{label}: dry-run live counters must remain zero: {non_zero}") + if not truth.get("truth_note"): + raise ValueError(f"{label}: dry_run_truth.truth_note is required") + + +def _require_templates(payload: dict[str, Any], label: str) -> None: + templates = payload.get("promotion_dry_run_templates") or [] + required = { + "dry_run_promote_action_required_capture", + "dry_run_promote_no_action_capture", + "dry_run_promote_verifier_degraded_capture", + "dry_run_promote_sre_route_capture", + "dry_run_promote_owner_acceptance_capture", + } + template_ids = {template.get("template_id") for template in templates} + if template_ids != required: + raise ValueError(f"{label}: promotion dry-run templates must match {sorted(required)}") + for template in templates: + template_id = template.get("template_id") + if template.get("target_dry_run") != _TARGET_DRY_RUN: + raise ValueError(f"{label}: template {template_id} must target {_TARGET_DRY_RUN}") + if template.get("dry_run_mode") != "preview_only": + raise ValueError(f"{label}: template {template_id} must remain preview_only") + if template.get("runtime_write_enabled") is not False: + raise ValueError(f"{label}: template {template_id} must not enable runtime write") + if template.get("status") not in {"ready_for_dry_run", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: template {template_id} status is invalid") + if not template.get("preview_summary"): + raise ValueError(f"{label}: template {template_id} preview_summary is required") + if not _is_redacted_sha256(template.get("evidence_hash")): + raise ValueError(f"{label}: template {template_id} must expose redacted evidence_hash") + + +def _require_fixtures(payload: dict[str, Any], label: str) -> None: + fixtures = payload.get("owner_acceptance_dry_run_fixtures") or [] + required = { + "fixture_owner_acceptance_record_complete", + "fixture_capture_scope_review", + "fixture_learning_boundary_review", + "fixture_playbook_trust_boundary_review", + "fixture_rollback_reverify_plan_review", + } + fixture_ids = {fixture.get("fixture_id") for fixture in fixtures} + if fixture_ids != required: + raise ValueError(f"{label}: owner acceptance dry-run fixtures must match {sorted(required)}") + for fixture in fixtures: + fixture_id = fixture.get("fixture_id") + if fixture.get("fixture_only") is not True: + raise ValueError(f"{label}: fixture {fixture_id} must remain fixture_only") + if fixture.get("runtime_write_enabled") is not False: + raise ValueError(f"{label}: fixture {fixture_id} must not enable runtime write") + if fixture.get("status") not in {"ready", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: fixture {fixture_id} status is invalid") + if not fixture.get("required_owner") or not fixture.get("verifies"): + raise ValueError(f"{label}: fixture {fixture_id} required_owner and verifies are required") + if not _is_redacted_sha256(fixture.get("evidence_hash")): + raise ValueError(f"{label}: fixture {fixture_id} must expose redacted evidence_hash") + + +def _require_verifier_checks(payload: dict[str, Any], label: str) -> None: + checks = payload.get("dry_run_verifier_checks") or [] + required = { + "no_result_capture_write_during_dry_run", + "no_learning_write_during_dry_run", + "no_playbook_trust_during_dry_run", + "no_gateway_queue_during_dry_run", + "promotion_dry_run_redaction_complete", + } + verifier_ids = {check.get("verifier_id") for check in checks} + if verifier_ids != required: + raise ValueError(f"{label}: dry-run verifier checks must match {sorted(required)}") + for check in checks: + verifier_id = check.get("verifier_id") + if check.get("live_execution_enabled") is not False: + raise ValueError(f"{label}: verifier {verifier_id} must not enable live execution") + if check.get("status") not in {"ready", "approval_required", "blocked_by_policy"}: + raise ValueError(f"{label}: verifier {verifier_id} status is invalid") + if not check.get("verifies") or not check.get("failure_if_missing"): + raise ValueError(f"{label}: verifier {verifier_id} must include verifies and failure_if_missing") + if not _is_redacted_sha256(check.get("evidence_hash")): + raise ValueError(f"{label}: verifier {verifier_id} must expose redacted evidence_hash") + + +def _require_blockers(payload: dict[str, Any], label: str) -> None: + blockers = payload.get("blocked_runtime_promotions") or [] + required = { + "result_capture_dry_run_write_not_authorized", + "learning_dry_run_write_not_authorized", + "playbook_trust_dry_run_write_not_authorized", + "gateway_queue_dry_run_write_not_authorized", + "production_dry_run_write_not_authorized", + } + blocker_ids = {blocker.get("blocker_id") for blocker in blockers} + if blocker_ids != required: + raise ValueError(f"{label}: blocked runtime promotions 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_owner_approved_promotion_dry_run", + "verify_promotion_dry_run_no_write_counts", + "confirm_owner_acceptance_fields", + "check_dry_run_redaction_contract", + "promote_to_p2_121", + } + action_ids = {action.get("action_id") for action in actions} + if action_ids != required: + raise ValueError(f"{label}: operator actions must match {sorted(required)}") + for action in actions: + action_id = action.get("action_id") + if action.get("runtime_write_allowed") is not False: + raise ValueError(f"{label}: action {action_id} must not allow runtime write") + if not action.get("operator_instruction"): + raise ValueError(f"{label}: action {action_id} operator_instruction is required") + + +def _require_display_redaction(payload: dict[str, Any], label: str) -> None: + contract = payload.get("display_redaction_contract") or {} + expected = { + "redaction_required": True, + "raw_prompt_display_allowed": False, + "private_reasoning_display_allowed": False, + "secret_value_display_allowed": False, + "raw_runtime_payload_display_allowed": False, + "internal_collaboration_content_display_allowed": False, + } + mismatches = _mismatches(contract, expected) + if mismatches: + raise ValueError(f"{label}: display_redaction_contract mismatch: {mismatches}") + if not contract.get("frontend_display_policy"): + raise ValueError(f"{label}: display_redaction_contract.frontend_display_policy is required") + + +def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None: + rollups = payload.get("rollups") or {} + templates = payload.get("promotion_dry_run_templates") or [] + fixtures = payload.get("owner_acceptance_dry_run_fixtures") or [] + verifiers = payload.get("dry_run_verifier_checks") or [] + blockers = payload.get("blocked_runtime_promotions") or [] + actions = payload.get("operator_actions") or [] + expected = { + "promotion_dry_run_template_count": len(templates), + "owner_acceptance_fixture_count": len(fixtures), + "dry_run_verifier_check_count": len(verifiers), + "blocked_runtime_promotion_count": len(blockers), + "operator_action_count": len(actions), + "approval_required_template_count": sum(1 for item in templates if item.get("status") == "approval_required"), + "blocked_template_count": sum(1 for item in templates if item.get("status") == "blocked_by_policy"), + "approval_required_fixture_count": sum(1 for item in fixtures if item.get("status") == "approval_required"), + "blocked_fixture_count": sum(1 for item in fixtures if item.get("status") == "blocked_by_policy"), + "approval_required_verifier_count": sum(1 for item in verifiers if item.get("status") == "approval_required"), + "critical_blocker_count": sum(1 for item in blockers if item.get("severity") == "critical"), + "owner_approval_received_count": 0, + "capture_promotion_approved_count": 0, + "dry_run_preview_generated_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_owner_approved_result_capture_promotion_dry_run.py b/apps/api/tests/test_ai_agent_owner_approved_result_capture_promotion_dry_run.py new file mode 100644 index 000000000..65d6f57c2 --- /dev/null +++ b/apps/api/tests/test_ai_agent_owner_approved_result_capture_promotion_dry_run.py @@ -0,0 +1,112 @@ +import copy +import json +from pathlib import Path + +import pytest + +from src.services.ai_agent_owner_approved_result_capture_promotion_dry_run import ( + load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run, +) + + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURE = REPO_ROOT / "docs/evaluations/ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json" + + +def test_load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run_snapshot() -> None: + data = load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run() + + assert data["schema_version"] == "ai_agent_owner_approved_result_capture_promotion_dry_run_v1" + assert data["program_status"]["current_task_id"] == "P2-120" + assert data["program_status"]["next_task_id"] == "P2-121" + assert data["program_status"]["overall_completion_percent"] == 100 + assert data["program_status"]["read_only_mode"] is True + + rollups = data["rollups"] + assert rollups["promotion_dry_run_template_count"] == 5 + assert rollups["owner_acceptance_fixture_count"] == 5 + assert rollups["dry_run_verifier_check_count"] == 5 + assert rollups["blocked_runtime_promotion_count"] == 5 + assert rollups["operator_action_count"] == 5 + assert rollups["approval_required_template_count"] == 2 + assert rollups["blocked_template_count"] == 2 + assert rollups["approval_required_fixture_count"] == 2 + assert rollups["blocked_fixture_count"] == 2 + assert rollups["approval_required_verifier_count"] == 2 + assert rollups["critical_blocker_count"] == 3 + + assert {template["target_dry_run"] for template in data["promotion_dry_run_templates"]} == { + "result_capture_promotion_dry_run_preview" + } + assert {template["dry_run_mode"] for template in data["promotion_dry_run_templates"]} == {"preview_only"} + + zero_fields = [ + "owner_approval_received_count", + "capture_promotion_approved_count", + "dry_run_preview_generated_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_owner_approved_promotion_dry_run_rejects_runtime_write_enabled(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["promotion_dry_run_templates"][0]["runtime_write_enabled"] = True + target = tmp_path / "ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="must not enable runtime write"): + load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run(tmp_path) + + +def test_owner_approved_promotion_dry_run_rejects_truth_write_flag(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["dry_run_truth"]["result_capture_write_enabled"] = True + target = tmp_path / "ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="live read/send/write flags"): + load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run(tmp_path) + + +def test_owner_approved_promotion_dry_run_rejects_target_drift(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["promotion_dry_run_templates"][0]["target_dry_run"] = "live_result_capture_promotion" + target = tmp_path / "ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="must target result_capture_promotion_dry_run_preview"): + load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run(tmp_path) + + +def test_owner_approved_promotion_dry_run_rejects_rollup_drift(tmp_path: Path) -> None: + source = json.loads(FIXTURE.read_text(encoding="utf-8")) + source["rollups"]["promotion_dry_run_template_count"] = 4 + target = tmp_path / "ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="rollup counts mismatch"): + load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run(tmp_path) + + +def test_owner_approved_promotion_dry_run_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_owner_approved_result_capture_promotion_dry_run_2026-06-13.json" + target.write_text(json.dumps(source), encoding="utf-8") + + with pytest.raises(ValueError, match="forbidden display terms"): + load_latest_ai_agent_owner_approved_result_capture_promotion_dry_run(tmp_path) diff --git a/apps/api/tests/test_ai_agent_owner_approved_result_capture_promotion_dry_run_api.py b/apps/api/tests/test_ai_agent_owner_approved_result_capture_promotion_dry_run_api.py new file mode 100644 index 000000000..d9f92c9eb --- /dev/null +++ b/apps/api/tests/test_ai_agent_owner_approved_result_capture_promotion_dry_run_api.py @@ -0,0 +1,40 @@ +import pytest +from httpx import ASGITransport, AsyncClient + +from src.main import app + + +@pytest.mark.asyncio +async def test_get_agent_owner_approved_result_capture_promotion_dry_run_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-owner-approved-result-capture-promotion-dry-run") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "ai_agent_owner_approved_result_capture_promotion_dry_run_v1" + assert data["program_status"]["current_task_id"] == "P2-120" + assert data["program_status"]["next_task_id"] == "P2-121" + assert data["program_status"]["overall_completion_percent"] == 100 + + rollups = data["rollups"] + assert rollups["promotion_dry_run_template_count"] == 5 + assert rollups["owner_acceptance_fixture_count"] == 5 + assert rollups["dry_run_verifier_check_count"] == 5 + assert rollups["blocked_runtime_promotion_count"] == 5 + assert rollups["operator_action_count"] == 5 + assert rollups["critical_blocker_count"] == 3 + assert rollups["owner_approval_received_count"] == 0 + assert rollups["capture_promotion_approved_count"] == 0 + assert rollups["dry_run_preview_generated_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 {template["target_dry_run"] for template in data["promotion_dry_run_templates"]} == { + "result_capture_promotion_dry_run_preview" + } + assert {template["runtime_write_enabled"] for template in data["promotion_dry_run_templates"]} == {False} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 72057823e..6cfa1ff1e 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -5675,6 +5675,40 @@ "blockedAction": "blocked:{value}", "runtimePromotionWriteAllowed": "runtime promotion write={value}" } + }, + "ownerApprovedResultCapturePromotionDryRun": { + "title": "P2-120 owner-approved result capture promotion dry-run", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 promotion approval gate", + "truthTitle": "Owner-approved promotion dry-run truth", + "metrics": { + "overall": "完成度", + "templates": "Dry-run templates", + "fixtures": "Owner fixtures", + "verifiers": "Verifiers", + "blockers": "Blocked writes", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "dryRunGenerated": "Dry-run generated", + "liveWrites": "Live dry-run / write" + }, + "flags": { + "gateLoaded": "gate loaded={value}", + "ownerApproval": "owner approval required={value}", + "previewAllowed": "preview allowed={value}" + }, + "labels": { + "targetDryRun": "目標 dry-run:{value}", + "resultWrites": "result write={value}", + "learningWrites": "learning write={value}", + "trustWrites": "trust write={value}", + "runtimeWriteEnabled": "runtime write={value}", + "requiredOwner": "owner:{value}", + "fixtureOnly": "fixture only={value}", + "blockedAction": "blocked:{value}", + "runtimeWriteAllowed": "runtime write={value}" + } } } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 72057823e..6cfa1ff1e 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -5675,6 +5675,40 @@ "blockedAction": "blocked:{value}", "runtimePromotionWriteAllowed": "runtime promotion write={value}" } + }, + "ownerApprovedResultCapturePromotionDryRun": { + "title": "P2-120 owner-approved result capture promotion dry-run", + "source": "產生 {generated};目前 {current};下一步 {next}", + "priorTitle": "前一關 promotion approval gate", + "truthTitle": "Owner-approved promotion dry-run truth", + "metrics": { + "overall": "完成度", + "templates": "Dry-run templates", + "fixtures": "Owner fixtures", + "verifiers": "Verifiers", + "blockers": "Blocked writes", + "actions": "操作選項", + "approvalRequired": "需批准", + "blocked": "阻擋", + "dryRunGenerated": "Dry-run generated", + "liveWrites": "Live dry-run / write" + }, + "flags": { + "gateLoaded": "gate loaded={value}", + "ownerApproval": "owner approval required={value}", + "previewAllowed": "preview allowed={value}" + }, + "labels": { + "targetDryRun": "目標 dry-run:{value}", + "resultWrites": "result write={value}", + "learningWrites": "learning write={value}", + "trustWrites": "trust write={value}", + "runtimeWriteEnabled": "runtime write={value}", + "requiredOwner": "owner:{value}", + "fixtureOnly": "fixture only={value}", + "blockedAction": "blocked:{value}", + "runtimeWriteAllowed": "runtime write={value}" + } } } }, diff --git a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx index bd7401701..ab97a5197 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 @@ -66,6 +66,7 @@ import { type AiAgentFailureReceiptNoSendReplaySnapshot, type AiAgentReviewerQueueNoWriteReadbackSnapshot, type AiAgentResultCaptureNoWriteReadbackSnapshot, + type AiAgentOwnerApprovedResultCapturePromotionDryRunSnapshot, type AiAgentResultCapturePromotionApprovalGateSnapshot, type AiAgentOwnerApprovedFixturePromotionGateSnapshot, type AiAgentRuntimeWorkerShadowGateSnapshot, @@ -456,6 +457,7 @@ export function AutomationInventoryTab() { const [reviewerQueueNoWriteReadback, setReviewerQueueNoWriteReadback] = useState(null) const [resultCaptureNoWriteReadback, setResultCaptureNoWriteReadback] = useState(null) const [resultCapturePromotionApprovalGate, setResultCapturePromotionApprovalGate] = useState(null) + const [ownerApprovedResultCapturePromotionDryRun, setOwnerApprovedResultCapturePromotionDryRun] = useState(null) const [reportTruthActionabilityReview, setReportTruthActionabilityReview] = useState(null) const [ownerDryRunPackage, setOwnerDryRunPackage] = useState(null) const [hostStatefulInventory, setHostStatefulInventory] = useState(null) @@ -512,6 +514,7 @@ export function AutomationInventoryTab() { apiClient.getAiAgentReviewerQueueNoWriteReadback(), apiClient.getAiAgentResultCaptureNoWriteReadback(), apiClient.getAiAgentResultCapturePromotionApprovalGate(), + apiClient.getAiAgentOwnerApprovedResultCapturePromotionDryRun(), apiClient.getAiAgentReportTruthActionabilityReview(), apiClient.getAiAgentOwnerApprovedFixtureDryRun(), apiClient.getAiAgentHostStatefulVersionInventory(), @@ -567,6 +570,7 @@ export function AutomationInventoryTab() { reviewerQueueNoWriteReadbackResult, resultCaptureNoWriteReadbackResult, resultCapturePromotionApprovalGateResult, + ownerApprovedResultCapturePromotionDryRunResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -619,6 +623,7 @@ export function AutomationInventoryTab() { setReviewerQueueNoWriteReadback(reviewerQueueNoWriteReadbackResult.status === 'fulfilled' ? reviewerQueueNoWriteReadbackResult.value : null) setResultCaptureNoWriteReadback(resultCaptureNoWriteReadbackResult.status === 'fulfilled' ? resultCaptureNoWriteReadbackResult.value : null) setResultCapturePromotionApprovalGate(resultCapturePromotionApprovalGateResult.status === 'fulfilled' ? resultCapturePromotionApprovalGateResult.value : null) + setOwnerApprovedResultCapturePromotionDryRun(ownerApprovedResultCapturePromotionDryRunResult.status === 'fulfilled' ? ownerApprovedResultCapturePromotionDryRunResult.value : null) setReportTruthActionabilityReview(reportTruthActionabilityReviewResult.status === 'fulfilled' ? reportTruthActionabilityReviewResult.value : null) setOwnerDryRunPackage(ownerDryRunPackageResult.status === 'fulfilled' ? ownerDryRunPackageResult.value : null) setHostStatefulInventory(hostStatefulInventoryResult.status === 'fulfilled' ? hostStatefulInventoryResult.value : null) @@ -669,6 +674,7 @@ export function AutomationInventoryTab() { reviewerQueueNoWriteReadbackResult, resultCaptureNoWriteReadbackResult, resultCapturePromotionApprovalGateResult, + ownerApprovedResultCapturePromotionDryRunResult, reportTruthActionabilityReviewResult, ownerDryRunPackageResult, hostStatefulInventoryResult, @@ -1911,7 +1917,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 || !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 || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { return (
@@ -2596,6 +2602,35 @@ export function AutomationInventoryTab() { + resultCapturePromotionApprovalGate.rollups.playbook_trust_write_count + resultCapturePromotionApprovalGate.rollups.production_write_count ) + const ownerPromotionDryRunOverall = ownerApprovedResultCapturePromotionDryRun.program_status.overall_completion_percent + const ownerPromotionDryRunTemplates = ownerApprovedResultCapturePromotionDryRun.rollups.promotion_dry_run_template_count + const ownerPromotionDryRunFixtures = ownerApprovedResultCapturePromotionDryRun.rollups.owner_acceptance_fixture_count + const ownerPromotionDryRunVerifiers = ownerApprovedResultCapturePromotionDryRun.rollups.dry_run_verifier_check_count + const ownerPromotionDryRunBlockers = ownerApprovedResultCapturePromotionDryRun.rollups.blocked_runtime_promotion_count + const ownerPromotionDryRunActions = ownerApprovedResultCapturePromotionDryRun.rollups.operator_action_count + const ownerPromotionDryRunApprovalRequired = ( + ownerApprovedResultCapturePromotionDryRun.rollups.approval_required_template_count + + ownerApprovedResultCapturePromotionDryRun.rollups.approval_required_fixture_count + + ownerApprovedResultCapturePromotionDryRun.rollups.approval_required_verifier_count + ) + const ownerPromotionDryRunBlocked = ( + ownerApprovedResultCapturePromotionDryRun.rollups.blocked_template_count + + ownerApprovedResultCapturePromotionDryRun.rollups.blocked_fixture_count + + ownerApprovedResultCapturePromotionDryRun.rollups.critical_blocker_count + ) + const ownerPromotionDryRunLiveWrites = ( + ownerApprovedResultCapturePromotionDryRun.rollups.canonical_runtime_target_read_count + + ownerApprovedResultCapturePromotionDryRun.rollups.live_query_count + + ownerApprovedResultCapturePromotionDryRun.rollups.reviewer_queue_write_count + + ownerApprovedResultCapturePromotionDryRun.rollups.gateway_queue_write_count + + ownerApprovedResultCapturePromotionDryRun.rollups.telegram_send_count + + ownerApprovedResultCapturePromotionDryRun.rollups.bot_api_call_count + + ownerApprovedResultCapturePromotionDryRun.rollups.report_receipt_write_count + + ownerApprovedResultCapturePromotionDryRun.rollups.result_capture_write_count + + ownerApprovedResultCapturePromotionDryRun.rollups.learning_write_count + + ownerApprovedResultCapturePromotionDryRun.rollups.playbook_trust_write_count + + ownerApprovedResultCapturePromotionDryRun.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 @@ -6881,6 +6916,144 @@ export function AutomationInventoryTab() {
+
+
+
+ + + {t('ownerApprovedResultCapturePromotionDryRun.title')} + +
+ +
+ +
+ } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
+ +
+
+ {t('ownerApprovedResultCapturePromotionDryRun.priorTitle')} + + {ownerApprovedResultCapturePromotionDryRun.prior_result_capture_promotion_gate.readiness_note} + +
+ + + +
+
+ +
+ {t('ownerApprovedResultCapturePromotionDryRun.truthTitle')} + + {ownerApprovedResultCapturePromotionDryRun.dry_run_truth.truth_note} + +
+ + + + + + + +
+
+
+ +
+ {ownerApprovedResultCapturePromotionDryRun.promotion_dry_run_templates.slice(0, 3).map(template => ( +
+
+ + {template.display_name} + + +
+ + {template.preview_summary} + +
+ + + +
+
+ ))} +
+ +
+ {ownerApprovedResultCapturePromotionDryRun.owner_acceptance_dry_run_fixtures.slice(0, 5).map(fixture => ( +
+
+ + {fixture.display_name} + + +
+ + {fixture.verifies} + +
+ + + +
+
+ ))} +
+ +
+ {ownerApprovedResultCapturePromotionDryRun.blocked_runtime_promotions.slice(0, 3).map(blocker => ( +
+
+ + {blocker.display_name} + + +
+
+ + +
+
+ ))} +
+ +
+ {ownerApprovedResultCapturePromotionDryRun.operator_actions.slice(0, 3).map(action => ( +
+
+ + {action.display_name} + + +
+ + {action.operator_instruction} + + +
+ ))} +
+
+
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index d93b5aab8..32f30cc66 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -495,6 +495,11 @@ export const apiClient = { return handleResponse(res) }, + async getAiAgentOwnerApprovedResultCapturePromotionDryRun() { + const res = await fetch(`${API_BASE_URL}/agents/agent-owner-approved-result-capture-promotion-dry-run`) + return handleResponse(res) + }, + async getAiAgentOwnerApprovedFixtureDryRun() { const res = await fetch(`${API_BASE_URL}/agents/agent-owner-approved-fixture-dry-run`) return handleResponse(res) @@ -5463,6 +5468,166 @@ export interface AiAgentResultCapturePromotionApprovalGateSnapshot { } } +export interface AiAgentOwnerApprovedResultCapturePromotionDryRunSnapshot { + schema_version: 'ai_agent_owner_approved_result_capture_promotion_dry_run_v1' + generated_at: string + program_status: { + overall_completion_percent: number + current_priority: 'P0' | 'P1' | 'P2' | 'P3' + current_task_id: 'P2-120' + next_task_id: 'P2-121' + read_only_mode: true + runtime_authority: 'owner_approved_result_capture_promotion_dry_run_only_no_live_write' + status_note: string + } + source_refs: string[] + prior_result_capture_promotion_gate: { + schema_version: string + promotion_approval_packet_count: number + acceptance_gate_template_count: number + promotion_verifier_check_count: number + blocked_promotion_write_count: number + operator_action_count: number + owner_approval_received_count: number + capture_promotion_approved_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 + } + dry_run_truth: { + p2_119_promotion_gate_loaded: boolean + owner_approval_required: boolean + dry_run_preview_allowed: boolean + promotion_packet_ready: boolean + acceptance_template_ready: boolean + verifier_dry_run_ready: boolean + operator_handoff_ready: boolean + owner_approval_received: boolean + capture_promotion_approved: boolean + canonical_runtime_target_read_enabled: boolean + live_query_enabled: boolean + reviewer_queue_write_enabled: boolean + gateway_queue_write_enabled: boolean + telegram_send_enabled: boolean + bot_api_call_enabled: boolean + report_receipt_write_enabled: boolean + result_capture_write_enabled: boolean + learning_write_enabled: boolean + playbook_trust_write_enabled: boolean + production_write_enabled: boolean + secret_read_enabled: boolean + destructive_operation_enabled: boolean + owner_approval_received_count: number + capture_promotion_approved_count: number + dry_run_preview_generated_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 + truth_note: string + } + promotion_dry_run_templates: Array<{ + template_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + source_packet_id: string + status: 'ready_for_dry_run' | 'approval_required' | 'blocked_by_policy' + target_dry_run: string + dry_run_mode: 'preview_only' + runtime_write_enabled: false + preview_summary: string + evidence_hash: string + }> + owner_acceptance_dry_run_fixtures: Array<{ + fixture_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready' | 'approval_required' | 'blocked_by_policy' + required_owner: string + fixture_only: true + runtime_write_enabled: false + verifies: string + evidence_hash: string + }> + dry_run_verifier_checks: Array<{ + verifier_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' + status: 'ready' | 'approval_required' | 'blocked_by_policy' + verifies: string + failure_if_missing: string + live_execution_enabled: false + evidence_hash: string + }> + blocked_runtime_promotions: 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: { + promotion_dry_run_template_count: number + owner_acceptance_fixture_count: number + dry_run_verifier_check_count: number + blocked_runtime_promotion_count: number + operator_action_count: number + approval_required_template_count: number + blocked_template_count: number + approval_required_fixture_count: number + blocked_fixture_count: number + approval_required_verifier_count: number + critical_blocker_count: number + owner_approval_received_count: number + capture_promotion_approved_count: number + dry_run_preview_generated_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_owner_approved_result_capture_promotion_dry_run_2026-06-13.json b/docs/evaluations/ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json new file mode 100644 index 000000000..0db5e337e --- /dev/null +++ b/docs/evaluations/ai_agent_owner_approved_result_capture_promotion_dry_run_2026-06-13.json @@ -0,0 +1,370 @@ +{ + "schema_version": "ai_agent_owner_approved_result_capture_promotion_dry_run_v1", + "generated_at": "2026-06-13T23:05:00+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P2", + "current_task_id": "P2-120", + "next_task_id": "P2-121", + "read_only_mode": true, + "runtime_authority": "owner_approved_result_capture_promotion_dry_run_only_no_live_write", + "status_note": "P2-120 只建立 owner-approved result capture promotion dry-run preview;不得寫 result capture、learning、PlayBook trust、Gateway queue、Telegram 或 production。" + }, + "source_refs": [ + "docs/evaluations/ai_agent_result_capture_promotion_approval_gate_2026-06-13.json", + "docs/evaluations/ai_agent_result_capture_no_write_readback_2026-06-13.json", + "docs/evaluations/ai_agent_owner_approved_result_capture_readback_2026-06-13.json", + "docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md#38-建立-reviewer-queue-no-write-readback" + ], + "prior_result_capture_promotion_gate": { + "schema_version": "ai_agent_result_capture_promotion_approval_gate_v1", + "promotion_approval_packet_count": 5, + "acceptance_gate_template_count": 5, + "promotion_verifier_check_count": 5, + "blocked_promotion_write_count": 5, + "operator_action_count": 5, + "owner_approval_received_count": 0, + "capture_promotion_approved_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-119 已正式驗證 result capture promotion approval gate;P2-120 只建立 owner-approved promotion dry-run preview、verifier 與 operator handoff。" + }, + "dry_run_truth": { + "p2_119_promotion_gate_loaded": true, + "owner_approval_required": true, + "dry_run_preview_allowed": true, + "promotion_packet_ready": true, + "acceptance_template_ready": true, + "verifier_dry_run_ready": true, + "operator_handoff_ready": true, + "owner_approval_received": false, + "capture_promotion_approved": false, + "canonical_runtime_target_read_enabled": false, + "live_query_enabled": false, + "reviewer_queue_write_enabled": false, + "gateway_queue_write_enabled": false, + "telegram_send_enabled": false, + "bot_api_call_enabled": false, + "report_receipt_write_enabled": false, + "result_capture_write_enabled": false, + "learning_write_enabled": false, + "playbook_trust_write_enabled": false, + "production_write_enabled": false, + "secret_read_enabled": false, + "destructive_operation_enabled": false, + "owner_approval_received_count": 0, + "capture_promotion_approved_count": 0, + "dry_run_preview_generated_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, + "truth_note": "promotion dry-run preview 已可審查;真正 result capture write、learning write、PlayBook trust write、Gateway queue write 與 Telegram send 仍全部為 0。" + }, + "promotion_dry_run_templates": [ + { + "template_id": "dry_run_promote_action_required_capture", + "display_name": "Promote action-required capture dry-run", + "owner_agent": "openclaw", + "source_packet_id": "promotion_packet_action_required", + "status": "ready_for_dry_run", + "target_dry_run": "result_capture_promotion_dry_run_preview", + "dry_run_mode": "preview_only", + "runtime_write_enabled": false, + "preview_summary": "產出 action-required capture promotion 的 owner decision、verifier、rollback 與 next manual action 預覽。", + "evidence_hash": "sha256:9191919191919191919191919191919191919191919191919191919191919191" + }, + { + "template_id": "dry_run_promote_no_action_capture", + "display_name": "Promote no-action capture dry-run", + "owner_agent": "hermes", + "source_packet_id": "promotion_packet_no_action_review", + "status": "approval_required", + "target_dry_run": "result_capture_promotion_dry_run_preview", + "dry_run_mode": "preview_only", + "runtime_write_enabled": false, + "preview_summary": "產出 no-action capture promotion 的風險剩餘量、觀察條件與 owner 判讀預覽。", + "evidence_hash": "sha256:9292929292929292929292929292929292929292929292929292929292929292" + }, + { + "template_id": "dry_run_promote_verifier_degraded_capture", + "display_name": "Promote verifier degraded capture dry-run", + "owner_agent": "nemotron", + "source_packet_id": "promotion_packet_verifier_degraded", + "status": "blocked_by_policy", + "target_dry_run": "result_capture_promotion_dry_run_preview", + "dry_run_mode": "preview_only", + "runtime_write_enabled": false, + "preview_summary": "產出 degraded verifier 的 rollback / reverify / evidence 補件預覽;未補齊不得 promotion。", + "evidence_hash": "sha256:9393939393939393939393939393939393939393939393939393939393939393" + }, + { + "template_id": "dry_run_promote_sre_route_capture", + "display_name": "Promote SRE route capture dry-run", + "owner_agent": "openclaw", + "source_packet_id": "promotion_packet_route_lock", + "status": "approval_required", + "target_dry_run": "result_capture_promotion_dry_run_preview", + "dry_run_mode": "preview_only", + "runtime_write_enabled": false, + "preview_summary": "產出 AwoooI SRE 戰情室路由與舊 bot 停用邊界的 promotion 預覽。", + "evidence_hash": "sha256:9494949494949494949494949494949494949494949494949494949494949494" + }, + { + "template_id": "dry_run_promote_owner_acceptance_capture", + "display_name": "Promote owner acceptance capture dry-run", + "owner_agent": "nemotron", + "source_packet_id": "promotion_packet_owner_acceptance", + "status": "blocked_by_policy", + "target_dry_run": "result_capture_promotion_dry_run_preview", + "dry_run_mode": "preview_only", + "runtime_write_enabled": false, + "preview_summary": "產出 owner acceptance、rollback owner、redaction 與 learning boundary 的 promotion 預覽。", + "evidence_hash": "sha256:9595959595959595959595959595959595959595959595959595959595959595" + } + ], + "owner_acceptance_dry_run_fixtures": [ + { + "fixture_id": "fixture_owner_acceptance_record_complete", + "display_name": "Owner acceptance record complete", + "owner_agent": "hermes", + "status": "ready", + "required_owner": "result_capture_owner", + "fixture_only": true, + "runtime_write_enabled": false, + "verifies": "owner, decision, decision reason, rollback owner and redacted evidence refs exist", + "evidence_hash": "sha256:9696969696969696969696969696969696969696969696969696969696969696" + }, + { + "fixture_id": "fixture_capture_scope_review", + "display_name": "Capture scope review", + "owner_agent": "openclaw", + "status": "approval_required", + "required_owner": "operator_owner", + "fixture_only": true, + "runtime_write_enabled": false, + "verifies": "capture scope is limited to one incident and one promotion target", + "evidence_hash": "sha256:9797979797979797979797979797979797979797979797979797979797979797" + }, + { + "fixture_id": "fixture_learning_boundary_review", + "display_name": "Learning boundary review", + "owner_agent": "hermes", + "status": "blocked_by_policy", + "required_owner": "learning_owner", + "fixture_only": true, + "runtime_write_enabled": false, + "verifies": "learning write scope is blocked until KM owner review", + "evidence_hash": "sha256:9898989898989898989898989898989898989898989898989898989898989898" + }, + { + "fixture_id": "fixture_playbook_trust_boundary_review", + "display_name": "PlayBook trust boundary review", + "owner_agent": "nemotron", + "status": "blocked_by_policy", + "required_owner": "playbook_owner", + "fixture_only": true, + "runtime_write_enabled": false, + "verifies": "PlayBook trust update remains blocked until verifier success criteria exist", + "evidence_hash": "sha256:9999999999999999999999999999999999999999999999999999999999999999" + }, + { + "fixture_id": "fixture_rollback_reverify_plan_review", + "display_name": "Rollback reverify plan review", + "owner_agent": "nemotron", + "status": "approval_required", + "required_owner": "rollback_owner", + "fixture_only": true, + "runtime_write_enabled": false, + "verifies": "rollback and reverify plan exists before owner-approved dry-run promotion", + "evidence_hash": "sha256:9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a" + } + ], + "dry_run_verifier_checks": [ + { + "verifier_id": "no_result_capture_write_during_dry_run", + "display_name": "No result capture write during dry-run", + "owner_agent": "openclaw", + "status": "ready", + "verifies": "result_capture_write_count remains zero during promotion dry-run", + "failure_if_missing": "若 result capture write 不是 0,必須停止 dry-run 並標記 unauthorized promotion write。", + "live_execution_enabled": false, + "evidence_hash": "sha256:9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b" + }, + { + "verifier_id": "no_learning_write_during_dry_run", + "display_name": "No learning write during dry-run", + "owner_agent": "hermes", + "status": "ready", + "verifies": "learning_write_count remains zero during promotion dry-run", + "failure_if_missing": "若 learning write 不是 0,必須退回 KM owner review gate。", + "live_execution_enabled": false, + "evidence_hash": "sha256:9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c" + }, + { + "verifier_id": "no_playbook_trust_during_dry_run", + "display_name": "No PlayBook trust during dry-run", + "owner_agent": "nemotron", + "status": "approval_required", + "verifies": "playbook_trust_write_count remains zero during promotion dry-run", + "failure_if_missing": "若 PlayBook trust write 不是 0,必須退回 trust owner review。", + "live_execution_enabled": false, + "evidence_hash": "sha256:9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d" + }, + { + "verifier_id": "no_gateway_queue_during_dry_run", + "display_name": "No Gateway queue during dry-run", + "owner_agent": "openclaw", + "status": "approval_required", + "verifies": "gateway_queue_write_count remains zero during promotion dry-run", + "failure_if_missing": "若 Gateway queue write 不是 0,必須退回 reviewer queue gate。", + "live_execution_enabled": false, + "evidence_hash": "sha256:9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e" + }, + { + "verifier_id": "promotion_dry_run_redaction_complete", + "display_name": "Promotion dry-run redaction complete", + "owner_agent": "hermes", + "status": "ready", + "verifies": "each promotion dry-run preview has redacted evidence and owner fields", + "failure_if_missing": "若缺少 redaction 或 owner 欄位,不能進入 P2-121。", + "live_execution_enabled": false, + "evidence_hash": "sha256:9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f" + } + ], + "blocked_runtime_promotions": [ + { + "blocker_id": "result_capture_dry_run_write_not_authorized", + "display_name": "Result capture dry-run write not authorized", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "result_capture_write", + "blocked_until": "owner_approved_promotion_write_gate", + "evidence_hash": "sha256:a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0" + }, + { + "blocker_id": "learning_dry_run_write_not_authorized", + "display_name": "Learning dry-run write not authorized", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "learning_write", + "blocked_until": "km_owner_review_approved", + "evidence_hash": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + }, + { + "blocker_id": "playbook_trust_dry_run_write_not_authorized", + "display_name": "PlayBook trust dry-run write not authorized", + "severity": "high", + "status": "approval_required", + "blocked_action": "playbook_trust_write", + "blocked_until": "playbook_owner_review_approved", + "evidence_hash": "sha256:a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2" + }, + { + "blocker_id": "gateway_queue_dry_run_write_not_authorized", + "display_name": "Gateway queue dry-run write not authorized", + "severity": "high", + "status": "approval_required", + "blocked_action": "gateway_queue_write", + "blocked_until": "reviewer_queue_owner_review_approved", + "evidence_hash": "sha256:a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3" + }, + { + "blocker_id": "production_dry_run_write_not_authorized", + "display_name": "Production dry-run write not authorized", + "severity": "critical", + "status": "blocked_by_policy", + "blocked_action": "production_write", + "blocked_until": "runtime_write_gate_approved", + "evidence_hash": "sha256:a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4" + } + ], + "operator_actions": [ + { + "action_id": "review_owner_approved_promotion_dry_run", + "display_name": "Review owner-approved promotion dry-run", + "owner_agent": "openclaw", + "operator_instruction": "審查 owner-approved promotion dry-run preview 是否限定單一 incident、單一 capture target 與 rollback 條件。", + "runtime_write_allowed": false + }, + { + "action_id": "verify_promotion_dry_run_no_write_counts", + "display_name": "Verify promotion dry-run no-write counts", + "owner_agent": "nemotron", + "operator_instruction": "確認 result capture、learning、PlayBook trust、Gateway、Telegram、Bot API 與 production write count 全部仍為 0。", + "runtime_write_allowed": false + }, + { + "action_id": "confirm_owner_acceptance_fields", + "display_name": "Confirm owner acceptance fields", + "owner_agent": "hermes", + "operator_instruction": "確認 owner、decision、decision reason、rollback owner 與 redacted evidence refs 已齊全。", + "runtime_write_allowed": false + }, + { + "action_id": "check_dry_run_redaction_contract", + "display_name": "Check dry-run redaction contract", + "owner_agent": "hermes", + "operator_instruction": "確認 dry-run preview 只顯示遮蔽後證據、摘要與 owner 欄位,不外露敏感 payload。", + "runtime_write_allowed": false + }, + { + "action_id": "promote_to_p2_121", + "display_name": "Promote to P2-121", + "owner_agent": "nemotron", + "operator_instruction": "若 P2-120 的 owner-approved promotion dry-run 全部通過,下一步才能建立 P2-121 result capture write gate review。", + "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": "治理頁只顯示 dry-run preview、owner、decision、blocked reason、verifier 與遮蔽後 evidence hash;不得顯示內部協作內容、敏感 payload 或 secret。" + }, + "rollups": { + "promotion_dry_run_template_count": 5, + "owner_acceptance_fixture_count": 5, + "dry_run_verifier_check_count": 5, + "blocked_runtime_promotion_count": 5, + "operator_action_count": 5, + "approval_required_template_count": 2, + "blocked_template_count": 2, + "approval_required_fixture_count": 2, + "blocked_fixture_count": 2, + "approval_required_verifier_count": 2, + "critical_blocker_count": 3, + "owner_approval_received_count": 0, + "capture_promotion_approved_count": 0, + "dry_run_preview_generated_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_owner_approved_result_capture_promotion_dry_run_v1.schema.json b/docs/schemas/ai_agent_owner_approved_result_capture_promotion_dry_run_v1.schema.json new file mode 100644 index 000000000..74f8d9051 --- /dev/null +++ b/docs/schemas/ai_agent_owner_approved_result_capture_promotion_dry_run_v1.schema.json @@ -0,0 +1,357 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.wooo.work/schemas/ai_agent_owner_approved_result_capture_promotion_dry_run_v1.schema.json", + "title": "AI Agent Owner Approved Result Capture Promotion Dry-run v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "generated_at", + "program_status", + "source_refs", + "prior_result_capture_promotion_gate", + "dry_run_truth", + "promotion_dry_run_templates", + "owner_acceptance_dry_run_fixtures", + "dry_run_verifier_checks", + "blocked_runtime_promotions", + "operator_actions", + "display_redaction_contract", + "rollups" + ], + "properties": { + "schema_version": { + "const": "ai_agent_owner_approved_result_capture_promotion_dry_run_v1" + }, + "generated_at": { + "type": "string", + "minLength": 1 + }, + "program_status": { + "type": "object", + "additionalProperties": false, + "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-120" + }, + "next_task_id": { + "const": "P2-121" + }, + "read_only_mode": { + "const": true + }, + "runtime_authority": { + "const": "owner_approved_result_capture_promotion_dry_run_only_no_live_write" + }, + "status_note": { + "type": "string", + "minLength": 1 + } + } + }, + "source_refs": { + "$ref": "#/$defs/string_array" + }, + "prior_result_capture_promotion_gate": { + "type": "object" + }, + "dry_run_truth": { + "type": "object" + }, + "promotion_dry_run_templates": { + "type": "array", + "minItems": 5, + "items": { + "$ref": "#/$defs/promotion_dry_run_template" + } + }, + "owner_acceptance_dry_run_fixtures": { + "type": "array", + "minItems": 5, + "items": { + "$ref": "#/$defs/owner_acceptance_fixture" + } + }, + "dry_run_verifier_checks": { + "type": "array", + "minItems": 5, + "items": { + "$ref": "#/$defs/dry_run_verifier_check" + } + }, + "blocked_runtime_promotions": { + "type": "array", + "minItems": 5, + "items": { + "$ref": "#/$defs/blocked_runtime_promotion" + } + }, + "operator_actions": { + "type": "array", + "minItems": 5, + "items": { + "$ref": "#/$defs/operator_action" + } + }, + "display_redaction_contract": { + "type": "object" + }, + "rollups": { + "type": "object" + } + }, + "$defs": { + "string_array": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "owner_agent": { + "enum": [ + "openclaw", + "hermes", + "nemotron" + ] + }, + "status": { + "enum": [ + "ready", + "ready_for_dry_run", + "ready_for_owner_review", + "approval_required", + "blocked_by_policy" + ] + }, + "severity": { + "enum": [ + "high", + "critical" + ] + }, + "promotion_dry_run_template": { + "type": "object", + "additionalProperties": false, + "required": [ + "template_id", + "display_name", + "owner_agent", + "source_packet_id", + "status", + "target_dry_run", + "dry_run_mode", + "runtime_write_enabled", + "preview_summary", + "evidence_hash" + ], + "properties": { + "template_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "owner_agent": { + "$ref": "#/$defs/owner_agent" + }, + "source_packet_id": { + "type": "string" + }, + "status": { + "$ref": "#/$defs/status" + }, + "target_dry_run": { + "const": "result_capture_promotion_dry_run_preview" + }, + "dry_run_mode": { + "const": "preview_only" + }, + "runtime_write_enabled": { + "const": false + }, + "preview_summary": { + "type": "string", + "minLength": 1 + }, + "evidence_hash": { + "$ref": "#/$defs/hash" + } + } + }, + "owner_acceptance_fixture": { + "type": "object", + "additionalProperties": false, + "required": [ + "fixture_id", + "display_name", + "owner_agent", + "status", + "required_owner", + "fixture_only", + "runtime_write_enabled", + "verifies", + "evidence_hash" + ], + "properties": { + "fixture_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "owner_agent": { + "$ref": "#/$defs/owner_agent" + }, + "status": { + "$ref": "#/$defs/status" + }, + "required_owner": { + "type": "string" + }, + "fixture_only": { + "const": true + }, + "runtime_write_enabled": { + "const": false + }, + "verifies": { + "type": "string", + "minLength": 1 + }, + "evidence_hash": { + "$ref": "#/$defs/hash" + } + } + }, + "dry_run_verifier_check": { + "type": "object", + "additionalProperties": false, + "required": [ + "verifier_id", + "display_name", + "owner_agent", + "status", + "verifies", + "failure_if_missing", + "live_execution_enabled", + "evidence_hash" + ], + "properties": { + "verifier_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "owner_agent": { + "$ref": "#/$defs/owner_agent" + }, + "status": { + "$ref": "#/$defs/status" + }, + "verifies": { + "type": "string", + "minLength": 1 + }, + "failure_if_missing": { + "type": "string", + "minLength": 1 + }, + "live_execution_enabled": { + "const": false + }, + "evidence_hash": { + "$ref": "#/$defs/hash" + } + } + }, + "blocked_runtime_promotion": { + "type": "object", + "additionalProperties": false, + "required": [ + "blocker_id", + "display_name", + "severity", + "status", + "blocked_action", + "blocked_until", + "evidence_hash" + ], + "properties": { + "blocker_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "severity": { + "$ref": "#/$defs/severity" + }, + "status": { + "$ref": "#/$defs/status" + }, + "blocked_action": { + "type": "string", + "minLength": 1 + }, + "blocked_until": { + "type": "string", + "minLength": 1 + }, + "evidence_hash": { + "$ref": "#/$defs/hash" + } + } + }, + "operator_action": { + "type": "object", + "additionalProperties": false, + "required": [ + "action_id", + "display_name", + "owner_agent", + "operator_instruction", + "runtime_write_allowed" + ], + "properties": { + "action_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "owner_agent": { + "$ref": "#/$defs/owner_agent" + }, + "operator_instruction": { + "type": "string", + "minLength": 1 + }, + "runtime_write_allowed": { + "const": false + } + } + } + } +}