Skip to content

symfonic.platform.transport_procedure_publish

transport_procedure_publish

Native HTTP adapter for platform publication review.

Own router (not part of mount_data_routes): both platform transport files sit at the module-size budget, and publication review is a distinct concern from the store reads those routes serve. Mounted by the scaffold next to the agent router; see the publish_service argument.

create_procedure_publish_router

create_procedure_publish_router(*, service: ProceduralPublishService, scope_resolver: Any = None) -> Any

Routes for proposing, reviewing, and revoking platform publications.

Parameters:

Name Type Description Default
service ProceduralPublishService

the publish service (owns graph + scrubber).

required
scope_resolver Any

authenticating resolver; None accepts the tenant header on trust (unit tests only, never production).

None
Source code in src/symfonic/platform/transport_procedure_publish.py
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