Merge remote-tracking branch 'gitea/main' into codex/github-backup-missing-targets-20260627

This commit is contained in:
Your Name
2026-06-27 19:33:53 +08:00
18 changed files with 1690 additions and 42 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

@@ -37,6 +37,7 @@ from src.services.iwooos_wazuh_managed_host_coverage import (
)
from src.services.iwooos_wazuh_manager_registry_reviewer_validation import (
load_latest_iwooos_wazuh_manager_registry_reviewer_validation,
validate_iwooos_wazuh_manager_registry_owner_export as validate_wazuh_manager_registry_owner_export_payload,
)
from src.services.iwooos_wazuh_owner_evidence_preflight import (
load_latest_iwooos_wazuh_owner_evidence_preflight,
@@ -178,6 +179,38 @@ async def get_iwooos_wazuh_manager_registry_reviewer_validation() -> dict[str, A
) from exc
@router.post(
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export",
response_model=dict[str, Any],
summary="驗證 Wazuh manager registry 脫敏 owner export",
description=(
"針對單次 owner-provided redacted Wazuh manager registry export 進行 no-persist reviewer "
"validation回傳 accepted / needs supplement / quarantined / rejected runtime action 分流。"
"此端點不保存 payload、不查 Wazuh API、不讀主機、不重新註冊 agent、不重啟服務、不讀或回傳"
"機密明文、不啟用主動回應、不改 Nginx / Docker / K8s / firewall也不更新 manager registry "
"accepted 總帳。"
),
)
async def validate_iwooos_wazuh_manager_registry_owner_export(owner_export: dict[str, Any]) -> dict[str, Any]:
"""回傳單次 Wazuh manager registry 脫敏匯出的公開安全驗證結果。"""
try:
payload = await asyncio.to_thread(
validate_wazuh_manager_registry_owner_export_payload,
owner_export,
)
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:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"IwoooS Wazuh manager registry owner export 驗證器無效:{exc}",
) from exc
@router.get(
"/api/v1/iwooos/runtime-security-readback",
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

@@ -1,15 +1,16 @@
"""
IwoooS Wazuh manager registry reviewer validation readback.
This service exposes a committed reviewer-validation contract for future
owner-provided redacted Wazuh manager registry exports. It never receives raw
payloads, queries Wazuh, reads host data, reads secrets, or authorizes runtime
actions.
This service exposes a committed reviewer-validation contract and a no-persist
validator for owner-provided redacted Wazuh manager registry exports. It never
queries Wazuh, reads host data, reads secrets, persists raw payloads, or
authorizes runtime actions.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
@@ -34,6 +35,82 @@ _REQUIRED_FALSE_BOUNDARIES = {
"wazuh_manager_restart_authorized",
}
_SENSITIVE_TEXT_PATTERNS = {
"internal_ip": re.compile(r"\b(?:10|127|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b"),
"authorization_header": re.compile(r"Authorization\s*:", re.IGNORECASE),
"bearer_token": re.compile(r"Bearer\s+[A-Za-z0-9._-]{10,}", re.IGNORECASE),
"basic_auth": re.compile(r"Basic\s+[A-Za-z0-9+/=]{10,}", re.IGNORECASE),
"password_assignment": re.compile(r"password\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
"token_assignment": re.compile(r"token\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
"cookie_assignment": re.compile(r"cookie\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
"client_keys": re.compile(r"client\.keys", re.IGNORECASE),
"private_key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
"work_window_text": re.compile(r"(工作視窗|批准!繼續|source_thread_id|owenhytsai/)", re.IGNORECASE),
}
_FORBIDDEN_KEY_FRAGMENTS = {
"authorization_header",
"basic_auth",
"bearer_token",
"client_keys",
"cookie",
"dashboard_api_secret",
"firewall_change",
"full_cli_output",
"full_journal",
"host_write",
"hostname",
"internal_ip",
"nginx_reload",
"password",
"private_key",
"raw_dashboard_request",
"raw_log",
"raw_wazuh_payload",
"stored_api_password",
"unredacted_screenshot",
}
_RUNTIME_ACTION_KEYS = {
"active_response_enable",
"agent_reenroll",
"agent_restart",
"argocd_sync",
"firewall_change",
"host_write",
"k8s_or_argocd_change",
"kali_active_scan",
"nginx_reload",
"runtime_execution_authorized",
"secret_rotation",
"wazuh_active_response",
"wazuh_agent_reenroll",
"wazuh_agent_restart",
"wazuh_api_live_query",
"wazuh_dashboard_secret_patch",
"wazuh_manager_restart",
}
_DASHBOARD_REQUIRED_FIELDS = {
"dashboard_api_connection_check_status",
"dashboard_api_version_check_status",
"dashboard_index_pattern_statuses",
"dashboard_api_degradation_root_cause",
"dashboard_api_repair_postcheck_ref",
}
_READONLY_CREDENTIAL_REQUIRED_FIELDS = {
"collection_method",
"manager_health_ref",
"redacted_evidence_refs",
}
_ACCOUNTABILITY_REQUIRED_FIELDS = {
"followup_owner",
"rollback_owner",
"postcheck_plan",
}
def load_latest_iwooos_wazuh_manager_registry_reviewer_validation(
security_dir: Path | None = None,
@@ -76,6 +153,10 @@ def load_latest_iwooos_wazuh_manager_registry_reviewer_validation(
f"docs/security/{_SNAPSHOT_FILE}",
"scripts/security/wazuh-manager-registry-reviewer-validation.py",
],
"owner_export_validation_endpoint": (
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export"
),
"owner_export_validation_mode": "no_persist_validation_no_runtime_action",
"summary": merged_summary,
"expected_scope_aliases": _strings(snapshot.get("expected_scope_aliases")),
"reviewer_validation_checks": _checks(snapshot.get("reviewer_validation_checks")),
@@ -173,6 +254,7 @@ def _evidence_slots(value: Any) -> list[dict[str, Any]]:
def _boundary_markers(summary: dict[str, int]) -> list[str]:
return [
"wazuh_manager_registry_reviewer_validation_visible=true",
"wazuh_manager_registry_owner_export_validation_api_available=true",
f"wazuh_manager_registry_reviewer_validation_expected_scope_alias_count={summary['expected_scope_alias_count']}",
f"wazuh_manager_registry_reviewer_validation_required_owner_field_count={summary['required_owner_field_count']}",
f"wazuh_manager_registry_reviewer_validation_per_host_required_field_count={summary['per_host_required_field_count']}",
@@ -225,3 +307,394 @@ def _require_boundaries(payload: dict[str, Any]) -> None:
raise ValueError(f"Wazuh manager registry reviewer validation execution_boundaries.{key} 必須維持 false")
if boundaries.get("not_authorization") is not True:
raise ValueError("Wazuh manager registry reviewer validation not_authorization 必須維持 true")
def validate_iwooos_wazuh_manager_registry_owner_export(
owner_export: dict[str, Any],
security_dir: Path | None = None,
) -> dict[str, Any]:
"""Validate one redacted owner export without persisting it or changing runtime truth."""
contract = load_latest_iwooos_wazuh_manager_registry_reviewer_validation(security_dir)
snapshot = _load_snapshot(security_dir or _DEFAULT_SECURITY_DIR)
expected_aliases = set(_strings(snapshot.get("expected_scope_aliases")))
required_owner_fields = _strings(snapshot.get("required_owner_fields"))
per_host_required_fields = _strings(snapshot.get("per_host_required_fields"))
findings: list[dict[str, Any]] = []
evidence_status = _initial_evidence_status(snapshot)
if not isinstance(owner_export, dict):
findings.append(_finding("RV-01", "blocker", "request_missing_fields", "owner export 必須是 JSON object", []))
return _validation_result(contract, "request_missing_fields", findings, evidence_status)
sensitive_hits = _collect_sensitive_hits(owner_export)
if sensitive_hits:
findings.append(
_finding(
"RV-07",
"critical",
"quarantine_sensitive_payload",
"owner export 含禁止內容或疑似未脫敏內容,已進隔離分流;回應不回傳原始值。",
[hit["path"] for hit in sensitive_hits[:12]],
{"categories": sorted({hit["category"] for hit in sensitive_hits})},
)
)
return _validation_result(contract, "quarantine_sensitive_payload", findings, evidence_status)
runtime_hits = _collect_runtime_action_hits(owner_export)
if runtime_hits:
findings.append(
_finding(
"RV-09",
"critical",
"reject_runtime_action_request",
"owner export 夾帶 runtime action request收件驗證只允許脫敏證據不授權 active response、restart、host write 或掃描。",
runtime_hits[:12],
)
)
return _validation_result(contract, "reject_runtime_action_request", findings, evidence_status)
missing_owner_fields = [field for field in required_owner_fields if not _present(owner_export.get(field))]
if missing_owner_fields:
findings.append(
_finding(
"RV-01",
"blocker",
"request_missing_fields",
"owner export envelope 欄位不足,需要補齊後再驗證。",
missing_owner_fields,
)
)
count_issue = _validate_counts(owner_export)
if count_issue:
findings.append(
_finding(
"RV-02",
"blocker",
"request_counts_arithmetic_fix",
count_issue,
["agent_total", "agent_active", "agent_disconnected", "agent_never_connected"],
)
)
alias_issue = _validate_aliases(owner_export.get("registry_export_scope_aliases"), expected_aliases)
if alias_issue:
findings.append(
_finding(
"RV-03",
"blocker",
"request_alias_scope_parity_fix",
alias_issue,
["registry_export_scope_aliases"],
)
)
matrix_issue_paths = _validate_per_host_matrix(
owner_export.get("per_host_registry_matrix"),
expected_aliases,
per_host_required_fields,
)
if matrix_issue_paths:
findings.append(
_finding(
"RV-04",
"blocker",
"request_per_host_matrix_supplement",
"逐主機矩陣未完整覆蓋 6 個公開別名或缺少必要欄位。",
matrix_issue_paths[:30],
)
)
dashboard_missing = [field for field in sorted(_DASHBOARD_REQUIRED_FIELDS) if not _present(owner_export.get(field))]
if dashboard_missing:
findings.append(
_finding(
"RV-05",
"blocker",
"request_dashboard_api_repair_postcheck",
"Dashboard API connection / version / index pattern / root cause / repair postcheck 必須分欄。",
dashboard_missing,
)
)
readonly_missing = [field for field in sorted(_READONLY_CREDENTIAL_REQUIRED_FIELDS) if not _present(owner_export.get(field))]
if readonly_missing:
findings.append(
_finding(
"RV-06",
"blocker",
"request_readonly_credential_metadata",
"唯讀 credential metadata 與 manager health ref 必須可追溯,且不得含 secret value。",
readonly_missing,
)
)
accountability_missing = [field for field in sorted(_ACCOUNTABILITY_REQUIRED_FIELDS) if not _present(owner_export.get(field))]
if accountability_missing:
findings.append(
_finding(
"RV-08",
"blocker",
"request_owner_accountability_supplement",
"followup owner、rollback owner 與 postcheck plan 必須可讀。",
accountability_missing,
)
)
_mark_evidence_status(evidence_status, owner_export)
outcome = _first_blocking_lane(findings) or "accepted_for_readonly_posture_only"
if outcome == "accepted_for_readonly_posture_only":
evidence_status = [
{**slot, "received": True, "accepted": True, "quarantined": False}
for slot in evidence_status
]
findings.append(
_finding(
"RV-10",
"info",
"waiting_post_enable_iwooos_readback",
"owner export 已通過 no-persist reviewer validation下一步仍只能進 post-enable IwoooS readback不開 runtime gate。",
["post_enable_iwooos_readback"],
)
)
return _validation_result(contract, outcome, findings, evidence_status)
def _validation_result(
contract: dict[str, Any],
outcome_lane: str,
findings: list[dict[str, Any]],
evidence_status: list[dict[str, Any]],
) -> dict[str, Any]:
accepted = outcome_lane == "accepted_for_readonly_posture_only"
quarantined = outcome_lane == "quarantine_sensitive_payload"
rejected_runtime = outcome_lane == "reject_runtime_action_request"
return {
"schema_version": "iwooos_wazuh_manager_registry_owner_export_validation_result_v1",
"contract_schema_version": contract["schema_version"],
"status": outcome_lane,
"mode": "no_persist_validation_no_runtime_no_secret_collection",
"outcome_lane": outcome_lane,
"accepted_for_readonly_posture_only": accepted,
"reviewer_validation_passed": accepted,
"quarantined": quarantined,
"runtime_action_rejected": rejected_runtime,
"summary": {
"owner_registry_export_received_count": 1,
"owner_registry_export_accepted_count": 1 if accepted else 0,
"reviewer_validation_passed_count": 1 if accepted else 0,
"reviewer_validation_quarantined_count": 1 if quarantined else 0,
"manager_registry_accepted_count": 0,
"post_enable_readback_passed_count": 0,
"runtime_gate_count": 0,
"host_write_authorized_count": 0,
"active_response_authorized_count": 0,
"secret_value_collection_allowed_count": 0,
"finding_count": len(findings),
},
"validation_findings": findings,
"evidence_slots": evidence_status,
"boundary_markers": [
"wazuh_manager_registry_owner_export_validation_received_count=1",
f"wazuh_manager_registry_owner_export_validation_accepted_count={1 if accepted else 0}",
f"wazuh_manager_registry_owner_export_validation_quarantined_count={1 if quarantined else 0}",
"wazuh_manager_registry_owner_export_validation_manager_registry_accepted_count=0",
"wazuh_manager_registry_owner_export_validation_runtime_gate_count=0",
"wazuh_manager_registry_owner_export_validation_no_persist=true",
"wazuh_api_live_query_authorized=false",
"wazuh_active_response_authorized=false",
"host_write_authorized=false",
"secret_value_collection_allowed=false",
"not_authorization=true",
],
"boundaries": {
"payload_persisted": False,
"wazuh_api_live_query_authorized": False,
"wazuh_agent_reenroll_authorized": False,
"wazuh_agent_restart_authorized": False,
"wazuh_manager_restart_authorized": False,
"wazuh_active_response_authorized": False,
"host_write_authorized": False,
"secret_value_collection_allowed": False,
"raw_wazuh_payload_storage_allowed": False,
"kali_active_scan_authorized": False,
"runtime_execution_authorized": False,
"manager_registry_accepted_updated": False,
"not_authorization": True,
},
"next_gate": "post_enable_iwooos_readback" if accepted else "owner_export_fix_and_resubmit",
}
def _finding(
check_id: str,
severity: str,
lane: str,
message: str,
field_paths: list[str],
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"check_id": check_id,
"severity": severity,
"lane": lane,
"message": message,
"field_paths": field_paths,
}
if extra:
payload.update(extra)
return payload
def _initial_evidence_status(snapshot: dict[str, Any]) -> list[dict[str, Any]]:
slots = _evidence_slots(snapshot.get("evidence_slots"))
return [{**slot, "received": False, "accepted": False, "quarantined": False} for slot in slots]
def _mark_evidence_status(evidence_status: list[dict[str, Any]], owner_export: dict[str, Any]) -> None:
for slot in evidence_status:
required_fields = slot.get("required_fields", [])
slot["received"] = bool(required_fields) and all(_present(owner_export.get(field)) for field in required_fields)
slot["accepted"] = False
slot["quarantined"] = False
def _present(value: Any) -> bool:
if value is None:
return False
if isinstance(value, str):
return bool(value.strip())
if isinstance(value, (list, dict, tuple, set)):
return bool(value)
return True
def _int_or_none(value: Any) -> int | None:
if isinstance(value, bool):
return None
if isinstance(value, int):
return value
if isinstance(value, str) and value.strip().isdigit():
return int(value.strip())
return None
def _validate_counts(owner_export: dict[str, Any]) -> str | None:
fields = ["agent_total", "agent_active", "agent_disconnected", "agent_never_connected"]
counts = {field: _int_or_none(owner_export.get(field)) for field in fields}
if any(value is None for value in counts.values()):
return "agent count 欄位必須是非負整數。"
if any(value is not None and value < 0 for value in counts.values()):
return "agent count 欄位不得為負數。"
total = counts["agent_total"]
active = counts["agent_active"]
disconnected = counts["agent_disconnected"]
never_connected = counts["agent_never_connected"]
if total is None or active is None or disconnected is None or never_connected is None:
return "agent count 欄位必須是非負整數。"
if total < active + disconnected + never_connected:
return "agent_total 不得小於 active + disconnected + never_connected。"
return None
def _validate_aliases(value: Any, expected_aliases: set[str]) -> str | None:
aliases = value if isinstance(value, list) else []
if not aliases or not all(isinstance(item, str) for item in aliases):
return "registry_export_scope_aliases 必須是公開別名字串陣列。"
alias_set = set(aliases)
if len(aliases) != len(alias_set):
return "registry_export_scope_aliases 不得重複。"
if alias_set != expected_aliases:
missing = sorted(expected_aliases - alias_set)
extra = sorted(alias_set - expected_aliases)
return f"registry_export_scope_aliases 必須剛好等於 6 個公開別名missing={missing} extra={extra}"
return None
def _validate_per_host_matrix(value: Any, expected_aliases: set[str], required_fields: list[str]) -> list[str]:
if not isinstance(value, list):
return ["per_host_registry_matrix"]
paths: list[str] = []
seen_aliases: list[str] = []
for index, item in enumerate(value):
if not isinstance(item, dict):
paths.append(f"per_host_registry_matrix[{index}]")
continue
alias = item.get("node_alias")
if not isinstance(alias, str) or alias not in expected_aliases:
paths.append(f"per_host_registry_matrix[{index}].node_alias")
else:
seen_aliases.append(alias)
for field in required_fields:
if not _present(item.get(field)):
paths.append(f"per_host_registry_matrix[{index}].{field}")
seen_set = set(seen_aliases)
if len(seen_aliases) != len(seen_set):
paths.append("per_host_registry_matrix.duplicate_node_alias")
for missing_alias in sorted(expected_aliases - seen_set):
paths.append(f"per_host_registry_matrix.missing.{missing_alias}")
for extra_alias in sorted(seen_set - expected_aliases):
paths.append(f"per_host_registry_matrix.extra.{extra_alias}")
return paths
def _collect_sensitive_hits(value: Any, path: str = "$") -> list[dict[str, str]]:
hits: list[dict[str, str]] = []
if isinstance(value, dict):
for key, item in value.items():
key_text = str(key)
key_lower = key_text.lower()
for fragment in _FORBIDDEN_KEY_FRAGMENTS:
if fragment in key_lower:
hits.append({"path": f"{path}.{key_text}", "category": f"forbidden_key:{fragment}"})
hits.extend(_collect_sensitive_hits(item, f"{path}.{key_text}"))
return hits
if isinstance(value, list):
for index, item in enumerate(value):
hits.extend(_collect_sensitive_hits(item, f"{path}[{index}]"))
return hits
if isinstance(value, str):
for category, pattern in _SENSITIVE_TEXT_PATTERNS.items():
if pattern.search(value):
hits.append({"path": path, "category": category})
return hits
def _collect_runtime_action_hits(value: Any, path: str = "$") -> list[str]:
hits: list[str] = []
if isinstance(value, dict):
for key, item in value.items():
key_text = str(key)
normalized_key = key_text.lower().replace("-", "_").replace(" ", "_")
if normalized_key in _RUNTIME_ACTION_KEYS and item not in (False, None, "", [], {}):
hits.append(f"{path}.{key_text}")
hits.extend(_collect_runtime_action_hits(item, f"{path}.{key_text}"))
return hits
if isinstance(value, list):
for index, item in enumerate(value):
hits.extend(_collect_runtime_action_hits(item, f"{path}[{index}]"))
return hits
if isinstance(value, str):
normalized = value.lower().replace("-", "_").replace(" ", "_")
if normalized in _RUNTIME_ACTION_KEYS:
hits.append(path)
return hits
def _first_blocking_lane(findings: list[dict[str, Any]]) -> str | None:
for lane in (
"quarantine_sensitive_payload",
"reject_runtime_action_request",
"request_missing_fields",
"request_counts_arithmetic_fix",
"request_alias_scope_parity_fix",
"request_per_host_matrix_supplement",
"request_dashboard_api_repair_postcheck",
"request_readonly_credential_metadata",
"request_owner_accountability_supplement",
):
if any(item.get("lane") == lane for item in findings):
return lane
return None

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

@@ -6,15 +6,78 @@ from fastapi.testclient import TestClient
from src.api.v1.iwooos import router
from src.services.iwooos_wazuh_manager_registry_reviewer_validation import (
load_latest_iwooos_wazuh_manager_registry_reviewer_validation,
validate_iwooos_wazuh_manager_registry_owner_export,
)
EXPECTED_ALIASES = [
"managed_core_node_a",
"managed_core_node_b",
"managed_dev_node_a",
"managed_dev_node_b",
"managed_control_node_a",
"managed_control_node_b",
]
def _client() -> TestClient:
app = FastAPI()
app.include_router(router)
return TestClient(app)
def _valid_owner_export() -> dict:
return {
"owner_role": "資安負責人",
"team": "IwoooS",
"decision": "accept_readonly_registry_export_for_reviewer_validation",
"decision_reason": "Wazuh manager registry 已脫敏,供 reviewer 驗證主機覆蓋與 Dashboard API 修復狀態。",
"affected_scope": "iwooos_wazuh_expected_scope_aliases",
"collection_method": "owner_export_redacted_summary",
"agent_total": 6,
"agent_active": 2,
"agent_disconnected": 3,
"agent_never_connected": 1,
"last_seen_window_start": "2026-06-27T15:00:00+08:00",
"last_seen_window_end": "2026-06-27T16:00:00+08:00",
"registry_collected_at": "2026-06-27T16:05:00+08:00",
"registry_export_scope_aliases": EXPECTED_ALIASES,
"per_host_registry_matrix": [
{
"node_alias": alias,
"scope_role": "managed_scope",
"registry_presence": "present",
"agent_status_bucket": "active" if index < 2 else "disconnected",
"last_seen_state": "within_owner_window" if index < 2 else "outside_owner_window",
"manager_group_ref": f"group-ref-{index}",
"agent_id_redacted_ref": f"agent-redacted-ref-{index}",
"gap_reason": "none" if index < 2 else "owner_followup_required",
"redacted_evidence_ref": f"evidence-ref-{index}",
}
for index, alias in enumerate(EXPECTED_ALIASES)
],
"registry_gap_reason_by_alias": {
alias: "none" if index < 2 else "owner_followup_required"
for index, alias in enumerate(EXPECTED_ALIASES)
},
"registry_export_summary_ref": "evidence-ref-registry-summary",
"manager_health_ref": "evidence-ref-manager-health",
"dashboard_api_status_ref": "evidence-ref-dashboard-api",
"dashboard_api_connection_check_status": "ok",
"dashboard_api_version_check_status": "ok",
"dashboard_index_pattern_statuses": ["alerts_ok", "monitoring_ok", "statistics_ok"],
"dashboard_api_degradation_root_cause": "stored_api_connection_repaired_by_owner",
"dashboard_api_repair_postcheck_ref": "evidence-ref-dashboard-postcheck",
"redacted_evidence_refs": [
"evidence-ref-registry-summary",
"evidence-ref-dashboard-postcheck",
],
"followup_owner": "IwoooS reviewer",
"rollback_owner": "IwoooS runtime owner",
"postcheck_plan": "post_enable_iwooos_readback_no_raw_payload",
}
def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_is_waiting_only() -> None:
payload = load_latest_iwooos_wazuh_manager_registry_reviewer_validation()
@@ -94,3 +157,82 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe()
assert "source_thread_id" not in response.text
assert "owenhytsai/" not in response.text
assert "WAZUH_API_PASSWORD" not in response.text
def test_iwooos_wazuh_manager_registry_owner_export_validation_accepts_redacted_payload() -> None:
payload = validate_iwooos_wazuh_manager_registry_owner_export(_valid_owner_export())
assert payload["schema_version"] == "iwooos_wazuh_manager_registry_owner_export_validation_result_v1"
assert payload["status"] == "accepted_for_readonly_posture_only"
assert payload["accepted_for_readonly_posture_only"] is True
assert payload["reviewer_validation_passed"] is True
assert payload["summary"]["owner_registry_export_received_count"] == 1
assert payload["summary"]["owner_registry_export_accepted_count"] == 1
assert payload["summary"]["manager_registry_accepted_count"] == 0
assert payload["summary"]["runtime_gate_count"] == 0
assert payload["boundaries"]["payload_persisted"] is False
assert payload["boundaries"]["runtime_execution_authorized"] is False
assert payload["boundaries"]["manager_registry_accepted_updated"] is False
assert all(slot["received"] is True for slot in payload["evidence_slots"])
assert all(slot["accepted"] is True for slot in payload["evidence_slots"])
def test_iwooos_wazuh_manager_registry_owner_export_validation_api_does_not_update_global_counters() -> None:
client = _client()
response = client.post(
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export",
json=_valid_owner_export(),
)
assert response.status_code == 200
result = response.json()
assert result["status"] == "accepted_for_readonly_posture_only"
assert result["summary"]["owner_registry_export_accepted_count"] == 1
assert result["summary"]["manager_registry_accepted_count"] == 0
assert result["summary"]["runtime_gate_count"] == 0
readback = client.get("/api/v1/iwooos/wazuh-manager-registry-reviewer-validation").json()
assert readback["summary"]["owner_registry_export_received_count"] == 0
assert readback["summary"]["owner_registry_export_accepted_count"] == 0
assert readback["summary"]["manager_registry_accepted_count"] == 0
assert readback["summary"]["runtime_gate_count"] == 0
def test_iwooos_wazuh_manager_registry_owner_export_validation_requests_missing_fields() -> None:
candidate = _valid_owner_export()
candidate.pop("decision_reason")
payload = validate_iwooos_wazuh_manager_registry_owner_export(candidate)
assert payload["status"] == "request_missing_fields"
assert payload["accepted_for_readonly_posture_only"] is False
assert payload["summary"]["owner_registry_export_received_count"] == 1
assert payload["summary"]["owner_registry_export_accepted_count"] == 0
assert any("decision_reason" in finding["field_paths"] for finding in payload["validation_findings"])
def test_iwooos_wazuh_manager_registry_owner_export_validation_quarantines_sensitive_payload() -> None:
candidate = _valid_owner_export()
candidate["manager_health_ref"] = "health evidence mentioned 10.250.250.250 by mistake"
payload = validate_iwooos_wazuh_manager_registry_owner_export(candidate)
assert payload["status"] == "quarantine_sensitive_payload"
assert payload["quarantined"] is True
assert payload["summary"]["reviewer_validation_quarantined_count"] == 1
assert payload["summary"]["owner_registry_export_accepted_count"] == 0
assert "10.250.250.250" not in str(payload)
assert any(finding["check_id"] == "RV-07" for finding in payload["validation_findings"])
def test_iwooos_wazuh_manager_registry_owner_export_validation_rejects_runtime_action_request() -> None:
candidate = _valid_owner_export()
candidate["requested_actions"] = ["wazuh_active_response"]
payload = validate_iwooos_wazuh_manager_registry_owner_export(candidate)
assert payload["status"] == "reject_runtime_action_request"
assert payload["runtime_action_rejected"] is True
assert payload["summary"]["owner_registry_export_accepted_count"] == 0
assert payload["summary"]["runtime_gate_count"] == 0
assert any(finding["check_id"] == "RV-09" for finding in payload["validation_findings"])

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():

View File

@@ -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",
@@ -20793,6 +20838,8 @@
"title": "Owner export 進來後,先由 reviewer 驗收脫敏清單",
"subtitle": "這張卡固定 Wazuh manager registry owner export 的驗收規則欄位、計數、公開別名矩陣、Dashboard API 修復讀回、唯讀 credential metadata、拒收內容與下一個 Gate 都先可視化;目前尚未收到、尚未接受,也不開 runtime。",
"loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API",
"validationEndpointLabel": "脫敏 owner export 驗證端點",
"validationModeLabel": "驗證模式",
"slotReceivedLabel": "已收件",
"slotAcceptedLabel": "已接受",
"slotNextGateLabel": "下一關",

View File

@@ -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",
@@ -20793,6 +20838,8 @@
"title": "Owner export 進來後,先由 reviewer 驗收脫敏清單",
"subtitle": "這張卡固定 Wazuh manager registry owner export 的驗收規則欄位、計數、公開別名矩陣、Dashboard API 修復讀回、唯讀 credential metadata、拒收內容與下一個 Gate 都先可視化;目前尚未收到、尚未接受,也不開 runtime。",
"loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API",
"validationEndpointLabel": "脫敏 owner export 驗證端點",
"validationModeLabel": "驗證模式",
"slotReceivedLabel": "已收件",
"slotAcceptedLabel": "已接受",
"slotNextGateLabel": "下一關",

View File

@@ -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<AiAgentAutomationInventorySnapshot | null>(null)
const [autonomousRuntimeControl, setAutonomousRuntimeControl] = useState<AiAgentAutonomousRuntimeControlSnapshot | null>(null)
const [backlog, setBacklog] = useState<AiAgentAutomationBacklogSnapshot | null>(null)
const [backupTargets, setBackupTargets] = useState<BackupDrTargetInventorySnapshot | null>(null)
const [backupReadiness, setBackupReadiness] = useState<BackupDrReadinessMatrixSnapshot | null>(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 ? (
<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 #15803d40',
background: 'rgba(21,128,61,0.08)',
color: '#15803d',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}>
<BellRing 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('globalControl.currentAutonomy.title')}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, color: '#4f6156', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
{autonomousRuntimeControl.program_status.status_note}
</span>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end', gap: 6, minWidth: 0 }}>
<Chip value={autonomousRuntimeControl.program_status.current_task_id} />
<Chip value={t('globalControl.currentAutonomy.badges.override')} />
<Chip value={t('globalControl.currentAutonomy.badges.gateway')} muted={autonomousRuntimeControl.rollups.telegram_gateway_delivery_enabled_count === 0} />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(128px, 1fr))', gap: 10 }} className="automation-inventory-global-control-kpi-grid">
<MetricCard label={t('globalControl.currentAutonomy.metrics.completion')} value={`${autonomousRuntimeControl.program_status.implementation_completion_percent}%`} tone="ok" icon={<Gauge size={16} />} />
<MetricCard label={t('globalControl.currentAutonomy.metrics.riskTiers')} value={`${autonomousRuntimeControl.rollups.automated_risk_tier_count}/3`} tone={autonomousRuntimeControl.rollups.automated_risk_tier_count === 3 ? 'ok' : 'warn'} icon={<ShieldCheck size={16} />} />
<MetricCard label={t('globalControl.currentAutonomy.metrics.reports')} value={autonomousRuntimeControl.rollups.report_cadence_enabled_count} tone="ok" icon={<CalendarClock size={16} />} />
<MetricCard label={t('globalControl.currentAutonomy.metrics.gateway')} value={autonomousRuntimeControl.rollups.telegram_gateway_delivery_enabled_count} tone="ok" icon={<BellRing size={16} />} />
<MetricCard label={t('globalControl.currentAutonomy.metrics.executor')} value={autonomousRuntimeControl.rollups.controlled_executor_operation_receipt_count} tone="ok" icon={<ClipboardCheck size={16} />} />
<MetricCard label={t('globalControl.currentAutonomy.metrics.hardBlockers')} value={autonomousRuntimeControl.rollups.hard_blocker_count} tone="warn" icon={<ShieldAlert size={16} />} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.2fr) minmax(260px, 0.8fr)', gap: 12 }} className="automation-inventory-global-control-grid">
<div style={{ padding: 12, border: '0.5px solid #cfe7d7', borderRadius: 7, background: '#f8fffa', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<SmallLabel>{t('globalControl.currentAutonomy.policyTitle')}</SmallLabel>
<Chip value={autonomousRuntimeControl.program_status.runtime_authority} muted />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 8 }} className="automation-inventory-global-control-pipeline-grid">
<GateMatrixRow
label={t('globalControl.currentAutonomy.policy.low')}
value={currentAutonomyPolicy?.low_risk_controlled_apply_allowed ? 'on' : 'off'}
detail={t('globalControl.currentAutonomy.policy.noOwnerReview', { value: String(currentAutonomyPolicy?.owner_review_required_for_low_medium_high === false) })}
tone={currentAutonomyPolicy?.low_risk_controlled_apply_allowed ? 'ok' : 'warn'}
/>
<GateMatrixRow
label={t('globalControl.currentAutonomy.policy.medium')}
value={currentAutonomyPolicy?.medium_risk_controlled_apply_allowed ? 'on' : 'off'}
detail={t('globalControl.currentAutonomy.policy.verifier', { value: String(currentAutonomyPolicy?.post_apply_verifier_required === true) })}
tone={currentAutonomyPolicy?.medium_risk_controlled_apply_allowed ? 'ok' : 'warn'}
/>
<GateMatrixRow
label={t('globalControl.currentAutonomy.policy.high')}
value={currentAutonomyPolicy?.high_risk_controlled_apply_allowed ? 'on' : 'off'}
detail={t('globalControl.currentAutonomy.policy.km', { value: String(currentAutonomyPolicy?.km_learning_writeback_required === true) })}
tone={currentAutonomyPolicy?.high_risk_controlled_apply_allowed ? 'ok' : 'warn'}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 8 }}>
{currentAutonomyCadences.map(cadence => (
<div key={cadence.cadence} style={{ padding: 10, border: '0.5px solid #d8e8df', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'flex-start' }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 760, color: '#141413', lineHeight: 1.2, overflowWrap: 'anywhere' }}>
{cadence.display_name}
</span>
<Chip value={cadence.telegram_gateway_delivery_enabled ? 'Gateway on' : 'Gateway off'} muted={!cadence.telegram_gateway_delivery_enabled} />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#50665a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{cadence.schedule}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#50665a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{cadence.worker}
</span>
</div>
))}
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #e5dbc7', borderRadius: 7, background: '#fffdf7', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('globalControl.currentAutonomy.runtimeTitle')}</SmallLabel>
<GateMatrixRow
label={t('globalControl.currentAutonomy.runtime.checkMode')}
value={currentAutonomySwitches?.ansible_check_mode_worker_enabled ? 'on' : 'off'}
detail={`interval=${currentAutonomySwitches?.ansible_check_mode_interval_seconds ?? '--'}s batch=${currentAutonomySwitches?.ansible_check_mode_batch_limit ?? '--'}`}
tone={currentAutonomySwitches?.ansible_check_mode_worker_enabled ? 'ok' : 'warn'}
/>
<GateMatrixRow
label={t('globalControl.currentAutonomy.runtime.apply')}
value={currentAutonomySwitches?.ansible_controlled_apply_enabled ? 'on' : 'off'}
detail={(currentAutonomySwitches?.ansible_controlled_apply_allowed_risk_levels ?? []).join(', ') || '--'}
tone={currentAutonomySwitches?.ansible_controlled_apply_enabled ? 'ok' : 'warn'}
/>
<GateMatrixRow
label={t('globalControl.currentAutonomy.runtime.botApi')}
value={currentAutonomyPolicy?.direct_bot_api_allowed ? 'on' : 'off'}
detail={t('globalControl.currentAutonomy.runtime.gatewayOnly')}
tone={currentAutonomyPolicy?.direct_bot_api_allowed ? 'danger' : 'ok'}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-global-control-runway-grid">
<div style={{ padding: 12, border: '0.5px solid #d7d2f4', borderRadius: 7, background: '#fbfaff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('globalControl.currentAutonomy.executorTitle')}</SmallLabel>
{currentAutonomyExecutorReceipts.slice(0, 5).map(receipt => (
<GateMatrixRow
key={receipt.operation_type}
label={receipt.operation_type}
value={receipt.writes_runtime_state ? 'write' : 'dry'}
detail={`${receipt.owner_agent}: ${receipt.purpose}`}
tone={receipt.writes_runtime_state ? 'warn' : 'ok'}
/>
))}
</div>
<div style={{ padding: 12, border: '0.5px solid #d7e2ea', borderRadius: 7, background: '#f9fcff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('globalControl.currentAutonomy.overrideTitle')}</SmallLabel>
{currentAutonomyOverrides.slice(0, 4).map(row => (
<GateMatrixRow
key={row.legacy_area}
label={row.legacy_area}
value={row.current_effect}
detail={row.new_behavior}
tone="ok"
/>
))}
</div>
<div style={{ padding: 12, border: '0.5px solid #f1d4d4', borderRadius: 7, background: '#fffafa', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('globalControl.currentAutonomy.hardBlockerTitle')}</SmallLabel>
{currentAutonomyHardBlockers.slice(0, 5).map(blocker => (
<GateMatrixRow
key={blocker}
label={blocker}
value="blocked"
detail={t('globalControl.currentAutonomy.hardBlockerDetail')}
tone="warn"
/>
))}
</div>
</div>
</div>
</GlassCard>
) : null}
<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' }}>

View File

@@ -2474,6 +2474,7 @@ const wazuhManagedHostCoverageBoundaries = [
const wazuhManagerRegistryReviewerValidationBoundaries = [
'wazuh_manager_registry_reviewer_validation_visible=true',
'wazuh_manager_registry_owner_export_validation_api_available=true',
'wazuh_manager_registry_reviewer_validation_expected_scope_alias_count=6',
'wazuh_manager_registry_reviewer_validation_required_owner_field_count=28',
'wazuh_manager_registry_reviewer_validation_per_host_required_field_count=9',
@@ -9845,6 +9846,9 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
: loading
? [t('loadingBoundary')]
: wazuhManagerRegistryReviewerValidationBoundaries
const validationEndpoint = data?.owner_export_validation_endpoint
?? '/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export'
const validationMode = data?.owner_export_validation_mode ?? 'no_persist_validation_no_runtime_action'
const evidenceSlots = data?.evidence_slots ?? []
const visibleChecks = data?.reviewer_validation_checks?.slice(0, 4) ?? []
const statusText = loading ? t('status.loading') : failed ? t('status.failed') : t('status.ready')
@@ -9870,6 +9874,10 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
<ToneDot tone={statusTone} />
{statusText}
</div>
<div style={{ marginTop: 10, display: 'grid', gap: 6, fontSize: 11, color: '#45686a', ...textWrap }}>
<span>{t('validationEndpointLabel')}<code style={{ color: '#2f6265', overflowWrap: 'anywhere' }}>{validationEndpoint}</code></span>
<span>{t('validationModeLabel')}<code style={{ color: '#2f6265', overflowWrap: 'anywhere' }}>{validationMode}</code></span>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(126px, 1fr))', gap: 8 }}>

View File

@@ -341,6 +341,8 @@ export interface IwoooSWazuhManagerRegistryReviewerValidationResponse {
status: string
mode: string
source_refs: string[]
owner_export_validation_endpoint: string
owner_export_validation_mode: string
summary: {
expected_scope_alias_count: number
required_owner_field_count: number
@@ -843,6 +845,11 @@ export const apiClient = {
return handleResponse<AiAgentAutomationInventorySnapshot>(res)
},
async getAiAgentAutonomousRuntimeControl() {
const res = await fetch(`${API_BASE_URL}/agents/agent-autonomous-runtime-control`)
return handleResponse<AiAgentAutonomousRuntimeControlSnapshot>(res)
},
async getAiAgentAutomationBacklogSnapshot() {
const res = await fetch(`${API_BASE_URL}/agents/automation-backlog-snapshot`)
return handleResponse<AiAgentAutomationBacklogSnapshot>(res)
@@ -1848,6 +1855,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

View File

@@ -1,3 +1,66 @@
## 2026-06-27P2-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-27AI 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-27IwoooS Wazuh manager registry reviewer validation 正式讀回完成
**時間與來源**

View File

@@ -29573,6 +29573,7 @@ def validate(root: Path) -> None:
"getIwoooSWazuhManagerRegistryReviewerValidation",
"apiClient.getIwoooSWazuhManagerRegistryReviewerValidation",
"Wazuh manager registry reviewer validation 已讀回",
"wazuh_manager_registry_owner_export_validation_api_available=true",
"wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=0",
"wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=0",
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",
@@ -29594,7 +29595,13 @@ def validate(root: Path) -> None:
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation",
"wazuh_manager_registry_reviewer_validation_v1",
"iwooos_wazuh_manager_registry_reviewer_validation_readback_v1",
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export",
"iwooos_wazuh_manager_registry_owner_export_validation_result_v1",
"validate_iwooos_wazuh_manager_registry_owner_export",
"test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe",
"test_iwooos_wazuh_manager_registry_owner_export_validation_accepts_redacted_payload",
"test_iwooos_wazuh_manager_registry_owner_export_validation_quarantines_sensitive_payload",
"test_iwooos_wazuh_manager_registry_owner_export_validation_rejects_runtime_action_request",
"wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=0",
"wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=0",
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",