Summary
The codebase is pinned to starlette<0.51.0 because every templates.TemplateResponse(...) call uses the pre-1.0 positional signature (name, context), which Starlette 1.x removed in favour of (request, name, context). On Starlette 1.x the old form raises TypeError: unhashable type: 'dict' (the context dict is treated as the template name). This blocks upgrading past Starlette 0.50.0.
Starlette 0.50.0 has 5 known vulnerabilities (PYSEC-2026-161/248/249/2280/2281), all fixed in Starlette 1.x. They are currently ignored in pip-audit (.github/workflows/ci.yml, "Dependency vulnerability scan") with a per-CVE reachability justification — none are reachable in the running app today, but that is a point-in-time audit, not a fix.
This is not blocked upstream: FastAPI ≥ 0.135 already requires starlette>=0.46.0 with no upper bound, so it accepts Starlette 1.x. The only blocker is our own TemplateResponse usage.
Scope
- 109
TemplateResponse call sites across 33 files (ui/routes/*, ui/app.py, and ~15 core plugins/*/admin/routes.py + portal/routes.py).
- All are the same multiline shape;
request is available in every case (in the context dict or via a get_template_context(request, ...) / get_ctx(request, ...) helper). No call uses status_code=/headers= kwargs.
- The transform is mechanical: insert
request as the first positional argument.
templates.TemplateResponse( templates.TemplateResponse(
"name.html", → request,
{"request": request, ...}, "name.html",
) {"request": request, ...},
)
Risk — this is a Starlette major upgrade, not just a rename
The 109 call sites are the known break. A 0.50 → 1.x major bump can carry others, and must be regression-tested across every admin/portal/plugin page. Specific things to check:
ui/app.py monkeypatches anyio internals (_anyio_asyncio.CancelScope._deliver_cancellation, the CPU-leak fix). A Starlette 1.x bump pulls a newer anyio that may break this patch.
- Middleware stack (CSRF, Session, ProxyHeaders, GZip, context) and custom static mounts.
- Any other removed/renamed Starlette 1.0 APIs.
Acceptance criteria
Interim state
starlette>=0.40.0,<0.51.0 pinned in pyproject.toml.
- The 5 CVEs ignored in CI with a documented reachability audit (unmounted
OAuth2BearerMiddleware is the only request.url.path security check; no request.url.hostname usage; no configured form limits; no HTTPEndpoint subclasses; Linux-only deploy).
Summary
The codebase is pinned to
starlette<0.51.0because everytemplates.TemplateResponse(...)call uses the pre-1.0 positional signature(name, context), which Starlette 1.x removed in favour of(request, name, context). On Starlette 1.x the old form raisesTypeError: unhashable type: 'dict'(the context dict is treated as the template name). This blocks upgrading past Starlette 0.50.0.Starlette 0.50.0 has 5 known vulnerabilities (PYSEC-2026-161/248/249/2280/2281), all fixed in Starlette 1.x. They are currently ignored in
pip-audit(.github/workflows/ci.yml, "Dependency vulnerability scan") with a per-CVE reachability justification — none are reachable in the running app today, but that is a point-in-time audit, not a fix.This is not blocked upstream: FastAPI ≥ 0.135 already requires
starlette>=0.46.0with no upper bound, so it accepts Starlette 1.x. The only blocker is our ownTemplateResponseusage.Scope
TemplateResponsecall sites across 33 files (ui/routes/*,ui/app.py, and ~15 coreplugins/*/admin/routes.py+portal/routes.py).requestis available in every case (in the context dict or via aget_template_context(request, ...)/get_ctx(request, ...)helper). No call usesstatus_code=/headers=kwargs.requestas the first positional argument.Risk — this is a Starlette major upgrade, not just a rename
The 109 call sites are the known break. A 0.50 → 1.x major bump can carry others, and must be regression-tested across every admin/portal/plugin page. Specific things to check:
ui/app.pymonkeypatches anyio internals (_anyio_asyncio.CancelScope._deliver_cancellation, the CPU-leak fix). A Starlette 1.x bump pulls a newer anyio that may break this patch.Acceptance criteria
TemplateResponsecalls to the(request, name, context)signature.ui/app.pystill applies (or replace it)./meportal, and all plugin admin pages.--ignore-vulnflags from.github/workflows/ci.yml.Interim state
starlette>=0.40.0,<0.51.0pinned inpyproject.toml.OAuth2BearerMiddlewareis the onlyrequest.url.pathsecurity check; norequest.url.hostnameusage; no configured form limits; noHTTPEndpointsubclasses; Linux-only deploy).