def create_procedure_publish_router(
*, service: ProceduralPublishService, scope_resolver: Any = None,
) -> Any:
"""Routes for proposing, reviewing, and revoking platform publications.
Args:
service: the publish service (owns graph + scrubber).
scope_resolver: authenticating resolver; None accepts the tenant
header on trust (unit tests only, never production).
"""
from fastapi import APIRouter, Header, HTTPException, Request
router = APIRouter(prefix="/api/v1")
async def _verified(request, x_tenant_id, authorization):
if scope_resolver is None:
from symfonic.agent.fastapi.identity import request_user_id
return SimpleNamespace(
scope=await _resolve_untrusted(x_tenant_id),
principal_id=request_user_id(request),
is_admin=False,
)
return await verified_principal(
scope_resolver, x_tenant_id, authorization, request=request,
)
@router.post("/procedures/{node_id}/publish")
async def propose_publication(
node_id: str,
request: Request,
x_tenant_id: str | None = Header(default=None), # noqa: B008
authorization: str | None = Header(default=None), # noqa: B008
) -> dict[str, Any]:
"""Propose an approved skill for the platform corpus (any member)."""
verified = await _verified(request, x_tenant_id, authorization)
_check_node_id(node_id)
_audit(
request, verified, action="propose_publication",
resource_id=node_id, metadata={},
)
try:
return await service.propose_publication(
memory_scope(verified.scope), node_id,
proposer=str(verified.principal_id),
)
except PublicationNotFound as missing:
raise HTTPException(404, str(missing)) from missing
except PublicationRefused as refused:
raise HTTPException(422, str(refused)) from refused
except Exception as unavailable: # noqa: BLE001 - stored data stays private
logger.error("Procedure publication proposal failed")
raise HTTPException(500, "Internal server error") from unavailable
@router.post("/procedures/proposals/{proposal_id}/review")
async def review_publication(
proposal_id: str,
request: Request,
x_tenant_id: str | None = Header(default=None), # noqa: B008
authorization: str | None = Header(default=None), # noqa: B008
) -> dict[str, Any]:
"""Approve or refuse a proposal (org owner of the source tenant)."""
verified = await _verified(request, x_tenant_id, authorization)
_require_admin(verified)
_check_node_id(proposal_id)
body = await request.json()
_audit(
request, verified, action="review_publication",
resource_id=proposal_id,
metadata={"decision": body.get("decision", "")},
)
try:
return await service.review_publication(
memory_scope(verified.scope), proposal_id,
decision=body.get("decision", ""),
reviewer=str(verified.principal_id),
reason=body.get("reason", "") or "",
)
except PublicationNotFound as missing:
raise HTTPException(404, str(missing)) from missing
except PublicationRefused as refused:
raise HTTPException(422, str(refused)) from refused
except PublicationConflict as conflict:
raise HTTPException(409, str(conflict)) from conflict
except Exception as unavailable: # noqa: BLE001 - stored data stays private
logger.error("Procedure publication review failed")
raise HTTPException(500, "Internal server error") from unavailable
@router.get("/procedures/proposals/pending")
async def list_pending_proposals(
request: Request,
x_tenant_id: str | None = Header(default=None), # noqa: B008
authorization: str | None = Header(default=None), # noqa: B008
) -> list[dict[str, Any]]:
"""The caller's tenant review queue (owners only)."""
verified = await _verified(request, x_tenant_id, authorization)
_require_admin(verified)
try:
return await service.list_proposals(memory_scope(verified.scope))
except Exception as unavailable: # noqa: BLE001 - stored data stays private
logger.error("Procedure proposal listing failed")
raise HTTPException(500, "Internal server error") from unavailable
@router.post("/procedures/published/{node_id}/revoke")
async def revoke_publication(
node_id: str,
request: Request,
x_tenant_id: str | None = Header(default=None), # noqa: B008
authorization: str | None = Header(default=None), # noqa: B008
) -> dict[str, Any]:
"""Revoke a platform copy (publishing org owner, or platform admin
calling with that org's tenant header)."""
verified = await _verified(request, x_tenant_id, authorization)
_require_admin(verified)
_check_node_id(node_id)
_audit(
request, verified, action="revoke_publication",
resource_id=node_id, metadata={},
)
try:
return await service.revoke_publication(
memory_scope(verified.scope), node_id,
actor=str(verified.principal_id),
)
except PublicationNotFound as missing:
raise HTTPException(404, str(missing)) from missing
except PublicationForbidden as forbidden:
raise HTTPException(403, str(forbidden)) from forbidden
except Exception as unavailable: # noqa: BLE001 - stored data stays private
logger.error("Procedure publication revoke failed")
raise HTTPException(500, "Internal server error") from unavailable
@router.get("/procedures/published")
async def list_published_procedures(
request: Request,
x_tenant_id: str | None = Header(default=None), # noqa: B008
authorization: str | None = Header(default=None), # noqa: B008
) -> list[dict[str, Any]]:
"""Active platform skills (any verified member of any tenant)."""
await _verified(request, x_tenant_id, authorization)
try:
return await service.list_published()
except Exception as unavailable: # noqa: BLE001 - stored data stays private
logger.error("Published procedure listing failed")
raise HTTPException(500, "Internal server error") from unavailable
return router