From 15f3a1f9a3a9797dd6e1f61d87ec98d0e5c7abbc Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 12:23:30 +0800 Subject: [PATCH] feat(automation): expose no-write typed route preview --- apps/api/src/api/v1/agents.py | 45 ++++++++++++++++ ...3s_controlled_automation_work_items_api.py | 51 +++++++++++++++++++ ...rolled-automation-work-items.snapshot.json | 6 ++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 3eb5c9733..e37dd4b20 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -383,6 +383,7 @@ from src.services.backup_notification_policy import ( from src.services.backup_restore_drill_approval_package_template import ( load_latest_backup_restore_drill_approval_package_template, ) +from src.services.controlled_alert_target_router import resolve_typed_alert_target from src.services.credential_escrow_evidence_intake_readiness import ( load_latest_credential_escrow_evidence_intake_readiness, validate_credential_escrow_evidence_owner_response, @@ -552,6 +553,17 @@ _PRIORITY_WORK_ORDER_LIVE_READBACK_TIMEOUT_SECONDS = 12.0 # Request/Response Models # ============================================================================= + +class ControlledAlertTargetPreviewRequest(BaseModel): + """只讀解析一筆告警的 canonical typed target。""" + + alertname: str = Field(min_length=1, max_length=160) + target_resource: str = Field(default="", max_length=240) + namespace: str = Field(default="", max_length=160) + alert_category: str = Field(default="", max_length=120) + labels: dict[str, str] = Field(default_factory=dict) + + class AnalyzeRequest(BaseModel): """分析請求""" incident_id: str | None = Field( @@ -1231,6 +1243,39 @@ async def get_sre_k3s_controlled_automation_work_items() -> dict[str, Any]: ) from exc +@router.post( + "/controlled-alert-target-preview", + response_model=dict[str, Any], + summary="只讀預覽告警的 canonical typed route", + description=( + "以 production service registry 執行 deterministic target resolution," + "只回傳經 LAN topology redaction 的 route。此端點不呼叫模型、不建立事件、" + "不 dispatch Agent99、不執行 Ansible/Kubernetes/DB 動作,也不更新完成狀態。" + ), +) +async def preview_controlled_alert_target( + payload: ControlledAlertTargetPreviewRequest, +) -> dict[str, Any]: + """提供可重播、零 side effect 的 production typed-router verifier。""" + + route = resolve_typed_alert_target( + alertname=payload.alertname, + target_resource=payload.target_resource, + namespace=payload.namespace, + labels=payload.labels, + alert_category=payload.alert_category, + ) + return redact_public_lan_topology( + { + "schema_version": "controlled_alert_target_preview_v1", + "preview_only": True, + "dispatch_performed": False, + "runtime_closure_verified": False, + "route": route, + } + ) + + @router.post( "/agent99/telegram-lifecycle", response_model=dict[str, Any], diff --git a/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py b/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py index d4345df4c..4fefc9eff 100644 --- a/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py +++ b/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py @@ -122,3 +122,54 @@ def test_endpoint_returns_full_work_ledger() -> None: assert payload["agent99_host_operations_bridge"][ "completion_callback" ] == "/api/v1/agents/agent99/completion-callback" + + +def test_typed_target_preview_is_no_write_and_fail_closed() -> None: + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + unresolved = client.post( + "/api/v1/agents/controlled-alert-target-preview", + json={ + "alertname": "VMwareGuestHealthDegraded", + "target_resource": "unregistered-vmware-guest", + "namespace": "virtualization", + "labels": {}, + "alert_category": "infrastructure", + }, + ) + agent99 = client.post( + "/api/v1/agents/controlled-alert-target-preview", + json={ + "alertname": "Agent99ServiceUnhealthy", + "target_resource": "agent99", + "namespace": "control-plane", + "labels": {}, + "alert_category": "infrastructure", + }, + ) + + assert unresolved.status_code == 200 + unresolved_payload = unresolved.json() + assert unresolved_payload["preview_only"] is True + assert unresolved_payload["dispatch_performed"] is False + assert unresolved_payload["runtime_closure_verified"] is False + assert unresolved_payload["route"]["resolution_status"] == ( + "asset_identity_unresolved" + ) + assert unresolved_payload["route"]["executor"] is None + assert unresolved_payload["route"]["agent99_bridge"][ + "dispatch_allowed" + ] is False + + assert agent99.status_code == 200 + agent99_payload = agent99.json() + assert agent99_payload["dispatch_performed"] is False + assert agent99_payload["route"]["canonical_asset_id"] == ( + "windows-vmware:host_99" + ) + assert agent99_payload["route"]["executor"] == "Agent99" + assert agent99_payload["route"]["agent99_bridge"][ + "dispatch_allowed" + ] is True diff --git a/docs/operations/sre-k3s-controlled-automation-work-items.snapshot.json b/docs/operations/sre-k3s-controlled-automation-work-items.snapshot.json index 7ee535c6b..8178da775 100644 --- a/docs/operations/sre-k3s-controlled-automation-work-items.snapshot.json +++ b/docs/operations/sre-k3s-controlled-automation-work-items.snapshot.json @@ -203,7 +203,8 @@ "source_refs": [ "apps/api/src/services/controlled_alert_target_router.py", "apps/api/src/services/awooop_ansible_audit_service.py", - "apps/api/src/services/awooop_ansible_check_mode_service.py" + "apps/api/src/services/awooop_ansible_check_mode_service.py", + "apps/api/src/api/v1/agents.py" ], "executor": "typed policy router", "verifier": "domain/catalog/host/canonical ID scope verifier", @@ -248,7 +249,8 @@ "source_refs": [ "apps/api/src/services/agent99_sre_bridge.py", "apps/api/src/services/agent99_controlled_dispatch_ledger.py", - "apps/api/src/services/agent99_completion_callback.py" + "apps/api/src/services/agent99_completion_callback.py", + "apps/api/src/api/v1/agents.py" ], "executor": "Agent99 for Windows/VMware/control-plane; relay-only elsewhere", "verifier": "Agent99 callback plus external same-run verifier",