feat(agents): expose controlled executor handoff runway
Some checks failed
Code Review / ai-code-review (push) Successful in 22s
CD Pipeline / tests (push) Successful in 1m47s
CD Pipeline / build-and-deploy (push) Successful in 6m20s
CD Pipeline / post-deploy-checks (push) Successful in 2m18s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 22s
CD Pipeline / tests (push) Successful in 1m47s
CD Pipeline / build-and-deploy (push) Successful in 6m20s
CD Pipeline / post-deploy-checks (push) Successful in 2m18s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -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],
|
||||
|
||||
406
apps/api/src/services/ai_agent_controlled_executor_handoff.py
Normal file
406
apps/api/src/services/ai_agent_controlled_executor_handoff.py
Normal file
@@ -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
|
||||
}
|
||||
147
apps/api/tests/test_ai_agent_controlled_executor_handoff.py
Normal file
147
apps/api/tests/test_ai_agent_controlled_executor_handoff.py
Normal file
@@ -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",
|
||||
)
|
||||
@@ -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
|
||||
@@ -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}。",
|
||||
|
||||
@@ -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}。",
|
||||
|
||||
@@ -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<AiAgentReportNoWriteAnalysisRuntimeSnapshot | null>(null)
|
||||
const [lowMediumRiskWhitelist, setLowMediumRiskWhitelist] = useState<AiAgentLowMediumRiskWhitelistSnapshot | null>(null)
|
||||
const [highRiskOwnerReviewQueue, setHighRiskOwnerReviewQueue] = useState<AiAgentHighRiskOwnerReviewQueueSnapshot | null>(null)
|
||||
const [controlledExecutorHandoff, setControlledExecutorHandoff] = useState<AiAgentControlledExecutorHandoffSnapshot | null>(null)
|
||||
const [actionAuditLedger, setActionAuditLedger] = useState<AiAgentActionAuditLedgerSnapshot | null>(null)
|
||||
const [actionOwnerAcceptanceEventBus, setActionOwnerAcceptanceEventBus] = useState<AiAgentActionOwnerAcceptanceEventBusSnapshot | null>(null)
|
||||
const [hostRunawayAiops, setHostRunawayAiops] = useState<HostRunawayAiopsLoopReadinessSnapshot | null>(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<string, number>
|
||||
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<string, number>
|
||||
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<string, number>
|
||||
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<string, number>
|
||||
@@ -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 (
|
||||
<div style={{ padding: 20 }}>
|
||||
<GlassCard variant="subtle" padding="lg">
|
||||
@@ -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() {
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 0 }}>
|
||||
<div style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 8,
|
||||
border: '0.5px solid #0f766e35',
|
||||
background: 'rgba(15,118,110,0.08)',
|
||||
color: '#0f766e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<Route size={18} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 18, fontWeight: 760, color: '#141413', lineHeight: 1.15, overflowWrap: 'anywhere' }}>
|
||||
{t('controlledExecutorHandoff.title')}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, color: '#5c5a55', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
|
||||
{t('controlledExecutorHandoff.subtitle', {
|
||||
current: controlledExecutorHandoff.program_status.current_task_id,
|
||||
next: controlledExecutorHandoff.program_status.next_task_id,
|
||||
ready: controlledExecutorReady,
|
||||
packets: controlledExecutorPackets,
|
||||
critical: controlledExecutorCritical,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={t('controlledExecutorHandoff.badges.mode')} />
|
||||
<Chip value={t('controlledExecutorHandoff.badges.dispatch', { count: controlledExecutorDispatches })} muted={controlledExecutorDispatches === 0} />
|
||||
<Chip value={t('controlledExecutorHandoff.badges.live', { count: controlledExecutorLiveApply })} muted={controlledExecutorLiveApply === 0} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(112px, 1fr))', gap: 10 }} className="automation-inventory-kpi-grid">
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.overall')} value={`${controlledExecutorOverall}%`} tone="ok" icon={<Gauge size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.packets')} value={controlledExecutorPackets} tone="ok" icon={<FileText size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.ready')} value={controlledExecutorReady} tone="ok" icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.critical')} value={controlledExecutorCritical} tone="danger" icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.ansible')} value={controlledExecutorAnsible} tone="ok" icon={<Server size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.mcp')} value={controlledExecutorMcp} tone="ok" icon={<Route size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.verifiers')} value={controlledExecutorVerifiers} tone="ok" icon={<ClipboardCheck size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.learning')} value={controlledExecutorLearning} tone="ok" icon={<BookOpenCheck size={16} />} />
|
||||
<MetricCard label={t('controlledExecutorHandoff.metrics.liveWrites')} value={controlledExecutorLiveWrites} tone={controlledExecutorLiveWrites === 0 ? 'ok' : 'danger'} icon={<BellOff size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.2fr) minmax(260px, 0.8fr)', gap: 12 }} className="automation-inventory-visual-grid">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<SmallLabel>{t('controlledExecutorHandoff.sections.packets')}</SmallLabel>
|
||||
{visibleControlledExecutorPackets.map(packet => {
|
||||
const tone = packet.risk_tier === 'critical' ? 'danger' : 'ok'
|
||||
return (
|
||||
<div key={packet.packet_id} style={{ padding: 11, border: '0.5px solid #cfe6df', borderRadius: 7, background: packet.risk_tier === 'critical' ? '#fffafa' : '#fff', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 760, color: '#141413', overflowWrap: 'anywhere' }}>
|
||||
{redactPublicText(packet.display_name)}
|
||||
</span>
|
||||
<Chip value={t(`controlledExecutorHandoff.riskTiers.${packet.risk_tier}` as never)} muted={tone !== 'danger'} />
|
||||
</div>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#5f5a4f', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
|
||||
{redactPublicText(packet.next_gate)}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={t(`controlledExecutorHandoff.agents.${packet.owner_agent}` as never)} muted />
|
||||
<Chip value={t('controlledExecutorHandoff.labels.executor', { value: t(`controlledExecutorHandoff.agents.${packet.executor_agent}` as never) })} muted />
|
||||
<Chip value={t(`controlledExecutorHandoff.executorTypes.${packet.executor_type}` as never)} />
|
||||
<Chip value={t(`controlledExecutorHandoff.statuses.${packet.handoff_status}` as never)} muted={packet.handoff_status === 'ready_for_controlled_executor'} />
|
||||
<Chip value={t('controlledExecutorHandoff.labels.check', { value: packet.check_mode_passed ? 'ready' : 'blocked' })} muted={packet.check_mode_passed} />
|
||||
<Chip value={t('controlledExecutorHandoff.labels.verifier', { value: packet.post_action_verifier_ready ? 'ready' : 'blocked' })} muted={packet.post_action_verifier_ready} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<div style={{ padding: 12, border: '0.5px solid #cfe6df', borderRadius: 7, background: '#f7fcfa', minWidth: 0 }}>
|
||||
<SmallLabel>{t('controlledExecutorHandoff.sections.routes')}</SmallLabel>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{visibleControlledExecutorRoutes.map(route => (
|
||||
<GateMatrixRow
|
||||
key={route.route_id}
|
||||
label={route.display_name}
|
||||
value={t(`controlledExecutorHandoff.routeStatuses.${route.route_status}` as never)}
|
||||
detail={t('controlledExecutorHandoff.labels.routeDetail', {
|
||||
agent: t(`controlledExecutorHandoff.agents.${route.executor_agent}` as never),
|
||||
inputs: route.required_inputs.length,
|
||||
blocked: route.blocked_actions.length,
|
||||
})}
|
||||
tone={route.route_status === 'ready_for_handoff' ? 'ok' : 'danger'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #cfe6df', borderRadius: 7, background: '#fff', minWidth: 0 }}>
|
||||
<SmallLabel>{t('controlledExecutorHandoff.sections.verifiers')}</SmallLabel>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{visibleControlledExecutorVerifierBindings.map(binding => (
|
||||
<GateMatrixRow
|
||||
key={binding.binding_id}
|
||||
label={binding.display_name}
|
||||
value={t('controlledExecutorHandoff.labels.learning', { value: t(`controlledExecutorHandoff.agents.${binding.owner_agent}` as never) })}
|
||||
detail={t('controlledExecutorHandoff.labels.bindingDetail', {
|
||||
ready: binding.ready_count,
|
||||
blocked: binding.blocked_count,
|
||||
})}
|
||||
tone={binding.blocked_count === 0 ? 'ok' : 'danger'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #cfe6df', borderRadius: 7, background: '#fff', minWidth: 0 }}>
|
||||
<SmallLabel>{t('controlledExecutorHandoff.sections.truth')}</SmallLabel>
|
||||
<span style={{ display: 'block', marginTop: 7, fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#5f5a4f', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
|
||||
{redactPublicText(controlledExecutorHandoff.handoff_truth.truth_note)}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 9 }}>
|
||||
<Chip value={t('controlledExecutorHandoff.labels.generated', {
|
||||
generated: formatDateTime(controlledExecutorHandoff.generated_at),
|
||||
})} muted />
|
||||
<Chip value={t('controlledExecutorHandoff.labels.ownerRequired', { count: controlledExecutorOwnerRequired })} muted />
|
||||
<Chip value={t('controlledExecutorHandoff.labels.liveWrites', { count: controlledExecutorLiveWrites })} muted={controlledExecutorLiveWrites === 0} />
|
||||
<Chip value={t('controlledExecutorHandoff.labels.redaction', { value: controlledExecutorHandoff.display_redaction_contract.redaction_required ? '是' : '否' })} muted />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -647,6 +647,11 @@ export const apiClient = {
|
||||
return handleResponse<AiAgentHighRiskOwnerReviewQueueSnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiAgentControlledExecutorHandoff() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/agent-controlled-executor-handoff`)
|
||||
return handleResponse<AiAgentControlledExecutorHandoffSnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiAgentActionAuditLedger() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/agent-action-audit-ledger`)
|
||||
return handleResponse<AiAgentActionAuditLedgerSnapshot>(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
|
||||
|
||||
@@ -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 沒有任何自動化進展。
|
||||
|
||||
@@ -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。"
|
||||
}
|
||||
]
|
||||
}
|
||||
201
docs/schemas/ai_agent_controlled_executor_handoff_v1.schema.json
Normal file
201
docs/schemas/ai_agent_controlled_executor_handoff_v1.schema.json
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user