feat(automation): expose no-write typed route preview
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user