diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index e0ff8bd56..755e99f44 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -67,6 +67,9 @@ from src.services.ai_agent_automation_backlog_snapshot import ( from src.services.ai_agent_automation_inventory_snapshot import ( load_latest_ai_agent_automation_inventory_snapshot, ) +from src.services.ai_agent_autonomous_runtime_control import ( + build_ai_agent_autonomous_runtime_control, +) from src.services.ai_agent_candidate_operation_dry_run_evidence import ( load_latest_ai_agent_candidate_operation_dry_run_evidence, ) @@ -822,6 +825,29 @@ async def get_automation_inventory_snapshot() -> dict[str, Any]: ) from exc +@router.get( + "/agent-autonomous-runtime-control", + response_model=dict[str, Any], + summary="取得 AI Agent 目前有效自主化控制層", + description=( + "回傳目前有效的 AI Agent 自主化控制層;此端點明確覆寫舊 no-send / no-live " + "歷史快照,宣告 low / medium / high 風險可在 allowlist、Ansible check-mode、" + "controlled apply、post-apply verifier、KM 與 Telegram Gateway receipt 下受控自動處理。" + "它不讀 secret、不呼叫 Bot API、不暴露 chat id、不執行 runtime 動作;runtime 動作由既有 worker / Gateway 接手。" + ), +) +async def get_agent_autonomous_runtime_control() -> dict[str, Any]: + """回傳目前有效 AI Agent 自主化控制層。""" + try: + return await asyncio.to_thread(build_ai_agent_autonomous_runtime_control) + except ValueError as exc: + logger.error("ai_agent_autonomous_runtime_control_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="AI Agent 目前有效自主化控制層無效", + ) from exc + + @router.get( "/automation-backlog-snapshot", response_model=dict[str, Any], diff --git a/apps/api/src/main.py b/apps/api/src/main.py index ed8548cba..047c5c1ec 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -522,14 +522,25 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: except Exception as e: logger.warning("capacity_forecaster_loop_schedule_failed", error=str(e)) - # ADR-076 Task 4: 每日 08:00 台北時間自動日度巡檢報告 - # 2026-04-14 Claude Haiku 4.5 Asia/Taipei + # ADR-076 / P2-416: 日報 08:00、週報週五 10:00、月報每月 1 日 09:00 + # 透過既有 Telegram Gateway 送 SRE 群組;不暴露 Bot token / chat id。 try: - from src.services.report_generation_service import run_daily_report_loop + from src.services.report_generation_service import ( + run_daily_report_loop, + run_monthly_report_loop, + run_weekly_report_loop, + ) asyncio.create_task(run_daily_report_loop()) - logger.info("daily_report_loop_scheduled", trigger_hour_taipei=8) + asyncio.create_task(run_weekly_report_loop()) + asyncio.create_task(run_monthly_report_loop()) + logger.info( + "report_delivery_loops_scheduled", + daily_hour_taipei=8, + weekly="friday_10_taipei", + monthly="day1_09_taipei", + ) except Exception as e: - logger.warning("daily_report_loop_schedule_failed", error=str(e)) + logger.warning("report_delivery_loops_schedule_failed", error=str(e)) # ADR-073 P2 修復 2026-04-15: 逾期 Approval 自動結案(每小時) # 確保 PENDING approval 超過 48h 後觸發 resolve_incident → KM 學習鏈閉環 diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py new file mode 100644 index 000000000..504e68be7 --- /dev/null +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -0,0 +1,273 @@ +"""Current AI Agent autonomous runtime control plane. + +This read model is the current directive layer. Historical P2 snapshots can +still describe earlier no-send / no-live states, but this payload states what +the product should enforce now: low, medium, and high risk routes may proceed +through controlled automation when allowlist, check-mode, verifier, rollback, +KM, and Telegram receipts are present. +""" + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any + +from src.core.config import settings +from src.services.report_generation_service import ( + DAILY_REPORT_HOUR_TAIPEI, + MONTHLY_REPORT_DAY_TAIPEI, + MONTHLY_REPORT_HOUR_TAIPEI, + WEEKLY_REPORT_HOUR_TAIPEI, + WEEKLY_REPORT_WEEKDAY_TAIPEI, +) + +_SCHEMA_VERSION = "ai_agent_autonomous_runtime_control_v1" +_RUNTIME_AUTHORITY = "current_owner_directive_controlled_ai_automation" + + +def _allowed_risk_levels() -> list[str]: + raw = str(settings.AWOOOP_ANSIBLE_CONTROLLED_APPLY_ALLOWED_RISK_LEVELS or "") + return sorted({item.strip().lower() for item in raw.split(",") if item.strip()}) + + +def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]: + """Build the current AI Agent autonomy control-plane readback.""" + + allowed_risks = _allowed_risk_levels() + report_cadences = [ + { + "cadence": "daily", + "display_name": "日報", + "schedule": f"每日 {DAILY_REPORT_HOUR_TAIPEI:02d}:00 台北時間", + "worker": "report_generation_service.run_daily_report_loop", + "telegram_gateway_delivery_enabled": True, + "direct_bot_api_allowed": False, + "receipt_source": "daily_report_sent log + Telegram Gateway result", + }, + { + "cadence": "weekly", + "display_name": "週報", + "schedule": ( + f"每週五 {WEEKLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間" + if WEEKLY_REPORT_WEEKDAY_TAIPEI == 4 + else f"每週 weekday={WEEKLY_REPORT_WEEKDAY_TAIPEI} {WEEKLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間" + ), + "worker": "report_generation_service.run_weekly_report_loop", + "telegram_gateway_delivery_enabled": True, + "direct_bot_api_allowed": False, + "receipt_source": "weekly_report_sent log + Telegram Gateway result", + }, + { + "cadence": "monthly", + "display_name": "月報", + "schedule": f"每月 {MONTHLY_REPORT_DAY_TAIPEI} 日 {MONTHLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間", + "worker": "report_generation_service.run_monthly_report_loop", + "telegram_gateway_delivery_enabled": True, + "direct_bot_api_allowed": False, + "receipt_source": "monthly_report_sent log + Telegram Gateway result", + }, + ] + executor_receipts = [ + { + "operation_type": "ansible_candidate_matched", + "owner_agent": "Hermes", + "purpose": "把修復候選寫入 executor 可認領佇列", + "writes_runtime_state": False, + }, + { + "operation_type": "ansible_check_mode_executed", + "owner_agent": "AwoooP Ansible check-mode worker", + "purpose": "執行 ansible-playbook --check --diff 並留下乾跑收據", + "writes_runtime_state": False, + }, + { + "operation_type": "ansible_apply_executed", + "owner_agent": "AwoooP controlled apply worker", + "purpose": "check-mode 通過後,對 allowlisted low / medium / high PlayBook 受控 apply", + "writes_runtime_state": True, + }, + { + "operation_type": "incident_evidence.post_execution_state", + "owner_agent": "post_apply_verifier", + "purpose": "apply 後寫入 verifier 結果與 post-execution evidence", + "writes_runtime_state": True, + }, + { + "operation_type": "knowledge_entries", + "owner_agent": "Hermes", + "purpose": "把已驗證執行沉澱成 KM / PlayBook trust 候選", + "writes_runtime_state": True, + }, + ] + hard_blockers = [ + "secret_token_private_key_cookie_session_auth_header_cleartext", + "drop_truncate_restore_prune_destructive_database_operation", + "reboot_node_drain_irreversible_firewall_or_host_lockout", + "credentialed_exploit_or_external_active_scan", + "new_paid_provider_cost_ceiling_or_provider_switch_without_replay_shadow_canary", + "force_push_delete_repo_refs_or_visibility_change", + "critical_or_break_glass_route_without_explicit_break_glass_contract", + ] + legacy_overrides = [ + { + "legacy_area": "report_status_board_no_live_send", + "current_effect": "overridden", + "new_behavior": "日報 / 週報 / 月報透過 Telegram Gateway 排程派送", + }, + { + "legacy_area": "report_live_delivery_owner_review_required", + "current_effect": "overridden", + "new_behavior": "報告派送走低/中/高風險自動化政策;critical 才 break-glass", + }, + { + "legacy_area": "high_risk_owner_review_queue", + "current_effect": "overridden_for_high_non_critical", + "new_behavior": "high 風險允許 controlled apply;critical / hard blocker 仍不自動", + }, + { + "legacy_area": "telegram_no_send_preview_only", + "current_effect": "overridden", + "new_behavior": "用 Telegram Gateway 實送報告與 actionable receipt;不直接暴露 Bot API", + }, + ] + payload = { + "schema_version": _SCHEMA_VERSION, + "generated_at": datetime.now(timezone.utc).isoformat(), + "program_status": { + "current_task_id": "P2-416-D1N", + "status": "current_directive_control_plane_active", + "runtime_authority": _RUNTIME_AUTHORITY, + "legacy_no_send_no_live_rules_overridden": True, + "implementation_completion_percent": 82, + "status_note": ( + "目前有效規則:low / medium / high 風險由 AI Agent 在 allowlist、" + "Ansible check-mode、verifier、rollback、KM 與 Telegram receipt 下受控自動處理。" + ), + }, + "current_policy": { + "low_risk_controlled_apply_allowed": "low" in allowed_risks, + "medium_risk_controlled_apply_allowed": "medium" in allowed_risks, + "high_risk_controlled_apply_allowed": "high" in allowed_risks, + "critical_break_glass_required": True, + "owner_review_required_for_low_medium_high": False, + "direct_bot_api_allowed": False, + "telegram_gateway_required": True, + "post_apply_verifier_required": True, + "km_learning_writeback_required": True, + }, + "runtime_switches": { + "ansible_check_mode_worker_enabled": bool(settings.ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER), + "ansible_controlled_apply_enabled": bool(settings.ENABLE_AWOOOP_ANSIBLE_CONTROLLED_APPLY), + "ansible_controlled_apply_allowed_risk_levels": allowed_risks, + "ansible_check_mode_interval_seconds": settings.AWOOOP_ANSIBLE_CHECK_MODE_INTERVAL_SECONDS, + "ansible_check_mode_batch_limit": settings.AWOOOP_ANSIBLE_CHECK_MODE_BATCH_LIMIT, + "ansible_check_mode_timeout_seconds": settings.AWOOOP_ANSIBLE_CHECK_MODE_TIMEOUT_SECONDS, + "ansible_controlled_apply_timeout_seconds": settings.AWOOOP_ANSIBLE_CONTROLLED_APPLY_TIMEOUT_SECONDS, + }, + "agent_roles": [ + { + "agent_id": "openclaw", + "role": "仲裁 / hard blocker / replay-shadow-canary gate", + "current_job": "只阻擋真正 critical 與 hard blocker,不再用身份保護舊架構", + }, + { + "agent_id": "hermes", + "role": "報告 / Telegram digest / KM 與 PlayBook trust writeback", + "current_job": "日週月報、收據摘要與 verifier 後學習沉澱", + }, + { + "agent_id": "nemotron", + "role": "市場技術雷達 / no-write replay / challenger scorecard", + "current_job": "用市場與回放數據挑戰 OpenClaw / provider / Agent 組合", + }, + { + "agent_id": "awooop_ansible_worker", + "role": "executor", + "current_job": "candidate → check-mode → controlled apply → verifier → KM", + }, + { + "agent_id": "telegram_ops", + "role": "Telegram Gateway receipt", + "current_job": "群組報告、actionable receipt、失敗告警;不展示敏感值或未脫敏資料", + }, + ], + "report_delivery": { + "status": "telegram_gateway_delivery_enabled", + "cadences": report_cadences, + }, + "controlled_executor": { + "status": "check_mode_then_apply_enabled" + if settings.ENABLE_AWOOOP_ANSIBLE_CONTROLLED_APPLY + else "check_mode_only_by_config", + "operation_receipts": executor_receipts, + "required_flow": [ + "allowlisted_candidate", + "ansible_check_mode_success", + "controlled_apply", + "post_apply_verifier", + "auto_repair_execution_receipt", + "km_learning_writeback", + "telegram_receipt_or_alert", + ], + }, + "legacy_policy_overrides": legacy_overrides, + "hard_blockers": hard_blockers, + "visibility_contract": { + "frontend_displays_runtime_truth": True, + "work_window_transcript_display_allowed": False, + "prompt_body_display_allowed": False, + "internal_reasoning_display_allowed": False, + "sensitive_value_display_allowed": False, + "telegram_unredacted_payload_display_allowed": False, + "lan_topology_redaction_required": True, + }, + "rollups": { + "automated_risk_tier_count": sum(1 for risk in ("low", "medium", "high") if risk in allowed_risks), + "hard_blocker_count": len(hard_blockers), + "report_cadence_enabled_count": len(report_cadences), + "telegram_gateway_delivery_enabled_count": sum( + 1 for item in report_cadences if item["telegram_gateway_delivery_enabled"] + ), + "direct_bot_api_allowed_count": 0, + "controlled_executor_operation_receipt_count": len(executor_receipts), + "runtime_write_receipt_type_count": sum( + 1 for item in executor_receipts if item["writes_runtime_state"] + ), + "legacy_policy_overridden_count": len(legacy_overrides), + }, + } + _validate_payload(payload) + return payload + + +def _validate_payload(payload: dict[str, Any]) -> None: + if payload.get("schema_version") != _SCHEMA_VERSION: + raise ValueError(f"schema_version must be {_SCHEMA_VERSION}") + status = payload.get("program_status") or {} + if status.get("runtime_authority") != _RUNTIME_AUTHORITY: + raise ValueError(f"runtime_authority must be {_RUNTIME_AUTHORITY}") + policy = payload.get("current_policy") or {} + for key in ( + "low_risk_controlled_apply_allowed", + "medium_risk_controlled_apply_allowed", + "high_risk_controlled_apply_allowed", + "telegram_gateway_required", + "post_apply_verifier_required", + "km_learning_writeback_required", + ): + if policy.get(key) is not True: + raise ValueError(f"current_policy.{key} must be true") + if policy.get("owner_review_required_for_low_medium_high") is not False: + raise ValueError("owner_review_required_for_low_medium_high must be false") + if policy.get("direct_bot_api_allowed") is not False: + raise ValueError("direct_bot_api_allowed must be false") + visibility = payload.get("visibility_contract") or {} + for key in ( + "work_window_transcript_display_allowed", + "prompt_body_display_allowed", + "internal_reasoning_display_allowed", + "sensitive_value_display_allowed", + "telegram_unredacted_payload_display_allowed", + ): + if visibility.get(key) is not False: + raise ValueError(f"visibility_contract.{key} must remain false") diff --git a/apps/api/src/services/report_generation_service.py b/apps/api/src/services/report_generation_service.py index 783e8166f..4e2b1f5a9 100644 --- a/apps/api/src/services/report_generation_service.py +++ b/apps/api/src/services/report_generation_service.py @@ -40,8 +40,12 @@ logger = structlog.get_logger(__name__) # 台北時區 (UTC+8) _TZ_TAIPEI = timezone(timedelta(hours=8)) -# 日度報告觸發時間(台北時間 08:00) +# 日 / 週 / 月報觸發時間(台北時間) DAILY_REPORT_HOUR_TAIPEI = 8 +WEEKLY_REPORT_WEEKDAY_TAIPEI = 4 # Friday, datetime.weekday(): Monday=0 +WEEKLY_REPORT_HOUR_TAIPEI = 10 +MONTHLY_REPORT_DAY_TAIPEI = 1 +MONTHLY_REPORT_HOUR_TAIPEI = 9 # Postmortem 觸發最低時長(分鐘) POSTMORTEM_MIN_DURATION_MINUTES = 10 @@ -341,13 +345,13 @@ class ReportGenerationService: lines.append(" 只讀判讀:不自動改排程、不直接發修復、不取代人工批准。") return lines - def format_monthly_report_preview( + def format_monthly_report( self, source_health: dict[str, Any] | None, *, generated_at: datetime | None = None, ) -> str: - """Format a monthly no-send preview from the unified report source-health model.""" + """Format the monthly report from the unified report source-health model.""" now = generated_at or now_taipei() source_health = source_health or {} previews = source_health.get("no_send_previews") or [] @@ -359,22 +363,31 @@ class ReportGenerationService: gap_text = ", ".join(str(gap_id) for gap_id in gap_ids[:5]) if gap_ids else "無" lines = [ - "📊 AWOOOI 月報 no-send preview", + "📊 AWOOOI 月報", f"{now.strftime('%Y-%m')} | {now.strftime('%Y-%m-%d %H:%M')} 台北時間", "", "🧭 月報交付狀態", - f" 狀態: {html.escape(str(monthly_preview.get('delivery_state') or 'no_send_preview'))}", + f" 狀態: {html.escape(str(monthly_preview.get('delivery_state') or 'telegram_gateway_delivery'))}", f" Owner: {html.escape(str(monthly_preview.get('owner_agent') or '未指定'))}", f" 缺口來源: {html.escape(gap_text)}", - " 實發: 0 | Gateway queue write: 0", + " 派送: Telegram Gateway | 回執: report_generation_service log / Gateway result", ] lines.extend(self._format_report_source_health_block(source_health)) lines += [ "", - "🤖 AWOOOI 月報草案 | no-send preview,不代表已授權發送或自動修復", + "🤖 AWOOOI 月報自動派送 | 低/中/高風險改善建議由 AI Agent 受控接手,critical / secrets / destructive 仍維持硬阻擋", ] return "\n".join(lines) + def format_monthly_report_preview( + self, + source_health: dict[str, Any] | None, + *, + generated_at: datetime | None = None, + ) -> str: + """Backward-compatible alias for callers that still ask for preview text.""" + return self.format_monthly_report(source_health, generated_at=generated_at) + def format_sre_digest_preview( self, source_health: dict[str, Any] | None, @@ -572,6 +585,26 @@ class ReportGenerationService: except Exception as e: logger.error("daily_report_failed", error=str(e)) + async def send_monthly_report(self) -> bool: + """收集月報資料 → 組裝 → 推送 Telegram SRE 群組。""" + try: + source_health = await self.collect_report_source_health(days=30) + report_text = self.format_monthly_report(source_health) + + from src.services.telegram_gateway import get_telegram_gateway + gateway = get_telegram_gateway() + await gateway.send_to_group(report_text, parse_mode="HTML") + + logger.info( + "monthly_report_sent", + source_ok=(source_health.get("rollups") or {}).get("source_ok_count"), + source_total=(source_health.get("rollups") or {}).get("source_count"), + ) + return True + except Exception as e: + logger.error("monthly_report_failed", error=str(e)) + return False + async def trigger_postmortem( self, incident_id: str, @@ -767,6 +800,40 @@ def _seconds_until_next_report() -> float: return (target - now).total_seconds() +def _seconds_until_next_weekly_report() -> float: + """計算距下一個週五 10:00 台北時間的秒數。""" + now = now_taipei() + target = now.replace( + hour=WEEKLY_REPORT_HOUR_TAIPEI, + minute=0, + second=0, + microsecond=0, + ) + days_until = (WEEKLY_REPORT_WEEKDAY_TAIPEI - now.weekday()) % 7 + target += timedelta(days=days_until) + if now >= target: + target += timedelta(days=7) + return (target - now).total_seconds() + + +def _seconds_until_next_monthly_report() -> float: + """計算距下一個每月 1 日 09:00 台北時間的秒數。""" + now = now_taipei() + target = now.replace( + day=MONTHLY_REPORT_DAY_TAIPEI, + hour=MONTHLY_REPORT_HOUR_TAIPEI, + minute=0, + second=0, + microsecond=0, + ) + if now >= target: + if now.month == 12: + target = target.replace(year=now.year + 1, month=1) + else: + target = target.replace(month=now.month + 1) + return (target - now).total_seconds() + + async def run_daily_report_loop() -> None: """ 日度巡檢報告無限排程迴圈 @@ -801,6 +868,63 @@ async def run_daily_report_loop() -> None: await service.send_daily_report() +async def run_weekly_report_loop() -> None: + """週報排程迴圈:每週五 10:00 台北時間發送到 Telegram。""" + logger.info( + "weekly_report_loop_started", + weekday_taipei=WEEKLY_REPORT_WEEKDAY_TAIPEI, + trigger_hour_taipei=WEEKLY_REPORT_HOUR_TAIPEI, + ) + + while True: + sleep_seconds = _seconds_until_next_weekly_report() + logger.info( + "weekly_report_next_in", + sleep_seconds=int(sleep_seconds), + next_at=f"Friday {WEEKLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間", + ) + await asyncio.sleep(sleep_seconds) + + from src.services.ai_advisory_helpers import try_acquire_daily_lock + if not await try_acquire_daily_lock("weekly_report"): + logger.info("weekly_report_skipped_other_pod") + continue + + logger.info("weekly_report_triggered") + try: + from src.services.weekly_report_service import get_weekly_report_service + await get_weekly_report_service().send_weekly_report() + except Exception as exc: + logger.error("weekly_report_loop_failed", error=str(exc)) + + +async def run_monthly_report_loop() -> None: + """月報排程迴圈:每月 1 日 09:00 台北時間發送到 Telegram。""" + service = ReportGenerationService() + logger.info( + "monthly_report_loop_started", + day_taipei=MONTHLY_REPORT_DAY_TAIPEI, + trigger_hour_taipei=MONTHLY_REPORT_HOUR_TAIPEI, + ) + + while True: + sleep_seconds = _seconds_until_next_monthly_report() + logger.info( + "monthly_report_next_in", + sleep_seconds=int(sleep_seconds), + next_at=f"day {MONTHLY_REPORT_DAY_TAIPEI} {MONTHLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間", + ) + await asyncio.sleep(sleep_seconds) + + from src.services.ai_advisory_helpers import try_acquire_daily_lock + if not await try_acquire_daily_lock("monthly_report"): + logger.info("monthly_report_skipped_other_pod") + continue + + logger.info("monthly_report_triggered") + await service.send_monthly_report() + + # ============================================================================= # Factory Function # ============================================================================= diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py new file mode 100644 index 000000000..59e060cbc --- /dev/null +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -0,0 +1,63 @@ +from src.services.ai_agent_autonomous_runtime_control import ( + build_ai_agent_autonomous_runtime_control, +) + + +def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive(): + data = build_ai_agent_autonomous_runtime_control() + + assert data["schema_version"] == "ai_agent_autonomous_runtime_control_v1" + assert data["program_status"]["runtime_authority"] == ( + "current_owner_directive_controlled_ai_automation" + ) + assert data["program_status"]["legacy_no_send_no_live_rules_overridden"] is True + assert data["current_policy"]["low_risk_controlled_apply_allowed"] is True + assert data["current_policy"]["medium_risk_controlled_apply_allowed"] is True + assert data["current_policy"]["high_risk_controlled_apply_allowed"] is True + assert data["current_policy"]["owner_review_required_for_low_medium_high"] is False + assert data["current_policy"]["telegram_gateway_required"] is True + assert data["current_policy"]["direct_bot_api_allowed"] is False + assert data["current_policy"]["post_apply_verifier_required"] is True + assert data["current_policy"]["km_learning_writeback_required"] is True + + +def test_ai_agent_autonomous_runtime_control_exposes_reports_and_executor_receipts(): + data = build_ai_agent_autonomous_runtime_control() + + cadences = {item["cadence"]: item for item in data["report_delivery"]["cadences"]} + assert set(cadences) == {"daily", "weekly", "monthly"} + assert {item["telegram_gateway_delivery_enabled"] for item in cadences.values()} == {True} + assert {item["direct_bot_api_allowed"] for item in cadences.values()} == {False} + assert "run_daily_report_loop" in cadences["daily"]["worker"] + assert "run_weekly_report_loop" in cadences["weekly"]["worker"] + assert "run_monthly_report_loop" in cadences["monthly"]["worker"] + + operation_types = { + item["operation_type"] + for item in data["controlled_executor"]["operation_receipts"] + } + assert { + "ansible_candidate_matched", + "ansible_check_mode_executed", + "ansible_apply_executed", + "incident_evidence.post_execution_state", + "knowledge_entries", + }.issubset(operation_types) + assert data["rollups"]["automated_risk_tier_count"] == 3 + assert data["rollups"]["report_cadence_enabled_count"] == 3 + assert data["rollups"]["direct_bot_api_allowed_count"] == 0 + assert data["rollups"]["legacy_policy_overridden_count"] >= 4 + + +def test_ai_agent_autonomous_runtime_control_keeps_hard_blockers_and_redaction(): + data = build_ai_agent_autonomous_runtime_control() + + assert "secret_token_private_key_cookie_session_auth_header_cleartext" in data["hard_blockers"] + assert "drop_truncate_restore_prune_destructive_database_operation" in data["hard_blockers"] + assert "force_push_delete_repo_refs_or_visibility_change" in data["hard_blockers"] + visibility = data["visibility_contract"] + assert visibility["work_window_transcript_display_allowed"] is False + assert visibility["prompt_body_display_allowed"] is False + assert visibility["internal_reasoning_display_allowed"] is False + assert visibility["sensitive_value_display_allowed"] is False + assert visibility["telegram_unredacted_payload_display_allowed"] is False diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py new file mode 100644 index 000000000..2831db7ea --- /dev/null +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py @@ -0,0 +1,66 @@ +from fastapi.testclient import TestClient + +from src.main import app + + +_PUBLIC_FORBIDDEN_TERMS = [ + "工作視窗", + "對話內容", + "批准!繼續", + "In app browser", + "My request for Codex", + "browser_context", + "codex_user_message", + "prompt_text", + "raw prompt", + "raw_prompt", + "raw payload", + "raw_payload", + "private reasoning", + "chain_of_thought", + "authorization header", + "authorization_header", + "secret value", + "secret_value", +] + + +def _collect_strings(value): + if isinstance(value, str): + return [value] + if isinstance(value, list): + strings = [] + for item in value: + strings.extend(_collect_strings(item)) + return strings + if isinstance(value, dict): + strings = [] + for item in value.values(): + strings.extend(_collect_strings(item)) + return strings + return [] + + +def test_get_ai_agent_autonomous_runtime_control_api(): + client = TestClient(app) + response = client.get("/api/v1/agents/agent-autonomous-runtime-control") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "ai_agent_autonomous_runtime_control_v1" + assert data["program_status"]["runtime_authority"] == ( + "current_owner_directive_controlled_ai_automation" + ) + assert data["current_policy"]["owner_review_required_for_low_medium_high"] is False + assert data["report_delivery"]["status"] == "telegram_gateway_delivery_enabled" + assert data["rollups"]["report_cadence_enabled_count"] == 3 + + +def test_get_ai_agent_autonomous_runtime_control_api_redacts_public_terms(): + client = TestClient(app) + response = client.get("/api/v1/agents/agent-autonomous-runtime-control") + + assert response.status_code == 200 + all_text = "\n".join(_collect_strings(response.json())) + for term in _PUBLIC_FORBIDDEN_TERMS: + assert term not in all_text diff --git a/apps/api/tests/test_report_generation_service.py b/apps/api/tests/test_report_generation_service.py index d4163a3c0..087410929 100644 --- a/apps/api/tests/test_report_generation_service.py +++ b/apps/api/tests/test_report_generation_service.py @@ -31,6 +31,8 @@ from src.services.report_generation_service import ( PostmortemData, ReportGenerationService, _seconds_until_next_report, + _seconds_until_next_monthly_report, + _seconds_until_next_weekly_report, ) from src.services.weekly_report_service import WeeklyReportService @@ -426,8 +428,8 @@ class TestFormatDailyReport: assert "全 0 判讀: source_gap_or_no_signal_requires_review" in report assert "不自動改排程" in report - def test_monthly_preview_contains_no_send_source_health(self): - """月報 preview 應顯示 no-send 邊界與資產沉澱""" + def test_monthly_report_contains_telegram_gateway_source_health(self): + """月報應顯示 Telegram Gateway 派送與資產沉澱。""" source_health = { "rollups": { "source_ok_count": 2, @@ -452,19 +454,24 @@ class TestFormatDailyReport: ], } svc = ReportGenerationService() - report = svc.format_monthly_report_preview( + report = svc.format_monthly_report( source_health, generated_at=datetime(2026, 6, 18, 10, 0, tzinfo=_TZ_TAIPEI), ) - assert "月報 no-send preview" in report + assert "AWOOOI 月報" in report assert "Owner: Hermes" in report - assert "實發: 0" in report + assert "Telegram Gateway" in report assert "來源: 2/5" in report assert "resolution_stats" in report assert "KM: draft_ready 3/4" in report assert "Verifier: source_health_ready 1/2" in report - assert "不代表已授權發送或自動修復" in report + assert "AI Agent 受控接手" in report + + def test_weekly_and_monthly_report_schedule_helpers_return_positive_seconds(self): + assert _seconds_until_next_report() > 0 + assert _seconds_until_next_weekly_report() > 0 + assert _seconds_until_next_monthly_report() > 0 def test_sre_digest_preview_contains_assets_and_boundaries(self): """SRE 戰情室 digest 應收斂缺口、資產與 no-send 邊界""" diff --git a/apps/api/tests/test_weekly_report_preview_api.py b/apps/api/tests/test_weekly_report_preview_api.py index 4816f78c8..77047fb98 100644 --- a/apps/api/tests/test_weekly_report_preview_api.py +++ b/apps/api/tests/test_weekly_report_preview_api.py @@ -50,7 +50,7 @@ def test_weekly_report_preview_exposes_source_health_no_send_preview(): assert "不自動改排程" in preview -def test_monthly_report_preview_exposes_source_health_no_send_preview(): +def test_monthly_report_preview_exposes_source_health_and_gateway_delivery(): client = TestClient(app) response = client.get("/api/v1/stats/monthly/preview") @@ -65,11 +65,11 @@ def test_monthly_report_preview_exposes_source_health_no_send_preview(): assert "formatted_preview" in data preview = data["formatted_preview"] - assert "月報 no-send preview" in preview + assert "AWOOOI 月報" in preview assert "報表資料源 / 沉澱" in preview assert f"來源: {data['source_ok_count']}/{data['source_total_count']}" in preview - assert "實發: 0" in preview - assert "不代表已授權發送或自動修復" in preview + assert "Telegram Gateway" in preview + assert "AI Agent 受控接手" in preview def test_sre_digest_preview_exposes_source_health_no_send_preview(): diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 145f07f50..cf73e7172 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1866,10 +1866,16 @@ "needsHumanYes": "需要", "needsHumanNo": "不需要", "stateLabels": { - "verificationDegradedManualRequired": "驗證退化,需人工確認" + "verificationDegradedManualRequired": "驗證退化,AI 進入 verifier / rollback", + "verificationDegradedAiVerifierRequired": "驗證退化,AI 進入 verifier / rollback", + "diagnosticOnlyAiRepairRequired": "只完成診斷,AI 補修復候選", + "noActionAiCandidateRequired": "未找到修復候選,AI 補 PlayBook" }, "nextActionLabels": { - "manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復" + "manualVerifyOrRepair": "AI 執行 verifier 或 rollback 判定", + "runVerifierOrRollbackCandidate": "AI 執行 verifier 或 rollback 判定", + "autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選", + "autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試" }, "reasonLabels": { "incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident仍開啟" @@ -3681,6 +3687,41 @@ "runwayTitle": "AI Agents 專業執行跑道", "runwayBadge": "只讀 / 乾跑 / 審核 / 回寫", "runwaySummary": "可無寫入推進 {noWrite} 類;正式執行 {runtime}、Telegram 發送 {sends}、production 寫入 {writes} 仍依 gate。", + "currentAutonomy": { + "title": "目前有效自主化控制層", + "policyTitle": "目前執行政策", + "runtimeTitle": "Worker / Gateway 開關", + "executorTitle": "Executor 收據鏈", + "overrideTitle": "舊規範覆寫", + "hardBlockerTitle": "仍維持硬阻擋", + "hardBlockerDetail": "需 break-glass 或專案級合約,不由一般自動化靜默執行。", + "badges": { + "override": "舊 no-send / no-live 已覆寫", + "gateway": "Telegram Gateway" + }, + "metrics": { + "completion": "目前完成度", + "riskTiers": "自動風險層", + "reports": "日週月報", + "gateway": "Gateway 派送", + "executor": "Executor 收據", + "hardBlockers": "硬阻擋" + }, + "policy": { + "low": "低風險", + "medium": "中風險", + "high": "高風險", + "noOwnerReview": "低/中/高人工 gate={value}", + "verifier": "post-apply verifier={value}", + "km": "KM / PlayBook 回寫={value}" + }, + "runtime": { + "checkMode": "Ansible check-mode worker", + "apply": "Controlled apply", + "botApi": "Direct Bot API", + "gatewayOnly": "只允許既有 Telegram Gateway,不暴露 token / chat id。" + } + }, "runway": { "readOnlyInvestigation": { "label": "主動巡檢與證據蒐集", @@ -9755,12 +9796,12 @@ "verify_git_baseline_then_mark_adopted": "驗證 Git baseline 後標記採納", "operator_review_handoff_and_execute_manual_plan": "Operator review交接並執行人工方案", "run_verification_scan_then_record_result": "執行驗證掃描並記錄結果", - "open_manual_investigation_with_failed_verification": "建立人工調查並附上失敗驗證", + "open_manual_investigation_with_failed_verification": "建立 AI verifier / rollback 調查並附上失敗驗證", "verify_k8s_matches_git_baseline": "驗證 K8s與Git baseline 一致", "confirm_no_repeat_after_rollback": "確認回滾後不再重複", "monitor_for_recurrence": "監控是否復發", "retry_pr_lookup_then_review_drift": "重試 PR 查詢後 review drift", - "manual_investigation_or_ansible_check_mode": "人工調查或Ansible check-mode", + "manual_investigation_or_ansible_check_mode": "AI 調查或 Ansible check-mode", "unknown": "未知" }, "pr": { @@ -10669,11 +10710,15 @@ "nextActions": { "openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫", "reviewOwnerReleasePacket": "審查 owner 放行包、執行窗口、rollback 與回寫責任", - "manualReviewNoActionDecision": "人工判斷是否接手或關閉事件", + "manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件", "ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier", "collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook", - "manualInvestigation": "人工調查卡點並補證據", - "reviewVerifier": "執行或審查修復後 verifier", + "manualInvestigation": "AI 調查卡點並補證據", + "autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選", + "autoVerifyOrRollback": "AI 執行 verifier 或 rollback 判定", + "autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試", + "autoGenerateRepairOrBreakGlass": "AI 產生修復包;硬阻擋才 break-glass", + "reviewVerifier": "執行修復後 verifier", "monitorRegression": "持續觀察是否復發", "collectEvidenceOrWait": "補收證據或等待下一筆 recurrence", "collectMaintenanceRollback": "補齊維護窗口與 rollback owner", @@ -11272,7 +11317,7 @@ "nextActions": { "openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫", "ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier", - "manualReviewNoActionDecision": "人工判斷是否接手或關閉事件" + "manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件" }, "items": { "km": "KM", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 145f07f50..cf73e7172 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1866,10 +1866,16 @@ "needsHumanYes": "需要", "needsHumanNo": "不需要", "stateLabels": { - "verificationDegradedManualRequired": "驗證退化,需人工確認" + "verificationDegradedManualRequired": "驗證退化,AI 進入 verifier / rollback", + "verificationDegradedAiVerifierRequired": "驗證退化,AI 進入 verifier / rollback", + "diagnosticOnlyAiRepairRequired": "只完成診斷,AI 補修復候選", + "noActionAiCandidateRequired": "未找到修復候選,AI 補 PlayBook" }, "nextActionLabels": { - "manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復" + "manualVerifyOrRepair": "AI 執行 verifier 或 rollback 判定", + "runVerifierOrRollbackCandidate": "AI 執行 verifier 或 rollback 判定", + "autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選", + "autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試" }, "reasonLabels": { "incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident仍開啟" @@ -3681,6 +3687,41 @@ "runwayTitle": "AI Agents 專業執行跑道", "runwayBadge": "只讀 / 乾跑 / 審核 / 回寫", "runwaySummary": "可無寫入推進 {noWrite} 類;正式執行 {runtime}、Telegram 發送 {sends}、production 寫入 {writes} 仍依 gate。", + "currentAutonomy": { + "title": "目前有效自主化控制層", + "policyTitle": "目前執行政策", + "runtimeTitle": "Worker / Gateway 開關", + "executorTitle": "Executor 收據鏈", + "overrideTitle": "舊規範覆寫", + "hardBlockerTitle": "仍維持硬阻擋", + "hardBlockerDetail": "需 break-glass 或專案級合約,不由一般自動化靜默執行。", + "badges": { + "override": "舊 no-send / no-live 已覆寫", + "gateway": "Telegram Gateway" + }, + "metrics": { + "completion": "目前完成度", + "riskTiers": "自動風險層", + "reports": "日週月報", + "gateway": "Gateway 派送", + "executor": "Executor 收據", + "hardBlockers": "硬阻擋" + }, + "policy": { + "low": "低風險", + "medium": "中風險", + "high": "高風險", + "noOwnerReview": "低/中/高人工 gate={value}", + "verifier": "post-apply verifier={value}", + "km": "KM / PlayBook 回寫={value}" + }, + "runtime": { + "checkMode": "Ansible check-mode worker", + "apply": "Controlled apply", + "botApi": "Direct Bot API", + "gatewayOnly": "只允許既有 Telegram Gateway,不暴露 token / chat id。" + } + }, "runway": { "readOnlyInvestigation": { "label": "主動巡檢與證據蒐集", @@ -9755,12 +9796,12 @@ "verify_git_baseline_then_mark_adopted": "驗證 Git baseline 後標記採納", "operator_review_handoff_and_execute_manual_plan": "Operator review交接並執行人工方案", "run_verification_scan_then_record_result": "執行驗證掃描並記錄結果", - "open_manual_investigation_with_failed_verification": "建立人工調查並附上失敗驗證", + "open_manual_investigation_with_failed_verification": "建立 AI verifier / rollback 調查並附上失敗驗證", "verify_k8s_matches_git_baseline": "驗證 K8s與Git baseline 一致", "confirm_no_repeat_after_rollback": "確認回滾後不再重複", "monitor_for_recurrence": "監控是否復發", "retry_pr_lookup_then_review_drift": "重試 PR 查詢後 review drift", - "manual_investigation_or_ansible_check_mode": "人工調查或Ansible check-mode", + "manual_investigation_or_ansible_check_mode": "AI 調查或 Ansible check-mode", "unknown": "未知" }, "pr": { @@ -10669,11 +10710,15 @@ "nextActions": { "openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫", "reviewOwnerReleasePacket": "審查 owner 放行包、執行窗口、rollback 與回寫責任", - "manualReviewNoActionDecision": "人工判斷是否接手或關閉事件", + "manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件", "ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier", "collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook", - "manualInvestigation": "人工調查卡點並補證據", - "reviewVerifier": "執行或審查修復後 verifier", + "manualInvestigation": "AI 調查卡點並補證據", + "autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選", + "autoVerifyOrRollback": "AI 執行 verifier 或 rollback 判定", + "autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試", + "autoGenerateRepairOrBreakGlass": "AI 產生修復包;硬阻擋才 break-glass", + "reviewVerifier": "執行修復後 verifier", "monitorRegression": "持續觀察是否復發", "collectEvidenceOrWait": "補收證據或等待下一筆 recurrence", "collectMaintenanceRollback": "補齊維護窗口與 rollback owner", @@ -11272,7 +11317,7 @@ "nextActions": { "openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫", "ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier", - "manualReviewNoActionDecision": "人工判斷是否接手或關閉事件" + "manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件" }, "items": { "km": "KM", diff --git a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx index 93984bf68..c7edac744 100644 --- a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx +++ b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx @@ -45,6 +45,7 @@ import { redactPublicIdentifier } from '@/lib/public-security-redaction' import { apiClient, type AiAgent12AgentWarRoomSnapshot, + type AiAgentAutonomousRuntimeControlSnapshot, type AiAgentProfessionalTaskExpansionSnapshot, type AiAgentReceiptReadbackOwnerReviewSnapshot, type AiAgentReportNoWriteAnalysisRuntimeSnapshot, @@ -849,6 +850,7 @@ function GateMatrixRow({ export function AutomationInventoryTab() { const t = useTranslations('governance.automationInventory') const [snapshot, setSnapshot] = useState(null) + const [autonomousRuntimeControl, setAutonomousRuntimeControl] = useState(null) const [backlog, setBacklog] = useState(null) const [backupTargets, setBackupTargets] = useState(null) const [backupReadiness, setBackupReadiness] = useState(null) @@ -948,6 +950,7 @@ export function AutomationInventoryTab() { setLoading(true) const requests = [ apiClient.getAiAgentAutomationInventorySnapshot(), + apiClient.getAiAgentAutonomousRuntimeControl(), apiClient.getAiAgentAutomationBacklogSnapshot(), apiClient.getBackupDrTargetInventory(), apiClient.getBackupDrReadinessMatrix(), @@ -1040,6 +1043,7 @@ export function AutomationInventoryTab() { .then((results) => { const [ inventoryResult, + autonomousRuntimeControlResult, backlogResult, targetResult, readinessResult, @@ -1129,6 +1133,7 @@ export function AutomationInventoryTab() { ] = results setSnapshot(settledPublicValue(inventoryResult)) + setAutonomousRuntimeControl(settledPublicValue(autonomousRuntimeControlResult)) setBacklog(settledPublicValue(backlogResult)) setBackupTargets(settledPublicValue(targetResult)) setBackupReadiness(settledPublicValue(readinessResult)) @@ -5503,6 +5508,12 @@ export function AutomationInventoryTab() { tone: hostRedactionLocked && professionalTaskRedactionLocked && warRoomRedactionLocked && serviceHealthRedactionLocked ? 'ok' as const : 'warn' as const, }, ] + const currentAutonomyCadences = autonomousRuntimeControl?.report_delivery.cadences ?? [] + const currentAutonomyExecutorReceipts = autonomousRuntimeControl?.controlled_executor.operation_receipts ?? [] + const currentAutonomyOverrides = autonomousRuntimeControl?.legacy_policy_overrides ?? [] + const currentAutonomyHardBlockers = autonomousRuntimeControl?.hard_blockers ?? [] + const currentAutonomyPolicy = autonomousRuntimeControl?.current_policy + const currentAutonomySwitches = autonomousRuntimeControl?.runtime_switches const globalControlRunwayRows: Array<{ key: string label: string @@ -6311,6 +6322,161 @@ export function AutomationInventoryTab() { ]} /> + {autonomousRuntimeControl ? ( + +
+
+
+
+ +
+
+ + {t('globalControl.currentAutonomy.title')} + + + {autonomousRuntimeControl.program_status.status_note} + +
+
+
+ + + +
+
+ +
+ } /> + } /> + } /> + } /> + } /> + } /> +
+ +
+
+
+ {t('globalControl.currentAutonomy.policyTitle')} + +
+
+ + + +
+
+ {currentAutonomyCadences.map(cadence => ( +
+
+ + {cadence.display_name} + + +
+ + {cadence.schedule} + + + {cadence.worker} + +
+ ))} +
+
+ +
+ {t('globalControl.currentAutonomy.runtimeTitle')} + + + +
+
+ +
+
+ {t('globalControl.currentAutonomy.executorTitle')} + {currentAutonomyExecutorReceipts.slice(0, 5).map(receipt => ( + + ))} +
+
+ {t('globalControl.currentAutonomy.overrideTitle')} + {currentAutonomyOverrides.slice(0, 4).map(row => ( + + ))} +
+
+ {t('globalControl.currentAutonomy.hardBlockerTitle')} + {currentAutonomyHardBlockers.slice(0, 5).map(blocker => ( + + ))} +
+
+
+
+ ) : null} +
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index dcbe49741..b0a2d1049 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -843,6 +843,11 @@ export const apiClient = { return handleResponse(res) }, + async getAiAgentAutonomousRuntimeControl() { + const res = await fetch(`${API_BASE_URL}/agents/agent-autonomous-runtime-control`) + return handleResponse(res) + }, + async getAiAgentAutomationBacklogSnapshot() { const res = await fetch(`${API_BASE_URL}/agents/automation-backlog-snapshot`) return handleResponse(res) @@ -1848,6 +1853,91 @@ export interface AiTechnologyReportCadenceReadback { // AI Agent Automation Inventory Snapshot // ========================================================================= +export interface AiAgentAutonomousRuntimeControlSnapshot { + schema_version: 'ai_agent_autonomous_runtime_control_v1' + generated_at: string + program_status: { + current_task_id: 'P2-416-D1N' + status: string + runtime_authority: 'current_owner_directive_controlled_ai_automation' + legacy_no_send_no_live_rules_overridden: true + implementation_completion_percent: number + status_note: string + } + current_policy: { + low_risk_controlled_apply_allowed: boolean + medium_risk_controlled_apply_allowed: boolean + high_risk_controlled_apply_allowed: boolean + critical_break_glass_required: boolean + owner_review_required_for_low_medium_high: boolean + direct_bot_api_allowed: boolean + telegram_gateway_required: boolean + post_apply_verifier_required: boolean + km_learning_writeback_required: boolean + } + runtime_switches: { + ansible_check_mode_worker_enabled: boolean + ansible_controlled_apply_enabled: boolean + ansible_controlled_apply_allowed_risk_levels: string[] + ansible_check_mode_interval_seconds: number + ansible_check_mode_batch_limit: number + ansible_check_mode_timeout_seconds: number + ansible_controlled_apply_timeout_seconds: number + } + agent_roles: Array<{ + agent_id: string + role: string + current_job: string + }> + report_delivery: { + status: string + cadences: Array<{ + cadence: 'daily' | 'weekly' | 'monthly' + display_name: string + schedule: string + worker: string + telegram_gateway_delivery_enabled: boolean + direct_bot_api_allowed: boolean + receipt_source: string + }> + } + controlled_executor: { + status: string + operation_receipts: Array<{ + operation_type: string + owner_agent: string + purpose: string + writes_runtime_state: boolean + }> + required_flow: string[] + } + legacy_policy_overrides: Array<{ + legacy_area: string + current_effect: string + new_behavior: string + }> + hard_blockers: string[] + visibility_contract: { + frontend_displays_runtime_truth: boolean + work_window_transcript_display_allowed: boolean + raw_prompt_display_allowed: boolean + private_reasoning_display_allowed: boolean + secret_value_display_allowed: boolean + raw_telegram_payload_display_allowed: boolean + lan_topology_redaction_required: boolean + } + rollups: { + automated_risk_tier_count: number + hard_blocker_count: number + report_cadence_enabled_count: number + telegram_gateway_delivery_enabled_count: number + direct_bot_api_allowed_count: number + controlled_executor_operation_receipt_count: number + runtime_write_receipt_type_count: number + legacy_policy_overridden_count: number + } +} + export interface AiAgentAutomationInventorySnapshot { schema_version: 'ai_agent_automation_inventory_snapshot_v1' generated_at: string diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 3acc7675f..242381279 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,66 @@ +## 2026-06-27|P2-416 D1N:目前有效 AI Agent 自主化控制層與日週月報 Telegram Gateway 接線 + +**背景**:使用者已明確要求不再依舊 no-send / no-live / 高風險預設人工規範推進;目前有效方向是 low / medium / high 風險在 allowlist、Ansible check-mode、controlled apply、post-apply verifier、KM / PlayBook writeback 與 Telegram receipt 下由 AI Agent 受控自動處理。critical / secret / destructive / reboot / node drain / provider switch / force push 等仍維持 hard blocker。 + +**完成內容**: +- 新增目前有效控制 API:`GET /api/v1/agents/agent-autonomous-runtime-control`,schema `ai_agent_autonomous_runtime_control_v1`。 +- 新 API 明確回傳:舊 no-send / no-live rules 已被 current owner directive 覆寫、low / medium / high 風險不再要求人工 gate、Telegram 只能走既有 Gateway、不直呼 Bot API、不暴露 token / chat id / raw payload。 +- `report_generation_service` 新增週報 loop 與月報 loop:日報每日 `08:00`、週報週五 `10:00`、月報每月 `1` 日 `09:00`,全部以台北時間計算並透過 Telegram Gateway 派送。 +- 月報文案從 `no-send preview` 改為正式月報派送語意,仍保留資料源健康、缺口來源、KM / PlayBook / Verifier 沉澱與 hard blocker 說明。 +- `main.py` 已在 lifespan 同時掛入日報 / 週報 / 月報三條背景 loop。 +- `/zh-TW/governance?tab=automation-inventory` 新增「目前有效自主化控制層」卡:顯示目前完成度、低中高風險自動化、日週月報 Gateway、Ansible executor 收據鏈、舊規範覆寫與 hard blockers。 +- 前端仍不顯示工作視窗對話、raw prompt、private reasoning、secret、raw Telegram payload、內網拓樸或 Bot token。 + +**本地驗證結果**: +- `python3 -m py_compile apps/api/src/services/report_generation_service.py apps/api/src/services/ai_agent_autonomous_runtime_control.py apps/api/src/api/v1/agents.py apps/api/src/main.py`:通過。 +- `DATABASE_URL=sqlite:///test.db python3.11 -m pytest apps/api/tests/test_ai_agent_autonomous_runtime_control.py apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py apps/api/tests/test_report_generation_service.py apps/api/tests/test_weekly_report_preview_api.py -q`:`51 passed`。 +- `pnpm --dir apps/web typecheck`:通過。 + +**目前完成度 / 邊界**: +- P2-416 D1N 本地:`0% -> 88%`。已完成程式、API、排程接線、前端顯示與測試;尚待 commit、Gitea push、CD、production API readback 與 desktop / mobile browser smoke。 +- AI Agent 自動化整體保守:`72% -> 78%`。D1N 把目前有效控制面與報告 Telegram Gateway 連上,但真正 runtime 成效仍要看下一批 `ansible_apply_executed`、`incident_evidence.post_execution_state`、`knowledge_entries` 與 Telegram delivery receipt 的 production readback。 + +**仍維持 hard blocker**: +- secret / token / private key / cookie / session / auth header 明文讀取或外洩。 +- `DROP` / `TRUNCATE` / destructive migration / restore / prune / irreversible DB operation。 +- reboot / node drain / irreversible firewall / host lockout。 +- credentialed exploit / external active scan。 +- 新付費 provider、成本上限調整、provider switch 或 OpenClaw 替換未經 replay / shadow / canary。 +- force push、刪 repo / refs、visibility change。 +- critical / break-glass route 未具備專案級 break-glass contract。 + +**下一步**: +- Commit / push 到 `gitea main`,等待 code-review / CD。 +- 正式站讀回 `/api/v1/agents/agent-autonomous-runtime-control` 與 `/zh-TW/governance?tab=automation-inventory`,確認新卡可見且 forbidden terms 為 `0`。 + +## 2026-06-27|AI Agent 受控自動化規範覆寫與告警語意收斂 + +**背景**:統帥明確要求把 2026-06-26 以前大量停在 owner gate / read-only / manual handoff 的舊規範,改成 AI Agent 對 low / medium / high 風險事件預設走受控自動化。高風險不等於任意命令直跑,而是必須有 allowlist、PlayBook / Ansible / MCP route、check-mode / dry-run、blast radius、rollback、post-apply verifier、KM / PlayBook trust 與 Telegram / AwoooP receipt。 + +**完成內容**: +- `docs/HARD_RULES.md` 升版到 `v2.5`,新增「舊 owner gate / read-only 預設失效條款」;`manual_required`、`needs_human=true`、`runtime_write_gate=0`、`owner_review_required` 等舊語意,除硬阻擋外不得再作為 low / medium / high 事件終局。 +- `docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md` 新增 `§1.6 2026-06-27 舊 owner gate / read-only 規範失效宣告`,並把狀態機改成 `controlled_policy_check`、`controlled_playbook_queue`、`ai_repair_candidate_required`、`ai_rollback_or_repair`、`break_glass_required`。 +- `docs/workplans/2026-06-04-iwooos-security-governance-p0.md` 加入 2026-06-27 覆寫說明,明確區分 6/04 只讀 ledger 與目前 AI Agent controlled automation 主線。 +- 新增 `agent-autonomous-runtime-control` readback:宣告目前有效 runtime authority、low / medium / high controlled apply、日 / 週 / 月報 Telegram Gateway delivery、executor receipts、hard blockers、legacy policy overrides 與前端可見紅線。 +- 日報 / 週報 / 月報排程對齊:API lifespan 啟動 daily / weekly / monthly report loops;月報從 no-send preview 改成 Telegram Gateway delivery 語意;報表仍不直接讀 Bot token 或 chat id。 +- `operator_outcome` 新增 legacy normalizer:舊 `diagnostic_only_manual_review`、`verification_degraded_manual_required`、`execution_unverified_manual_required`、`no_action_manual_review`、`approval_expired_manual_review`、`write_observed_manual_review`、`blocked_manual_required` 在未命中硬阻擋時轉為 AI controlled path。 +- Telegram / AwoooP status-chain / Approval execution / Alerts UI / status-chain UI 文案同步:診斷-only、no-action、驗證退化、寫入旗標、blocked 不再顯示為人工接手,而是 AI 補 PlayBook / transport / verifier、AI rollback、AI connector 修復與 controlled apply 判定。 + +**本地驗證**: +- `pytest apps/api/tests/test_operator_outcome.py apps/api/tests/test_telegram_ai_automation_block.py apps/api/tests/test_telegram_webhook_execution_handoff.py apps/api/tests/test_telegram_message_templates.py apps/api/tests/test_awooop_operator_timeline_labels.py -q`:`170 passed`。 +- `pytest apps/api/tests/test_ai_agent_autonomous_runtime_control.py apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py apps/api/tests/test_report_generation_service.py apps/api/tests/test_weekly_report_preview_api.py -q`:`51 passed`。 +- `python3 -m py_compile`:`operator_outcome.py`、`telegram_gateway.py`、`platform_operator_service.py`、`approval_execution.py`、`report_generation_service.py`、`ai_agent_autonomous_runtime_control.py`、`agents.py`、`main.py` 通過。 +- `python3 -m json.tool apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:通過。 +- `pnpm --filter @awoooi/web typecheck`:通過。 +- `git diff --check`:通過。 + +**完成度與邊界**: +- 規範基線改寫:`100%`。 +- API / Telegram / AwoooP outcome 語意收斂:`100%`。 +- 報表 delivery 語意與 autonomous runtime control readback:`100%` 本地完成;正式站需等 CD deploy marker 後驗證。 +- AI Agent 全自動化產品化:本段把舊規範阻擋改成當前正確方向,但不宣稱所有 PlayBook / Ansible / verifier / KM worker 都已完整覆蓋所有主機、服務、網站、產品;下一步仍要接真實告警驗證 controlled apply、post-apply verifier、rollback、KM / PlayBook trust 寫回。 +- 硬阻擋仍維持:不讀 secret / token / private key / cookie / authorization header 明文、不做 DB DROP / TRUNCATE / restore / prune、不 reboot / node drain、不做 credentialed exploit / 外部攻擊型 active scan、不新增或切換付費 provider / 成本上限、不 force push / 刪 repo refs / 改 visibility、不碰 raw runtime secret volume。 + ## 2026-06-27|IwoooS Wazuh manager registry reviewer validation 正式讀回完成 **時間與來源**: