def mount_procedure_update_route(
router: Any, *, procedures: Any, resolve: Any, scope_resolver: Any = None,
) -> None:
"""Mount PATCH when a procedure administration port is configured."""
from fastapi import Header, HTTPException, Request
@router.patch("/procedures/{node_id}")
async def update_procedure(
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]:
if scope_resolver is None:
from symfonic.agent.fastapi.identity import request_user_id
scope = await resolve(x_tenant_id, authorization)
user_id = request_user_id(request)
else:
verified = await verified_principal(
scope_resolver, x_tenant_id, authorization, request=request,
)
scope = verified.scope
user_id = str(verified.principal_id)
body = await request.json()
# Outside the catch-all: a v1-declared router's typed refusal is a 501.
v0_scope = memory_scope(scope)
try:
row = await procedures.amend_procedure(
v0_scope, node_id,
steps=body.get("steps"), properties=body.get("properties"),
active=body.get("active"),
)
except Exception as unavailable: # noqa: BLE001 - stored data stays private
logger.error("Procedure update failed")
raise HTTPException(500, "Internal server error") from unavailable
if row is None:
raise HTTPException(404, f"Node {node_id} not found")
fields = sorted(field for field in ("steps", "properties", "active") if field in body)
emit_audit_event(AuditLogEntry(
tenant_id=str(scope.tenant_id), user_id=user_id,
action="update_procedure", resource_type="procedure", resource_id=node_id,
metadata={"fields": fields},
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
))
return row