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:
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user