feat(agents): expose autonomous runtime control
Some checks failed
CD Pipeline / tests (push) Successful in 1m43s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
Code Review / ai-code-review (push) Has been cancelled
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-27 19:31:02 +08:00
parent d359587316
commit ce0c7cbaf8
13 changed files with 1017 additions and 38 deletions

View File

@@ -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],

View File

@@ -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 學習鏈閉環

View File

@@ -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 applycritical / 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")

View File

@@ -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 = [
"<b>📊 AWOOOI 月報 no-send preview</b>",
"<b>📊 AWOOOI 月報</b>",
f"<b>{now.strftime('%Y-%m')}</b> | {now.strftime('%Y-%m-%d %H:%M')} 台北時間",
"",
"<b>🧭 月報交付狀態</b>",
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 += [
"",
"<i>🤖 AWOOOI 月報草案 | no-send preview不代表已授權發送或自動修復</i>",
"<i>🤖 AWOOOI 月報自動派送 | 低/中/高風險改善建議由 AI Agent 受控接手critical / secrets / destructive 仍維持硬阻擋</i>",
]
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
# =============================================================================

View File

@@ -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

View File

@@ -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

View File

@@ -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 "來源: <code>2/5</code>" 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 邊界"""

View File

@@ -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"來源: <code>{data['source_ok_count']}/{data['source_total_count']}</code>" 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():