aps-agent/server/gateway/governance_api.py

269 lines
8.1 KiB
Python
Raw Normal View History

# ============================================================
# 治理 API(moduleId: gateway-governance-api, 可重生 ✅)
# R71.6:约束/自动化规则 CRUD + 启停 + 规则审计轨迹(/api/governance)
# 与审计统计聚合(/api/gov/audit/stats)。
# 前缀建议:/api/governance(规则管理)与 /api/gov(审计统计)。
# ============================================================
from __future__ import annotations
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, ConfigDict, Field
from server.aps_domain.governance_rules import (
RULE_ID_PATTERN,
GovernanceRuleError,
GovernanceRuleService,
)
from server.auth.context import get_identity
router = APIRouter(prefix="/api/governance", tags=["governance"])
gov_router = APIRouter(prefix="/api/gov", tags=["governance-audit"])
class RuleCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
ruleId: str | None = Field(default=None, pattern=RULE_ID_PATTERN.pattern)
roomId: str = Field(min_length=1, max_length=128)
trigger: dict[str, Any]
gear: str = Field(default="G1", pattern="^G[0-4]$")
action: str = "notify"
guardrails: list[str] = Field(default_factory=list)
enabled: bool = True
description: str = ""
params: dict[str, Any] = Field(default_factory=dict)
class RuleUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
ruleId: str | None = Field(default=None, pattern=RULE_ID_PATTERN.pattern)
roomId: str | None = Field(default=None, min_length=1, max_length=128)
trigger: dict[str, Any] | None = None
gear: str | None = Field(default=None, pattern="^G[0-4]$")
action: str | None = None
guardrails: list[str] | None = None
enabled: bool | None = None
description: str | None = None
params: dict[str, Any] | None = None
class RuleView(BaseModel):
ruleId: str
roomId: str
trigger: dict[str, Any]
gear: str
action: str
guardrails: list[str]
enabled: bool
paused: bool = False
description: str
params: dict[str, Any]
class RuleEnvelope(BaseModel):
rule: RuleView
class RuleListEnvelope(BaseModel):
rules: list[RuleView]
class RuleDeleteEnvelope(BaseModel):
deleted: bool
ruleId: str
class RuleStageEnvelope(BaseModel):
message: str
block: dict[str, Any]
class RuleAuditEnvelope(BaseModel):
ruleId: str
total: int
events: list[dict[str, Any]]
class AuditStatsEnvelope(BaseModel):
ok: bool
source: str
total: int
filtered: int
window: dict[str, Any]
byModule: list[dict[str, Any]]
byAction: list[dict[str, Any]]
byResult: list[dict[str, Any]]
trend: list[dict[str, Any]]
rows: list[dict[str, Any]]
def get_governance_service() -> GovernanceRuleService:
"""默认服务接入网关既有 AutomationRuntime 单例与当前 store。"""
return GovernanceRuleService()
ServiceDep = Annotated[GovernanceRuleService, Depends(get_governance_service)]
def _raise_governance_error(exc: GovernanceRuleError) -> None:
if exc.code == "RULE_NOT_FOUND":
http_code = status.HTTP_404_NOT_FOUND
elif exc.code in ("RULE_EXISTS", "RULE_ID_MISMATCH", "RULE_CONFLICT"):
http_code = status.HTTP_409_CONFLICT
elif exc.code in ("INVALID_GRANULARITY", "INVALID_TIME", "INVALID_TIME_WINDOW"):
http_code = status.HTTP_400_BAD_REQUEST
elif exc.code == "PERSISTENCE_FAILED":
http_code = status.HTTP_503_SERVICE_UNAVAILABLE
else:
http_code = 422
raise HTTPException(
status_code=http_code,
detail={"code": exc.code, "message": str(exc)},
) from exc
def _stage_governance_change(
service: GovernanceRuleService,
action: str,
payload: dict[str, Any],
) -> RuleStageEnvelope:
identity = get_identity(required=True)
try:
block = service.stage_change(
action,
payload,
session_id=f"governance:{identity.user_id}",
)
except GovernanceRuleError as exc:
_raise_governance_error(exc)
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
return RuleStageEnvelope(
message="治理规则变更已进入 P2 确认队列,确认前不会写入规则状态。",
block=block.model_dump(),
)
@router.get("/rules", response_model=RuleListEnvelope)
def list_governance_rules(
service: ServiceDep,
enabled: bool | None = None,
) -> RuleListEnvelope:
"""规则清单(可按启用状态过滤)。"""
return RuleListEnvelope(rules=service.list_rules(enabled=enabled))
@router.get("/rules/{rule_id}", response_model=RuleEnvelope)
def get_governance_rule(
rule_id: str,
service: ServiceDep,
) -> RuleEnvelope:
"""单条规则详情。"""
try:
return RuleEnvelope(rule=service.get_rule(rule_id))
except GovernanceRuleError as exc:
_raise_governance_error(exc)
@router.post("/rules", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
def create_governance_rule(
request: RuleCreateRequest,
service: ServiceDep,
) -> RuleStageEnvelope:
"""创建规则 P2 出卡;确认前不写规则注册表。"""
return _stage_governance_change(
service,
"governance.rule.create",
request.model_dump(),
)
@router.put("/rules/{rule_id}", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
def update_governance_rule(
rule_id: str,
request: RuleUpdateRequest,
service: ServiceDep,
) -> RuleStageEnvelope:
"""更新规则 P2 出卡;确认前不替换规则定义。"""
return _stage_governance_change(
service,
"governance.rule.update",
{"ruleId": rule_id, "patch": request.model_dump(exclude_none=True)},
)
@router.delete("/rules/{rule_id}", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
def delete_governance_rule(
rule_id: str,
service: ServiceDep,
) -> RuleStageEnvelope:
"""删除规则 P2 出卡;确认前不移除规则。"""
return _stage_governance_change(
service,
"governance.rule.delete",
{"ruleId": rule_id},
)
@router.post("/rules/{rule_id}/enable", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
def enable_governance_rule(
rule_id: str,
service: ServiceDep,
) -> RuleStageEnvelope:
"""启用规则 P2 出卡;确认前不参与 tick。"""
return _stage_governance_change(
service,
"governance.rule.enable",
{"ruleId": rule_id},
)
@router.post("/rules/{rule_id}/disable", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
def disable_governance_rule(
rule_id: str,
service: ServiceDep,
) -> RuleStageEnvelope:
"""停用规则 P2 出卡;确认前保持原启用状态。"""
return _stage_governance_change(
service,
"governance.rule.disable",
{"ruleId": rule_id},
)
@router.get("/rules/{rule_id}/audit", response_model=RuleAuditEnvelope)
def governance_rule_audit(
rule_id: str,
service: ServiceDep,
limit: int = Query(default=50, ge=1, le=1000),
) -> RuleAuditEnvelope:
"""规则审计轨迹:target 命中该规则的全部审计事件。"""
return RuleAuditEnvelope(**service.rule_audit(rule_id, limit=limit))
@gov_router.get("/audit/stats", response_model=AuditStatsEnvelope)
def governance_audit_stats(
service: ServiceDep,
startTime: str | None = None,
endTime: str | None = None,
module: str | None = None,
action: str | None = None,
result: str | None = None,
granularity: str = Query(default="day", pattern="^(day|hour)$"),
) -> AuditStatsEnvelope:
"""审计量聚合:按模块/动作/结果/时间窗口统计(数据源复用 audit ledger)。"""
try:
return AuditStatsEnvelope(**service.audit_stats(
start_time=startTime,
end_time=endTime,
module=module,
action=action,
result=result,
granularity=granularity,
))
except GovernanceRuleError as exc:
_raise_governance_error(exc)