From 4303538686674cf3167b5e16ac641e2de4e74827 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 7 Sep 2026 12:54:41 +0200 Subject: [PATCH] fix(frontend): block creation when the username policy is unknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no safe default. `create_workspace` refuses a username on an instance that automates them and requires one on an instance that does not (`workspaces.rs:5820`), so a client that cannot read the setting has two request shapes available and the server rejects both. Last round's "ask for one" was as wrong as the "automated" guess it replaced. So the loader reports the failure instead of inventing an answer, and the form says so: Create stays disabled, with a line explaining why and a link to try again. Verified in the browser both ways — unreadable policy disables Create and shows the message, a healthy load prefills the name and enables it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 --- .../SimpleCreateWorkspace.svelte | 32 ++++++++++++------- frontend/src/lib/workspaceCreation.test.ts | 9 +++--- frontend/src/lib/workspaceCreation.ts | 23 ++++++------- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/frontend/src/lib/components/workspaceSettings/SimpleCreateWorkspace.svelte b/frontend/src/lib/components/workspaceSettings/SimpleCreateWorkspace.svelte index 35bbaf4903..49e7181e9a 100644 --- a/frontend/src/lib/components/workspaceSettings/SimpleCreateWorkspace.svelte +++ b/frontend/src/lib/components/workspaceSettings/SimpleCreateWorkspace.svelte @@ -41,19 +41,21 @@ let automateUsername = $state(true) let suggestedUsername = $state(undefined) /** - * Whether the policy is settled, which is what this form may not submit without: - * `automateUsername` starts at the common case, and posting that guess to an instance - * that derives no usernames sends none where one is required. `loadUsernamePolicy` - * always answers — "ask for one" when it cannot read the setting — so this turns true - * on a known answer rather than on the attempt finishing. + * Whether the username policy is known, which is what this form may not submit without. + * `create_workspace` refuses a username on an instance that automates them and requires + * one on an instance that does not, so a client that has not read the setting cannot + * pick a request shape — there is no safe default to fall back to, only two shapes the + * server rejects. Unknown therefore blocks Create and says why, with a retry. */ let policyLoaded = $state(false) + let policyFailed = $state(false) /** Someone typed while the prefill was in flight; their name wins over the suggestion. */ let nameEdited = false async function load() { // Settled apart: the policy decides whether this form may submit at all, the suggested // name is cosmetic, and neither failure should decide the other. + policyFailed = false const [me, policy] = await Promise.allSettled([ UserService.globalWhoami(), loadUsernamePolicy() @@ -64,12 +66,14 @@ ? defaultWorkspaceName(me.value.name, me.value.email) : 'My workspace' } - // `loadUsernamePolicy` answers "ask for one" rather than rejecting when the setting - // cannot be read, so the fallback here is the same answer by another route. - const answer = policy.status === 'fulfilled' ? policy.value : { automate: false } - automateUsername = answer.automate - suggestedUsername = answer.suggested - if (!answer.automate && !answer.suggested) advanced = true + if (policy.status === 'rejected') { + console.error('Could not read the username policy:', policy.reason) + policyFailed = true + return + } + automateUsername = policy.value.automate + suggestedUsername = policy.value.suggested + if (!policy.value.automate && !policy.value.suggested) advanced = true policyLoaded = true } void load() @@ -175,6 +179,12 @@ {#if problem && name.trim()} {problem} {/if} + {#if policyFailed} + + This instance's settings could not be read, so a workspace cannot be created yet. + + + {/if}
diff --git a/frontend/src/lib/workspaceCreation.test.ts b/frontend/src/lib/workspaceCreation.test.ts index a79e0d0ee5..975d1599b5 100644 --- a/frontend/src/lib/workspaceCreation.test.ts +++ b/frontend/src/lib/workspaceCreation.test.ts @@ -71,11 +71,12 @@ describe('usernameFromName', () => { }) describe('loadUsernamePolicy', () => { - // Fail-closed matters because a caller told "automated" hides its username field and - // posts none: an instance that derives none refuses that, with nowhere to supply one. - it('asks for a username when the setting cannot be read', async () => { + // Neither default is safe — `create_workspace` refuses a username on an automating + // instance and requires one otherwise — so an unreadable setting has to reach the caller + // as a failure rather than as a guess it cannot tell apart from an answer. + it('rejects rather than guessing when the setting cannot be read', async () => { getGlobal.mockRejectedValueOnce(new Error('502')) - expect(await loadUsernamePolicy()).toEqual({ automate: false }) + await expect(loadUsernamePolicy()).rejects.toThrow('502') }) it('automates when the setting says so, and when it is unset', async () => { diff --git a/frontend/src/lib/workspaceCreation.ts b/frontend/src/lib/workspaceCreation.ts index b10955135d..e4a9045f42 100644 --- a/frontend/src/lib/workspaceCreation.ts +++ b/frontend/src/lib/workspaceCreation.ts @@ -68,20 +68,15 @@ export function usernameFromName(name: string): string | undefined { * requires one when it does not, so the field only exists in the second case. */ export async function loadUsernamePolicy(): Promise { - let automate: boolean - try { - automate = - ((await SettingService.getGlobal({ - key: 'automate_username_creation' - })) as boolean | null) ?? true - } catch (error) { - // Unreadable is not "automated". A caller told yes hides its username field and posts - // none, which an instance that derives none then refuses — with nowhere on screen to - // supply what it wanted. Answering no asks for one, which is right either way: an - // instance that does automate ignores a username it was sent. - console.error('Could not read the username policy; asking for one instead:', error) - return { automate: false } - } + // Rejects rather than defaulting when the setting cannot be read, because neither + // default is safe: `create_workspace` refuses a username on an instance that automates + // them and requires one on an instance that does not (`workspaces.rs:5820`). A caller + // that cannot read this cannot pick a request shape, and must say so instead of posting + // one of the two the server rejects. + const automate = + ((await SettingService.getGlobal({ + key: 'automate_username_creation' + })) as boolean | null) ?? true if (automate) return { automate: true } try { const me = await UserService.globalWhoami()