feat(cloud): enforce host governance budgets
Tests / Test passed: 662

This commit is contained in:
2026-07-13 22:56:31 +08:00
parent 2cd314b183
commit b4803f90e6
28 changed files with 1311 additions and 14 deletions
@@ -5,10 +5,12 @@ from uuid import uuid4
from fastapi import APIRouter, HTTPException, Request, status
from cloud.auth import AuthProvider, GOVERNANCE_ADMIN_SCOPE, GOVERNANCE_READ_SCOPE
from cloud.governance import GovernancePolicyConflictError
from cloud.observability import current_correlation_id
from cloud.sdk.models import (
HostGovernancePolicyRequest,
HostGovernancePolicyResponse,
HostTokenUsageSummaryResponse,
UserSubmissionPolicyRequest,
UserSubmissionPolicyResponse,
)
@@ -79,10 +81,13 @@ def create_governance_router(*, repository, auth_provider: AuthProvider) -> APIR
if payload.allowed_device_targets is not None
else None
),
expected_revision=payload.expected_revision,
updated_at=utc_now(),
)
except KeyError as exc:
raise HTTPException(status_code=404, detail="user not found") from exc
except GovernancePolicyConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
_audit(repository, principal.id, user_id, "user_submission_policy_update")
return UserSubmissionPolicyResponse(
user_id=policy.user_id,
@@ -111,6 +116,27 @@ def create_governance_router(*, repository, auth_provider: AuthProvider) -> APIR
raise HTTPException(status_code=404, detail="Host policy not found")
return _host_response(policy)
@router.get(
"/hosts/{host_id}/token-usage",
response_model=HostTokenUsageSummaryResponse,
)
def get_host_token_usage(
host_id: str, request: Request
) -> HostTokenUsageSummaryResponse:
authorize(request, GOVERNANCE_READ_SCOPE)
now = utc_now()
summary = repository.get_host_token_usage_summary(
host_id=host_id, usage_day=now.date().isoformat(), now=now
)
return HostTokenUsageSummaryResponse(
host_id=summary.host_id,
usage_day=summary.usage_day,
daily_token_budget=summary.daily_token_budget,
used_tokens=summary.used_tokens,
reserved_tokens=summary.reserved_tokens,
remaining_tokens=summary.remaining_tokens,
)
@router.put(
"/hosts/{host_id}/governance-policy",
response_model=HostGovernancePolicyResponse,
@@ -127,10 +153,13 @@ def create_governance_router(*, repository, auth_provider: AuthProvider) -> APIR
self_submission_enabled=payload.self_submission_enabled,
max_active_tasks=payload.max_active_tasks,
daily_token_budget=payload.daily_token_budget,
expected_revision=payload.expected_revision,
updated_at=utc_now(),
)
except KeyError as exc:
raise HTTPException(status_code=404, detail="Host not found") from exc
except GovernancePolicyConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
_audit(repository, principal.id, host_id, "host_governance_policy_update")
return _host_response(policy)
+19 -2
View File
@@ -162,11 +162,15 @@ class UserSubmissionPolicyRequest(BaseModel):
submission_enabled: bool = True
allowed_host_ids: list[str] | None = None
allowed_device_targets: list[DeviceTargetModel] | None = None
expected_revision: int | None = Field(default=None, ge=0)
class UserSubmissionPolicyResponse(UserSubmissionPolicyRequest):
class UserSubmissionPolicyResponse(BaseModel):
user_id: str
revision: int
submission_enabled: bool
allowed_host_ids: list[str] | None = None
allowed_device_targets: list[DeviceTargetModel] | None = None
updated_at: datetime
@@ -174,9 +178,22 @@ class HostGovernancePolicyRequest(BaseModel):
self_submission_enabled: bool = True
max_active_tasks: int | None = Field(default=None, ge=1)
daily_token_budget: int | None = Field(default=None, ge=1)
expected_revision: int | None = Field(default=None, ge=0)
class HostGovernancePolicyResponse(HostGovernancePolicyRequest):
class HostGovernancePolicyResponse(BaseModel):
host_id: str
revision: int
self_submission_enabled: bool
max_active_tasks: int | None = None
daily_token_budget: int | None = None
updated_at: datetime
class HostTokenUsageSummaryResponse(BaseModel):
host_id: str
usage_day: str
daily_token_budget: int | None = None
used_tokens: int
reserved_tokens: int
remaining_tokens: int | None = None