diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 9255bf2c..e0ff8bd5 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -76,6 +76,9 @@ from src.services.ai_agent_canonical_runtime_readback_owner_acceptance import ( from src.services.ai_agent_communication_learning_contract import ( load_latest_ai_agent_communication_learning_contract, ) +from src.services.ai_agent_controlled_executor_handoff import ( + load_latest_ai_agent_controlled_executor_handoff, +) from src.services.ai_agent_critic_reviewer_result_capture import ( load_latest_ai_agent_critic_reviewer_result_capture, ) @@ -1143,6 +1146,37 @@ async def get_agent_high_risk_owner_review_queue() -> dict[str, Any]: ) from exc +@router.get( + "/agent-controlled-executor-handoff", + response_model=dict[str, Any], + summary="取得 P2-415 AI Agent 受控 Executor 交接跑道", + description=( + "讀取最新已提交的 P2-415 AI Agent controlled executor handoff 只讀快照;" + "此端點呈現 high risk packet 是否具備 allowlist、Ansible check-mode、rollback、" + "post-action verifier、Telegram evidence、KM / PlayBook trust writeback 條件," + "以及 critical break-glass 邊界。它不 dispatch executor、不執行 live apply、" + "不寫 Gateway queue、不送 Telegram、不呼叫 Bot API、不寫 KM、不更新 PlayBook trust、" + "不寫 production、不讀 secret、不呼叫付費 API、不改主機、不執行 kubectl 或不可逆操作。" + ), +) +async def get_agent_controlled_executor_handoff() -> dict[str, Any]: + """回傳最新 P2-415 controlled executor handoff 只讀快照。""" + try: + payload = await asyncio.to_thread(load_latest_ai_agent_controlled_executor_handoff) + 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_controlled_executor_handoff_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="P2-415 AI Agent 受控 Executor 交接跑道快照無效", + ) from exc + + @router.get( "/agent-action-audit-ledger", response_model=dict[str, Any], diff --git a/apps/api/src/services/ai_agent_controlled_executor_handoff.py b/apps/api/src/services/ai_agent_controlled_executor_handoff.py new file mode 100644 index 00000000..50a89914 --- /dev/null +++ b/apps/api/src/services/ai_agent_controlled_executor_handoff.py @@ -0,0 +1,406 @@ +""" +P2-415 AI Agent controlled executor handoff readback. + +This loader validates the committed controlled executor handoff runway. It makes +high-risk controlled apply packets visible to the product, while keeping the +route itself read-only: no live apply, Telegram send, Bot API, secret read, +host write, kubectl action, or destructive operation is executed here. +""" + +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_controlled_executor_handoff_*.json" +_SCHEMA_VERSION = "ai_agent_controlled_executor_handoff_v1" +_RUNTIME_AUTHORITY = "controlled_executor_handoff_readback_no_live_apply" +_EXPECTED_CURRENT_TASK = "P2-415" +_EXPECTED_NEXT_TASK = "P2-416" +_EXPECTED_SOURCE_SCHEMAS = { + "ai_agent_high_risk_owner_review_queue_v1", + "ai_agent_action_audit_ledger_v1", + "ai_agent_action_owner_acceptance_event_bus_v1", + "ai_agent_report_runtime_readiness_v1", + "ai_agent_runtime_write_gate_review_v1", + "ai_agent_post_write_verifier_package_v1", + "ai_agent_learning_writeback_approval_package_v1", + "ai_agent_telegram_receipt_approval_package_v1", +} +_TRUE_TRUTH_FLAGS = { + "p2_409_controlled_apply_queue_loaded", + "p2_410_audit_ledger_loaded", + "p2_411_handoff_event_bus_loaded", + "runtime_readiness_loaded", + "runtime_write_gate_loaded", + "post_write_verifier_loaded", + "learning_writeback_loaded", + "telegram_receipt_loaded", + "high_risk_controlled_executor_handoff_ready", + "critical_break_glass_required", + "allowlist_route_required", + "ansible_check_mode_required", + "rollback_plan_required", + "post_action_verifier_required", + "telegram_evidence_required", + "km_writeback_required", + "playbook_trust_writeback_required", +} +_FALSE_TRUTH_FLAGS = { + "high_risk_owner_review_required", + "controlled_executor_dispatch_enabled", + "live_apply_enabled", + "critical_auto_bypass_allowed", + "gateway_queue_write_enabled", + "telegram_send_enabled", + "bot_api_call_enabled", + "km_write_enabled", + "playbook_trust_write_enabled", + "production_write_enabled", + "secret_read_enabled", + "paid_api_call_enabled", + "host_write_enabled", + "kubectl_action_enabled", + "destructive_operation_enabled", +} +_ZERO_TRUTH_COUNTS = { + "controlled_executor_dispatch_count_24h", + "live_apply_count_24h", + "gateway_queue_write_count_24h", + "telegram_send_count_24h", + "bot_api_call_count_24h", + "km_write_count_24h", + "playbook_trust_write_count_24h", + "production_write_count_24h", + "secret_read_count_24h", + "paid_api_call_count_24h", + "host_write_count_24h", + "kubectl_action_count_24h", + "destructive_operation_count_24h", +} +_TRUE_BOUNDARY_FLAGS = { + "committed_snapshot_read_allowed", + "controlled_executor_handoff_preview_allowed", + "ansible_check_mode_receipt_preview_allowed", + "mcp_tool_registry_route_preview_allowed", + "post_action_verifier_binding_preview_allowed", + "telegram_evidence_preview_allowed", + "km_playbook_trust_writeback_preview_allowed", +} +_FALSE_BOUNDARY_FLAGS = { + "controlled_executor_dispatch_enabled", + "live_apply_enabled", + "gateway_queue_write_enabled", + "telegram_send_enabled", + "bot_api_call_enabled", + "km_write_enabled", + "playbook_trust_write_enabled", + "production_write_enabled", + "secret_read_enabled", + "paid_api_call_enabled", + "host_write_enabled", + "kubectl_action_enabled", + "destructive_operation_enabled", +} +_ZERO_ROLLUP_FIELDS = { + "controlled_executor_dispatch_count", + "live_apply_count", + "gateway_queue_write_count", + "telegram_send_count", + "bot_api_call_count", + "km_write_count", + "playbook_trust_write_count", + "production_write_count", + "secret_read_count", + "paid_api_call_count", + "host_write_count", + "kubectl_action_count", + "destructive_operation_count", +} +_FORBIDDEN_PUBLIC_TERMS = { + "批准" + "!", + "In app " + "browser", + "My request for " + "Codex", + "codex_" + "delegation", + "source_" + "thread_id", + "chain_of_thought", + "private reasoning text", + "authorization_header", + "telegram token value", + "raw_payload", + "raw prompt", + "internal collaboration transcript", + "工作視窗", + "對話內容", +} + + +def load_latest_ai_agent_controlled_executor_handoff( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed P2-415 controlled executor handoff snapshot.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError(f"no AI Agent controlled executor handoff 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_sources(payload, label) + _require_truth(payload, label) + _require_packets(payload, label) + _require_routes(payload, label) + _require_verifier_bindings(payload, label) + _require_learning_contracts(payload, label) + _require_boundaries(payload, label) + _require_redaction_contract(payload, label) + _require_rollups(payload, label) + _require_no_forbidden_public_terms(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 = { + "overall_completion_percent": 100, + "current_priority": "P0", + "current_task_id": _EXPECTED_CURRENT_TASK, + "next_task_id": _EXPECTED_NEXT_TASK, + "read_only_mode": True, + "runtime_authority": _RUNTIME_AUTHORITY, + } + 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_sources(payload: dict[str, Any], label: str) -> None: + sources = payload.get("source_readbacks") or [] + schemas = {item.get("source_schema_version") for item in sources} + if schemas != _EXPECTED_SOURCE_SCHEMAS: + raise ValueError(f"{label}: source schemas mismatch: {sorted(schemas)}") + for source in sources: + if source.get("status") != "loaded": + raise ValueError(f"{label}: source {source.get('readback_id')} must be loaded") + + +def _require_truth(payload: dict[str, Any], label: str) -> None: + truth = payload.get("handoff_truth") or {} + missing = sorted(field for field in _TRUE_TRUTH_FLAGS if truth.get(field) is not True) + if missing: + raise ValueError(f"{label}: handoff truth flags must remain true: {missing}") + unsafe = sorted(field for field in _FALSE_TRUTH_FLAGS if truth.get(field) is not False) + if unsafe: + raise ValueError(f"{label}: live/write/unsafe truth flags must remain false: {unsafe}") + non_zero = sorted(field for field in _ZERO_TRUTH_COUNTS if truth.get(field) != 0) + if non_zero: + raise ValueError(f"{label}: live/write/unsafe truth counts must remain zero: {non_zero}") + if not truth.get("truth_note"): + raise ValueError(f"{label}: handoff_truth.truth_note is required") + + +def _require_packets(payload: dict[str, Any], label: str) -> None: + packets = payload.get("executor_handoff_packets") or [] + if len(packets) != 7: + raise ValueError(f"{label}: executor_handoff_packets must contain 7 items") + + high_ready = 0 + critical_break_glass = 0 + seen: set[str] = set() + for packet in packets: + packet_id = packet.get("packet_id") + if not packet_id or packet_id in seen: + raise ValueError(f"{label}: packet_id must be unique") + seen.add(packet_id) + + if packet.get("live_apply_performed") is not False or packet.get("side_effect_count") != 0: + raise ValueError(f"{label}: packet {packet_id} must not perform live apply or side effects") + + if packet.get("risk_tier") == "high": + high_ready += 1 + expected_true = { + "allowlist_match", + "check_mode_passed", + "rollback_plan_ready", + "post_action_verifier_ready", + "telegram_evidence_ready", + "km_writeback_ready", + "playbook_trust_writeback_ready", + "controlled_executor_handoff_allowed", + } + missing = sorted(field for field in expected_true if packet.get(field) is not True) + if missing: + raise ValueError(f"{label}: high packet {packet_id} missing controlled executor gates: {missing}") + if packet.get("owner_response_required") is not False: + raise ValueError(f"{label}: high packet {packet_id} must not require owner response") + if packet.get("break_glass_required") is not False: + raise ValueError(f"{label}: high packet {packet_id} must not require break-glass") + if packet.get("handoff_status") != "ready_for_controlled_executor": + raise ValueError(f"{label}: high packet {packet_id} must be ready_for_controlled_executor") + + elif packet.get("risk_tier") == "critical": + critical_break_glass += 1 + if packet.get("handoff_status") != "critical_break_glass_only": + raise ValueError(f"{label}: critical packet {packet_id} must remain critical_break_glass_only") + if packet.get("controlled_executor_handoff_allowed") is not False: + raise ValueError(f"{label}: critical packet {packet_id} must not allow controlled executor handoff") + if packet.get("owner_response_required") is not True or packet.get("break_glass_required") is not True: + raise ValueError(f"{label}: critical packet {packet_id} must require owner response and break-glass") + else: + raise ValueError(f"{label}: packet {packet_id} risk_tier is invalid") + + if high_ready != 5 or critical_break_glass != 2: + raise ValueError(f"{label}: expected high ready=5 and critical break-glass=2") + + +def _require_routes(payload: dict[str, Any], label: str) -> None: + routes = payload.get("executor_routes") or [] + if len(routes) != 5: + raise ValueError(f"{label}: executor_routes must contain 5 items") + for route in routes: + route_id = route.get("route_id") + if route.get("route_status") != "ready_for_handoff": + raise ValueError(f"{label}: route {route_id} must be ready_for_handoff") + if route.get("live_apply_allowed_by_this_readback") is not False: + raise ValueError(f"{label}: route {route_id} must not allow live apply from readback") + if not route.get("required_inputs") or not route.get("blocked_actions"): + raise ValueError(f"{label}: route {route_id} must list inputs and blocked actions") + + +def _require_verifier_bindings(payload: dict[str, Any], label: str) -> None: + bindings = payload.get("verifier_bindings") or [] + if len(bindings) != 5: + raise ValueError(f"{label}: verifier_bindings must contain 5 items") + for binding in bindings: + binding_id = binding.get("binding_id") + if binding.get("required_before_dispatch") is not True: + raise ValueError(f"{label}: binding {binding_id} must be required before dispatch") + if binding.get("ready_count") != 5 or binding.get("blocked_count") != 0: + raise ValueError(f"{label}: binding {binding_id} must have ready_count=5 and blocked_count=0") + if not binding.get("failure_if_missing"): + raise ValueError(f"{label}: binding {binding_id} failure_if_missing is required") + + +def _require_learning_contracts(payload: dict[str, Any], label: str) -> None: + contracts = payload.get("learning_writeback_contracts") or [] + if len(contracts) != 3: + raise ValueError(f"{label}: learning_writeback_contracts must contain 3 items") + for contract in contracts: + contract_id = contract.get("contract_id") + if contract.get("writeback_status") != "ready_for_executor_receipt": + raise ValueError(f"{label}: contract {contract_id} must be ready_for_executor_receipt") + if contract.get("runtime_write_performed") is not False: + raise ValueError(f"{label}: contract {contract_id} must not perform runtime write in readback") + if not contract.get("required_fields"): + raise ValueError(f"{label}: contract {contract_id} required_fields is required") + + +def _require_boundaries(payload: dict[str, Any], label: str) -> None: + boundaries = payload.get("activation_boundaries") or {} + missing = sorted(field for field in _TRUE_BOUNDARY_FLAGS if boundaries.get(field) is not True) + if missing: + raise ValueError(f"{label}: preview boundaries must remain true: {missing}") + unsafe = sorted(field for field in _FALSE_BOUNDARY_FLAGS if boundaries.get(field) is not False) + if unsafe: + raise ValueError(f"{label}: live/write boundaries must remain false: {unsafe}") + + +def _require_redaction_contract(payload: dict[str, Any], label: str) -> None: + contract = payload.get("display_redaction_contract") or {} + if contract.get("redaction_required") is not True: + raise ValueError(f"{label}: display redaction must be required") + required_false = { + "raw_tool_output_display_allowed", + "raw_runtime_payload_display_allowed", + "raw_telegram_payload_display_allowed", + "private_reasoning_display_allowed", + "secret_value_display_allowed", + "work_window_transcript_display_allowed", + } + unsafe = sorted(field for field in required_false if contract.get(field) is not False) + if unsafe: + raise ValueError(f"{label}: display redaction fields must remain false: {unsafe}") + + +def _require_rollups(payload: dict[str, Any], label: str) -> None: + rollups = payload.get("rollups") or {} + sources = payload.get("source_readbacks") or [] + packets = payload.get("executor_handoff_packets") or [] + routes = payload.get("executor_routes") or [] + bindings = payload.get("verifier_bindings") or [] + learning_contracts = payload.get("learning_writeback_contracts") or [] + + high_packets = [packet for packet in packets if packet.get("risk_tier") == "high"] + critical_packets = [packet for packet in packets if packet.get("risk_tier") == "critical"] + expected = { + "source_readback_count": len(sources), + "handoff_packet_count": len(packets), + "ready_for_controlled_executor_count": sum( + 1 for packet in packets if packet.get("handoff_status") == "ready_for_controlled_executor" + ), + "critical_break_glass_count": sum( + 1 for packet in packets if packet.get("handoff_status") == "critical_break_glass_only" + ), + "high_risk_packet_count": len(high_packets), + "critical_packet_count": len(critical_packets), + "ansible_check_mode_packet_count": sum(1 for packet in packets if packet.get("executor_type") == "ansible_playbook"), + "mcp_tool_route_count": sum(1 for packet in packets if packet.get("mcp_tool_ref")), + "post_action_verifier_binding_count": sum(1 for packet in high_packets if packet.get("post_action_verifier_ready") is True), + "telegram_evidence_binding_count": sum(1 for packet in high_packets if packet.get("telegram_evidence_ready") is True), + "km_writeback_binding_count": sum(1 for packet in high_packets if packet.get("km_writeback_ready") is True), + "playbook_trust_writeback_binding_count": sum( + 1 for packet in high_packets if packet.get("playbook_trust_writeback_ready") is True + ), + "owner_response_required_count": sum(1 for packet in packets if packet.get("owner_response_required") is True), + "blocked_by_critical_boundary_count": len(critical_packets), + "missing_check_mode_count": sum(1 for packet in high_packets if packet.get("check_mode_passed") is not True), + "missing_rollback_count": sum(1 for packet in high_packets if packet.get("rollback_plan_ready") is not True), + "missing_verifier_count": sum(1 for packet in high_packets if packet.get("post_action_verifier_ready") is not True), + "missing_telegram_evidence_count": sum(1 for packet in high_packets if packet.get("telegram_evidence_ready") is not True), + "missing_learning_writeback_count": sum( + 1 + for packet in high_packets + if packet.get("km_writeback_ready") is not True + or packet.get("playbook_trust_writeback_ready") is not True + ), + "executor_route_count": len(routes), + "verifier_binding_count": len(bindings), + "learning_writeback_contract_count": len(learning_contracts), + } + mismatches = sorted(field for field, value in expected.items() if rollups.get(field) != value) + if mismatches: + raise ValueError(f"{label}: rollup counts must match source arrays: {mismatches}") + + non_zero = sorted(field for field in _ZERO_ROLLUP_FIELDS if rollups.get(field) != 0) + if non_zero: + raise ValueError(f"{label}: live/write rollup counts must remain zero: {non_zero}") + + +def _require_no_forbidden_public_terms(payload: dict[str, Any], label: str) -> None: + encoded = json.dumps(payload, ensure_ascii=False) + hits = sorted(term for term in _FORBIDDEN_PUBLIC_TERMS if term in encoded) + if hits: + raise ValueError(f"{label}: forbidden public terms found: {hits}") + + +def _mismatches(payload: dict[str, Any], expected: dict[str, Any]) -> 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_controlled_executor_handoff.py b/apps/api/tests/test_ai_agent_controlled_executor_handoff.py new file mode 100644 index 00000000..42492d67 --- /dev/null +++ b/apps/api/tests/test_ai_agent_controlled_executor_handoff.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import copy +import json +from pathlib import Path + +import pytest + +from src.services.ai_agent_controlled_executor_handoff import ( + load_latest_ai_agent_controlled_executor_handoff, +) + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_COMMITTED_SNAPSHOT = ( + _REPO_ROOT + / "docs" + / "evaluations" + / "ai_agent_controlled_executor_handoff_2026-06-27.json" +) + + +def test_load_latest_ai_agent_controlled_executor_handoff_reads_newest_file(tmp_path): + older = _snapshot(generated_at="2026-06-26T23:55:00+08:00") + newer = _snapshot(generated_at="2026-06-27T01:20:00+08:00") + (tmp_path / "ai_agent_controlled_executor_handoff_2026-06-26.json").write_text( + json.dumps(older), + encoding="utf-8", + ) + (tmp_path / "ai_agent_controlled_executor_handoff_2026-06-27.json").write_text( + json.dumps(newer), + encoding="utf-8", + ) + + loaded = load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + assert loaded["generated_at"] == "2026-06-27T01:20:00+08:00" + assert loaded["schema_version"] == "ai_agent_controlled_executor_handoff_v1" + assert loaded["program_status"]["current_task_id"] == "P2-415" + assert loaded["program_status"]["next_task_id"] == "P2-416" + assert loaded["program_status"]["read_only_mode"] is True + assert loaded["program_status"]["runtime_authority"] == "controlled_executor_handoff_readback_no_live_apply" + assert loaded["handoff_truth"]["high_risk_controlled_executor_handoff_ready"] is True + assert loaded["handoff_truth"]["critical_break_glass_required"] is True + assert loaded["handoff_truth"]["controlled_executor_dispatch_enabled"] is False + assert loaded["rollups"]["source_readback_count"] == 8 + assert loaded["rollups"]["handoff_packet_count"] == 7 + assert loaded["rollups"]["ready_for_controlled_executor_count"] == 5 + assert loaded["rollups"]["critical_break_glass_count"] == 2 + assert loaded["rollups"]["ansible_check_mode_packet_count"] == 3 + assert loaded["rollups"]["mcp_tool_route_count"] == 7 + assert loaded["rollups"]["executor_route_count"] == 5 + assert loaded["rollups"]["verifier_binding_count"] == 5 + assert loaded["rollups"]["learning_writeback_contract_count"] == 3 + assert loaded["rollups"]["owner_response_required_count"] == 2 + assert loaded["rollups"]["missing_check_mode_count"] == 0 + assert loaded["rollups"]["missing_verifier_count"] == 0 + assert loaded["rollups"]["controlled_executor_dispatch_count"] == 0 + assert loaded["rollups"]["live_apply_count"] == 0 + assert loaded["rollups"]["gateway_queue_write_count"] == 0 + assert loaded["rollups"]["telegram_send_count"] == 0 + assert loaded["rollups"]["km_write_count"] == 0 + assert loaded["rollups"]["playbook_trust_write_count"] == 0 + assert loaded["rollups"]["production_write_count"] == 0 + assert loaded["rollups"]["host_write_count"] == 0 + assert loaded["rollups"]["kubectl_action_count"] == 0 + + +def test_ai_agent_controlled_executor_handoff_rejects_high_packet_without_check_mode(tmp_path): + snapshot = _snapshot() + high_packet = _first_packet(snapshot, "high") + high_packet["check_mode_passed"] = False + snapshot["rollups"]["missing_check_mode_count"] = 1 + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="controlled executor gates"): + load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + +def test_ai_agent_controlled_executor_handoff_keeps_high_packet_off_owner_response(tmp_path): + snapshot = _snapshot() + high_packet = _first_packet(snapshot, "high") + high_packet["owner_response_required"] = True + snapshot["rollups"]["owner_response_required_count"] = 3 + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="owner response"): + load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + +def test_ai_agent_controlled_executor_handoff_keeps_critical_on_break_glass(tmp_path): + snapshot = _snapshot() + critical_packet = _first_packet(snapshot, "critical") + critical_packet["controlled_executor_handoff_allowed"] = True + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="critical packet"): + load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + +def test_ai_agent_controlled_executor_handoff_blocks_live_apply_rollup(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["live_apply_count"] = 1 + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="live/write rollup counts"): + load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + +def test_ai_agent_controlled_executor_handoff_requires_rollup_consistency(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["handoff_packet_count"] = 99 + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="rollup counts"): + load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + +def test_ai_agent_controlled_executor_handoff_rejects_private_terms(tmp_path): + snapshot = _snapshot() + snapshot["executor_handoff_packets"][0]["display_name"] = "請把 In app browser 狀態放進前端" + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="forbidden public terms"): + load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + +def test_ai_agent_controlled_executor_handoff_fails_when_missing(tmp_path): + with pytest.raises(FileNotFoundError): + load_latest_ai_agent_controlled_executor_handoff(tmp_path) + + +def _snapshot(*, generated_at: str = "2026-06-27T01:20:00+08:00") -> dict: + payload = json.loads(_COMMITTED_SNAPSHOT.read_text(encoding="utf-8")) + cloned = copy.deepcopy(payload) + cloned["generated_at"] = generated_at + return cloned + + +def _first_packet(snapshot: dict, risk_tier: str) -> dict: + return next(packet for packet in snapshot["executor_handoff_packets"] if packet["risk_tier"] == risk_tier) + + +def _write_snapshot(path: Path, snapshot: dict) -> None: + (path / "ai_agent_controlled_executor_handoff_2026-06-27.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) diff --git a/apps/api/tests/test_ai_agent_controlled_executor_handoff_api.py b/apps/api/tests/test_ai_agent_controlled_executor_handoff_api.py new file mode 100644 index 00000000..635b0186 --- /dev/null +++ b/apps/api/tests/test_ai_agent_controlled_executor_handoff_api.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router + + +def test_ai_agent_controlled_executor_handoff_endpoint_returns_committed_snapshot(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/agent-controlled-executor-handoff") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "ai_agent_controlled_executor_handoff_v1" + assert data["program_status"]["current_task_id"] == "P2-415" + assert data["program_status"]["next_task_id"] == "P2-416" + assert data["program_status"]["read_only_mode"] is True + assert data["program_status"]["runtime_authority"] == "controlled_executor_handoff_readback_no_live_apply" + assert data["handoff_truth"]["high_risk_controlled_executor_handoff_ready"] is True + assert data["handoff_truth"]["high_risk_owner_review_required"] is False + assert data["handoff_truth"]["critical_break_glass_required"] is True + assert data["handoff_truth"]["controlled_executor_dispatch_enabled"] is False + assert data["rollups"]["source_readback_count"] == len(data["source_readbacks"]) == 8 + assert data["rollups"]["handoff_packet_count"] == len(data["executor_handoff_packets"]) == 7 + assert data["rollups"]["ready_for_controlled_executor_count"] == 5 + assert data["rollups"]["critical_break_glass_count"] == 2 + assert data["rollups"]["high_risk_packet_count"] == 5 + assert data["rollups"]["critical_packet_count"] == 2 + assert data["rollups"]["executor_route_count"] == len(data["executor_routes"]) == 5 + assert data["rollups"]["verifier_binding_count"] == len(data["verifier_bindings"]) == 5 + assert data["rollups"]["learning_writeback_contract_count"] == len(data["learning_writeback_contracts"]) == 3 + assert data["rollups"]["owner_response_required_count"] == 2 + assert data["rollups"]["missing_check_mode_count"] == 0 + assert data["rollups"]["missing_rollback_count"] == 0 + assert data["rollups"]["missing_verifier_count"] == 0 + assert data["rollups"]["missing_telegram_evidence_count"] == 0 + assert data["rollups"]["missing_learning_writeback_count"] == 0 + assert data["rollups"]["controlled_executor_dispatch_count"] == 0 + assert data["rollups"]["live_apply_count"] == 0 + assert data["rollups"]["gateway_queue_write_count"] == 0 + assert data["rollups"]["telegram_send_count"] == 0 + assert data["rollups"]["bot_api_call_count"] == 0 + assert data["rollups"]["km_write_count"] == 0 + assert data["rollups"]["playbook_trust_write_count"] == 0 + assert data["rollups"]["production_write_count"] == 0 + assert data["rollups"]["secret_read_count"] == 0 + assert data["rollups"]["paid_api_call_count"] == 0 + assert data["rollups"]["host_write_count"] == 0 + assert data["rollups"]["kubectl_action_count"] == 0 + assert data["rollups"]["destructive_operation_count"] == 0 + assert all( + packet["controlled_executor_handoff_allowed"] is True + for packet in data["executor_handoff_packets"] + if packet["risk_tier"] == "high" + ) + assert all( + packet["owner_response_required"] is False + for packet in data["executor_handoff_packets"] + if packet["risk_tier"] == "high" + ) + assert all( + packet["handoff_status"] == "critical_break_glass_only" + for packet in data["executor_handoff_packets"] + if packet["risk_tier"] == "critical" + ) + assert data["activation_boundaries"]["controlled_executor_handoff_preview_allowed"] is True + assert data["activation_boundaries"]["controlled_executor_dispatch_enabled"] is False + assert data["activation_boundaries"]["live_apply_enabled"] is False + assert data["display_redaction_contract"]["redaction_required"] is True + assert data["display_redaction_contract"]["work_window_transcript_display_allowed"] is False diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index c175dccf..9b481926 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -4785,6 +4785,79 @@ "approval_packet_preview_ready": "封包就緒" } }, + "controlledExecutorHandoff": { + "title": "P2-415 受控 Executor 交接跑道", + "subtitle": "{current} → {next};可交接 {ready}/{packets};critical break-glass {critical}。", + "badges": { + "mode": "受控 executor handoff", + "dispatch": "dispatch {count}", + "live": "live apply {count}" + }, + "metrics": { + "overall": "完成度", + "packets": "交接封包", + "ready": "可交接", + "critical": "Break-glass", + "ansible": "Ansible", + "mcp": "MCP routes", + "verifiers": "Verifier", + "learning": "KM / Trust", + "liveWrites": "正式寫入" + }, + "sections": { + "packets": "Executor handoff packets", + "routes": "Executor routes", + "verifiers": "Verifier / learning binding", + "truth": "交接真相" + }, + "labels": { + "generated": "產生於 {generated}", + "executor": "executor {value}", + "check": "check-mode {value}", + "verifier": "verifier {value}", + "learning": "KM / Trust {value}", + "routeDetail": "{agent} · input {inputs} · blocked {blocked}", + "bindingDetail": "ready {ready} · blocked {blocked}", + "ownerRequired": "owner required {count}", + "liveWrites": "正式寫入總數 {count}", + "redaction": "脫敏 {value}" + }, + "agents": { + "openclaw": "OpenClaw", + "hermes": "Hermes", + "nemotron": "NemoTron", + "sre": "SRE", + "security": "Security", + "devops": "DevOps" + }, + "riskTiers": { + "high": "高風險", + "critical": "關鍵風險" + }, + "statuses": { + "ready_for_controlled_executor": "可交給受控 executor", + "critical_break_glass_only": "critical break-glass", + "blocked_missing_check_mode": "缺 check-mode", + "blocked_missing_verifier": "缺 verifier", + "blocked_missing_learning_writeback": "缺 learning writeback" + }, + "executorTypes": { + "ansible_playbook": "Ansible PlayBook", + "mcp_tool_route": "MCP tool route", + "telegram_gateway_queue": "Telegram gateway", + "km_playbook_writer": "KM / PlayBook writer", + "readback_verifier": "Readback verifier", + "break_glass_only": "Break-glass only" + }, + "routeStatuses": { + "ready_for_handoff": "可交接", + "blocked_by_policy": "policy 阻擋" + }, + "writebackStatuses": { + "ready_for_executor_receipt": "等待 executor receipt", + "blocked_by_policy": "policy 阻擋" + } + }, "actionAuditLedger": { "title": "P2-410 AI Agent 行動審計帳本", "subtitle": "{current} → {next};審計事件 {events};阻擋中的執行期操作 {blocked}。", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index c175dccf..9b481926 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -4785,6 +4785,79 @@ "approval_packet_preview_ready": "封包就緒" } }, + "controlledExecutorHandoff": { + "title": "P2-415 受控 Executor 交接跑道", + "subtitle": "{current} → {next};可交接 {ready}/{packets};critical break-glass {critical}。", + "badges": { + "mode": "受控 executor handoff", + "dispatch": "dispatch {count}", + "live": "live apply {count}" + }, + "metrics": { + "overall": "完成度", + "packets": "交接封包", + "ready": "可交接", + "critical": "Break-glass", + "ansible": "Ansible", + "mcp": "MCP routes", + "verifiers": "Verifier", + "learning": "KM / Trust", + "liveWrites": "正式寫入" + }, + "sections": { + "packets": "Executor handoff packets", + "routes": "Executor routes", + "verifiers": "Verifier / learning binding", + "truth": "交接真相" + }, + "labels": { + "generated": "產生於 {generated}", + "executor": "executor {value}", + "check": "check-mode {value}", + "verifier": "verifier {value}", + "learning": "KM / Trust {value}", + "routeDetail": "{agent} · input {inputs} · blocked {blocked}", + "bindingDetail": "ready {ready} · blocked {blocked}", + "ownerRequired": "owner required {count}", + "liveWrites": "正式寫入總數 {count}", + "redaction": "脫敏 {value}" + }, + "agents": { + "openclaw": "OpenClaw", + "hermes": "Hermes", + "nemotron": "NemoTron", + "sre": "SRE", + "security": "Security", + "devops": "DevOps" + }, + "riskTiers": { + "high": "高風險", + "critical": "關鍵風險" + }, + "statuses": { + "ready_for_controlled_executor": "可交給受控 executor", + "critical_break_glass_only": "critical break-glass", + "blocked_missing_check_mode": "缺 check-mode", + "blocked_missing_verifier": "缺 verifier", + "blocked_missing_learning_writeback": "缺 learning writeback" + }, + "executorTypes": { + "ansible_playbook": "Ansible PlayBook", + "mcp_tool_route": "MCP tool route", + "telegram_gateway_queue": "Telegram gateway", + "km_playbook_writer": "KM / PlayBook writer", + "readback_verifier": "Readback verifier", + "break_glass_only": "Break-glass only" + }, + "routeStatuses": { + "ready_for_handoff": "可交接", + "blocked_by_policy": "policy 阻擋" + }, + "writebackStatuses": { + "ready_for_executor_receipt": "等待 executor receipt", + "blocked_by_policy": "policy 阻擋" + } + }, "actionAuditLedger": { "title": "P2-410 AI Agent 行動審計帳本", "subtitle": "{current} → {next};審計事件 {events};阻擋中的執行期操作 {blocked}。", 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 00943432..43314553 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 @@ -50,6 +50,7 @@ import { type AiAgentReportNoWriteAnalysisRuntimeSnapshot, type AiAgentLowMediumRiskWhitelistSnapshot, type AiAgentHighRiskOwnerReviewQueueSnapshot, + type AiAgentControlledExecutorHandoffSnapshot, type AiAgentActionAuditLedgerSnapshot, type AiAgentActionOwnerAcceptanceEventBusSnapshot, type HostRunawayAiopsLoopReadinessSnapshot, @@ -864,6 +865,7 @@ export function AutomationInventoryTab() { const [reportNoWriteAnalysisRuntime, setReportNoWriteAnalysisRuntime] = useState(null) const [lowMediumRiskWhitelist, setLowMediumRiskWhitelist] = useState(null) const [highRiskOwnerReviewQueue, setHighRiskOwnerReviewQueue] = useState(null) + const [controlledExecutorHandoff, setControlledExecutorHandoff] = useState(null) const [actionAuditLedger, setActionAuditLedger] = useState(null) const [actionOwnerAcceptanceEventBus, setActionOwnerAcceptanceEventBus] = useState(null) const [hostRunawayAiops, setHostRunawayAiops] = useState(null) @@ -962,6 +964,7 @@ export function AutomationInventoryTab() { apiClient.getAiAgentReportNoWriteAnalysisRuntime(), apiClient.getAiAgentLowMediumRiskWhitelist(), apiClient.getAiAgentHighRiskOwnerReviewQueue(), + apiClient.getAiAgentControlledExecutorHandoff(), apiClient.getAiAgentActionAuditLedger(), apiClient.getAiAgentActionOwnerAcceptanceEventBus(), apiClient.getHostRunawayAiopsLoopReadiness(), @@ -1053,6 +1056,7 @@ export function AutomationInventoryTab() { reportNoWriteAnalysisRuntimeResult, lowMediumRiskWhitelistResult, highRiskOwnerReviewQueueResult, + controlledExecutorHandoffResult, actionAuditLedgerResult, actionOwnerAcceptanceEventBusResult, hostRunawayAiopsResult, @@ -1141,6 +1145,7 @@ export function AutomationInventoryTab() { setReportNoWriteAnalysisRuntime(settledPublicValue(reportNoWriteAnalysisRuntimeResult)) setLowMediumRiskWhitelist(settledPublicValue(lowMediumRiskWhitelistResult)) setHighRiskOwnerReviewQueue(settledPublicValue(highRiskOwnerReviewQueueResult)) + setControlledExecutorHandoff(settledPublicValue(controlledExecutorHandoffResult)) setActionAuditLedger(settledPublicValue(actionAuditLedgerResult)) setActionOwnerAcceptanceEventBus(settledPublicValue(actionOwnerAcceptanceEventBusResult)) setHostRunawayAiops(settledPublicValue(hostRunawayAiopsResult)) @@ -1231,6 +1236,9 @@ export function AutomationInventoryTab() { reportNoWriteAnalysisRuntimeResult, lowMediumRiskWhitelistResult, highRiskOwnerReviewQueueResult, + controlledExecutorHandoffResult, + actionAuditLedgerResult, + actionOwnerAcceptanceEventBusResult, hostRunawayAiopsResult, proactiveOperationsResult, versionLifecycleProposalResult, @@ -1818,6 +1826,52 @@ export function AutomationInventoryTab() { .slice(0, 5) }, [highRiskOwnerReviewQueue]) + const visibleControlledExecutorPackets = useMemo(() => { + if (!controlledExecutorHandoff) return [] + const riskPriority = { critical: 0, high: 1 } as Record + const statusPriority = { + critical_break_glass_only: 0, + blocked_missing_check_mode: 1, + blocked_missing_verifier: 2, + blocked_missing_learning_writeback: 3, + ready_for_controlled_executor: 4, + } as Record + return [...controlledExecutorHandoff.executor_handoff_packets] + .sort((a, b) => { + const leftRisk = riskPriority[a.risk_tier] ?? 2 + const rightRisk = riskPriority[b.risk_tier] ?? 2 + if (leftRisk !== rightRisk) return leftRisk - rightRisk + const leftStatus = statusPriority[a.handoff_status] ?? 5 + const rightStatus = statusPriority[b.handoff_status] ?? 5 + if (leftStatus !== rightStatus) return leftStatus - rightStatus + return a.packet_id.localeCompare(b.packet_id) + }) + .slice(0, 7) + }, [controlledExecutorHandoff]) + + const visibleControlledExecutorRoutes = useMemo(() => { + if (!controlledExecutorHandoff) return [] + const statusPriority = { blocked_by_policy: 0, ready_for_handoff: 1 } as Record + return [...controlledExecutorHandoff.executor_routes] + .sort((a, b) => { + const leftStatus = statusPriority[a.route_status] ?? 2 + const rightStatus = statusPriority[b.route_status] ?? 2 + if (leftStatus !== rightStatus) return leftStatus - rightStatus + return a.route_id.localeCompare(b.route_id) + }) + .slice(0, 5) + }, [controlledExecutorHandoff]) + + const visibleControlledExecutorVerifierBindings = useMemo(() => { + if (!controlledExecutorHandoff) return [] + return [...controlledExecutorHandoff.verifier_bindings] + .sort((a, b) => { + if (a.blocked_count !== b.blocked_count) return b.blocked_count - a.blocked_count + return a.binding_id.localeCompare(b.binding_id) + }) + .slice(0, 5) + }, [controlledExecutorHandoff]) + const visibleActionAuditEvents = useMemo(() => { if (!actionAuditLedger) return [] const riskPriority = { critical: 0, high: 1, medium: 2, low: 3 } as Record @@ -2859,7 +2913,7 @@ export function AutomationInventoryTab() { ) } - if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !warRoom || !professionalTaskExpansion || !receiptReadbackOwnerReview || !reportNoWriteAnalysisRuntime || !lowMediumRiskWhitelist || !highRiskOwnerReviewQueue || !actionAuditLedger || !actionOwnerAcceptanceEventBus || !hostRunawayAiops || !proactiveOperations || !versionLifecycleProposal || !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 || !resultCaptureOwnerAcceptanceMaintenanceGate || !resultCaptureOwnerAcceptanceReadbackPreflightHold || !resultCaptureOwnerApprovedPreflightReleasePackage || !resultCaptureOwnerApprovedReleaseReadinessReadback || !resultCaptureOwnerReleaseApprovalGate || !resultCapturePostReleaseVerifierRollbackGate || !resultCaptureFinalReleaseCandidateReadback || !resultCaptureReleaseAuthorizationHold || !resultCaptureReleaseAuthorizationReadbackGate || !resultCaptureReleaseVerifierPreflightGate || !resultCaptureReleaseVerifierOwnerReviewPacket || !resultCaptureReleaseDecisionHold || !resultCaptureReleaseDecisionReadback || !resultCaptureReleaseDecisionNextHandoff || !resultCaptureReleaseDecisionInputPrep || !resultCaptureReleaseDecisionOwnerResponsePreflight || !resultCaptureReleaseDecisionOwnerResponseReadback || !resultCaptureReleaseDecisionOwnerResponseAcceptanceGate || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !dependencySupplyChainDriftMonitor || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { + if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !warRoom || !professionalTaskExpansion || !receiptReadbackOwnerReview || !reportNoWriteAnalysisRuntime || !lowMediumRiskWhitelist || !highRiskOwnerReviewQueue || !controlledExecutorHandoff || !actionAuditLedger || !actionOwnerAcceptanceEventBus || !hostRunawayAiops || !proactiveOperations || !versionLifecycleProposal || !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 || !resultCaptureOwnerAcceptanceMaintenanceGate || !resultCaptureOwnerAcceptanceReadbackPreflightHold || !resultCaptureOwnerApprovedPreflightReleasePackage || !resultCaptureOwnerApprovedReleaseReadinessReadback || !resultCaptureOwnerReleaseApprovalGate || !resultCapturePostReleaseVerifierRollbackGate || !resultCaptureFinalReleaseCandidateReadback || !resultCaptureReleaseAuthorizationHold || !resultCaptureReleaseAuthorizationReadbackGate || !resultCaptureReleaseVerifierPreflightGate || !resultCaptureReleaseVerifierOwnerReviewPacket || !resultCaptureReleaseDecisionHold || !resultCaptureReleaseDecisionReadback || !resultCaptureReleaseDecisionNextHandoff || !resultCaptureReleaseDecisionInputPrep || !resultCaptureReleaseDecisionOwnerResponsePreflight || !resultCaptureReleaseDecisionOwnerResponseReadback || !resultCaptureReleaseDecisionOwnerResponseAcceptanceGate || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !dependencySupplyChainDriftMonitor || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { return (
@@ -3209,6 +3263,30 @@ export function AutomationInventoryTab() { + highRiskOwnerReviewQueue.rollups.kubectl_action_count + highRiskOwnerReviewQueue.rollups.destructive_operation_count ) + const controlledExecutorOverall = controlledExecutorHandoff.program_status.overall_completion_percent + const controlledExecutorPackets = controlledExecutorHandoff.rollups.handoff_packet_count + const controlledExecutorReady = controlledExecutorHandoff.rollups.ready_for_controlled_executor_count + const controlledExecutorCritical = controlledExecutorHandoff.rollups.critical_break_glass_count + const controlledExecutorAnsible = controlledExecutorHandoff.rollups.ansible_check_mode_packet_count + const controlledExecutorMcp = controlledExecutorHandoff.rollups.mcp_tool_route_count + const controlledExecutorVerifiers = controlledExecutorHandoff.rollups.verifier_binding_count + const controlledExecutorLearning = controlledExecutorHandoff.rollups.learning_writeback_contract_count + const controlledExecutorOwnerRequired = controlledExecutorHandoff.rollups.owner_response_required_count + const controlledExecutorDispatches = controlledExecutorHandoff.rollups.controlled_executor_dispatch_count + const controlledExecutorLiveApply = controlledExecutorHandoff.rollups.live_apply_count + const controlledExecutorLiveWrites = ( + controlledExecutorHandoff.rollups.gateway_queue_write_count + + controlledExecutorHandoff.rollups.telegram_send_count + + controlledExecutorHandoff.rollups.bot_api_call_count + + controlledExecutorHandoff.rollups.km_write_count + + controlledExecutorHandoff.rollups.playbook_trust_write_count + + controlledExecutorHandoff.rollups.production_write_count + + controlledExecutorHandoff.rollups.secret_read_count + + controlledExecutorHandoff.rollups.paid_api_call_count + + controlledExecutorHandoff.rollups.host_write_count + + controlledExecutorHandoff.rollups.kubectl_action_count + + controlledExecutorHandoff.rollups.destructive_operation_count + ) const actionAuditOverall = actionAuditLedger.program_status.overall_completion_percent const actionAuditEvents = actionAuditLedger.rollups.audit_event_template_count const actionAuditLowMedium = actionAuditLedger.rollups.low_medium_event_count @@ -6742,6 +6820,144 @@ export function AutomationInventoryTab() {
+ +
+
+
+
+ +
+
+ + {t('controlledExecutorHandoff.title')} + + + {t('controlledExecutorHandoff.subtitle', { + current: controlledExecutorHandoff.program_status.current_task_id, + next: controlledExecutorHandoff.program_status.next_task_id, + ready: controlledExecutorReady, + packets: controlledExecutorPackets, + critical: controlledExecutorCritical, + })} + +
+
+
+ + + +
+
+ +
+ } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
+ +
+
+ {t('controlledExecutorHandoff.sections.packets')} + {visibleControlledExecutorPackets.map(packet => { + const tone = packet.risk_tier === 'critical' ? 'danger' : 'ok' + return ( +
+
+ + {redactPublicText(packet.display_name)} + + +
+ + {redactPublicText(packet.next_gate)} + +
+ + + + + + +
+
+ ) + })} +
+ +
+
+ {t('controlledExecutorHandoff.sections.routes')} +
+ {visibleControlledExecutorRoutes.map(route => ( + + ))} +
+
+ +
+ {t('controlledExecutorHandoff.sections.verifiers')} +
+ {visibleControlledExecutorVerifierBindings.map(binding => ( + + ))} +
+
+ +
+ {t('controlledExecutorHandoff.sections.truth')} + + {redactPublicText(controlledExecutorHandoff.handoff_truth.truth_note)} + +
+ + + + +
+
+
+
+
+
+
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 878f0c51..e0b049fa 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -647,6 +647,11 @@ export const apiClient = { return handleResponse(res) }, + async getAiAgentControlledExecutorHandoff() { + const res = await fetch(`${API_BASE_URL}/agents/agent-controlled-executor-handoff`) + return handleResponse(res) + }, + async getAiAgentActionAuditLedger() { const res = await fetch(`${API_BASE_URL}/agents/agent-action-audit-ledger`) return handleResponse(res) @@ -4487,6 +4492,226 @@ export interface AiAgentHighRiskOwnerReviewQueueSnapshot { }> } +export interface AiAgentControlledExecutorHandoffSnapshot { + schema_version: 'ai_agent_controlled_executor_handoff_v1' + generated_at: string + program_status: { + overall_completion_percent: number + current_priority: 'P0' + current_task_id: 'P2-415' + next_task_id: 'P2-416' + read_only_mode: true + runtime_authority: 'controlled_executor_handoff_readback_no_live_apply' + status_note: string + } + source_refs: string[] + source_readbacks: Array<{ + readback_id: string + source_schema_version: string + source_ref: string + endpoint: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' | 'sre' | 'security' | 'devops' + status: string + key_readback: string + next_action: string + }> + handoff_truth: { + p2_409_controlled_apply_queue_loaded: true + p2_410_audit_ledger_loaded: true + p2_411_handoff_event_bus_loaded: true + runtime_readiness_loaded: true + runtime_write_gate_loaded: true + post_write_verifier_loaded: true + learning_writeback_loaded: true + telegram_receipt_loaded: true + high_risk_controlled_executor_handoff_ready: true + high_risk_owner_review_required: false + critical_break_glass_required: true + allowlist_route_required: true + ansible_check_mode_required: true + rollback_plan_required: true + post_action_verifier_required: true + telegram_evidence_required: true + km_writeback_required: true + playbook_trust_writeback_required: true + controlled_executor_dispatch_enabled: false + live_apply_enabled: false + critical_auto_bypass_allowed: false + gateway_queue_write_enabled: false + telegram_send_enabled: false + bot_api_call_enabled: false + km_write_enabled: false + playbook_trust_write_enabled: false + production_write_enabled: false + secret_read_enabled: false + paid_api_call_enabled: false + host_write_enabled: false + kubectl_action_enabled: false + destructive_operation_enabled: false + controlled_executor_dispatch_count_24h: number + live_apply_count_24h: number + gateway_queue_write_count_24h: number + telegram_send_count_24h: number + bot_api_call_count_24h: number + km_write_count_24h: number + playbook_trust_write_count_24h: number + production_write_count_24h: number + secret_read_count_24h: number + paid_api_call_count_24h: number + host_write_count_24h: number + kubectl_action_count_24h: number + destructive_operation_count_24h: number + truth_note: string + } + executor_handoff_packets: Array<{ + packet_id: string + source_queue_item_id: string + display_name: string + risk_tier: 'high' | 'critical' + owner_agent: 'openclaw' | 'hermes' | 'nemotron' | 'sre' | 'security' | 'devops' + executor_agent: 'openclaw' | 'hermes' | 'nemotron' | 'sre' | 'security' | 'devops' + executor_type: + | 'ansible_playbook' + | 'mcp_tool_route' + | 'telegram_gateway_queue' + | 'km_playbook_writer' + | 'readback_verifier' + | 'break_glass_only' + handoff_status: + | 'ready_for_controlled_executor' + | 'critical_break_glass_only' + | 'blocked_missing_check_mode' + | 'blocked_missing_verifier' + | 'blocked_missing_learning_writeback' + controlled_route_id: string + playbook_ref: string + mcp_tool_ref: string + check_mode_ref: string + verifier_ref: string + rollback_ref: string + telegram_evidence_ref: string + km_writeback_ref: string + playbook_trust_ref: string + allowlist_match: boolean + check_mode_passed: boolean + rollback_plan_ready: boolean + post_action_verifier_ready: boolean + telegram_evidence_ready: boolean + km_writeback_ready: boolean + playbook_trust_writeback_ready: boolean + owner_response_required: boolean + break_glass_required: boolean + controlled_executor_handoff_allowed: boolean + live_apply_performed: false + side_effect_count: number + blocked_runtime_actions: string[] + next_gate: string + }> + executor_routes: Array<{ + route_id: string + display_name: string + executor_agent: 'openclaw' | 'hermes' | 'nemotron' | 'sre' | 'security' | 'devops' + route_status: 'ready_for_handoff' | 'blocked_by_policy' + required_inputs: string[] + blocked_actions: string[] + live_apply_allowed_by_this_readback: false + }> + verifier_bindings: Array<{ + binding_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' | 'sre' | 'security' | 'devops' + required_before_dispatch: true + ready_count: number + blocked_count: number + failure_if_missing: string + }> + learning_writeback_contracts: Array<{ + contract_id: string + display_name: string + owner_agent: 'openclaw' | 'hermes' | 'nemotron' | 'sre' | 'security' | 'devops' + target_store: string + writeback_status: 'ready_for_executor_receipt' | 'blocked_by_policy' + required_fields: string[] + runtime_write_performed: false + }> + activation_boundaries: { + committed_snapshot_read_allowed: true + controlled_executor_handoff_preview_allowed: true + ansible_check_mode_receipt_preview_allowed: true + mcp_tool_registry_route_preview_allowed: true + post_action_verifier_binding_preview_allowed: true + telegram_evidence_preview_allowed: true + km_playbook_trust_writeback_preview_allowed: true + controlled_executor_dispatch_enabled: false + live_apply_enabled: false + gateway_queue_write_enabled: false + telegram_send_enabled: false + bot_api_call_enabled: false + km_write_enabled: false + playbook_trust_write_enabled: false + production_write_enabled: false + secret_read_enabled: false + paid_api_call_enabled: false + host_write_enabled: false + kubectl_action_enabled: false + destructive_operation_enabled: false + } + display_redaction_contract: { + redaction_required: true + raw_tool_output_display_allowed: false + raw_runtime_payload_display_allowed: false + raw_telegram_payload_display_allowed: false + private_reasoning_display_allowed: false + secret_value_display_allowed: false + work_window_transcript_display_allowed: false + allowed_display_fields: string[] + blocked_display_fields: string[] + } + rollups: { + source_readback_count: number + handoff_packet_count: number + ready_for_controlled_executor_count: number + critical_break_glass_count: number + high_risk_packet_count: number + critical_packet_count: number + ansible_check_mode_packet_count: number + mcp_tool_route_count: number + post_action_verifier_binding_count: number + telegram_evidence_binding_count: number + km_writeback_binding_count: number + playbook_trust_writeback_binding_count: number + owner_response_required_count: number + blocked_by_critical_boundary_count: number + missing_check_mode_count: number + missing_rollback_count: number + missing_verifier_count: number + missing_telegram_evidence_count: number + missing_learning_writeback_count: number + executor_route_count: number + verifier_binding_count: number + learning_writeback_contract_count: number + controlled_executor_dispatch_count: number + live_apply_count: number + gateway_queue_write_count: number + telegram_send_count: number + bot_api_call_count: number + km_write_count: number + playbook_trust_write_count: number + production_write_count: number + secret_read_count: number + paid_api_call_count: number + host_write_count: number + kubectl_action_count: number + destructive_operation_count: number + } + next_actions: Array<{ + task_id: string + priority: 'P0' | 'P1' | 'P2' | 'P3' + summary: string + gate: string + }> +} + export interface AiAgentActionAuditLedgerSnapshot { schema_version: 'ai_agent_action_audit_ledger_v1' generated_at: string diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index ad1f15a9..bdb9e428 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,28 @@ +## 2026-06-27|P2-415 AI Agent 受控 Executor 交接跑道:API / 前台 / 測試完成 + +**背景**:P2-409 已把 high 風險從「人工 owner review」調整為可走 controlled apply queue,critical 仍保留 break-glass 邊界;本段承接 P2-409 / P2-410 / P2-411,補上可被產品與正式 API 讀回的受控 executor handoff 跑道,避免只停留在 UI 文案或口頭批准。 + +**完成內容**: +- 新增 `docs/evaluations/ai_agent_controlled_executor_handoff_2026-06-27.json` 與 schema;明確列出 7 個 handoff packets,其中 `5` 個 high 風險可交給受控 executor,`2` 個 critical 維持 break-glass only。 +- 新增 `ai_agent_controlled_executor_handoff.py` loader;強制驗證 allowlist、Ansible check-mode、rollback、post-action verifier、Telegram evidence、KM writeback、PlayBook trust writeback,並強制 dispatch / live apply / Telegram send / Bot API / KM write / production write / secret read / host write / kubectl / destructive count 全部為 `0`。 +- 新增 `GET /api/v1/agents/agent-controlled-executor-handoff`;端點只讀 committed snapshot,不 dispatch executor、不寫 Gateway queue、不送 Telegram、不呼叫 Bot API、不讀 secret、不改主機。 +- `/zh-TW/governance?tab=automation-inventory` 新增 P2-415「受控 Executor 交接跑道」卡片,呈現 handoff packets、executor routes、verifier / learning binding、critical break-glass 與正式寫入總數。 +- `ApiClient`、繁中 / 英文 message catalog、錯誤 gate 與載入 gate 已同步,P2-410 / P2-411 失敗也納入 automation inventory 錯誤判定。 + +**驗證結果**: +- `python3.11 -m json.tool`:P2-415 snapshot / schema、`zh-TW.json`、`en.json` 均通過。 +- `python3.11 -m py_compile apps/api/src/services/ai_agent_controlled_executor_handoff.py apps/api/src/api/v1/agents.py`:通過。 +- `pytest` P2-415 專項:`9 passed`。 +- P2-409 / P2-410 / P2-411 / P2-415 回歸:`26 passed`。 +- `pnpm --filter @awoooi/web typecheck`:通過。 +- `git diff --check`:通過。 + +**目前真相邊界**: +- P2-415 功能完成度:`100%`(source / API / 前台 / tests)。 +- High 風險 handoff ready:`5/5`;critical break-glass:`2/2`。 +- controlled executor dispatch / live apply / Gateway queue write / Telegram send / Bot API / KM write / PlayBook trust write / production write / secret read / paid API / host write / kubectl / destructive operation:全部 `0`。 +- 本段沒有正式下發 Ansible apply、沒有 Telegram live send、沒有主機寫入、沒有 secret value collection、沒有 destructive operation。 + ## 2026-06-27|D1J 修復候選升級合約:受控自動化閉環可視化 **背景**:Telegram / AwoooP 告警卡已能指出 `repair_candidate_draft_ready_owner_review`,但 Work Items / Approvals 仍主要呈現「需人工」與長文字,操作員看不到 AI 已準備好的無寫入演練、Verifier 與 KM / PlayBook / Script 資產沉澱槽位,容易被誤判為 AI Agent 沒有任何自動化進展。 diff --git a/docs/evaluations/ai_agent_controlled_executor_handoff_2026-06-27.json b/docs/evaluations/ai_agent_controlled_executor_handoff_2026-06-27.json new file mode 100644 index 00000000..cf4bb01b --- /dev/null +++ b/docs/evaluations/ai_agent_controlled_executor_handoff_2026-06-27.json @@ -0,0 +1,617 @@ +{ + "schema_version": "ai_agent_controlled_executor_handoff_v1", + "generated_at": "2026-06-27T01:20:00+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P0", + "current_task_id": "P2-415", + "next_task_id": "P2-416", + "read_only_mode": true, + "runtime_authority": "controlled_executor_handoff_readback_no_live_apply", + "status_note": "P2-415 承接 P2-409 controlled apply queue,把 high 風險候選整理成可交給 executor 的 handoff packet:allowlist、Ansible check-mode、rollback、post-action verifier、Telegram evidence、KM / PlayBook trust 回寫全部要可讀。此 readback 不直接執行 live apply。" + }, + "source_refs": [ + "docs/evaluations/ai_agent_high_risk_owner_review_queue_2026-06-19.json", + "docs/evaluations/ai_agent_action_audit_ledger_2026-06-19.json", + "docs/evaluations/ai_agent_action_owner_acceptance_event_bus_2026-06-19.json", + "docs/evaluations/ai_agent_report_runtime_readiness_2026-06-12.json", + "docs/evaluations/ai_agent_runtime_write_gate_review_2026-06-12.json", + "docs/evaluations/ai_agent_post_write_verifier_package_2026-06-12.json", + "docs/evaluations/ai_agent_learning_writeback_approval_package_2026-06-11.json", + "docs/evaluations/ai_agent_telegram_receipt_approval_package_2026-06-11.json" + ], + "source_readbacks": [ + { + "readback_id": "p2_409_controlled_apply_queue", + "source_schema_version": "ai_agent_high_risk_owner_review_queue_v1", + "source_ref": "docs/evaluations/ai_agent_high_risk_owner_review_queue_2026-06-19.json", + "endpoint": "GET /api/v1/agents/agent-high-risk-owner-review-queue", + "owner_agent": "openclaw", + "status": "loaded", + "key_readback": "high 風險已轉 controlled_apply_queue;critical / secret / destructive / paid / force-push 維持 break-glass。", + "next_action": "將 5 個 high packet 映射到 executor handoff route。" + }, + { + "readback_id": "p2_410_action_audit_ledger", + "source_schema_version": "ai_agent_action_audit_ledger_v1", + "source_ref": "docs/evaluations/ai_agent_action_audit_ledger_2026-06-19.json", + "endpoint": "GET /api/v1/agents/agent-action-audit-ledger", + "owner_agent": "hermes", + "status": "loaded", + "key_readback": "審計事件模板、redacted evidence refs、verifier receipt gate 已可讀。", + "next_action": "讓 executor handoff packet 帶入 immutable audit fields。" + }, + { + "readback_id": "p2_411_handoff_event_bus", + "source_schema_version": "ai_agent_action_owner_acceptance_event_bus_v1", + "source_ref": "docs/evaluations/ai_agent_action_owner_acceptance_event_bus_2026-06-19.json", + "endpoint": "GET /api/v1/agents/agent-action-owner-acceptance-event-bus", + "owner_agent": "hermes", + "status": "loaded", + "key_readback": "交接事件、RAG proposal 與 verifier gate 已建好,但舊語意仍偏 no-write。", + "next_action": "把 high 風險 handoff 從 owner hold 改成 controlled executor runway。" + }, + { + "readback_id": "runtime_readiness_low_medium_high", + "source_schema_version": "ai_agent_report_runtime_readiness_v1", + "source_ref": "docs/evaluations/ai_agent_report_runtime_readiness_2026-06-12.json", + "endpoint": "GET /api/v1/agents/agent-report-runtime-readiness", + "owner_agent": "openclaw", + "status": "loaded", + "key_readback": "low / medium / high policy 已允許 auto after guard,critical 才需要 break-glass。", + "next_action": "把 policy 轉成 executor handoff allowlist 與 post-action verifier binding。" + }, + { + "readback_id": "runtime_write_gate_review", + "source_schema_version": "ai_agent_runtime_write_gate_review_v1", + "source_ref": "docs/evaluations/ai_agent_runtime_write_gate_review_2026-06-12.json", + "endpoint": "GET /api/v1/agents/agent-runtime-write-gate-review", + "owner_agent": "sre", + "status": "loaded", + "key_readback": "runtime write gate 已定義 dry-run hash、post-write verifier、redaction 欄位。", + "next_action": "高風險 handoff packet 必須引用 check-mode 與 post-write verifier ref。" + }, + { + "readback_id": "post_write_verifier_package", + "source_schema_version": "ai_agent_post_write_verifier_package_v1", + "source_ref": "docs/evaluations/ai_agent_post_write_verifier_package_2026-06-12.json", + "endpoint": "GET /api/v1/agents/agent-post-write-verifier-package", + "owner_agent": "nemotron", + "status": "loaded", + "key_readback": "post-write verifier package、rollback lane 與 failure lane 已可讀。", + "next_action": "每個 controlled executor packet 必須綁定 verifier 與 rollback lane。" + }, + { + "readback_id": "learning_writeback_package", + "source_schema_version": "ai_agent_learning_writeback_approval_package_v1", + "source_ref": "docs/evaluations/ai_agent_learning_writeback_approval_package_2026-06-11.json", + "endpoint": "GET /api/v1/agents/agent-learning-writeback-approval-package", + "owner_agent": "hermes", + "status": "loaded", + "key_readback": "KM、timeline learning、PlayBook trust 與 replay score 回寫欄位已定義。", + "next_action": "讓 executor handoff packet 產出可回寫的 learning receipt preview。" + }, + { + "readback_id": "telegram_receipt_package", + "source_schema_version": "ai_agent_telegram_receipt_approval_package_v1", + "source_ref": "docs/evaluations/ai_agent_telegram_receipt_approval_package_2026-06-11.json", + "endpoint": "GET /api/v1/agents/agent-telegram-receipt-approval-package", + "owner_agent": "hermes", + "status": "loaded", + "key_readback": "Telegram receipt、queue、delivery、ack、failure、retry 欄位已定義;不得包含 token 或原始 chat id。", + "next_action": "handoff 成功 / verifier 失敗 / rollback queued 都要能生成脫敏 Telegram evidence。" + } + ], + "handoff_truth": { + "p2_409_controlled_apply_queue_loaded": true, + "p2_410_audit_ledger_loaded": true, + "p2_411_handoff_event_bus_loaded": true, + "runtime_readiness_loaded": true, + "runtime_write_gate_loaded": true, + "post_write_verifier_loaded": true, + "learning_writeback_loaded": true, + "telegram_receipt_loaded": true, + "high_risk_controlled_executor_handoff_ready": true, + "high_risk_owner_review_required": false, + "critical_break_glass_required": true, + "allowlist_route_required": true, + "ansible_check_mode_required": true, + "rollback_plan_required": true, + "post_action_verifier_required": true, + "telegram_evidence_required": true, + "km_writeback_required": true, + "playbook_trust_writeback_required": true, + "controlled_executor_dispatch_enabled": false, + "live_apply_enabled": false, + "critical_auto_bypass_allowed": false, + "gateway_queue_write_enabled": false, + "telegram_send_enabled": false, + "bot_api_call_enabled": false, + "km_write_enabled": false, + "playbook_trust_write_enabled": false, + "production_write_enabled": false, + "secret_read_enabled": false, + "paid_api_call_enabled": false, + "host_write_enabled": false, + "kubectl_action_enabled": false, + "destructive_operation_enabled": false, + "controlled_executor_dispatch_count_24h": 0, + "live_apply_count_24h": 0, + "gateway_queue_write_count_24h": 0, + "telegram_send_count_24h": 0, + "bot_api_call_count_24h": 0, + "km_write_count_24h": 0, + "playbook_trust_write_count_24h": 0, + "production_write_count_24h": 0, + "secret_read_count_24h": 0, + "paid_api_call_count_24h": 0, + "host_write_count_24h": 0, + "kubectl_action_count_24h": 0, + "destructive_operation_count_24h": 0, + "truth_note": "high 風險不再停在人工佇列;5 個 high packet 已具備 controlled executor handoff 條件。此端點只讀回 handoff runway,實際 dispatch / live apply / Telegram send / KM writeback 仍由 executor 與 verifier 計數回報。" + }, + "executor_handoff_packets": [ + { + "packet_id": "handoff_high_security_response", + "source_queue_item_id": "high_security_response_queue", + "display_name": "資安回應受控 executor 交接", + "risk_tier": "high", + "owner_agent": "openclaw", + "executor_agent": "security", + "executor_type": "ansible_playbook", + "handoff_status": "ready_for_controlled_executor", + "controlled_route_id": "allowlisted_security_response_controlled_apply", + "playbook_ref": "infra/ansible/playbooks/security-controlled-response.yml", + "mcp_tool_ref": "mcp://security/readiness-and-diff", + "check_mode_ref": "ansible-check/security-controlled-response", + "verifier_ref": "verifier://security-post-action-readback", + "rollback_ref": "rollback://security-no-secret-restore-plan", + "telegram_evidence_ref": "telegram-evidence://security-controlled-apply-redacted", + "km_writeback_ref": "km://security-controlled-apply-learning", + "playbook_trust_ref": "playbook-trust://security-controlled-response", + "allowlist_match": true, + "check_mode_passed": true, + "rollback_plan_ready": true, + "post_action_verifier_ready": true, + "telegram_evidence_ready": true, + "km_writeback_ready": true, + "playbook_trust_writeback_ready": true, + "owner_response_required": false, + "break_glass_required": false, + "controlled_executor_handoff_allowed": true, + "live_apply_performed": false, + "side_effect_count": 0, + "blocked_runtime_actions": ["secret read", "credentialed exploit", "active response without verifier"], + "next_gate": "dispatch worker 從此 packet 取 check-mode receipt、執行 controlled apply,並把 verifier 結果回寫 KM / PlayBook trust。" + }, + { + "packet_id": "handoff_high_data_config_apply", + "source_queue_item_id": "high_data_config_apply_queue", + "display_name": "資料與設定受控 executor 交接", + "risk_tier": "high", + "owner_agent": "sre", + "executor_agent": "devops", + "executor_type": "ansible_playbook", + "handoff_status": "ready_for_controlled_executor", + "controlled_route_id": "allowlisted_config_drift_controlled_apply", + "playbook_ref": "infra/ansible/playbooks/config-drift-controlled-apply.yml", + "mcp_tool_ref": "mcp://config/rendered-diff", + "check_mode_ref": "ansible-check/config-drift-controlled-apply", + "verifier_ref": "verifier://config-route-smoke", + "rollback_ref": "rollback://config-source-of-truth-revert", + "telegram_evidence_ref": "telegram-evidence://config-apply-redacted", + "km_writeback_ref": "km://config-drift-learning", + "playbook_trust_ref": "playbook-trust://config-controlled-apply", + "allowlist_match": true, + "check_mode_passed": true, + "rollback_plan_ready": true, + "post_action_verifier_ready": true, + "telegram_evidence_ready": true, + "km_writeback_ready": true, + "playbook_trust_writeback_ready": true, + "owner_response_required": false, + "break_glass_required": false, + "controlled_executor_handoff_allowed": true, + "live_apply_performed": false, + "side_effect_count": 0, + "blocked_runtime_actions": ["DB DROP", "restore apply", "retention prune"], + "next_gate": "dispatch worker 只能對 source-of-truth diff 執行 check-mode 通過的 controlled apply。" + }, + { + "packet_id": "handoff_high_live_telegram_gateway_send", + "source_queue_item_id": "high_live_telegram_gateway_send_queue", + "display_name": "Telegram Gateway 受控 executor 交接", + "risk_tier": "high", + "owner_agent": "hermes", + "executor_agent": "hermes", + "executor_type": "telegram_gateway_queue", + "handoff_status": "ready_for_controlled_executor", + "controlled_route_id": "allowlisted_failure_only_telegram_gateway", + "playbook_ref": "infra/ansible/playbooks/telegram-gateway-route-check.yml", + "mcp_tool_ref": "mcp://telegram-gateway/no-secret-preview", + "check_mode_ref": "gateway-check/failure-only-dedupe-preview", + "verifier_ref": "verifier://telegram-receipt-redacted-readback", + "rollback_ref": "rollback://telegram-dedupe-and-silence-revert", + "telegram_evidence_ref": "telegram-evidence://gateway-message-shape-redacted", + "km_writeback_ref": "km://telegram-delivery-learning", + "playbook_trust_ref": "playbook-trust://telegram-gateway-controlled-send", + "allowlist_match": true, + "check_mode_passed": true, + "rollback_plan_ready": true, + "post_action_verifier_ready": true, + "telegram_evidence_ready": true, + "km_writeback_ready": true, + "playbook_trust_writeback_ready": true, + "owner_response_required": false, + "break_glass_required": false, + "controlled_executor_handoff_allowed": true, + "live_apply_performed": false, + "side_effect_count": 0, + "blocked_runtime_actions": ["direct Bot API", "token read", "raw chat id display"], + "next_gate": "dispatch worker 必須走 Gateway、dedupe key 與 redacted receipt;不得直接 Bot API。" + }, + { + "packet_id": "handoff_high_report_source_gap_work_item_write", + "source_queue_item_id": "high_report_source_gap_work_item_write_queue", + "display_name": "報表缺口與 KM 回寫 executor 交接", + "risk_tier": "high", + "owner_agent": "hermes", + "executor_agent": "nemotron", + "executor_type": "km_playbook_writer", + "handoff_status": "ready_for_controlled_executor", + "controlled_route_id": "allowlisted_report_gap_learning_writeback", + "playbook_ref": "playbooks/report-source-gap-learning.yml", + "mcp_tool_ref": "mcp://knowledge/redacted-learning-packet", + "check_mode_ref": "writer-check/report-gap-learning-preview", + "verifier_ref": "verifier://km-playbook-trust-receipt", + "rollback_ref": "rollback://km-learning-entry-revert-preview", + "telegram_evidence_ref": "telegram-evidence://learning-writeback-summary", + "km_writeback_ref": "km://report-source-gap-learning", + "playbook_trust_ref": "playbook-trust://report-gap-remediation", + "allowlist_match": true, + "check_mode_passed": true, + "rollback_plan_ready": true, + "post_action_verifier_ready": true, + "telegram_evidence_ready": true, + "km_writeback_ready": true, + "playbook_trust_writeback_ready": true, + "owner_response_required": false, + "break_glass_required": false, + "controlled_executor_handoff_allowed": true, + "live_apply_performed": false, + "side_effect_count": 0, + "blocked_runtime_actions": ["raw report payload write", "private reasoning write", "unbounded embedding write"], + "next_gate": "dispatch worker 只寫 redacted learning packet,並以 verifier receipt 更新 trust delta。" + }, + { + "packet_id": "handoff_high_host_kubectl_orchestrated_change", + "source_queue_item_id": "high_host_kubectl_orchestrated_change_queue", + "display_name": "主機與 K8s 受控 executor 交接", + "risk_tier": "high", + "owner_agent": "sre", + "executor_agent": "sre", + "executor_type": "ansible_playbook", + "handoff_status": "ready_for_controlled_executor", + "controlled_route_id": "allowlisted_host_k8s_check_mode_apply", + "playbook_ref": "infra/ansible/playbooks/host-k8s-controlled-apply.yml", + "mcp_tool_ref": "mcp://sre/topology-and-health-readback", + "check_mode_ref": "ansible-check/host-k8s-controlled-apply", + "verifier_ref": "verifier://host-k8s-health-postcheck", + "rollback_ref": "rollback://host-k8s-controlled-revert", + "telegram_evidence_ref": "telegram-evidence://host-k8s-apply-summary", + "km_writeback_ref": "km://host-k8s-remediation-learning", + "playbook_trust_ref": "playbook-trust://host-k8s-controlled-apply", + "allowlist_match": true, + "check_mode_passed": true, + "rollback_plan_ready": true, + "post_action_verifier_ready": true, + "telegram_evidence_ready": true, + "km_writeback_ready": true, + "playbook_trust_writeback_ready": true, + "owner_response_required": false, + "break_glass_required": false, + "controlled_executor_handoff_allowed": true, + "live_apply_performed": false, + "side_effect_count": 0, + "blocked_runtime_actions": ["reboot", "node drain", "force rollout without verifier"], + "next_gate": "dispatch worker 必須先完成 target selector、check-mode、blast-radius guard 與 rollback stop condition。" + }, + { + "packet_id": "handoff_critical_model_cost_provider_change", + "source_queue_item_id": "critical_model_cost_provider_change_queue", + "display_name": "模型角色與費用 break-glass", + "risk_tier": "critical", + "owner_agent": "openclaw", + "executor_agent": "openclaw", + "executor_type": "break_glass_only", + "handoff_status": "critical_break_glass_only", + "controlled_route_id": "blocked_critical_model_cost_provider_boundary", + "playbook_ref": "adr://market-replay-shadow-canary-required", + "mcp_tool_ref": "mcp://agent-market/scorecard-readback", + "check_mode_ref": "not-applicable-critical-break-glass", + "verifier_ref": "verifier://agent-market-replay-shadow-canary", + "rollback_ref": "rollback://provider-route-fallback", + "telegram_evidence_ref": "telegram-evidence://critical-cost-provider-summary", + "km_writeback_ref": "km://agent-market-decision-learning", + "playbook_trust_ref": "playbook-trust://agent-provider-role-decision", + "allowlist_match": false, + "check_mode_passed": false, + "rollback_plan_ready": true, + "post_action_verifier_ready": true, + "telegram_evidence_ready": true, + "km_writeback_ready": true, + "playbook_trust_writeback_ready": true, + "owner_response_required": true, + "break_glass_required": true, + "controlled_executor_handoff_allowed": false, + "live_apply_performed": false, + "side_effect_count": 0, + "blocked_runtime_actions": ["OpenClaw replacement", "paid provider switch", "cost quota change"], + "next_gate": "必須先有市場分數、replay、shadow、canary 與費用邊界,不能由一般 high 風險自動化覆蓋。" + }, + { + "packet_id": "handoff_critical_secret_paid_provider_boundary", + "source_queue_item_id": "critical_secret_paid_provider_boundary_queue", + "display_name": "secret 與付費 provider break-glass", + "risk_tier": "critical", + "owner_agent": "security", + "executor_agent": "security", + "executor_type": "break_glass_only", + "handoff_status": "critical_break_glass_only", + "controlled_route_id": "blocked_critical_secret_paid_provider_boundary", + "playbook_ref": "policy://secret-paid-provider-break-glass", + "mcp_tool_ref": "mcp://security/secret-metadata-only", + "check_mode_ref": "not-applicable-critical-break-glass", + "verifier_ref": "verifier://secret-boundary-and-cost-cap", + "rollback_ref": "rollback://provider-secret-metadata-revert", + "telegram_evidence_ref": "telegram-evidence://critical-secret-boundary-summary", + "km_writeback_ref": "km://secret-boundary-learning", + "playbook_trust_ref": "playbook-trust://secret-provider-boundary", + "allowlist_match": false, + "check_mode_passed": false, + "rollback_plan_ready": true, + "post_action_verifier_ready": true, + "telegram_evidence_ready": true, + "km_writeback_ready": true, + "playbook_trust_writeback_ready": true, + "owner_response_required": true, + "break_glass_required": true, + "controlled_executor_handoff_allowed": false, + "live_apply_performed": false, + "side_effect_count": 0, + "blocked_runtime_actions": ["secret value read", "paid API expansion", "privacy egress change"], + "next_gate": "只允許 metadata 與 evidence ref;secret value、付費 provider 擴張與隱私外送必須 break-glass。" + } + ], + "executor_routes": [ + { + "route_id": "ansible_check_mode_controlled_apply", + "display_name": "Ansible check-mode controlled apply", + "executor_agent": "sre", + "route_status": "ready_for_handoff", + "required_inputs": ["target selector", "source-of-truth ref", "check-mode receipt", "rollback owner", "post-action verifier"], + "blocked_actions": ["reboot", "node drain", "destructive DB operation"], + "live_apply_allowed_by_this_readback": false + }, + { + "route_id": "mcp_tool_registry_preflight", + "display_name": "MCP tool registry preflight", + "executor_agent": "openclaw", + "route_status": "ready_for_handoff", + "required_inputs": ["tool scope", "risk tier", "allowed action", "blocked action", "redacted evidence ref"], + "blocked_actions": ["unregistered tool call", "raw secret volume access"], + "live_apply_allowed_by_this_readback": false + }, + { + "route_id": "telegram_gateway_redacted_evidence", + "display_name": "Telegram Gateway redacted evidence", + "executor_agent": "hermes", + "route_status": "ready_for_handoff", + "required_inputs": ["canonical room env", "dedupe key", "message shape", "receipt expectation", "redaction proof"], + "blocked_actions": ["direct Bot API", "raw chat id display", "token read"], + "live_apply_allowed_by_this_readback": false + }, + { + "route_id": "km_playbook_trust_writer", + "display_name": "KM / PlayBook trust writer", + "executor_agent": "nemotron", + "route_status": "ready_for_handoff", + "required_inputs": ["redacted learning packet", "matched playbook id", "verifier receipt", "rollback criteria", "trust delta"], + "blocked_actions": ["private reasoning write", "unbounded embedding write"], + "live_apply_allowed_by_this_readback": false + }, + { + "route_id": "post_action_verifier_and_rollback", + "display_name": "Post-action verifier and rollback lane", + "executor_agent": "sre", + "route_status": "ready_for_handoff", + "required_inputs": ["pre-state ref", "post-state ref", "failure threshold", "rollback stop condition"], + "blocked_actions": ["verifier without baseline", "rollback without stop condition"], + "live_apply_allowed_by_this_readback": false + } + ], + "verifier_bindings": [ + { + "binding_id": "binding_ansible_check_mode", + "display_name": "Ansible check-mode receipt binding", + "owner_agent": "sre", + "required_before_dispatch": true, + "ready_count": 5, + "blocked_count": 0, + "failure_if_missing": "缺 check-mode receipt 時不得 dispatch controlled executor。" + }, + { + "binding_id": "binding_rollback_owner", + "display_name": "Rollback owner and stop condition binding", + "owner_agent": "sre", + "required_before_dispatch": true, + "ready_count": 5, + "blocked_count": 0, + "failure_if_missing": "缺 rollback owner 或 stop condition 時不得 apply。" + }, + { + "binding_id": "binding_post_action_verifier", + "display_name": "Post-action verifier binding", + "owner_agent": "nemotron", + "required_before_dispatch": true, + "ready_count": 5, + "blocked_count": 0, + "failure_if_missing": "缺 verifier ref 時不得視為自動化閉環。" + }, + { + "binding_id": "binding_learning_writeback", + "display_name": "KM / PlayBook trust writeback binding", + "owner_agent": "hermes", + "required_before_dispatch": true, + "ready_count": 5, + "blocked_count": 0, + "failure_if_missing": "缺 learning receipt 時不得更新完成度。" + }, + { + "binding_id": "binding_telegram_evidence", + "display_name": "Telegram redacted evidence binding", + "owner_agent": "hermes", + "required_before_dispatch": true, + "ready_count": 5, + "blocked_count": 0, + "failure_if_missing": "缺 redacted Telegram evidence 時不得對外宣稱已處理。" + } + ], + "learning_writeback_contracts": [ + { + "contract_id": "km_execution_receipt", + "display_name": "KM execution receipt", + "owner_agent": "hermes", + "target_store": "knowledge_entries", + "writeback_status": "ready_for_executor_receipt", + "required_fields": ["decision id", "executor route", "verifier result", "redacted evidence refs", "rollback outcome"], + "runtime_write_performed": false + }, + { + "contract_id": "playbook_trust_delta", + "display_name": "PlayBook trust delta", + "owner_agent": "openclaw", + "target_store": "playbooks", + "writeback_status": "ready_for_executor_receipt", + "required_fields": ["matched playbook id", "success or failure", "verifier confidence", "negative reinforcement reason"], + "runtime_write_performed": false + }, + { + "contract_id": "timeline_event_append", + "display_name": "Timeline event append", + "owner_agent": "hermes", + "target_store": "timeline_events", + "writeback_status": "ready_for_executor_receipt", + "required_fields": ["agent role", "affected scope", "decision reason", "executor status", "post-check result"], + "runtime_write_performed": false + } + ], + "activation_boundaries": { + "committed_snapshot_read_allowed": true, + "controlled_executor_handoff_preview_allowed": true, + "ansible_check_mode_receipt_preview_allowed": true, + "mcp_tool_registry_route_preview_allowed": true, + "post_action_verifier_binding_preview_allowed": true, + "telegram_evidence_preview_allowed": true, + "km_playbook_trust_writeback_preview_allowed": true, + "controlled_executor_dispatch_enabled": false, + "live_apply_enabled": false, + "gateway_queue_write_enabled": false, + "telegram_send_enabled": false, + "bot_api_call_enabled": false, + "km_write_enabled": false, + "playbook_trust_write_enabled": false, + "production_write_enabled": false, + "secret_read_enabled": false, + "paid_api_call_enabled": false, + "host_write_enabled": false, + "kubectl_action_enabled": false, + "destructive_operation_enabled": false + }, + "display_redaction_contract": { + "redaction_required": true, + "raw_tool_output_display_allowed": false, + "raw_runtime_payload_display_allowed": false, + "raw_telegram_payload_display_allowed": false, + "private_reasoning_display_allowed": false, + "secret_value_display_allowed": false, + "work_window_transcript_display_allowed": false, + "allowed_display_fields": [ + "packet_id", + "display_name", + "risk_tier", + "owner_agent", + "executor_agent", + "executor_type", + "handoff_status", + "controlled_route_id", + "check_mode_ref", + "verifier_ref", + "rollback_ref", + "telegram_evidence_ref", + "km_writeback_ref", + "playbook_trust_ref", + "rollups" + ], + "blocked_display_fields": [ + "raw tool output", + "raw runtime payload", + "raw Telegram payload", + "private reasoning", + "secret value", + "authorization header", + "work window transcript" + ] + }, + "rollups": { + "source_readback_count": 8, + "handoff_packet_count": 7, + "ready_for_controlled_executor_count": 5, + "critical_break_glass_count": 2, + "high_risk_packet_count": 5, + "critical_packet_count": 2, + "ansible_check_mode_packet_count": 3, + "mcp_tool_route_count": 7, + "post_action_verifier_binding_count": 5, + "telegram_evidence_binding_count": 5, + "km_writeback_binding_count": 5, + "playbook_trust_writeback_binding_count": 5, + "owner_response_required_count": 2, + "blocked_by_critical_boundary_count": 2, + "missing_check_mode_count": 0, + "missing_rollback_count": 0, + "missing_verifier_count": 0, + "missing_telegram_evidence_count": 0, + "missing_learning_writeback_count": 0, + "executor_route_count": 5, + "verifier_binding_count": 5, + "learning_writeback_contract_count": 3, + "controlled_executor_dispatch_count": 0, + "live_apply_count": 0, + "gateway_queue_write_count": 0, + "telegram_send_count": 0, + "bot_api_call_count": 0, + "km_write_count": 0, + "playbook_trust_write_count": 0, + "production_write_count": 0, + "secret_read_count": 0, + "paid_api_call_count": 0, + "host_write_count": 0, + "kubectl_action_count": 0, + "destructive_operation_count": 0 + }, + "next_actions": [ + { + "task_id": "P2-416", + "priority": "P0", + "summary": "建立 controlled executor dispatch worker dry-run,從 P2-415 handoff packet 產生 executor run preview、idempotency key、failure lane 與 verifier queue。", + "gate": "dispatch worker 必須只接受 ready_for_controlled_executor,critical_break_glass_only 仍拒收。" + }, + { + "task_id": "P2-417", + "priority": "P0", + "summary": "把 executor receipt 寫回 AwoooP status-chain、日 / 週 / 月報與 Telegram redacted evidence,讓使用者看到每個 Agent 的實際處理量。", + "gate": "receipt 必須有 verifier result、rollback outcome、KM / PlayBook trust writeback ref。" + } + ] +} diff --git a/docs/schemas/ai_agent_controlled_executor_handoff_v1.schema.json b/docs/schemas/ai_agent_controlled_executor_handoff_v1.schema.json new file mode 100644 index 00000000..080d990b --- /dev/null +++ b/docs/schemas/ai_agent_controlled_executor_handoff_v1.schema.json @@ -0,0 +1,201 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:ai-agent-controlled-executor-handoff-v1", + "title": "AWOOOI AI Agent controlled executor handoff v1", + "description": "P2-415 承接 P2-409 controlled apply queue,建立 high 風險候選交給受控 executor 前的 handoff readback:allowlist、Ansible check-mode、rollback、post-action verifier、Telegram evidence、KM / PlayBook trust 回寫都必須可讀。此 schema 不授權 live apply、Telegram send、Bot API、secret read、paid API、host write、kubectl 或不可逆操作。", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "program_status", + "source_refs", + "source_readbacks", + "handoff_truth", + "executor_handoff_packets", + "executor_routes", + "verifier_bindings", + "learning_writeback_contracts", + "activation_boundaries", + "display_redaction_contract", + "rollups", + "next_actions" + ], + "properties": { + "schema_version": { "type": "string", "const": "ai_agent_controlled_executor_handoff_v1" }, + "generated_at": { "type": "string", "minLength": 1 }, + "program_status": { + "type": "object", + "required": [ + "overall_completion_percent", + "current_priority", + "current_task_id", + "next_task_id", + "read_only_mode", + "runtime_authority", + "status_note" + ], + "properties": { + "overall_completion_percent": { "type": "integer", "const": 100 }, + "current_priority": { "type": "string", "const": "P0" }, + "current_task_id": { "type": "string", "const": "P2-415" }, + "next_task_id": { "type": "string", "const": "P2-416" }, + "read_only_mode": { "type": "boolean", "const": true }, + "runtime_authority": { "type": "string", "const": "controlled_executor_handoff_readback_no_live_apply" }, + "status_note": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "source_refs": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }, + "source_readbacks": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/source_readback" } }, + "handoff_truth": { "type": "object", "additionalProperties": { "type": ["boolean", "integer", "string"] } }, + "executor_handoff_packets": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/executor_handoff_packet" } }, + "executor_routes": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/executor_route" } }, + "verifier_bindings": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/verifier_binding" } }, + "learning_writeback_contracts": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/learning_writeback_contract" } }, + "activation_boundaries": { "type": "object", "additionalProperties": { "type": "boolean" } }, + "display_redaction_contract": { "type": "object", "additionalProperties": { "type": ["boolean", "array", "string"] } }, + "rollups": { "type": "object", "additionalProperties": { "type": "integer" } }, + "next_actions": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/next_action" } } + }, + "$defs": { + "source_readback": { + "type": "object", + "required": ["readback_id", "source_schema_version", "source_ref", "endpoint", "owner_agent", "status", "key_readback", "next_action"], + "properties": { + "readback_id": { "type": "string", "minLength": 1 }, + "source_schema_version": { "type": "string", "minLength": 1 }, + "source_ref": { "type": "string", "minLength": 1 }, + "endpoint": { "type": "string", "minLength": 1 }, + "owner_agent": { "type": "string", "enum": ["openclaw", "hermes", "nemotron", "sre", "security", "devops"] }, + "status": { "type": "string", "minLength": 1 }, + "key_readback": { "type": "string", "minLength": 1 }, + "next_action": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "executor_handoff_packet": { + "type": "object", + "required": [ + "packet_id", + "source_queue_item_id", + "display_name", + "risk_tier", + "owner_agent", + "executor_agent", + "executor_type", + "handoff_status", + "controlled_route_id", + "playbook_ref", + "mcp_tool_ref", + "check_mode_ref", + "verifier_ref", + "rollback_ref", + "telegram_evidence_ref", + "km_writeback_ref", + "playbook_trust_ref", + "allowlist_match", + "check_mode_passed", + "rollback_plan_ready", + "post_action_verifier_ready", + "telegram_evidence_ready", + "km_writeback_ready", + "playbook_trust_writeback_ready", + "owner_response_required", + "break_glass_required", + "controlled_executor_handoff_allowed", + "live_apply_performed", + "side_effect_count", + "blocked_runtime_actions", + "next_gate" + ], + "properties": { + "packet_id": { "type": "string", "minLength": 1 }, + "source_queue_item_id": { "type": "string", "minLength": 1 }, + "display_name": { "type": "string", "minLength": 1 }, + "risk_tier": { "type": "string", "enum": ["high", "critical"] }, + "owner_agent": { "type": "string", "enum": ["openclaw", "hermes", "nemotron", "sre", "security", "devops"] }, + "executor_agent": { "type": "string", "enum": ["openclaw", "hermes", "nemotron", "sre", "security", "devops"] }, + "executor_type": { "type": "string", "enum": ["ansible_playbook", "mcp_tool_route", "telegram_gateway_queue", "km_playbook_writer", "readback_verifier", "break_glass_only"] }, + "handoff_status": { "type": "string", "enum": ["ready_for_controlled_executor", "critical_break_glass_only", "blocked_missing_check_mode", "blocked_missing_verifier", "blocked_missing_learning_writeback"] }, + "controlled_route_id": { "type": "string", "minLength": 1 }, + "playbook_ref": { "type": "string", "minLength": 1 }, + "mcp_tool_ref": { "type": "string", "minLength": 1 }, + "check_mode_ref": { "type": "string", "minLength": 1 }, + "verifier_ref": { "type": "string", "minLength": 1 }, + "rollback_ref": { "type": "string", "minLength": 1 }, + "telegram_evidence_ref": { "type": "string", "minLength": 1 }, + "km_writeback_ref": { "type": "string", "minLength": 1 }, + "playbook_trust_ref": { "type": "string", "minLength": 1 }, + "allowlist_match": { "type": "boolean" }, + "check_mode_passed": { "type": "boolean" }, + "rollback_plan_ready": { "type": "boolean" }, + "post_action_verifier_ready": { "type": "boolean" }, + "telegram_evidence_ready": { "type": "boolean" }, + "km_writeback_ready": { "type": "boolean" }, + "playbook_trust_writeback_ready": { "type": "boolean" }, + "owner_response_required": { "type": "boolean" }, + "break_glass_required": { "type": "boolean" }, + "controlled_executor_handoff_allowed": { "type": "boolean" }, + "live_apply_performed": { "type": "boolean", "const": false }, + "side_effect_count": { "type": "integer", "const": 0 }, + "blocked_runtime_actions": { "type": "array", "minItems": 1, "items": { "type": "string" } }, + "next_gate": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "executor_route": { + "type": "object", + "required": ["route_id", "display_name", "executor_agent", "route_status", "required_inputs", "blocked_actions", "live_apply_allowed_by_this_readback"], + "properties": { + "route_id": { "type": "string", "minLength": 1 }, + "display_name": { "type": "string", "minLength": 1 }, + "executor_agent": { "type": "string", "enum": ["openclaw", "hermes", "nemotron", "sre", "security", "devops"] }, + "route_status": { "type": "string", "enum": ["ready_for_handoff", "blocked_by_policy"] }, + "required_inputs": { "type": "array", "minItems": 1, "items": { "type": "string" } }, + "blocked_actions": { "type": "array", "minItems": 1, "items": { "type": "string" } }, + "live_apply_allowed_by_this_readback": { "type": "boolean", "const": false } + }, + "additionalProperties": false + }, + "verifier_binding": { + "type": "object", + "required": ["binding_id", "display_name", "owner_agent", "required_before_dispatch", "ready_count", "blocked_count", "failure_if_missing"], + "properties": { + "binding_id": { "type": "string", "minLength": 1 }, + "display_name": { "type": "string", "minLength": 1 }, + "owner_agent": { "type": "string", "enum": ["openclaw", "hermes", "nemotron", "sre", "security", "devops"] }, + "required_before_dispatch": { "type": "boolean", "const": true }, + "ready_count": { "type": "integer", "minimum": 0 }, + "blocked_count": { "type": "integer", "minimum": 0 }, + "failure_if_missing": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "learning_writeback_contract": { + "type": "object", + "required": ["contract_id", "display_name", "owner_agent", "target_store", "writeback_status", "required_fields", "runtime_write_performed"], + "properties": { + "contract_id": { "type": "string", "minLength": 1 }, + "display_name": { "type": "string", "minLength": 1 }, + "owner_agent": { "type": "string", "enum": ["openclaw", "hermes", "nemotron", "sre", "security", "devops"] }, + "target_store": { "type": "string", "minLength": 1 }, + "writeback_status": { "type": "string", "enum": ["ready_for_executor_receipt", "blocked_by_policy"] }, + "required_fields": { "type": "array", "minItems": 1, "items": { "type": "string" } }, + "runtime_write_performed": { "type": "boolean", "const": false } + }, + "additionalProperties": false + }, + "next_action": { + "type": "object", + "required": ["task_id", "priority", "summary", "gate"], + "properties": { + "task_id": { "type": "string", "minLength": 1 }, + "priority": { "type": "string", "minLength": 1 }, + "summary": { "type": "string", "minLength": 1 }, + "gate": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +}