chore(skills): docs + ruff format for skill-management-console
Tests / Test passed: 855

Documents Skill Management in CLOUD_DEPLOYMENT.md (cloud-skill store,
per-host entitlement, incremental sync, local authoring/override,
inventory report, skills:admin scope) and applies ruff check/format to
all touched modules. All tasks complete; full non-integration suite
green (593 passed) and openspec validate --strict passes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 08:10:20 +08:00
co-authored by Claude Opus 4.6
parent fd0ea3a066
commit f8054cb58c
9 changed files with 83 additions and 53 deletions
+37 -11
View File
@@ -8,6 +8,7 @@ Two routers:
heartbeat/planner-decision) endpoints an agent uses to pull incremental
skill deltas and report its local-skill inventory.
"""
from __future__ import annotations
import json
@@ -16,7 +17,12 @@ from uuid import uuid4
from fastapi import APIRouter, HTTPException, Query, Request, status
from cloud.auth import AuthProvider, HostAuthorizationError, Principal, SKILLS_ADMIN_SCOPE
from cloud.auth import (
AuthProvider,
HostAuthorizationError,
Principal,
SKILLS_ADMIN_SCOPE,
)
from cloud.observability import current_correlation_id
from cloud.skills import (
CloudSkillConflictError,
@@ -86,7 +92,9 @@ def create_skill_management_router(
authorize(request)
skill = service.get_skill(skill_id)
if skill is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Skill not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Skill not found"
)
return _skill_response(skill)
@router.post(
@@ -112,9 +120,13 @@ def create_skill_management_router(
now=utc_now(),
)
except CloudSkillValidationError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
) from exc
except CloudSkillConflictError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
) from exc
_audit(repository, principal, skill.id, "cloud_skill_create")
return _skill_response(skill)
@@ -139,9 +151,13 @@ def create_skill_management_router(
now=utc_now(),
)
except CloudSkillValidationError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Skill not found") from exc
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Skill not found"
) from exc
except CloudSkillConflictError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
) from exc
_audit(repository, principal, skill.id, "cloud_skill_update")
return _skill_response(skill)
@@ -152,14 +168,18 @@ def create_skill_management_router(
try:
service.delete_skill(skill_id)
except KeyError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Skill not found") from exc
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Skill not found"
) from exc
_audit(repository, principal, skill_id, "cloud_skill_delete")
@router.get(
"/skills/{skill_id}/entitlements",
response_model=CloudSkillEntitlementListResponse,
)
def list_entitlements(skill_id: str, request: Request) -> CloudSkillEntitlementListResponse:
def list_entitlements(
skill_id: str, request: Request
) -> CloudSkillEntitlementListResponse:
authorize(request)
return CloudSkillEntitlementListResponse(
skill_id=skill_id,
@@ -176,7 +196,9 @@ def create_skill_management_router(
try:
service.grant_entitlement(skill_id, host_id, now=utc_now())
except CloudSkillValidationError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
) from exc
_audit(repository, principal, skill_id, "cloud_skill_entitlement_grant")
@router.delete(
@@ -193,7 +215,9 @@ def create_skill_management_router(
"/hosts/{host_id}/skill-inventory",
response_model=HostSkillInventoryResponse,
)
def get_host_inventory(host_id: str, request: Request) -> HostSkillInventoryResponse:
def get_host_inventory(
host_id: str, request: Request
) -> HostSkillInventoryResponse:
authorize(request)
entry = service.get_host_inventory(host_id)
if entry is None:
@@ -231,7 +255,9 @@ def create_skill_host_router(
try:
principal.require_host(host_id)
except HostAuthorizationError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)
) from exc
@router.get("/hosts/{host_id}/skills/sync", response_model=CloudSkillSyncResponse)
def sync_skills(
+3 -5
View File
@@ -7,14 +7,14 @@ here: any change that affects a host's visible skill set advances that host's
monotonic ``entitlement_version`` and is recorded in a per-host changelog so
:meth:`fetch_host_delta` can serve a correct incremental delta.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Literal
from uuid import uuid4
from core.models import utc_now
SkillKind = Literal["knowledge", "flow_template"]
SUPPORTED_SKILL_KINDS = frozenset({"knowledge", "flow_template"})
@@ -86,9 +86,7 @@ def validate_skill_input(
) -> tuple[str, str, SkillKind]:
display = " ".join(name.split())
if not display or len(display) > 200:
raise CloudSkillValidationError(
"Skill name must contain 1 to 200 characters"
)
raise CloudSkillValidationError("Skill name must contain 1 to 200 characters")
if kind not in SUPPORTED_SKILL_KINDS:
raise CloudSkillValidationError("Skill kind must be knowledge or flow_template")
if len(description) > 2000: