Problem
PATCH /api/transcriber/providers changes the active cloud ASR provider for the whole backend process, but the UI presents it as a personal preference, and any signed-in user can call it.
# autobot-backend/transcriber/routes/providers.py
@router.patch("/providers")
async def set_provider(body: SetProviderRequest) -> Dict[str, Any]:
"""Set the active cloud ASR provider.
Persistence limitation: this is in-process only and is lost on restart.
"""
set_active_provider(body.provider)
set_active_provider mutates module-level state, so one user's choice becomes every user's provider until the next change or restart. The caller is src/components/transcriber/AsrProviderSelector.vue:60 (await api.setAsrProvider(next)), which carries no admin guard and sits in the ordinary transcriber UI.
So the same action is a per-user control in the frontend and a global setting in the backend. A user choosing a provider for themselves silently changes it for everyone, including which third party receives everyone's audio.
What changed recently
Until #15758's fix this route was reachable with no credentials at all — the transcriber sub-routers carried no dependency. That fix gates every transcriber route behind authentication, so anonymous callers can no longer change the provider. What remains is the authorization question below, which is a decision rather than a bug fix.
Why this is not simply "make it admin-only"
Admin-gating the route is the obvious fix and it would break AsrProviderSelector for every non-admin user who uses it today. The two coherent designs are:
- Global, admin-only — the provider is a system setting. Gate
PATCH with check_admin_permission, and move or hide the selector for non-admins.
- Per-user — the provider is a user preference. Store it against the caller and resolve it per request, so one user's choice cannot affect another's.
Which is right depends on whether provider choice is meant to be a deployment decision (cost, data-processing agreements, where audio is sent) or an individual one.
Acceptance criteria
Found while gating the transcriber routes for #15758.
Problem
PATCH /api/transcriber/providerschanges the active cloud ASR provider for the whole backend process, but the UI presents it as a personal preference, and any signed-in user can call it.set_active_providermutates module-level state, so one user's choice becomes every user's provider until the next change or restart. The caller issrc/components/transcriber/AsrProviderSelector.vue:60(await api.setAsrProvider(next)), which carries no admin guard and sits in the ordinary transcriber UI.So the same action is a per-user control in the frontend and a global setting in the backend. A user choosing a provider for themselves silently changes it for everyone, including which third party receives everyone's audio.
What changed recently
Until #15758's fix this route was reachable with no credentials at all — the transcriber sub-routers carried no dependency. That fix gates every transcriber route behind authentication, so anonymous callers can no longer change the provider. What remains is the authorization question below, which is a decision rather than a bug fix.
Why this is not simply "make it admin-only"
Admin-gating the route is the obvious fix and it would break
AsrProviderSelectorfor every non-admin user who uses it today. The two coherent designs are:PATCHwithcheck_admin_permission, and move or hide the selector for non-admins.Which is right depends on whether provider choice is meant to be a deployment decision (cost, data-processing agreements, where audio is sent) or an individual one.
Acceptance criteria
AsrProviderSelector.vueagree on that model — a per-user control must not mutate global state, and a global setting must not be offered to users who cannot change it.PATCHdoes not change the provider another user's request resolves to (per-user), or returns 403 (global-admin).Found while gating the transcriber routes for #15758.