Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514const std = @import("std");const config_runtime = @import("../config/config_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const host = @import("../hosts/host.zig");const runtime_profile = @import("../hosts/runtime_profile.zig");const host_target = @import("../hosts/target.zig");const io_mod = @import("../shared/io.zig");const credentials = @import("../auth/credentials.zig");const auth_runtime = @import("../auth/auth_runtime.zig");const login_flow = @import("../auth/login_flow.zig");const chatgpt_oauth = @import("../auth/chatgpt_oauth.zig");const grok_oauth = @import("../auth/grok_oauth.zig");const provider_catalog = @import("../auth/provider_catalog.zig");const auth_transition = @import("../auth/auth_transition.zig");const model_provider = @import("../config/model_provider.zig");const model_catalog = @import("../gateway/model_catalog.zig");const provider_runtime = @import("provider_runtime.zig");const picker_state = @import("../input/picker_state.zig");const provider_picker_runtime = @import("provider_picker_runtime.zig");const types = @import("../shared/types.zig");
fn oauthAuthEnabled(comptime App: type) bool { return runtime_profile.allows(App, .native_auth) or runtime_profile.allows(App, .js_host_auth);}
const ProviderSwitchDecision = auth_transition.ProviderSwitchDecision;const ProviderSwitchIntent = auth_transition.ProviderSwitchIntent;const decideProviderSwitch = auth_transition.decideProviderSwitch;const provider_busy_message = "Provider switching is unavailable until active and queued work finishes.";
fn providerFailureMessage( intent: ProviderSwitchIntent, ordinary: []const u8, after_oauth: []const u8,) []const u8 { return if (intent == .post_oauth) after_oauth else ordinary;}
fn selectCatalogModel( entries: []const model_catalog.ModelCatalogEntry, primary: ?[]const u8, secondary: ?[]const u8,) ?[]const u8 { for ([_]?[]const u8{ primary, secondary }) |maybe_candidate| { const candidate = maybe_candidate orelse continue; for (entries) |entry| { if (std.mem.eql(u8, candidate, entry.id)) return entry.id; } } return if (entries.len > 0) entries[0].id else null;}
fn optionalGatewayApiKey(credential: anytype) ?[]const u8 { if (comptime @typeInfo(@TypeOf(credential.api_key)) == .optional) { return credential.api_key; } return credential.api_key;}
fn gatewayCredentialSource(credential: anytype) ?credentials.Source { if (comptime @hasField(@TypeOf(credential), "source")) { return credential.source; } return null;}
fn hostManagesAuth(app: anytype) bool { if (comptime @hasDecl(@TypeOf(app.auth), "isHostManaged")) { return app.auth.isHostManaged(); } return false;}
const TeamCatalogValidation = union(enum) { rejected, accepted: ?[]u8,
fn deinit(self: *TeamCatalogValidation, alloc: std.mem.Allocator) void { switch (self.*) { .accepted => |model| if (model) |owned| alloc.free(owned), .rejected => {}, } self.* = undefined; }};
pub const PendingPromptCredentialReadiness = enum { pending, current, rejected,};
pub fn Runtime(comptime App: type) type { return struct { fn compactionOwnsCredentialFeedback(app: *const App) bool { return if (comptime @hasField(App, "submission")) app.submission.compaction_pending else false; }
fn ensurePromptCredential(app: *App) !bool { if (comptime @hasField(App, "provider_selection")) { if (app.provider_selection.model_requests_blocked) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Repair profile settings and restart fx before sending a message.", }); return false; } } if (try rejectPendingPreparation(app)) return false; if (comptime provider_runtime.supported(App) and @hasDecl(@TypeOf(app.auth), "selectForProvider")) { const provider = provider_runtime.provider(app); if (provider == .configured or !model_provider.authorizesCredential(provider, app.auth.credentialSource())) { const selection = selectProviderCredential(app, provider) catch |err| { if (err == error.OutOfMemory) return err; debug_trace.logf("auth", "prompt credential preference load failed err={s}", .{@errorName(err)}); if (compactionOwnsCredentialFeedback(app)) return false; try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Could not load authentication settings. Check user settings, then press enter to retry.", }); return false; }; switch (selection) { .selected => applyCredentialChange(app, true), .unchanged => {}, .failed => |failure| return recoverCredentialFailure(app, failure.source, failure.err), .missing => return missingPromptCredential(app, provider), } } } if (app.auth.credentialSource() != null) return true; return missingPromptCredential(app, .gateway); }
fn selectProviderCredential(app: *App, provider: model_provider.ProviderId) !auth_runtime.ProviderCredentialSelection { if (hostManagesAuth(app) or (provider != .configured and model_provider.authorizesCredential(provider, app.auth.credentialSource()))) return .unchanged; var preferred: ?credentials.Source = null; if (provider == .gateway and !host_target.is_wasm) { var settings = try config_runtime.loadMergedSettings(app.alloc, app.workspace_root); defer settings.deinit(app.alloc); preferred = settings.credential_source; } return app.auth.selectForProvider(app.alloc, provider, preferred); }
pub fn restoreSessionCredential(app: *App, previous_provider: model_provider.ProviderId) !void { // Hydration can run before App.init returns, so it must not start background tasks. const provider = provider_runtime.provider(app); const provider_changed = !previous_provider.same_authority(provider); if (provider_changed) { app.auth.cancelPromptCredentialRefresh(); app.model_cache.resetForProviderChange(); } if (comptime host_target.is_wasm) return; if (hostManagesAuth(app)) return; const selection = selectProviderCredential(app, provider) catch |err| { if (err == error.OutOfMemory) return err; debug_trace.logf("auth", "resumed credential preference load failed err={s}", .{@errorName(err)}); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Could not load authentication settings for the resumed session. Check user settings and retry.", }, true); app.shell.render_requests.request(.footer); return; }; switch (selection) { .selected => app.model_cache.reset(), .unchanged => {}, .missing => try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = switch (provider) { .gateway => credentials.missing_interactive_credential_message, .codex => credentials.missing_chatgpt_interactive_credential_message, .grok => credentials.missing_grok_interactive_credential_message, .configured => "Configured provider authentication is unavailable. Check settings.json and its environment variable.", }, }, true), .failed => |failure| { const classified = auth_runtime.classifyCredentialFailure(failure.source, failure.err); const message = if (auth_runtime.preparationError(classified)) |err| auth_runtime.preparationFailureNotice(err).? else "Authentication is unavailable. Run /provider to repair this source."; debug_trace.logf("auth", "resumed credential unavailable source={t} err={s}", .{ failure.source, @errorName(failure.err) }); const body = try std.fmt.allocPrint(app.alloc, "{s}: {s}", .{ credentials.sourceLabel(failure.source), message }); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = body }, true); }, } app.shell.render_requests.request(.footer); }
fn missingPromptCredential(app: *App, provider: model_provider.ProviderId) !bool { if (provider != .gateway) { if (compactionOwnsCredentialFeedback(app)) return false; try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = if (provider == .configured) "Configured provider authentication is unavailable. Check settings.json and its environment variable." else if (provider == .grok) credentials.missing_grok_interactive_credential_message else credentials.missing_chatgpt_interactive_credential_message, }, true); app.shell.render_requests.request(.footer); return false; }
const auth_view = app.auth.view(); if (auth_view.onboarding_skipped) { if (compactionOwnsCredentialFeedback(app)) return false; try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = credentials.missing_interactive_credential_message, }, true); } else if (!app.auth.pickerView().active) { try app.auth.refreshSourceInventory(app.alloc); app.auth.openOnboardingPicker(app.alloc); } app.shell.render_requests.request(.footer); return false; }
pub fn runLoginCommand(app: *App) !void { if (hostManagesAuth(app)) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .neutral, .body = credentials.host_managed_auth_message }); return; } if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Set FX_API_KEY through createFxTerminal() to authenticate this WASM session.", }, true); return; } if (comptime host_target.is_wasm) { try beginSignIn(app, false); return; } try beginProviderPickerInventoryRefresh(app, .provider_picker_login); }
pub fn runLogoutCommand(app: *App, target: []const u8) !void { if (try rejectPendingPreparation(app)) return; if (hostManagesAuth(app)) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .neutral, .body = credentials.host_managed_auth_message }); return; } if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Authentication is owned by the embedding SDK for this WASM session.", }, true); return; } const requested_provider = if (std.mem.trim(u8, target, " \t\r\n").len == 0) null else provider_catalog.parse(std.mem.trim(u8, target, " \t\r\n")) orelse { try writeAuthNotice(app, .{ .topic = "", .tone = .warning, .body = "usage: /logout [vercel|codex|grok]", }); return; }; try app.flushBeforeBlockingExternalWork(); const selected_provider: model_provider.ProviderId = if (comptime provider_runtime.supported(App)) provider_runtime.provider(app) else .gateway; const provider_inventory = if (comptime @hasDecl(@TypeOf(app.auth), "pickerView")) inventory: { try app.auth.refreshSourceInventory(app.alloc); break :inventory app.auth.pickerView().available_sources; } else @as(auth_runtime.SourceSet, .empty); const logout_provider = auth_transition.decideLogoutProvider(.{ .requested = requested_provider, .selected = selected_provider, .active_source = app.auth.credentialSource(), .available_sources = provider_inventory, }); const hold_turn_start = logout_provider.eql(selected_provider) and logout_provider != .gateway; if (hold_turn_start and (app.stream.active or !app.worker.tryHoldTurnStart())) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = "Sign out is unavailable until active and queued work finishes.", }); return; } defer if (hold_turn_start) app.worker.releaseTurnStartHold(); if (logout_provider == .grok) { const outcome = grok_oauth.logout(app.alloc, app.auth.oauthTransport()) catch { try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Could not durably sign out of Grok. The current source is unchanged.", }); return; }; const changed = if (comptime @hasDecl(@TypeOf(app.auth), "reconcileAfterGrokLogout")) try app.auth.reconcileAfterGrokLogout(app.alloc) else false; applyCredentialChange(app, changed); try writeAuthNotice(app, switch (outcome.deletion) { .deleted => .{ .topic = "auth", .tone = .neutral, .body = "Signed out of Grok." }, .missing => .{ .topic = "auth", .tone = .neutral, .body = "No Grok login session found." }, .deleted_not_durable => .{ .topic = "auth", .tone = .warning, .body = "Signed out of Grok, but could not confirm the profile directory update." }, }); if (outcome.revocation_failed) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = "The local Grok session was removed, but remote revocation could not be confirmed.", }); } try reconcileSubscriptionLogout(app, .grok); return; } if (logout_provider == .codex) { const outcome = chatgpt_oauth.logout() catch { try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Could not durably sign out of Codex. The current source is unchanged.", }); return; }; const changed = if (comptime @hasDecl(@TypeOf(app.auth), "reconcileAfterChatGptLogout")) try app.auth.reconcileAfterChatGptLogout(app.alloc) else false; applyCredentialChange(app, changed); try writeAuthNotice(app, switch (outcome) { .deleted => .{ .topic = "auth", .tone = .neutral, .body = "Signed out of Codex." }, .missing => .{ .topic = "auth", .tone = .neutral, .body = "No Codex login session found." }, .deleted_not_durable => .{ .topic = "auth", .tone = .warning, .body = "Signed out of Codex, but could not confirm the profile directory update." }, }); try reconcileSubscriptionLogout(app, .codex); return; } const result = login_flow.logout(app.alloc, app.auth.oauthTransport()) catch |err| switch (err) { error.SessionDeleteFailed => { try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Could not complete fx logout. The current source is unchanged.", }); return; }, }; try applyLogoutResult(app, result); }
fn reconcileSubscriptionLogout(app: *App, removed: model_provider.ProviderId) !void { const selected = provider_runtime.provider(app); if (!selected.eql(removed)) return; const candidates = auth_transition.logoutFallbackProviders(.{ .requested = removed, .selected = selected, .active_source = app.auth.credentialSource(), .available_sources = app.auth.pickerView().available_sources, }); if (candidates[0]) |target| { try startProviderSwitch(app, target, false, .manual, candidates[1]); return; } try app.writeDomainNotice(.{ .topic = "provider", .tone = .warning, .body = "No connected provider is available. Use /provider to sign in.", }, true); }
pub fn runProviderCommand(app: *App) !void { if (comptime !runtime_profile.allows(App, .native_auth)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "API key setup is unavailable in this WASM session.", }, true); return; } try beginProviderPickerInventoryRefresh(app, .provider_picker_command); }
pub fn openSetupHub(app: *App) !void { if (hostManagesAuth(app)) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .neutral, .body = credentials.host_managed_auth_message }); return; } if (comptime !runtime_profile.allows(App, .native_auth)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "API key setup is unavailable in this WASM session.", }, true); return; } switch (app.auth.beginSourceInventoryRefresh(app.alloc, .{ .provider = provider_runtime.provider(app), })) { .started => {}, .busy => try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = "Authentication inventory refresh is already in progress.", }), .failed => try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Authentication sources could not be checked. The picker remains closed.", }), } }
/// Reports blocked provider interaction without changing the composer. pub fn reject_provider_picker_if_busy(app: *App) !bool { if (!auth_transition.provider_work_busy(app.stream.active, app.worker.queuedPromptCount())) return false; try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = provider_busy_message, }, true); app.shell.render_requests.request(.footer); return true; }
fn beginProviderPickerInventoryRefresh( app: *App, destination: auth_runtime.InventoryRefreshDestination, ) !void { if (try reject_provider_picker_if_busy(app)) return; const prefix = switch (destination) { .provider_picker_login => picker_state.login_prefix, .provider_picker_command => picker_state.provider_prefix, .auth_picker => unreachable, }; var prepared = try app.input_runtime.textReplacementState().prepare(app.alloc, prefix); defer prepared.deinit(app.alloc); switch (app.auth.beginSourceInventoryRefresh(app.alloc, .{ .provider = provider_runtime.provider(app), .destination = destination, })) { .started => { app.input_runtime.textReplacementState().commit(app.alloc, &prepared); app.shell.render_requests.request(.footer); }, .busy => try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = "Authentication inventory refresh is already in progress.", }), .failed => try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Authentication sources could not be checked. The picker remains closed.", }), } }
pub fn collectSourceInventoryFacts(app: *App) !void { const result = app.auth.takeSourceInventoryRefresh() orelse return; switch (result) { .ready => |action| { if (action.destination != .auth_picker and try reject_provider_picker_if_busy(app)) { debug_trace.logf("auth", "provider picker publication dropped destination={t} reason=work_in_progress", .{action.destination}); if (comptime @hasField(App, "input_runtime")) { if (app.input_runtime.picker.activeProviderPickerQuery(&app.input_runtime.edit_state) != null) { app.input_runtime.picker.dismissInlinePicker(.provider); } } return; } var unavailable = app.auth.pickerView().unavailable_sources.iterator(); while (unavailable.next()) |source| { const body = try std.fmt.allocPrint( app.alloc, "{s} is unavailable. Check the saved credential or choose another option.", .{credentials.sourceLabel(source)}, ); defer app.alloc.free(body); try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = body }); } switch (action.destination) { .auth_picker => app.auth.openPickerForProvider(app.alloc, action.provider), .provider_picker_login, .provider_picker_command => {}, } app.shell.render_requests.request(.footer); }, .failed => |action| { if (comptime @hasField(App, "input_runtime")) { if (action.destination != .auth_picker and app.input_runtime.picker.activeProviderPickerQuery(&app.input_runtime.edit_state) != null) { app.input_runtime.picker.dismissInlinePicker(.provider); } } try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Authentication sources could not be checked. The picker was not opened with stale data.", }); }, } }
fn applyLogoutResult(app: *App, result: login_flow.LogoutResult) !void { // Logging out is an explicit rejection of that credential, so a // remembered pointer to it would silently reactivate on next login. // A remembered source always wins resolution, so an active fx login // is the only way one can be remembered; clearing otherwise is a // no-op against a store that holds nothing. if (app.auth.credentialSource() == .fx_login) forgetCredentialSource(app); applyCredentialChange(app, try app.auth.reconcileAfterFxLoginLogout(app.alloc)); try writeAuthNotice(app, if (result.local_durability_failed) .{ .topic = "auth", .tone = .warning, .body = "Could not confirm durable fx logout. The active source was recalculated.", } else if (result.session_deleted) .{ .topic = "auth", .tone = .neutral, .body = "Signed out of fx.", } else .{ .topic = "auth", .tone = .neutral, .body = "No fx login session found.", }); if (result.remote_revocation_failed) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = login_flow.remote_revocation_warning, }); } }
pub fn applyPickerChoice(app: *App, choice: auth_runtime.Choice) !void { if (try rejectPendingPreparation(app)) return; if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Browser authentication is supplied by the embedding SDK.", }, true); return; } switch (choice) { .provider => |provider| try switchProvider(app, provider, true, .manual), .source => |source| _ = try applySourceChoice(app, source), .action => |action| switch (action) { .connections => unreachable, .login => try beginSignIn(app, true), .chatgpt_login => try beginChatGptSignIn(app), .grok_login => try beginGrokSignIn(app), .setup => { if (comptime !runtime_profile.allows(App, .native_auth)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "API key setup is unavailable in this WASM session.", }, true); return; } prepareApiKeyInputBoundary(app); app.auth.openApiKeyPickerFromRoot(app.alloc); }, .change_team => try beginTeamPicker(app), .switch_credential => app.auth.openSwitchCredentialPicker(app.alloc), .switch_provider => app.auth.openProviderPicker(app.alloc, provider_runtime.provider(app)), .automatic => try applyAutomaticCredential(app), }, .team => |index| _ = try applyTeamChoice(app, index), } }
pub fn routeAuthPickerByte(app: *App, byte: u8) !bool { if (app.auth.signInEntryActive()) { if (byte == '\t') { _ = app.auth.toggleSignInCodeEntry(); } else if (app.auth.signInCodeEntryActive()) { switch (byte) { 3, 4 => cancelAndPopPickerStage(app), '\r', '\n' => _ = try app.auth.submitSignInCode(app.alloc), 8, 127 => _ = app.auth.deleteSignInCodeByte(), else => _ = try app.auth.appendSignInCodeByte(app.alloc, byte), } } else { switch (byte) { 3, 4 => cancelAndPopPickerStage(app), '\r', '\n' => try openSignInBrowser(app), else => {}, } } app.shell.render_requests.request(.footer); return true; } if (app.auth.teamPickerActive()) { const consumed = switch (byte) { 8, 127 => app.auth.deleteTeamQueryByte(), else => try app.auth.appendTeamQueryByte(app.alloc, byte), }; if (consumed) { app.shell.render_requests.request(.footer); return true; } } if (!app.auth.apiKeyEntryActive()) return false; switch (byte) { 3, 4 => cancelAndPopPickerStage(app), '\r', '\n' => try submitApiKeyEntry(app), 8, 127 => _ = app.auth.deleteApiKeyByte(), else => _ = try app.auth.appendApiKeyByte(app.alloc, byte), } app.shell.render_requests.request(.footer); return true; }
pub fn routeAuthPickerEscapeAction(app: *App, action: anytype) bool { if (!app.auth.signInEntryActive() and !app.auth.apiKeyEntryActive()) return false; return switch (action) { .escape, .remapped_byte => false, .paste_start => blk: { if (app.auth.signInCodeEntryActive()) break :blk false; if (!app.auth.toggleSignInCodeEntry()) break :blk true; app.shell.render_requests.request(.footer); break :blk false; }, .paste_end => !app.auth.signInCodeEntryActive(), else => true, }; }
fn cancelAndPopPickerStage(app: *App) void { cancelPromptRetryAfterAuth(app); _ = app.auth.popPickerStage(app.alloc); }
pub fn collectSignInFacts(app: *App) !void { if (comptime !oauthAuthEnabled(App)) return; const sign_in_source: credentials.Source = if (comptime @hasDecl(@TypeOf(app.auth), "pickerView")) app.auth.pickerView().sign_in_source else .fx_login; app.auth.pulseSignIn(app.alloc); switch (app.auth.pollSignInTransition(app.alloc)) { .none => {}, .cancelled => { cancelPromptRetryAfterAuth(app); app.shell.render_requests.request(.footer); }, .failed => |err| { cancelPromptRetryAfterAuth(app); debug_trace.logf("auth", "login failed source={t} err={s}", .{ sign_in_source, @errorName(err) }); _ = app.auth.popPickerStage(app.alloc); try writeLoginError(app, sign_in_source, err); }, .succeeded => |completed| { var owned = completed; defer owned.deinit(app.alloc); switch (owned) { .vercel => |*selection| { if (selection.teams.items.len == 0) { _ = app.auth.popPickerStage(app.alloc); try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Signed in to Vercel, but no Vercel teams could be loaded. The current credential is unchanged.", }); cancelPromptRetryAfterAuth(app); return; } try app.auth.refreshSourceInventory(app.alloc); app.auth.openTeamPicker(app.alloc, selection); try writeAuthNotice(app, .{ .topic = "auth", .tone = .neutral, .body = "Signed in to Vercel. Choose a team to finish setup.", }); }, .chatgpt => { try finishSubscriptionSignIn(app, .codex); return; }, .grok => { try finishSubscriptionSignIn(app, .grok); return; }, } }, } }
fn finishSubscriptionSignIn( app: *App, provider: model_provider.ProviderId, ) !void { try app.auth.refreshSourceInventory(app.alloc); switch (auth_transition.signInCompletion( provider, comptime provider_runtime.supported(App), )) { .vercel => unreachable, .switch_provider => |target| { app.auth.closePicker(app.alloc); try switchProvider(app, target, false, .post_oauth); }, .activate_source => |source| { if (!try selectCredentialSource(app, source)) { cancelPromptRetryAfterAuth(app); _ = app.auth.popPickerStage(app.alloc); try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = if (provider == .codex) "Signed in, but the Codex subscription credential could not be loaded." else "Signed in, but the Grok subscription credential could not be loaded.", }); return; } app.auth.closePicker(app.alloc); try writeAuthNotice(app, .{ .topic = "auth", .tone = .neutral, .body = if (provider == .codex) "Signed in with Codex." else "Signed in with Grok.", }); try resumePromptAfterAuth(app); }, } }
fn prepareApiKeyInputBoundary(app: *App) void { if (comptime @hasDecl(App, "prepareApiKeyInputBoundary")) { app.prepareApiKeyInputBoundary(); } }
fn submitApiKeyEntry(app: *App) !void { if (app.auth.pickerView().api_key_mask_count == 0) return; switch (app.auth.beginApiKeySave(app.alloc)) { .started => app.shell.render_requests.request(.footer), .empty => {}, .busy => try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Still saving the previous API key. Nothing was stored for this one; try again in a moment.", }, true), } }
/// Polled from the event loop so a save that blocks on a locked key store /// or a slow gateway never stalls rendering. pub fn collectApiKeySaveFacts(app: *App) !void { // Runs every tick: the key column has to retire whenever its entry // ended, whether it was saved, cancelled, or replaced. defer provider_picker_runtime.Runtime(App).closeKeyColumn(app); const result = app.auth.takeApiKeySaveResult(app.alloc) orelse return; try applyApiKeySaveResult(app, result); }
fn applyApiKeySaveResult(app: *App, result: auth_runtime.ApiKeySaveResult) !void { switch (result) { .empty => return, .saved => |changed| { applyCredentialChange(app, changed); rememberCredentialSource(app, .stored_key); const body = try std.fmt.allocPrint( app.alloc, "Saved the API key to {s} and made it active.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); // Only the inline `/provider vercel api-key` path implies // "use the gateway now": the user named the provider on the // way in. The staged hub also reaches this save, and there // adding a key is not a request to switch. if (comptime provider_runtime.supported(App) and provider_picker_runtime.supported(App)) { if (app.input_runtime.picker.provider_picker_stage == .api_key and provider_runtime.provider(app) != .gateway) { try switchProvider(app, .gateway, false, .manual); } } }, .gateway_refused => try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "The AI Gateway refused that API key. Nothing was stored.", }, true), .gateway_unavailable => try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Could not verify that API key with AI Gateway. Nothing was stored.", }, true), .store_failed => { const body = try std.fmt.allocPrint( app.alloc, "Could not save the API key to {s}. Nothing was stored.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = body, }, true); }, .reload_failed => { const body = try std.fmt.allocPrint( app.alloc, "Saved the API key to {s}, but could not make it active.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = body, }, true); }, } }
/// Clearing the remembered choice must also re-resolve, otherwise the /// session would keep running on a source precedence no longer selects. fn applyAutomaticCredential(app: *App) !void { forgetCredentialSource(app); app.auth.closePicker(app.alloc); applyCredentialChange(app, try app.auth.reselectByPrecedence(app.alloc)); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = "Using automatic credential precedence again.", }, true); try resumePromptAfterAuth(app); }
fn forgetCredentialSource(app: *App) void { var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .clear_credential_source = true }, ); defer attempt.deinit(app.alloc); switch (attempt) { .outcome => debug_trace.logf("auth", "credential choice cleared", .{}), .failure => |failure| debug_trace.logf( "auth", "credential choice not cleared err={s}", .{@errorName(failure.err)}, ), } }
/// Reports whether the credential actually switched, so callers that /// chain further work (the inline picker's provider switch) can stop /// when it did not. Failure is already explained to the user here. pub fn applySourceChoice(app: *App, source: credentials.Source) !bool { if (try rejectPendingPreparation(app)) return false; const body = try std.fmt.allocPrint( app.alloc, "Switched credential to {s}.", .{credentials.sourceLabel(source)}, ); defer app.alloc.free(body);
if (!try selectCredentialSource(app, source)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "That credential is no longer available. The current source is unchanged.", }, true); return false; }
rememberCredentialSource(app, source); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); try resumePromptAfterAuth(app); return true; }
/// An explicit source choice outlives the session. Failing to persist /// leaves the source active for this run rather than refusing a working /// credential the user already selected. fn rememberCredentialSource(app: *App, source: credentials.Source) void { // ChatGPT is selected by model route, not as a global Gateway // credential preference. Its saved session coexists independently. if (source == .chatgpt_subscription or source == .grok_subscription) return; if (comptime @hasDecl(App, "persistCredentialSourcePreference")) { app.persistCredentialSourcePreference(source); return; }
var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .credential_source = source }, ); defer attempt.deinit(app.alloc); switch (attempt) { .outcome => debug_trace.logf("auth", "credential choice persisted source={t}", .{source}), .failure => |failure| debug_trace.logf( "auth", "credential choice not persisted source={t} err={s}", .{ source, @errorName(failure.err) }, ), } }
fn beginChatGptSignIn(app: *App) !void { if (comptime provider_runtime.supported(App)) { const decision = decideProviderSwitch(.{ .current = provider_runtime.provider(app), .target = .codex, .target_credential_ready = false, .intent = .post_oauth, .stream_active = app.stream.active, .queued_prompts = app.worker.queuedPromptCount(), }); if (decision == .busy) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Codex sign-in is unavailable until active and queued work finishes.", }, true); return; } } try app.flushBeforeBlockingExternalWork(); const started = app.auth.openChatGptSignInPickerFromRoot(app.alloc); if (started catch |err| { cancelPromptRetryAfterAuth(app); debug_trace.logf("auth", "ChatGPT login failed err={s}", .{@errorName(err)}); try writeLoginError(app, .chatgpt_subscription, err); return; }) { app.shell.render_requests.request(.footer); if (io_mod.getenv("FX_NO_OPEN_BROWSER") == null) try openSignInBrowser(app); } }
fn beginGrokSignIn(app: *App) !void { if (comptime provider_runtime.supported(App)) { const decision = decideProviderSwitch(.{ .current = provider_runtime.provider(app), .target = .grok, .target_credential_ready = false, .intent = .post_oauth, .stream_active = app.stream.active, .queued_prompts = app.worker.queuedPromptCount(), }); if (decision == .busy) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Grok sign-in is unavailable until active and queued work finishes.", }, true); return; } } try app.flushBeforeBlockingExternalWork(); const started = app.auth.openGrokSignInPickerFromRoot(app.alloc); if (started catch |err| { cancelPromptRetryAfterAuth(app); debug_trace.logf("auth", "Grok login failed err={s}", .{@errorName(err)}); try writeLoginError(app, .grok_subscription, err); return; }) { app.shell.render_requests.request(.footer); if (io_mod.getenv("FX_NO_OPEN_BROWSER") == null) try openSignInBrowser(app); } }
fn beginCodexSignInForProviderSwitch(app: *App) !void { try app.flushBeforeBlockingExternalWork(); const started = app.auth.openChatGptSignInPickerForProviderSwitch(app.alloc); if (started catch |err| { debug_trace.logf("auth", "Codex login failed err={s}", .{@errorName(err)}); try writeLoginError(app, .chatgpt_subscription, err); return; }) { app.shell.render_requests.request(.footer); if (io_mod.getenv("FX_NO_OPEN_BROWSER") == null) try openSignInBrowser(app); } }
fn beginGrokSignInForProviderSwitch(app: *App) !void { try app.flushBeforeBlockingExternalWork(); const started = app.auth.openGrokSignInPickerForProviderSwitch(app.alloc); if (started catch |err| { debug_trace.logf("auth", "Grok login failed err={s}", .{@errorName(err)}); try writeLoginError(app, .grok_subscription, err); return; }) { app.shell.render_requests.request(.footer); if (io_mod.getenv("FX_NO_OPEN_BROWSER") == null) try openSignInBrowser(app); } }
fn switchProvider( app: *App, target: model_provider.ProviderId, allow_login: bool, intent: ProviderSwitchIntent, ) !void { try startProviderSwitch(app, target, allow_login, intent, null); }
fn startProviderSwitch( app: *App, target: model_provider.ProviderId, allow_login: bool, intent: ProviderSwitchIntent, fallback: ?model_provider.ProviderId, ) !void { if (comptime !provider_runtime.supported(App) or !@hasDecl(App, "providerCatalog") or !@hasDecl(@TypeOf(app.auth), "beginProviderPreparation") or host_target.is_wasm) { try app.writeDomainNotice(.{ .topic = "provider", .tone = .warning, .body = "Provider switching is unavailable in this host.", }, true); return; } if (try rejectPendingPreparation(app)) return; switch (decideProviderSwitch(.{ .current = provider_runtime.provider(app), .target = target, .target_credential_ready = model_provider.authorizesCredential(target, app.auth.credentialSource()), .intent = intent, .stream_active = app.stream.active or pendingPromptBlocksPreparation(app), .queued_prompts = app.worker.queuedPromptCount(), })) { .prepare => {}, .no_change => { const body = try std.fmt.allocPrint(app.alloc, "Already using {s}.", .{provider_catalog.label(target)}); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = body, }, true); return; }, .busy => { try app.writeDomainNotice(.{ .topic = "provider", .tone = .warning, .body = providerFailureMessage(intent, provider_busy_message, "Subscription sign-in completed, but provider activation is unavailable until active and queued work finishes. The current provider is unchanged."), }, true); return; }, } var settings = config_runtime.loadMergedSettings(app.alloc, app.workspace_root) catch |err| { debug_trace.logf("provider", "settings load failed err={s}", .{@errorName(err)}); try app.writeDomainNotice(.{ .topic = "provider", .tone = .@"error", .body = "Could not load the saved provider selection. The current provider is unchanged.", }, true); return; }; defer settings.deinit(app.alloc); const catalog_provider = app.providerCatalog(target) orelse { try app.writeDomainNotice(.{ .topic = "provider", .tone = .@"error", .body = "The target provider catalog is unavailable. The current provider is unchanged.", }, true); return; }; try beginPreparation(app, .{ .intent = .{ .provider = .{ .target = target, .allow_login = allow_login, .origin = intent, .fallback = fallback } }, .catalog_provider = catalog_provider, .models_path = app.model_cache.models_path, .preferred_source = if (target == .gateway) settings.credential_source else null, .primary_model = if (intent == .post_oauth and provider_runtime.provider(app).eql(target)) provider_runtime.model(app) else null, .preferred_model = if (intent == .post_oauth) settings.models.get(target) else io_mod.getenv("FX_MODEL") orelse settings.models.get(target), }); }
fn pendingPromptBlocksPreparation(app: *const App) bool { if (comptime !@hasField(App, "submission")) return false; const pending = app.submission.pending orelse return false; return pending.credential_admitted or pending.phase == .queued; }
fn pendingPromptNeedsAdoption(app: *const App) bool { if (comptime !@hasField(App, "submission")) return false; const pending = app.submission.pending orelse return false; return pending.phase == .awaiting_frame or pending.phase == .awaiting_adoption; }
fn rejectPendingPreparation(app: *App) !bool { if (comptime !@hasDecl(@TypeOf(app.auth), "providerPreparationPending")) return false; if (!app.auth.providerPreparationPending()) return false; try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = "Provider preparation is still in progress. ctrl+c cancels.", }, true); return true; }
fn beginPreparation(app: *App, input: auth_runtime.ProviderPreparationInput) !void { app.auth.beginProviderPreparation(app.alloc, input) catch |err| { debug_trace.logf("provider", "preparation start failed err={s}", .{@errorName(err)}); try app.writeDomainNotice(.{ .topic = "provider", .tone = .@"error", .body = "Could not start provider preparation. The current provider is unchanged.", }, true); return; }; const body = try std.fmt.allocPrint(app.alloc, "Preparing {s}.", .{provider_catalog.label(input.target())}); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = body, }, true); }
pub fn collectProviderPreparationFacts(app: *App) !void { if (comptime !@hasDecl(@TypeOf(app.auth), "takeProviderPreparation") or host_target.is_wasm) return; if (pendingPromptNeedsAdoption(app)) return; const task = app.auth.takeProviderPreparation() orelse return; defer task.deinit(); if (task.cancel_requested.load(.seq_cst)) { holdPromptAfterPreparationFailure(app); try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = "Provider preparation cancelled. The current provider is unchanged.", }, true); return; } const applied = switch (task.input.intent) { .provider => try finishProviderSwitch(app, task), .team => try finishTeamPreparation(app, task), }; if (applied) { if (comptime @hasField(App, "submission")) { if (app.submission.pending) |pending| { if (pending.phase == .awaiting_auth) requestPromptRetryAfterAuth(app); } } try resumePromptAfterAuth(app); return; } if (task.input.intent == .provider) { if (task.input.intent.provider.fallback) |target| { try startProviderSwitch(app, target, false, .manual, null); if (app.auth.providerPreparationPending()) return; } } holdPromptAfterPreparationFailure(app); }
fn holdPromptAfterPreparationFailure(app: *App) void { if (comptime !@hasField(App, "submission")) return; if (app.submission.pending) |*pending| { if (pending.phase == .adopted) { pending.phase = .awaiting_auth; app.submission.retry_after_auth = app.auth.signInEntryActive(); debug_trace.logf("provider", "pending prompt retained after preparation failure", .{}); } } }
fn finishProviderSwitch(app: *App, task: *auth_runtime.ProviderPreparation) !bool { const request = task.input.intent.provider; const target = request.target; const intent = request.origin; if (task.failure) |err| { debug_trace.logf("provider", "preparation failed target={t} err={s}", .{ target, @errorName(err) }); if (task.credential == null and !hostManagesAuth(app)) { const body = try auth_runtime.preparationFailureText(app.alloc, target, err); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = body, }, true); } else { try app.writeDomainNotice(.{ .topic = "provider", .tone = .@"error", .body = providerFailureMessage(intent, "Could not load the target provider catalog. The current provider is unchanged.", "Subscription sign-in completed, but its model catalog could not be loaded. The current provider is unchanged."), }, true); } return false; } if (task.credential == null and !hostManagesAuth(app)) { if (request.allow_login) { switch (target) { .codex => try beginCodexSignInForProviderSwitch(app), .grok => try beginGrokSignInForProviderSwitch(app), .gateway, .configured => {}, } } if (target == .gateway or !request.allow_login) { try app.writeDomainNotice(.{ .topic = "provider", .tone = .warning, .body = if (intent == .post_oauth) "Subscription sign-in completed, but its saved credential is unavailable. The current provider is unchanged." else if (target == .codex) "Run fx login codex, then try switching again." else if (target == .grok) "Run fx login grok, then try switching again." else credentials.missing_interactive_credential_message, }, true); } return false; } const fetched = task.catalog orelse return false; task.catalog = null; var catalog = switch (fetched) { .catalog => |catalog| catalog, .failure => |failure| { debug_trace.logf("provider", "catalog rejected provider={t} category={t}", .{ target, failure.category }); try app.writeDomainNotice(.{ .topic = "provider", .tone = .@"error", .body = if (failure.category == .cancellation) "Provider switching was cancelled. The current provider is unchanged." else providerFailureMessage(intent, "The target provider catalog could not be validated. The current provider is unchanged.", "Subscription sign-in completed, but its model catalog could not be validated. The current provider is unchanged."), }, true); return false; }, }; defer model_catalog.freeModelCatalog(app.alloc, &catalog); const selected_model = selectCatalogModel(catalog.items, task.input.primary_model, task.input.preferred_model) orelse { try app.writeDomainNotice(.{ .topic = "provider", .tone = .@"error", .body = providerFailureMessage(intent, "The target provider returned no supported models. The current provider is unchanged.", "Subscription sign-in completed, but its model catalog returned no supported models. The current provider is unchanged."), }, true); return false; }; var credential = task.credential; task.credential = null; defer if (credential) |*value| value.deinit(app.alloc); const access: credentials.CatalogAccess = if (hostManagesAuth(app)) .host_managed else credentials.catalogAccessForCredentialAndAccount(credential.?.source, credential.?.token, credential.?.gatewayTeam(), credential.?.accountId()); var owned_model = try app.alloc.dupe(u8, selected_model); defer app.alloc.free(owned_model);
if (auth_transition.provider_work_busy(app.stream.active, app.worker.queuedPromptCount())) { try app.writeDomainNotice(.{ .topic = "provider", .tone = .warning, .body = providerFailureMessage( intent, provider_busy_message, "Subscription sign-in completed, but provider activation is unavailable until active and queued work finishes. The current provider is unchanged.", ), }, true); return false; }
app.model_cache.adoptOwnedCatalog(access, &catalog); app.provider_selection.adoptOwned(target, &owned_model); if (credential) |*value| _ = app.auth.adoptCredential(app.alloc, value); reconcileGatewayCredential(app);
const body = try std.fmt.allocPrint( app.alloc, "Switched to {s} with {s}.", .{ provider_catalog.label(target), provider_runtime.model(app) }, ); defer app.alloc.free(body); if (comptime @hasDecl(App, "persistRuntimePreferences")) { var persistence = app.persistRuntimePreferences(.{ .provider = target, .model = provider_runtime.model(app), }); defer persistence.deinit(app.alloc); if (persistence.settings_error != null or persistence.session_error != null) { debug_trace.logf( "provider", "runtime switch persistence failed settings={s} session={s}", .{ if (persistence.settings_error) |err| @errorName(err) else "none", if (persistence.session_error) |err| @errorName(err) else "none", }, ); try app.writeDomainNotice(.{ .topic = "provider", .tone = .warning, .body = "Provider switched for this run, but the selection could not be saved.", }, true); } else { try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = body, }, true); } } else { var persistence = config_runtime.attemptUserPreferences(app.alloc, .{ .provider = target, .model_preference = .{ .provider = target, .model = provider_runtime.model(app), }, }); defer persistence.deinit(app.alloc); switch (persistence) { .outcome => try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = body }, true), .failure => |failure| { debug_trace.logf("provider", "runtime switch persistence failed err={s}", .{@errorName(failure.err)}); try app.writeDomainNotice(.{ .topic = "provider", .tone = .warning, .body = "Provider switched for this run, but the selection could not be saved.", }, true); }, } } app.shell.render_requests.request(.footer); return true; }
/// What the inline `/provider` picker should do after the user chooses to /// connect with a Vercel account. pub const TeamColumn = enum { /// Teams are loaded and ready to render as the next column. ready, /// No session yet, so the account has to be connected first. needs_sign_in, /// A session exists but its teams could not be listed. The reason /// has already been reported. unavailable, /// Credential preparation failed; no source may be activated. blocked, };
pub fn loadTeamsForProviderPicker(app: *App) !TeamColumn { const view = app.auth.pickerView(); if (view.unavailable_sources.contains(.fx_login)) { const body = try auth_runtime.preparationFailureText(app.alloc, .gateway, error.CredentialStorageUnavailable); defer app.alloc.free(body); try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = body }); return .blocked; } if (comptime @hasDecl(@TypeOf(app.auth), "credentialFailure")) { if (app.auth.credentialFailure()) |failure| { if (failure.source == .fx_login and failure.requiresSignIn()) { return .needs_sign_in; } } } if (!view.fx_login_session_available) return .needs_sign_in; try app.flushBeforeBlockingExternalWork();
var selection = login_flow.loadTeamSelection(app.alloc, app.auth.oauthTransport()) catch |err| { debug_trace.logf("auth", "provider picker team load failed err={s}", .{@errorName(err)}); // A session file that exists but can no longer be refreshed is // not a listing failure: there is nothing to list until the // user signs in again. if (err == error.NoSession or auth_runtime.classifyCredentialFailure(.fx_login, err).requiresSignIn()) { return .needs_sign_in; } if (err != error.NoTeams and err != error.TeamRequestFailed) { if (err == error.OutOfMemory) return err; const body = try auth_runtime.preparationFailureText(app.alloc, .gateway, err); defer app.alloc.free(body); try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = body }); return .blocked; } try app.writeDomainNotice(.{ .topic = "auth", .tone = if (err == error.NoTeams) .neutral else .@"error", .body = switch (err) { error.NoTeams => "No Vercel teams are available for this account.", else => "Could not reach Vercel to list teams. The current team is unchanged.", }, }, true); return .unavailable; }; defer selection.deinit(app.alloc); if (selection.teams.items.len == 0) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = "No Vercel teams are available for this account.", }, true); return .unavailable; } app.auth.adoptTeamSelection(app.alloc, &selection); return .ready; }
fn beginTeamPicker(app: *App) !void { if (!app.auth.pickerView().fx_login_session_available) return; try app.flushBeforeBlockingExternalWork();
var selection = login_flow.loadTeamSelection(app.alloc, app.auth.oauthTransport()) catch |err| { debug_trace.logf("auth", "team picker load failed err={s}", .{@errorName(err)}); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = switch (err) { error.NoSession => "The fx login session is no longer available. Sign in to change teams.", error.NoTeams => "No Vercel teams are available for this account.", else => "Could not load Vercel teams. The current team is unchanged.", }, }, true); return; }; defer selection.deinit(app.alloc); app.auth.openTeamPicker(app.alloc, &selection); app.shell.render_requests.request(.footer); }
pub fn applyTeamChoice(app: *App, index: usize) !bool { if (pendingPromptBlocksPreparation(app)) { try app.writeDomainNotice(.{ .topic = "provider", .tone = .neutral, .body = provider_busy_message }, true); return false; } if (try rejectPendingPreparation(app)) return false; if (try reject_provider_picker_if_busy(app)) return false; const selection = app.auth.loadedTeamSelection() orelse return false; if (index >= selection.teams.items.len) return false; var candidate = selection.validationCredential(app.alloc, index) catch |err| { cancelPromptRetryAfterAuth(app); if (err == error.OutOfMemory) return err; debug_trace.logf("auth", "team validation credential failed err={s}", .{@errorName(err)}); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "The selected Vercel team could not be validated. The current team is unchanged.", }, true); return false; }; defer candidate.deinit(app.alloc); if (comptime @hasDecl(@TypeOf(app.auth), "beginProviderPreparation") and @hasDecl(App, "providerCatalog") and !host_target.is_wasm) { const catalog_provider = app.providerCatalog(.gateway) orelse return false; var settings = config_runtime.loadMergedSettings(app.alloc, app.workspace_root) catch |err| { debug_trace.logf("auth", "team preparation settings failed err={s}", .{@errorName(err)}); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Could not load provider preferences. The current team is unchanged." }, true); return false; }; defer settings.deinit(app.alloc); try beginPreparation(app, .{ .intent = .{ .team = .{ .index = index, .activate_gateway = provider_runtime.provider(app) != .gateway } }, .preferred_model = io_mod.getenv("FX_MODEL") orelse settings.models.get(.gateway), .catalog_provider = catalog_provider, .models_path = app.model_cache.models_path, .candidate = candidate, }); return false; } else { var validation = try validateTeamCredential(app, candidate); defer validation.deinit(app.alloc); return finishTeamChoice(app, index, validation, null); } }
fn finishTeamPreparation(app: *App, task: *auth_runtime.ProviderPreparation) !bool { const request = task.input.intent.team; if (try reject_provider_picker_if_busy(app)) return false; if (task.failure) |err| debug_trace.logf("auth", "team preparation failed err={s}", .{@errorName(err)}); const selection = app.auth.loadedTeamSelection() orelse { debug_trace.logf("auth", "team preparation discarded reason=selection_closed", .{}); return false; }; const candidate = task.credential orelse return false; if (request.index >= selection.teams.items.len or !std.mem.eql(u8, selection.teams.items[request.index].id, candidate.team_id orelse "")) { debug_trace.logf("auth", "team preparation discarded reason=selection_changed", .{}); return false; } var validation: TeamCatalogValidation = if (task.catalog) |catalog| try validateTeamCatalog(app, catalog) else .rejected; defer validation.deinit(app.alloc); return finishTeamChoice(app, request.index, validation, if (request.activate_gateway) task else null); }
fn finishTeamChoice(app: *App, index: usize, validation: TeamCatalogValidation, activation: ?*auth_runtime.ProviderPreparation) !bool { const selection = app.auth.loadedTeamSelection() orelse return false; if (index >= selection.teams.items.len) return false; const team = selection.teams.items[index]; const body = try std.fmt.allocPrint(app.alloc, "Changed Vercel team to {s} ({s}).", .{ team.name, team.slug }); defer app.alloc.free(body); if (validation == .rejected) { cancelPromptRetryAfterAuth(app); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "The selected Vercel team could not be validated for AI Gateway. The current team is unchanged.", }, true); return false; }
var selected_team = selection.select(app.alloc, index) catch |err| { cancelPromptRetryAfterAuth(app); debug_trace.logf("auth", "team change failed err={s}", .{@errorName(err)}); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = switch (err) { error.SessionChanged, error.NoSession => "The fx login session changed before the team could be saved.", else => "Could not change the Vercel team. The current team is unchanged.", }, }, true); return false; }; defer selected_team.deinit(app.alloc);
if (comptime provider_runtime.supported(App) and @hasDecl(App, "providerCatalog") and @hasDecl(@TypeOf(app.auth), "beginProviderPreparation")) { if (activation) |task| { task.input.intent = .{ .provider = .{ .target = .gateway, .allow_login = false, .origin = .manual } }; if (!try finishProviderSwitch(app, task)) return false; rememberCredentialSource(app, .fx_login); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body }, true); return true; } }
var model_persistence_failed = false; if (comptime provider_runtime.supported(App)) { if (validation.accepted) |selected_model| { try provider_runtime.replaceModel(app, selected_model); if (comptime @hasDecl(App, "persistRuntimePreferences")) { var persistence = app.persistRuntimePreferences(.{ .provider = .gateway, .model = provider_runtime.model(app), }); defer persistence.deinit(app.alloc); model_persistence_failed = persistence.settings_error != null or persistence.session_error != null; } else { var persistence = config_runtime.attemptUserPreferences(app.alloc, .{ .model_preference = .{ .provider = .gateway, .model = provider_runtime.model(app), }, }); defer persistence.deinit(app.alloc); model_persistence_failed = persistence == .failure; } } }
if (!try selectCredentialSource(app, .fx_login)) { cancelPromptRetryAfterAuth(app); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Changed the Vercel team, but the fx login credential could not be loaded.", }, true); return false; } rememberCredentialSource(app, .fx_login); app.auth.closePicker(app.alloc); try app.writeDomainNotice(if (model_persistence_failed) .{ .topic = "auth", .tone = .warning, .body = "The Vercel team changed and a valid model was selected for this run, but the model preference could not be saved.", } else .{ .topic = "auth", .tone = .neutral, .body = body, }, true); try resumePromptAfterAuth(app); return true; }
/// Sign-in launched from the inline `/provider` picker. Esc must close /// the sign-in surface entirely; the legacy staged hub is not the /// screen the user came from. pub fn beginSignInForProviderPicker(app: *App) !void { try beginSignIn(app, false); }
fn validateTeamCredential( app: *App, candidate: credentials.Credential, ) std.mem.Allocator.Error!TeamCatalogValidation { if (comptime !@hasDecl(App, "providerCatalog")) { return .{ .accepted = null }; } const access = credentials.catalogAccessAt(candidate, io_mod.milliTimestamp()); if (access.authorizationCredential() == null) return .rejected; const provider = app.providerCatalog(.gateway) orelse return .rejected; var cancelled = std.atomic.Value(bool).init(false); const fetched = provider.fetch(app.alloc, .{ .access = access, .endpoint = app.model_cache.models_path, .cancel_flag = &cancelled, .view = .picker, }) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; debug_trace.logf("auth", "team catalog validation failed err={s}", .{@errorName(err)}); return .rejected; }; defer if (fetched == .catalog) { var owned = fetched.catalog; model_catalog.freeModelCatalog(app.alloc, &owned); }; return validateTeamCatalog(app, fetched); }
fn validateTeamCatalog(app: *App, fetched: model_catalog.ProviderResult) std.mem.Allocator.Error!TeamCatalogValidation { return switch (fetched) { .failure => |failure| result: { debug_trace.logf("auth", "team catalog rejected category={t}", .{failure.category}); break :result .rejected; }, .catalog => |catalog_value| result: { const catalog = catalog_value; if (catalog.items.len == 0) break :result .rejected; if (comptime provider_runtime.supported(App)) { const current_model = provider_runtime.model(app); for (catalog.items) |entry| { if (std.mem.eql(u8, entry.id, current_model)) { break :result .{ .accepted = null }; } } break :result .{ .accepted = try app.alloc.dupe(u8, catalog.items[0].id), }; } break :result .{ .accepted = null }; }, }; }
fn beginSignIn(app: *App, from_root: bool) !void { try app.flushBeforeBlockingExternalWork();
const started = if (from_root) app.auth.openSignInPickerFromRoot(app.alloc) else app.auth.openSignInPicker(app.alloc); if (started catch |err| { cancelPromptRetryAfterAuth(app); debug_trace.logf("auth", "login failed err={s}", .{@errorName(err)}); try writeLoginError(app, .fx_login, err); return; }) { app.shell.render_requests.request(.footer); // Open the browser as soon as the device code is ready instead of // waiting for Enter; Enter stays as a manual re-open, and // FX_NO_OPEN_BROWSER opts out for headless/SSH sessions. if (io_mod.getenv("FX_NO_OPEN_BROWSER") == null) try openSignInBrowser(app); } }
fn openSignInBrowser(app: *App) !void { const url = (try app.auth.signInBrowserUrlAlloc(app.alloc)) orelse return; defer app.alloc.free(url); if (!try app.urlOpener().open(app.alloc, url)) { debug_trace.logf("auth", "login browser launcher failed", .{}); } }
pub fn selectCredentialSource(app: *App, source: credentials.Source) !bool { const changed = (try app.auth.selectSource(app.alloc, source)) orelse return false; applyCredentialChange(app, changed); return true; }
fn refreshFxLoginCredentialIfNeeded(app: *App) !void { const change = try app.auth.refreshSelectedCredentialIfNeeded(app.alloc); applyCredentialRefreshChange(app, change); }
fn applyCredentialRefreshChange( app: *App, change: auth_transition.CredentialChange, ) void { if (change == .none) return; reconcileGatewayCredential(app); if (app.auth.modelCatalogAccess().authorizationCredential() == null) return; if (change == .authority) app.model_cache.reset(); if (comptime @hasDecl(App, "startModelCacheWarmup")) { app.startModelCacheWarmup(); } }
pub fn admitPromptCredential(app: *App) !bool { if (comptime !oauthAuthEnabled(App)) { if (app.auth.apiKey() != null) return true; if (compactionOwnsCredentialFeedback(app)) return false; try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Missing FX_API_KEY. Supply it through createFxTerminal().", }, true); return false; } if (!try ensurePromptCredential(app)) return false; return preparePromptCredential(app); }
pub fn startPromptCredentialPrewarm(app: *App) void { if (comptime @hasDecl(@TypeOf(app.auth), "providerPreparationPending")) { if (app.auth.providerPreparationPending()) return; } if (comptime !@hasDecl(@TypeOf(app.auth), "beginPromptCredentialRefresh")) return; if (comptime provider_runtime.supported(App)) { if (!model_provider.authorizesCredential(provider_runtime.provider(app), app.auth.credentialSource())) return; } const outcome = app.auth.beginPromptCredentialRefresh(); debug_trace.logf( "auth", "prompt credential prewarm start outcome={s}", .{@tagName(outcome)}, ); }
pub fn collectPendingPromptCredential( app: *App, ) !PendingPromptCredentialReadiness { if (comptime @hasDecl(@TypeOf(app.auth), "providerPreparationPending")) { if (app.auth.providerPreparationPending()) return .pending; } if (comptime host_target.is_wasm) { return if (try admitPromptCredential(app)) .current else .rejected; } if (!try ensurePromptCredential(app)) return .rejected; const source = app.auth.credentialSource() orelse return .rejected; if (!credentials.sourceRefreshable(source)) { return if (app.auth.gatewayCredential() != null) .current else .rejected; }
var refresh = app.auth.pollPromptCredentialRefresh(); defer refresh.deinit(); switch (refresh) { .idle => { return switch (app.auth.beginPromptCredentialRefresh()) { .started, .pending => .pending, .not_needed => if (app.auth.gatewayCredential() != null) .current else .rejected, .failed => if (try admitPromptCredential(app)) .current else .rejected, }; }, .pending => return .pending, .failed => |failure| { _ = try recoverCredentialFailure(app, failure.source, failure.err); return .rejected; }, .ready => |*refreshed| { var owned = try refreshed.clone(app.alloc); defer owned.deinit(app.alloc); const change = app.auth.adoptPreparedCredential(app.alloc, &owned); applyCredentialRefreshChange(app, change); return if (app.auth.gatewayCredential() != null) .current else .pending; }, } }
pub fn retryPendingPromptCredential( app: *App, ) !PendingPromptCredentialReadiness { if (comptime @hasDecl(@TypeOf(app.auth), "providerPreparationPending")) { if (app.auth.providerPreparationPending()) return .pending; } if (comptime host_target.is_wasm) { return if (try admitPromptCredential(app)) .current else .rejected; } if (!try ensurePromptCredential(app)) return .rejected; if (comptime @hasDecl(@TypeOf(app.auth), "credentialFailure")) { if (app.auth.credentialFailure()) |failure| { if (!failure.retryable()) { return if (try preparePromptCredential(app)) .current else .rejected; } } } app.auth.cancelPromptCredentialRefresh(); return switch (app.auth.beginPromptCredentialRefresh()) { .started, .pending => .pending, .not_needed => if (app.auth.gatewayCredential() != null) .current else .rejected, .failed => if (try preparePromptCredential(app)) .current else .rejected, }; }
fn preparePromptCredential(app: *App) !bool { if (comptime @hasDecl(@TypeOf(app.auth), "credentialFailure")) { if (app.auth.credentialFailure()) |failure| { if (failure.requiresSignIn()) { try beginCredentialRepair(app, failure); return false; } } } for (0..2) |_| { refreshFxLoginCredentialIfNeeded(app) catch |err| switch (err) { error.OutOfMemory => return err, else => return recoverPromptCredentialRefreshFailure(app, err), }; if (app.auth.gatewayCredential() != null) return true; } return recoverPromptCredentialRefreshFailure(app, error.CredentialRefreshUnavailable); }
fn beginCredentialRepair( app: *App, failure: auth_runtime.CredentialFailure, ) !void { requestPromptRetryAfterAuth(app); switch (failure.source) { .fx_login => try beginSignIn(app, false), .chatgpt_subscription => try beginChatGptSignIn(app), .grok_subscription => try beginGrokSignIn(app), .vercel_oidc_token, .ai_gateway_api_key, .stored_key, .host_managed, .configured, => {}, } }
fn requestPromptRetryAfterAuth(app: *App) void { if (comptime @hasDecl(App, "requestPromptRetryAfterAuth")) { app.requestPromptRetryAfterAuth(); } }
fn cancelPromptRetryAfterAuth(app: *App) void { if (comptime @hasDecl(App, "cancelPromptRetryAfterAuth")) { app.cancelPromptRetryAfterAuth(); } }
fn resumePromptAfterAuth(app: *App) !void { if (comptime @hasDecl(App, "resumePromptAfterAuth")) { try app.resumePromptAfterAuth(); } }
fn recoverPromptCredentialRefreshFailure(app: *App, err: anyerror) !bool { const active_source = app.auth.credentialSource(); const source = if (active_source) |active| if (credentials.sourceRefreshable(active)) active else .fx_login else .fx_login; return recoverCredentialFailure(app, source, err); }
fn recoverCredentialFailure(app: *App, source: credentials.Source, err: anyerror) !bool { debug_trace.logf("auth", "prompt credential refresh failed source={t} err={s}", .{ source, @errorName(err) }); const failure = auth_runtime.classifyCredentialFailure(source, err); debug_trace.logf( "auth", "credential failure source={t} reason={t} retryable={s}", .{ failure.source, failure.reason, if (failure.retryable()) "true" else "false" }, ); const notify = !compactionOwnsCredentialFeedback(app); const should_notify = if (app.auth.credentialSource() == source and comptime @hasDecl(@TypeOf(app.auth), "recordCredentialFailure")) app.auth.recordCredentialFailure(failure, .{ .notify = notify }) else notify; if (!should_notify) return false; const recovery = try credentialRecoveryText(app.alloc, failure); defer app.alloc.free(recovery); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = recovery, }, true); app.shell.render_requests.request(.footer); return false; }
fn credentialRecoveryText( alloc: std.mem.Allocator, failure: auth_runtime.CredentialFailure, ) ![]u8 { const source_label = credentials.sourceLabel(failure.source); return switch (failure.reason) { .invalid_credential => std.fmt.allocPrint( alloc, "{s} sign-in expired.\npress enter to sign in again. Your prompt is saved.", .{source_label}, ), .invalid_storage => std.fmt.allocPrint( alloc, "{s}: Saved credential storage is unavailable.\nCheck credential storage, then press enter to retry. Your prompt is saved.", .{source_label}, ), .persistence_uncertain => std.fmt.allocPrint( alloc, "{s} refresh could not be saved.\npress enter to sign in again. Your prompt is saved.", .{source_label}, ), .authority_changed => std.fmt.allocPrint( alloc, "{s} account or team changed during refresh.\nReview authentication before retrying. Your prompt is saved.", .{source_label}, ), .temporary_unavailable => std.fmt.allocPrint( alloc, "{s} credential refresh failed.\npress enter to retry. Your prompt is saved.", .{source_label}, ), }; }
fn applyCredentialChange(app: *App, changed: bool) void { if (!changed) return; if (comptime @hasDecl(@TypeOf(app.auth), "cancelPromptCredentialRefresh")) { app.auth.cancelPromptCredentialRefresh(); } reconcileGatewayCredential(app); app.model_cache.reset(); if (comptime @hasDecl(App, "startModelCacheWarmup")) { app.startModelCacheWarmup(); } }
fn reconcileGatewayCredential(app: *App) void { if (comptime !runtime_profile.allows(App, .generation_usage)) return; if (comptime @hasField(App, "session") and @hasField(@TypeOf(app.session), "usage")) { if (app.auth.gatewayCredential()) |credential| { if (gatewayCredentialSource(credential) == .host_managed) { if (comptime @hasDecl(@TypeOf(app.session.usage), "replaceHostManagedReconciliationAuthority")) { app.session.usage.replaceHostManagedReconciliationAuthority( app.alloc, provider_runtime.provider(app), ); } return; } const subscription = if (comptime @hasField(@TypeOf(credential), "source")) credential.source == .chatgpt_subscription or credential.source == .grok_subscription else false; if (subscription) { app.session.usage.clearReconciliationCredential(); } else { if (comptime @hasDecl( @TypeOf(app.session.usage), "replaceProviderReconciliationCredential", )) { if (optionalGatewayApiKey(credential)) |api_key| { app.session.usage.replaceProviderReconciliationCredential( app.alloc, .gateway, credential.source, null, api_key, ); } } else { if (optionalGatewayApiKey(credential)) |api_key| { app.session.usage.replaceReconciliationCredential( app.alloc, api_key, ); } } } } else { app.session.usage.clearReconciliationCredential(); } } }
fn writeLoginError(app: *App, source: credentials.Source, err: anyerror) !void { const failure = auth_runtime.classifyCredentialFailure(source, err); if (failure.reason == .invalid_storage or failure.reason == .persistence_uncertain) { const notice = auth_runtime.preparationFailureNotice(auth_runtime.preparationError(failure).?).?; const body = try std.fmt.allocPrint(app.alloc, "{s}: {s}", .{ credentials.sourceLabel(source), notice }); defer app.alloc.free(body); try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = body }); return; } const notice: types.SemanticNotice = if (source == .chatgpt_subscription) switch (err) { error.ChatGptAuthorizationFailed => .{ .topic = "auth", .tone = .@"error", .body = "Codex sign-in was denied. The current credential is unchanged." }, error.ChatGptLoginTimedOut, error.LoginTimedOut => .{ .topic = "auth", .tone = .warning, .body = "Codex sign-in expired. The current credential is unchanged; run /login to try again." }, else => .{ .topic = "auth", .tone = .@"error", .body = "Codex sign-in failed. The current credential is unchanged." }, } else if (source == .grok_subscription) switch (err) { error.GrokAuthorizationFailed => .{ .topic = "auth", .tone = .@"error", .body = "Grok sign-in was denied. The current credential is unchanged." }, error.GrokLoginTimedOut, error.LoginTimedOut => .{ .topic = "auth", .tone = .warning, .body = "Grok sign-in expired. The current credential is unchanged; run /login to try again." }, else => .{ .topic = "auth", .tone = .@"error", .body = "Grok sign-in failed. The current credential is unchanged." }, } else switch (err) { error.ClientIdMissing => .{ .topic = "auth", .tone = .@"error", .body = "fx login is not configured yet. The current credential is unchanged." }, error.AccessDenied => .{ .topic = "auth", .tone = .@"error", .body = "Vercel sign-in was denied. The current credential is unchanged." }, error.ExpiredToken, error.LoginTimedOut => .{ .topic = "auth", .tone = .warning, .body = "The Vercel sign-in code expired. The current credential is unchanged; run /login to try again." }, else => .{ .topic = "auth", .tone = .@"error", .body = "Vercel sign-in failed. The current credential is unchanged." }, }; try writeAuthNotice(app, notice); }
fn writeAuthNotice(app: *App, notice: types.SemanticNotice) !void { try app.writeDomainNotice(notice, true); app.shell.render_requests.request(.first_frame); try app.flushBeforeBlockingExternalWork(); } };}
test "provider preparation waits for prompt adoption and protects admitted prompts" { const submission = @import("input_submit_runtime.zig"); const FakeApp = struct { submission: submission.State = .{} }; var app: FakeApp = .{}; try std.testing.expect(!Runtime(FakeApp).pendingPromptBlocksPreparation(&app)); try std.testing.expect(!Runtime(FakeApp).pendingPromptNeedsAdoption(&app)); app.submission.pending = .{ .draft = .{ .turn_id = 1, .prompt = &.{}, .images = &.{}, .skill_display_spans = &.{} } }; for ([_]submission.PendingPhase{ .awaiting_frame, .awaiting_adoption }) |phase| { app.submission.pending.?.phase = phase; try std.testing.expect(!Runtime(FakeApp).pendingPromptBlocksPreparation(&app)); try std.testing.expect(Runtime(FakeApp).pendingPromptNeedsAdoption(&app)); } app.submission.pending.?.phase = .adopted; try std.testing.expect(!Runtime(FakeApp).pendingPromptBlocksPreparation(&app)); app.submission.pending.?.credential_admitted = true; try std.testing.expect(Runtime(FakeApp).pendingPromptBlocksPreparation(&app)); app.submission.pending.?.credential_admitted = false; for ([_]submission.PendingPhase{.queued}) |phase| { app.submission.pending.?.phase = phase; try std.testing.expect(Runtime(FakeApp).pendingPromptBlocksPreparation(&app)); try std.testing.expect(!Runtime(FakeApp).pendingPromptNeedsAdoption(&app)); } app.submission.pending.?.phase = .awaiting_auth; try std.testing.expect(!Runtime(FakeApp).pendingPromptBlocksPreparation(&app)); try std.testing.expect(!Runtime(FakeApp).pendingPromptNeedsAdoption(&app));}
test "provider switch state machine no-ops rejects busy work and prepares only idle changes" { try std.testing.expectEqual( ProviderSwitchDecision.no_change, decideProviderSwitch(.{ .current = .gateway, .target = .gateway, .target_credential_ready = true, .intent = .manual, .stream_active = true, .queued_prompts = 2, }), ); try std.testing.expectEqual( ProviderSwitchDecision.prepare, decideProviderSwitch(.{ .current = .codex, .target = .codex, .target_credential_ready = false, .intent = .manual, .stream_active = false, .queued_prompts = 0, }), ); try std.testing.expectEqual( ProviderSwitchDecision.busy, decideProviderSwitch(.{ .current = .codex, .target = .codex, .target_credential_ready = false, .intent = .manual, .stream_active = true, .queued_prompts = 0, }), ); try std.testing.expectEqual( ProviderSwitchDecision.busy, decideProviderSwitch(.{ .current = .gateway, .target = .codex, .target_credential_ready = false, .intent = .manual, .stream_active = false, .queued_prompts = 1, }), ); try std.testing.expectEqual( ProviderSwitchDecision.prepare, decideProviderSwitch(.{ .current = .codex, .target = .codex, .target_credential_ready = true, .intent = .post_oauth, .stream_active = false, .queued_prompts = 0, }), ); try std.testing.expectEqual( ProviderSwitchDecision.busy, decideProviderSwitch(.{ .current = .codex, .target = .codex, .target_credential_ready = true, .intent = .post_oauth, .stream_active = false, .queued_prompts = 1, }), ); try std.testing.expectEqual( ProviderSwitchDecision.prepare, decideProviderSwitch(.{ .current = .gateway, .target = .codex, .target_credential_ready = false, .intent = .manual, .stream_active = false, .queued_prompts = 0, }), );}
test "post OAuth catalog selection keeps valid current then saved then first" { const entries = [_]model_catalog.ModelCatalogEntry{ .{ .id = @constCast("first"), .model_type = @constCast("language") }, .{ .id = @constCast("current"), .model_type = @constCast("language") }, .{ .id = @constCast("saved"), .model_type = @constCast("language") }, };
try std.testing.expectEqualStrings("current", selectCatalogModel(&entries, "current", "saved").?); try std.testing.expectEqualStrings("saved", selectCatalogModel(&entries, "missing", "saved").?); try std.testing.expectEqualStrings("first", selectCatalogModel(&entries, "missing", "also-missing").?); try std.testing.expect(selectCatalogModel(&.{}, "current", "saved") == null);}
const TestModelCache = struct { models_path: []const u8 = "/models", reset_count: usize = 0,
fn reset(self: *TestModelCache) void { self.reset_count += 1; }};
const BusySignInAuth = struct { start_count: usize = 0,
fn openChatGptSignInPickerFromRoot(self: *BusySignInAuth, _: std.mem.Allocator) !bool { self.start_count += 1; return true; }
fn openGrokSignInPickerFromRoot(self: *BusySignInAuth, _: std.mem.Allocator) !bool { self.start_count += 1; return true; }
fn signInBrowserUrlAlloc(_: *BusySignInAuth, _: std.mem.Allocator) !?[]u8 { return null; }};
const BusySignInApp = struct { pub const host_profile = runtime_profile.native;
alloc: std.mem.Allocator = std.testing.allocator, selected_provider: model_provider.ProviderId = .gateway, selected_model: std.ArrayList(u8) = .empty, auth: BusySignInAuth = .{}, stream: struct { active: bool = false } = .{}, worker: struct { queued_prompts: usize = 0,
fn queuedPromptCount(self: @This()) usize { return self.queued_prompts; } } = .{}, shell: struct { render_requests: TestRenderRequests = .{} } = .{}, notice_count: usize = 0, flush_count: usize = 0,
fn deinit(self: *BusySignInApp) void { self.selected_model.deinit(self.alloc); }
fn writeDomainNotice(self: *BusySignInApp, _: types.SemanticNotice, _: bool) !void { self.notice_count += 1; }
fn flushBeforeBlockingExternalWork(self: *BusySignInApp) !void { self.flush_count += 1; }
fn urlOpener(_: *BusySignInApp) host.UrlOpener { return host.unavailable_url_opener; }};
test "interactive subscription sign-in rejects active and queued work before OAuth" { const cases = [_]struct { stream_active: bool, queued_prompts: usize, }{ .{ .stream_active = true, .queued_prompts = 0 }, .{ .stream_active = false, .queued_prompts = 1 }, };
for (cases) |case| { inline for ([_]model_provider.ProviderId{ .codex, .grok }) |provider| { var app: BusySignInApp = .{}; defer app.deinit(); app.stream.active = case.stream_active; app.worker.queued_prompts = case.queued_prompts;
switch (provider) { .codex => try Runtime(BusySignInApp).beginChatGptSignIn(&app), .grok => try Runtime(BusySignInApp).beginGrokSignIn(&app), .gateway, .configured => unreachable, }
try std.testing.expectEqual(@as(usize, 0), app.auth.start_count); try std.testing.expectEqual(@as(usize, 0), app.flush_count); try std.testing.expectEqual(@as(usize, 1), app.notice_count); } }}
const TestTeam = struct { name: []const u8, slug: []const u8,};
const test_teams = [_]TestTeam{.{ .name = "Vercel Labs", .slug = "vercel-labs",}};
const TestSelectedTeam = struct { fn deinit(_: *TestSelectedTeam, _: std.mem.Allocator) void {}};
const TestTeamSelection = struct { teams: struct { items: []const TestTeam = &test_teams, } = .{}, select_count: usize = 0,
fn validationCredential( self: *const TestTeamSelection, alloc: std.mem.Allocator, index: usize, ) !credentials.Credential { if (index >= self.teams.items.len) return error.InvalidTeamSelection; return .{ .token = try alloc.dupe(u8, "candidate-token"), .source = .fx_login, .team_slug = try alloc.dupe(u8, self.teams.items[index].slug), }; }
fn select( self: *TestTeamSelection, _: std.mem.Allocator, index: usize, ) error{ InvalidTeamSelection, SessionChanged, NoSession }!TestSelectedTeam { if (index >= self.teams.items.len) return error.InvalidTeamSelection; self.select_count += 1; return .{}; }};
const TestAuth = struct { select_result: ?bool = false, sign_in_transition: login_flow.SignInTransition = .none, logout_changed: bool = false, refresh_change: auth_transition.CredentialChange = .none, refresh_error: ?anyerror = null, selected_source: ?credentials.Source = null, active_source: ?credentials.Source = .ai_gateway_api_key, onboarding_skipped: bool = false, refresh_count: usize = 0, logout_reconcile_count: usize = 0, source_inventory_refresh_count: usize = 0, credential_failure: ?auth_runtime.CredentialFailure = null, credential_notice_claimed: bool = false, picker_opened: bool = false, picker_provider: model_provider.ProviderId = .gateway, picker_closed: bool = false, gateway_ready: bool = true, catalog_ready: bool = true, gateway_ready_after_refresh_count: ?usize = null, team_selection: TestTeamSelection = .{}, sign_in_url: ?[]const u8 = null, picker_pop_count: usize = 0, sign_in_entry_active: bool = false, sign_in_start_count: usize = 0, sign_in_code_entry_active: bool = false, sign_in_code_toggle_count: usize = 0, sign_in_code_toggle_succeeds: bool = true, sign_in_code_submit_count: usize = 0, sign_in_code_submit_succeeds: bool = true, inventory_refresh_action: ?auth_runtime.InventoryRefreshAction = null, inventory_refresh_fails: bool = false, prompt_refresh_start: auth_runtime.PromptCredentialRefreshStart = .not_needed, prompt_refresh_start_count: usize = 0,
fn credentialSource(self: *const TestAuth) ?credentials.Source { return self.active_source; }
fn view(self: *const TestAuth) auth_runtime.View { return .{ .active_source = self.active_source, .available_inactive_sources = .empty, .selected_team = null, .refreshable = if (self.active_source) |source| credentials.sourceRefreshable(source) else false, .stored_key_status = .not_attempted, .fx_login_status = .not_attempted, .onboarding_skipped = self.onboarding_skipped, }; }
fn beginPromptCredentialRefresh(self: *TestAuth) auth_runtime.PromptCredentialRefreshStart { self.prompt_refresh_start_count += 1; return self.prompt_refresh_start; }
fn pollPromptCredentialRefresh(_: *TestAuth) auth_runtime.PromptCredentialRefreshPoll { return .idle; }
fn cancelPromptCredentialRefresh(_: *TestAuth) void {}
fn selectSource(self: *TestAuth, _: std.mem.Allocator, source: credentials.Source) !?bool { self.selected_source = source; if (self.select_result != null) self.active_source = source; return self.select_result; }
fn pollSignInTransition(self: *TestAuth, _: std.mem.Allocator) login_flow.SignInTransition { const transition = self.sign_in_transition; self.sign_in_transition = .none; return transition; }
fn pulseSignIn(_: *TestAuth, _: std.mem.Allocator) void {}
fn popPickerStage(self: *TestAuth, _: std.mem.Allocator) bool { self.picker_pop_count += 1; return true; }
fn signInEntryActive(self: *const TestAuth) bool { return self.sign_in_entry_active; }
fn openSignInPicker(self: *TestAuth, _: std.mem.Allocator) !bool { if (self.sign_in_entry_active) return false; self.sign_in_entry_active = true; self.sign_in_start_count += 1; return true; }
fn openSignInPickerFromRoot(self: *TestAuth, alloc: std.mem.Allocator) !bool { return self.openSignInPicker(alloc); }
fn openChatGptSignInPickerFromRoot(self: *TestAuth, alloc: std.mem.Allocator) !bool { return self.openSignInPicker(alloc); }
fn openGrokSignInPickerFromRoot(self: *TestAuth, alloc: std.mem.Allocator) !bool { return self.openSignInPicker(alloc); }
fn signInCodeEntryActive(self: *const TestAuth) bool { return self.sign_in_code_entry_active; }
fn toggleSignInCodeEntry(self: *TestAuth) bool { self.sign_in_code_toggle_count += 1; if (!self.sign_in_code_toggle_succeeds) return false; self.sign_in_code_entry_active = !self.sign_in_code_entry_active; return true; }
fn submitSignInCode(self: *TestAuth, _: std.mem.Allocator) !bool { self.sign_in_code_submit_count += 1; return self.sign_in_code_submit_succeeds; }
fn deleteSignInCodeByte(_: *TestAuth) bool { return true; }
fn appendSignInCodeByte(_: *TestAuth, _: std.mem.Allocator, _: u8) !bool { return true; }
fn teamPickerActive(_: *const TestAuth) bool { return false; }
fn deleteTeamQueryByte(_: *TestAuth) bool { return false; }
fn appendTeamQueryByte(_: *TestAuth, _: std.mem.Allocator, _: u8) !bool { return false; }
fn apiKeyEntryActive(_: *const TestAuth) bool { return false; }
fn deleteApiKeyByte(_: *TestAuth) bool { return false; }
fn appendApiKeyByte(_: *TestAuth, _: std.mem.Allocator, _: u8) !bool { return false; }
fn pickerView(_: *const TestAuth) auth_runtime.PickerView { return .{ .active = false, .available_sources = .empty, .selected_choice = null, .active_source = null, .include_skip = false, }; }
fn beginApiKeySave(_: *TestAuth, _: std.mem.Allocator) auth_runtime.ApiKeySaveStart { return .empty; }
fn openTeamPicker(_: *TestAuth, _: std.mem.Allocator, _: *login_flow.TeamSelection) void {}
fn refreshSelectedCredentialIfNeeded( self: *TestAuth, _: std.mem.Allocator, ) !auth_transition.CredentialChange { self.refresh_count += 1; if (self.refresh_error) |err| return err; if (self.gateway_ready_after_refresh_count == self.refresh_count) self.gateway_ready = true; return self.refresh_change; }
fn gatewayCredential(self: *const TestAuth) ?TestGatewayCredential { return if (self.gateway_ready) .{ .api_key = "refreshed-key" } else null; }
fn adoptPreparedCredential( self: *TestAuth, _: std.mem.Allocator, _: *credentials.Credential, ) auth_transition.CredentialChange { return self.refresh_change; }
fn modelCatalogAccess(self: *const TestAuth) credentials.CatalogAccess { return if (self.catalog_ready) credentials.catalogAccessForCredential(.fx_login, "refreshed-key", "team_123") else .{ .public_only = .fx_login_team_required }; }
fn reconcileAfterFxLoginLogout(self: *TestAuth, _: std.mem.Allocator) !bool { self.logout_reconcile_count += 1; return self.logout_changed; }
fn refreshSourceInventory(self: *TestAuth, _: std.mem.Allocator) !void { self.source_inventory_refresh_count += 1; }
fn openOnboardingPicker(self: *TestAuth, _: std.mem.Allocator) void { self.picker_opened = true; }
fn beginSourceInventoryRefresh( self: *TestAuth, _: std.mem.Allocator, action: auth_runtime.InventoryRefreshAction, ) auth_runtime.InventoryRefreshStart { if (self.inventory_refresh_action != null) return .busy; self.source_inventory_refresh_count += 1; self.inventory_refresh_action = action; return .started; }
fn takeSourceInventoryRefresh( self: *TestAuth, ) ?auth_runtime.InventoryRefreshResult { const action = self.inventory_refresh_action orelse return null; self.inventory_refresh_action = null; return if (self.inventory_refresh_fails) .{ .failed = action } else .{ .ready = action }; }
fn recordCredentialFailure( self: *TestAuth, failure: auth_runtime.CredentialFailure, options: struct { notify: bool = true }, ) bool { const same_failure = if (self.credential_failure) |current| current.source == failure.source and current.reason == failure.reason else false; if (!same_failure) { self.credential_failure = failure; self.credential_notice_claimed = false; } if (!options.notify or self.credential_notice_claimed) return false; self.credential_notice_claimed = true; return true; }
fn credentialFailure(self: *const TestAuth) ?auth_runtime.CredentialFailure { return self.credential_failure; }
fn openPickerForProvider( self: *TestAuth, _: std.mem.Allocator, provider: model_provider.ProviderId, ) void { self.picker_opened = true; self.picker_provider = provider; }
fn loadedTeamSelection(self: *TestAuth) ?*TestTeamSelection { return &self.team_selection; }
fn closePicker(self: *TestAuth, _: std.mem.Allocator) void { self.picker_closed = true; }
fn signInBrowserUrlAlloc(self: *TestAuth, alloc: std.mem.Allocator) !?[]u8 { const url = self.sign_in_url orelse return null; return try alloc.dupe(u8, url); }};
const TestGatewayCredential = struct { api_key: []const u8,};
const TestUsage = struct { refresh_count: usize = 0, clear_count: usize = 0, last_key: ?[]const u8 = null,
fn replaceReconciliationCredential( self: *TestUsage, _: std.mem.Allocator, api_key: []const u8, ) void { self.refresh_count += 1; self.last_key = api_key; }
fn clearReconciliationCredential(self: *TestUsage) void { self.clear_count += 1; self.last_key = null; }};
const TestRenderRequests = struct { footer_requested: bool = false,
fn request(self: *TestRenderRequests, _: anytype) void { self.footer_requested = true; }};
const TestUrlOpener = struct { calls: usize = 0, succeeds: bool = true, error_on_open: bool = false, opened_url: [256]u8 = undefined, opened_url_len: usize = 0,
fn opener(self: *TestUrlOpener) host.UrlOpener { return .{ .context = self, .open_fn = open, }; }
fn open( raw_context: ?*anyopaque, _: std.mem.Allocator, url: []const u8, ) host.UrlOpenError!bool { const self: *TestUrlOpener = @ptrCast(@alignCast(raw_context.?)); self.calls += 1; if (url.len > self.opened_url.len) return error.OutOfMemory; @memcpy(self.opened_url[0..url.len], url); self.opened_url_len = url.len; if (self.error_on_open) return error.OutOfMemory; return self.succeeds; }
fn openedUrl(self: *const TestUrlOpener) []const u8 { return self.opened_url[0..self.opened_url_len]; }};
const TestApp = struct { alloc: std.mem.Allocator = std.testing.allocator, submission: @import("input_submit_runtime.zig").State = .{}, selected_provider: model_provider.ProviderId = .gateway, auth: TestAuth = .{}, input_runtime: @import("../input/runtime.zig").Runtime = .{}, stream: struct { active: bool = false } = .{}, worker: struct { queued_prompts: usize = 0,
fn queuedPromptCount(self: @This()) usize { return self.queued_prompts; } } = .{}, model_cache: TestModelCache = .{}, session: struct { usage: TestUsage = .{}, } = .{}, model_cache_warmup_count: usize = 0, notice_write_count: usize = 0, transcript: std.ArrayList(u8) = .empty, test_url_opener: TestUrlOpener = .{}, preference_write_count: usize = 0, last_preference_source: ?credentials.Source = null, preference_write_succeeds: bool = true, team_catalog_accepted: bool = true, shell: struct { render_requests: TestRenderRequests = .{}, } = .{},
fn deinit(self: *TestApp) void { self.input_runtime.deinit(self.alloc); self.transcript.deinit(self.alloc); }
fn startModelCacheWarmup(self: *TestApp) void { self.model_cache_warmup_count += 1; }
fn writeTranscriptClassified(self: *TestApp, text: []const u8, _: bool, _: anytype) !void { try self.transcript.appendSlice(self.alloc, text); }
fn writeDomainNotice(self: *TestApp, notice: types.SemanticNotice, _: bool) !void { self.notice_write_count += 1; try self.transcript.appendSlice(self.alloc, notice.body); try self.transcript.append(self.alloc, '\n'); }
fn flushBeforeBlockingExternalWork(_: *TestApp) !void {}
fn urlOpener(self: *TestApp) host.UrlOpener { return self.test_url_opener.opener(); }
fn persistCredentialSourcePreference(self: *TestApp, source: credentials.Source) void { self.preference_write_count += 1; if (self.preference_write_succeeds) self.last_preference_source = source; }
fn providerCatalog(self: *TestApp, _: model_provider.ProviderId) ?model_catalog.Provider { return .{ .context = self, .fetch_fn = fetchTestCatalog }; }
fn fetchTestCatalog(raw: ?*anyopaque, _: std.mem.Allocator, _: model_catalog.FetchInput) std.mem.Allocator.Error!model_catalog.ProviderResult { const self: *TestApp = @ptrCast(@alignCast(raw.?)); if (!self.team_catalog_accepted) { return .{ .failure = .{ .category = .authentication } }; } var entries: std.ArrayList(model_catalog.ModelCatalogEntry) = .empty; errdefer model_catalog.freeModelCatalog(self.alloc, &entries); try entries.append(self.alloc, .{ .id = try self.alloc.dupe(u8, "test/model"), .model_type = try self.alloc.dupe(u8, "language"), }); return .{ .catalog = entries }; }};
test "setup hub projects the selected provider into the auth picker" { var app: TestApp = .{ .selected_provider = .codex }; defer app.deinit();
try Runtime(TestApp).openSetupHub(&app);
try std.testing.expect(!app.auth.picker_opened); try Runtime(TestApp).collectSourceInventoryFacts(&app); try std.testing.expect(app.auth.picker_opened); try std.testing.expectEqual(model_provider.ProviderId.codex, app.auth.picker_provider);}
test "login prepares the inline picker before its asynchronous inventory refresh completes" { var app: TestApp = .{ .selected_provider = .grok }; defer app.deinit();
try Runtime(TestApp).runLoginCommand(&app);
try std.testing.expectEqual(@as(usize, 1), app.auth.source_inventory_refresh_count); try std.testing.expect(!app.auth.picker_opened); const action = app.auth.inventory_refresh_action orelse return error.TestExpectedEqual; try std.testing.expectEqual(model_provider.ProviderId.grok, action.provider); try std.testing.expectEqual(auth_runtime.InventoryRefreshDestination.provider_picker_login, action.destination); try Runtime(TestApp).collectSourceInventoryFacts(&app); try std.testing.expect(!app.auth.picker_opened); try std.testing.expect(app.shell.render_requests.footer_requested);}
test "provider picker preserves type-ahead cursor undo and dismissal through inventory completion" { for ([_]bool{ false, true }) |login| { var app: TestApp = .{}; defer app.deinit(); if (login) { try Runtime(TestApp).runLoginCommand(&app); } else { try Runtime(TestApp).runProviderCommand(&app); } const prefix = if (login) picker_state.login_prefix else picker_state.provider_prefix; try std.testing.expectEqualStrings(prefix, app.input_runtime.edit_state.input.items); try app.input_runtime.insertionState().insertSlice(app.alloc, "codex", .preserve); _ = app.input_runtime.edit_state.setCursor(prefix.len + 2); app.input_runtime.picker.dismissInlinePicker(.provider);
try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expectEqualStrings(if (login) "/login codex" else "/provider codex", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(prefix.len + 2, app.input_runtime.edit_state.cursor); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.provider)); try std.testing.expect(try app.input_runtime.undoState().undo(app.alloc)); try std.testing.expectEqualStrings(prefix, app.input_runtime.edit_state.input.items); }}
test "provider inventory completion does not reclaim a changed composer" { for ([_]bool{ false, true }) |fails| { var app: TestApp = .{}; defer app.deinit(); app.auth.inventory_refresh_fails = fails; try Runtime(TestApp).runProviderCommand(&app); try app.input_runtime.textReplacementState().replace(app.alloc, "/model other"); _ = app.input_runtime.edit_state.setCursor(8);
try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expectEqualStrings("/model other", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 8), app.input_runtime.edit_state.cursor); try std.testing.expect(!app.input_runtime.picker.isInlinePickerSuppressed(.provider)); }}
test "provider picker preparation failure starts no inventory task" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); var app: TestApp = .{ .alloc = failing.allocator() }; defer app.deinit();
try std.testing.expectError(error.OutOfMemory, Runtime(TestApp).runProviderCommand(&app)); try std.testing.expectEqual(@as(usize, 0), app.auth.source_inventory_refresh_count); try std.testing.expect(app.auth.inventory_refresh_action == null);}
test "provider picker repeated opening preserves input while inventory is pending" { var app: TestApp = .{}; defer app.deinit(); try Runtime(TestApp).runProviderCommand(&app); try app.input_runtime.insertionState().insertSlice(app.alloc, "codex", .preserve);
try Runtime(TestApp).runLoginCommand(&app); try std.testing.expectEqual(@as(usize, 1), app.auth.source_inventory_refresh_count); try std.testing.expectEqualStrings("/provider codex", app.input_runtime.edit_state.input.items); try Runtime(TestApp).collectSourceInventoryFacts(&app); try std.testing.expectEqualStrings("/provider codex", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count);}
test "provider picker stays dismissed when work starts during inventory refresh" { for ([_]bool{ false, true }) |active| { var app: TestApp = .{}; defer app.deinit(); try Runtime(TestApp).runProviderCommand(&app); try app.input_runtime.insertionState().insertSlice(app.alloc, "codex", .preserve); app.stream.active = active; app.worker.queued_prompts = if (active) 0 else 1;
try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expectEqualStrings("/provider codex", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.provider)); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); }}
test "provider picker rejects active and queued work before refreshing inventory" { const cases = [_]struct { active: bool, queued: usize }{ .{ .active = true, .queued = 0 }, .{ .active = false, .queued = 1 }, }; for (cases) |case| { for ([_]bool{ false, true }) |login| { var app: TestApp = .{}; defer app.deinit(); app.stream.active = case.active; app.worker.queued_prompts = case.queued; try app.input_runtime.textReplacementState().replace(app.alloc, "existing draft");
if (login) { try Runtime(TestApp).runLoginCommand(&app); } else { try Runtime(TestApp).runProviderCommand(&app); } try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expectEqual(@as(usize, 0), app.auth.source_inventory_refresh_count); try std.testing.expect(app.auth.inventory_refresh_action == null); try std.testing.expectEqualStrings("existing draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings(provider_busy_message ++ "\n", app.transcript.items); } }}
test "provider picker rechecks work before publishing a completed refresh" { const cases = [_]struct { active: bool, queued: usize }{ .{ .active = true, .queued = 0 }, .{ .active = false, .queued = 1 }, }; for (cases) |case| { for ([_]bool{ false, true }) |login| { var app: TestApp = .{}; defer app.deinit(); if (login) { try Runtime(TestApp).runLoginCommand(&app); } else { try Runtime(TestApp).runProviderCommand(&app); } try std.testing.expectEqual(@as(usize, 1), app.auth.source_inventory_refresh_count); app.stream.active = case.active; app.worker.queued_prompts = case.queued; try app.input_runtime.textReplacementState().replace(app.alloc, "new draft");
try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expect(app.auth.inventory_refresh_action == null); try std.testing.expectEqualStrings("new draft", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings(provider_busy_message ++ "\n", app.transcript.items);
app.stream.active = false; app.worker.queued_prompts = 0; if (login) { try Runtime(TestApp).runLoginCommand(&app); } else { try Runtime(TestApp).runProviderCommand(&app); } try Runtime(TestApp).collectSourceInventoryFacts(&app); try std.testing.expectEqualStrings( if (login) picker_state.login_prefix else picker_state.provider_prefix, app.input_runtime.edit_state.input.items, ); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); } }}
test "login inventory failure leaves the picker closed and reports one error" { var app: TestApp = .{ .selected_provider = .gateway }; defer app.deinit(); app.auth.inventory_refresh_fails = true;
try Runtime(TestApp).runLoginCommand(&app); try app.input_runtime.insertionState().insertSlice(app.alloc, "codex", .preserve); try Runtime(TestApp).collectSourceInventoryFacts(&app); try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expect(!app.auth.picker_opened); try std.testing.expectEqualStrings("/login codex", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.provider)); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expect(std.mem.find( u8, app.transcript.items, "picker was not opened with stale data", ) != null);}
test "OAuth app gating accepts native auth or JS-host auth and rejects neither" { const NativeApp = struct { pub const host_profile = runtime_profile.native; }; const JsHostApp = struct { pub const host_profile = runtime_profile.wasm; }; const NeitherApp = struct { pub const host_profile = blk: { var profile = runtime_profile.wasm; profile.js_host_auth = false; break :blk profile; }; };
try std.testing.expect(oauthAuthEnabled(NativeApp)); try std.testing.expect(oauthAuthEnabled(JsHostApp)); try std.testing.expect(!oauthAuthEnabled(NeitherApp));}
test "interactive sign-in opens the owned browser URL through the host" { var app: TestApp = .{}; app.auth.sign_in_url = "https://vercel.test/verify?code=TEST-CODE";
try Runtime(TestApp).openSignInBrowser(&app);
try std.testing.expectEqual(@as(usize, 1), app.test_url_opener.calls); try std.testing.expectEqualStrings( "https://vercel.test/verify?code=TEST-CODE", app.test_url_opener.openedUrl(), );}
test "interactive sign-in routes Tab to the auth-owned manual code toggle" { var app: TestApp = .{}; defer app.deinit(); app.auth.sign_in_entry_active = true; app.auth.sign_in_url = "https://issuer.test/authorize";
try std.testing.expect(try Runtime(TestApp).routeAuthPickerByte(&app, '\t'));
try std.testing.expectEqual(@as(usize, 1), app.auth.sign_in_code_toggle_count); try std.testing.expect(app.auth.sign_in_code_entry_active); try std.testing.expectEqual(@as(usize, 0), app.test_url_opener.calls); try std.testing.expect(app.shell.render_requests.footer_requested);}
test "interactive hidden manual code paste reveals entry for the paste owner" { var app: TestApp = .{}; defer app.deinit(); app.auth.sign_in_entry_active = true;
try std.testing.expect(!Runtime(TestApp).routeAuthPickerEscapeAction(&app, .paste_start));
try std.testing.expectEqual(@as(usize, 1), app.auth.sign_in_code_toggle_count); try std.testing.expect(app.auth.sign_in_code_entry_active); try std.testing.expect(app.shell.render_requests.footer_requested);}
test "interactive sign-in without manual fallback consumes hidden paste" { var app: TestApp = .{}; defer app.deinit(); app.auth.sign_in_entry_active = true; app.auth.sign_in_code_toggle_succeeds = false;
try std.testing.expect(Runtime(TestApp).routeAuthPickerEscapeAction(&app, .paste_start));
try std.testing.expectEqual(@as(usize, 1), app.auth.sign_in_code_toggle_count); try std.testing.expect(!app.auth.sign_in_code_entry_active); try std.testing.expect(!app.shell.render_requests.footer_requested);}
test "interactive manual code entry never reopens the browser on empty submit" { var app: TestApp = .{}; defer app.deinit(); app.auth.sign_in_entry_active = true; app.auth.sign_in_code_entry_active = true; app.auth.sign_in_code_submit_succeeds = false; app.auth.sign_in_url = "https://issuer.test/authorize";
try std.testing.expect(try Runtime(TestApp).routeAuthPickerByte(&app, '\r'));
try std.testing.expectEqual(@as(usize, 1), app.auth.sign_in_code_submit_count); try std.testing.expectEqual(@as(usize, 0), app.test_url_opener.calls);}
test "interactive sign-in preserves manual fallback when the host launcher fails" { var app: TestApp = .{}; app.auth.sign_in_url = "https://vercel.test/verify"; app.test_url_opener.succeeds = false;
try Runtime(TestApp).openSignInBrowser(&app);
try std.testing.expectEqual(@as(usize, 1), app.test_url_opener.calls); try std.testing.expectEqualStrings( "https://vercel.test/verify", app.test_url_opener.openedUrl(), ); try std.testing.expectEqualStrings("https://vercel.test/verify", app.auth.sign_in_url.?);}
test "interactive sign-in frees its browser URL when the host opener errors" { var app: TestApp = .{}; app.auth.sign_in_url = "https://vercel.test/verify"; app.test_url_opener.error_on_open = true;
try std.testing.expectError( error.OutOfMemory, Runtime(TestApp).openSignInBrowser(&app), ); try std.testing.expectEqual(@as(usize, 1), app.test_url_opener.calls);}
test "auth source changes invalidate the catalog and failed selection preserves it" { var app: TestApp = .{}; const runtime = Runtime(TestApp);
app.auth.select_result = true; try std.testing.expect(try runtime.selectCredentialSource(&app, .fx_login)); try std.testing.expectEqual(credentials.Source.fx_login, app.auth.selected_source.?); try std.testing.expectEqual(@as(usize, 1), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache_warmup_count);
try std.testing.expect(try runtime.selectCredentialSource(&app, .stored_key)); try std.testing.expectEqual(credentials.Source.stored_key, app.auth.selected_source.?); try std.testing.expectEqual(@as(usize, 2), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 2), app.model_cache_warmup_count); try std.testing.expectEqual(@as(usize, 2), app.session.usage.refresh_count); try std.testing.expectEqualStrings( "refreshed-key", app.session.usage.last_key.?, );
app.auth.select_result = null; try std.testing.expect(!try runtime.selectCredentialSource(&app, .ai_gateway_api_key)); try std.testing.expectEqual(credentials.Source.stored_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 2), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 2), app.model_cache_warmup_count);}
test "VT-4 unavailable picker source preserves active source and reports unavailability" { var app: TestApp = .{}; defer app.deinit(); app.auth.select_result = null;
try std.testing.expect(!try Runtime(TestApp).applySourceChoice(&app, .stored_key));
try std.testing.expectEqual(credentials.Source.ai_gateway_api_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings( "That credential is no longer available. The current source is unchanged.\n", app.transcript.items, );}
test "completed credential switch emits exactly one transcript line" { var app: TestApp = .{}; defer app.deinit(); app.auth.select_result = true;
try std.testing.expect(try Runtime(TestApp).applySourceChoice(&app, .stored_key));
try std.testing.expectEqual(credentials.Source.stored_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 1), app.preference_write_count); try std.testing.expectEqual(credentials.Source.stored_key, app.last_preference_source.?); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); const expected = try std.fmt.allocPrint( app.alloc, "Switched credential to {s}.\n", .{credentials.sourceLabel(.stored_key)}, ); defer app.alloc.free(expected); try std.testing.expectEqualStrings(expected, app.transcript.items);}
test "team change from an environment source activates and remembers fx login" { var app: TestApp = .{}; defer app.deinit(); app.auth.select_result = true;
try std.testing.expect(try Runtime(TestApp).applyTeamChoice(&app, 0));
try std.testing.expectEqual(@as(usize, 1), app.auth.team_selection.select_count); try std.testing.expectEqual(credentials.Source.fx_login, app.auth.active_source.?); try std.testing.expectEqual(credentials.Source.fx_login, app.auth.selected_source.?); try std.testing.expectEqual(@as(usize, 1), app.preference_write_count); try std.testing.expectEqual(credentials.Source.fx_login, app.last_preference_source.?); try std.testing.expect(app.auth.picker_closed); try std.testing.expectEqual(@as(usize, 1), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache_warmup_count); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings( "Changed Vercel team to Vercel Labs (vercel-labs).\n", app.transcript.items, );}
test "team change on an active fx login reloads and remembers the selected credential" { var app: TestApp = .{}; defer app.deinit(); app.auth.active_source = .fx_login;
try std.testing.expect(try Runtime(TestApp).applyTeamChoice(&app, 0));
try std.testing.expectEqual(credentials.Source.fx_login, app.auth.selected_source.?); try std.testing.expectEqual(@as(usize, 1), app.preference_write_count); try std.testing.expectEqual(credentials.Source.fx_login, app.last_preference_source.?);}
test "teamless direct login does not activate or persist an unvalidated authority" { var app: TestApp = .{}; defer app.deinit(); app.auth.select_result = true; app.auth.sign_in_transition = .{ .succeeded = .{ .vercel = .{} } };
try Runtime(TestApp).collectSignInFacts(&app);
try std.testing.expectEqual(credentials.Source.ai_gateway_api_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 0), app.preference_write_count); try std.testing.expectEqual(@as(?credentials.Source, null), app.last_preference_source); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expect(std.mem.find(u8, app.transcript.items, "no Vercel teams") != null);}
test "failed preference persistence keeps a validated team credential active" { var app: TestApp = .{}; defer app.deinit(); app.preference_write_succeeds = false;
_ = try Runtime(TestApp).applyTeamChoice(&app, 0);
try std.testing.expectEqual(credentials.Source.fx_login, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 1), app.preference_write_count); try std.testing.expectEqual(@as(?credentials.Source, null), app.last_preference_source);}
test "successful API key save persists even when the live credential is unchanged" { var app: TestApp = .{}; defer app.deinit(); app.auth.active_source = .stored_key;
try Runtime(TestApp).applyApiKeySaveResult(&app, .{ .saved = false });
try std.testing.expectEqual(credentials.Source.stored_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 1), app.preference_write_count); try std.testing.expectEqual(credentials.Source.stored_key, app.last_preference_source.?);}
test "successful API key save remembers the newly active stored key" { var app: TestApp = .{}; defer app.deinit(); app.auth.active_source = .stored_key;
try Runtime(TestApp).applyApiKeySaveResult(&app, .{ .saved = true });
try std.testing.expectEqual(credentials.Source.stored_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 1), app.preference_write_count); try std.testing.expectEqual(credentials.Source.stored_key, app.last_preference_source.?); try std.testing.expectEqual(@as(usize, 1), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache_warmup_count);}
test "cancelled login and rejected API key do not persist a source" { var app: TestApp = .{}; defer app.deinit(); app.auth.sign_in_transition = .cancelled;
try Runtime(TestApp).collectSignInFacts(&app); try Runtime(TestApp).applyApiKeySaveResult(&app, .gateway_refused);
try std.testing.expectEqual(@as(usize, 0), app.preference_write_count); try std.testing.expectEqual(credentials.Source.ai_gateway_api_key, app.auth.active_source.?);}
test "team source load failure preserves the environment source and preference" { var app: TestApp = .{}; defer app.deinit(); app.auth.select_result = null;
try std.testing.expect(!try Runtime(TestApp).applyTeamChoice(&app, 0));
try std.testing.expectEqual(credentials.Source.ai_gateway_api_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 0), app.preference_write_count); try std.testing.expect(app.auth.picker_closed); try std.testing.expect(std.mem.find(u8, app.transcript.items, "could not be loaded") != null);}
test "team catalog rejection preserves the previous authority before commit" { var app: TestApp = .{ .team_catalog_accepted = false }; defer app.deinit();
_ = try Runtime(TestApp).applyTeamChoice(&app, 0);
try std.testing.expectEqual(@as(usize, 0), app.auth.team_selection.select_count); try std.testing.expectEqual(credentials.Source.ai_gateway_api_key, app.auth.active_source.?); try std.testing.expectEqual(@as(usize, 0), app.preference_write_count); try std.testing.expect(std.mem.find(u8, app.transcript.items, "could not be validated") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Changed Vercel team") == null);}
test "prompt credential prewarm ignores the previous provider credential" { const ProviderApp = struct { selected_model: std.ArrayList(u8) = .empty, selected_provider: model_provider.ProviderId, auth: TestAuth = .{}, }; const cases = .{ .{ model_provider.ProviderId.gateway, credentials.Source.chatgpt_subscription, credentials.Source.fx_login }, .{ model_provider.ProviderId.codex, credentials.Source.fx_login, credentials.Source.chatgpt_subscription }, .{ model_provider.ProviderId.grok, credentials.Source.fx_login, credentials.Source.grok_subscription }, }; inline for (cases) |case| { var app = ProviderApp{ .selected_provider = case[0] }; app.auth.active_source = case[1]; Runtime(ProviderApp).startPromptCredentialPrewarm(&app); try std.testing.expectEqual(@as(usize, 0), app.auth.prompt_refresh_start_count); app.auth.active_source = case[2]; Runtime(ProviderApp).startPromptCredentialPrewarm(&app); try std.testing.expectEqual(@as(usize, 1), app.auth.prompt_refresh_start_count); }}
test "prompt credential refresh preserves catalog for secret rotation" { var app: TestApp = .{}; defer app.deinit(); const runtime = Runtime(TestApp);
try runtime.refreshFxLoginCredentialIfNeeded(&app); try std.testing.expectEqual(@as(usize, 1), app.auth.refresh_count); try std.testing.expectEqual(@as(usize, 0), app.model_cache.reset_count);
app.auth.refresh_change = .secret_only; try runtime.refreshFxLoginCredentialIfNeeded(&app); try std.testing.expectEqual(@as(usize, 2), app.auth.refresh_count); try std.testing.expectEqual(@as(usize, 0), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache_warmup_count); try std.testing.expectEqual(@as(usize, 1), app.session.usage.refresh_count);
app.auth.refresh_change = .authority; try runtime.refreshFxLoginCredentialIfNeeded(&app); try std.testing.expectEqual(@as(usize, 3), app.auth.refresh_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 2), app.model_cache_warmup_count); try std.testing.expectEqual(@as(usize, 2), app.session.usage.refresh_count);}
test "credential removal clears the reconciliation credential" { var app: TestApp = .{}; app.auth.gateway_ready = false;
Runtime(TestApp).applyCredentialChange(&app, true);
try std.testing.expectEqual(@as(usize, 1), app.session.usage.clear_count); try std.testing.expectEqual(@as(usize, 0), app.session.usage.refresh_count);}
test "logout result reconciles live auth and renders only sanitized notices" { var app: TestApp = .{}; defer app.deinit(); app.auth.logout_changed = true;
try Runtime(TestApp).applyLogoutResult(&app, .{ .session_deleted = true, .remote_revocation_failed = true, });
try std.testing.expectEqual(@as(usize, 1), app.auth.logout_reconcile_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache.reset_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache_warmup_count); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Signed out of fx.") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, login_flow.remote_revocation_warning) != null); for ([_][]const u8{ "access-secret", "refresh-secret", "RemoteRevokeFailed", "https://issuer.example" }) |detail| { try std.testing.expect(std.mem.find(u8, app.transcript.items, detail) == null); }}
test "logout durability failure still reconciles live auth" { var app: TestApp = .{}; defer app.deinit(); app.auth.logout_changed = true;
try Runtime(TestApp).applyLogoutResult(&app, .{ .session_deleted = true, .local_durability_failed = true, .remote_revocation_failed = true, });
try std.testing.expectEqual(@as(usize, 1), app.auth.logout_reconcile_count); try std.testing.expectEqual(@as(usize, 1), app.model_cache.reset_count); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Could not confirm durable fx logout.") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, login_flow.remote_revocation_warning) != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "current source is unchanged") == null);}
test "prompt credential refresh failure is recoverable and detail-free" { var app: TestApp = .{}; defer app.deinit(); app.auth.refresh_error = error.OAuthRequestFailed;
try std.testing.expect(!try Runtime(TestApp).preparePromptCredential(&app)); try std.testing.expect(std.mem.find(u8, app.transcript.items, "fx login credential refresh failed.") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "press enter to retry.") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Your prompt is saved.") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Choose another source") == null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "OAuthRequestFailed") == null); try std.testing.expect(app.shell.render_requests.footer_requested); try std.testing.expectEqual(@as(usize, 0), app.auth.source_inventory_refresh_count); try std.testing.expect(app.auth.credential_failure == null); try std.testing.expect(!app.auth.picker_opened); try std.testing.expectEqual(@as(usize, 0), app.model_cache.reset_count);}
test "permanent prompt credential failure is one repair episode" { var app: TestApp = .{}; defer app.deinit(); app.auth.active_source = .fx_login; app.auth.refresh_error = error.InvalidGrant;
try std.testing.expect(!try Runtime(TestApp).preparePromptCredential(&app)); try std.testing.expect(!try Runtime(TestApp).preparePromptCredential(&app));
try std.testing.expectEqual(@as(usize, 1), app.auth.refresh_count); try std.testing.expectEqual(@as(usize, 1), app.auth.sign_in_start_count); try std.testing.expect(app.auth.sign_in_entry_active); try std.testing.expectEqual( auth_runtime.CredentialFailureReason.invalid_credential, app.auth.credential_failure.?.reason, ); try std.testing.expectEqual( @as(usize, 1), std.mem.count(u8, app.transcript.items, "fx login sign-in expired."), ); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Your prompt is saved.") != null);}
test "prompt credential admission retries a crossed readiness deadline" { var app: TestApp = .{}; defer app.deinit(); app.auth.gateway_ready = false; app.auth.gateway_ready_after_refresh_count = 2;
try std.testing.expect(try Runtime(TestApp).preparePromptCredential(&app)); try std.testing.expectEqual(@as(usize, 2), app.auth.refresh_count); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expect(!app.auth.picker_opened);}
test "prompt credential admission rejects a credential that remains unavailable" { var app: TestApp = .{}; defer app.deinit(); app.auth.gateway_ready = false;
try std.testing.expect(!try Runtime(TestApp).preparePromptCredential(&app)); try std.testing.expectEqual(@as(usize, 2), app.auth.refresh_count); try std.testing.expect(std.mem.find(u8, app.transcript.items, "fx login sign-in expired.") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "press enter to sign in again.") != null); try std.testing.expect(std.mem.find(u8, app.transcript.items, "Your prompt is saved.") != null); try std.testing.expect(!app.auth.picker_opened);}
test "prompt credential refresh allows only OutOfMemory to escape" { var app: TestApp = .{}; defer app.deinit(); app.auth.refresh_error = error.OutOfMemory;
try std.testing.expectError(error.OutOfMemory, Runtime(TestApp).preparePromptCredential(&app)); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expect(!app.shell.render_requests.footer_requested); try std.testing.expectEqual(@as(usize, 0), app.auth.source_inventory_refresh_count); try std.testing.expect(!app.auth.picker_opened);}
test "manual compaction missing credentials leave transcript feedback to its owner" { for ([_]model_provider.ProviderId{ .gateway, .codex, .grok }) |provider| { for ([_]bool{ false, true }) |for_compaction| { var app: TestApp = .{}; defer app.deinit(); app.auth.active_source = null; app.auth.onboarding_skipped = true; app.submission.compaction_pending = for_compaction;
try std.testing.expect(!try Runtime(TestApp).missingPromptCredential(&app, provider)); try std.testing.expectEqual(@as(usize, if (for_compaction) 0 else 1), app.notice_write_count); try std.testing.expectEqual(for_compaction, app.transcript.items.len == 0); try std.testing.expect(!app.auth.picker_opened); } }}
test "manual compaction missing credentials preserves onboarding when it is enabled" { var app: TestApp = .{}; defer app.deinit(); app.auth.active_source = null; app.submission.compaction_pending = true;
try std.testing.expect(!try Runtime(TestApp).missingPromptCredential(&app, .gateway)); try std.testing.expect(app.auth.picker_opened); try std.testing.expectEqual(@as(usize, 1), app.auth.source_inventory_refresh_count); try std.testing.expectEqual(@as(usize, 0), app.notice_write_count);}
test "manual compaction credential failure leaves feedback to its lifecycle owner" { var app: TestApp = .{}; defer app.deinit(); app.submission.compaction_pending = true; app.auth.active_source = .fx_login; try std.testing.expect(!try Runtime(TestApp).recoverCredentialFailure(&app, .fx_login, error.CredentialRefreshUnavailable)); try std.testing.expect(app.auth.credential_failure != null); try std.testing.expectEqual(@as(usize, 0), app.notice_write_count); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len);}
test "compaction credential failure preserves the first ordinary recovery notice" { const AuthApp = struct { alloc: std.mem.Allocator = std.testing.allocator, auth: auth_runtime.Runtime = .{}, submission: @import("input_submit_runtime.zig").State = .{}, shell: struct { render_requests: TestRenderRequests = .{} } = .{}, notice_write_count: usize = 0, transcript: std.ArrayList(u8) = .empty,
fn writeDomainNotice(self: *@This(), notice: types.SemanticNotice, _: bool) !void { try std.testing.expectEqualStrings("auth", notice.topic); try self.transcript.appendSlice(self.alloc, notice.body); self.notice_write_count += 1; } }; var app: AuthApp = .{}; defer app.auth.deinit(app.alloc); defer app.transcript.deinit(app.alloc); var credential: credentials.Credential = .{ .token = try app.alloc.dupe(u8, "login-token"), .source = .fx_login, }; defer credential.deinit(app.alloc); _ = app.auth.adoptCredential(app.alloc, &credential); const runtime = Runtime(AuthApp); const failure = auth_runtime.classifyCredentialFailure(.fx_login, error.OAuthRequestFailed);
app.submission.compaction_pending = true; try std.testing.expect(!try runtime.recoverCredentialFailure(&app, .fx_login, error.OAuthRequestFailed)); try std.testing.expectEqual(@as(usize, 0), app.notice_write_count); try std.testing.expectEqual(@as(usize, 0), app.transcript.items.len); try std.testing.expectEqual(failure, app.auth.credentialFailure().?); try std.testing.expectEqual( credentials.CatalogPublicOnlyReason.credential_refresh_failed, app.auth.modelCatalogAccess().publicOnlyReason().?, );
app.submission.compaction_pending = false; app.auth.cancelPromptCredentialRefresh(); app.submission.pending = .{ .draft = .{ .turn_id = 1, .prompt = try app.alloc.dupe(u8, "keep this ordinary prompt"), .images = &.{}, .skill_display_spans = &.{}, }, .phase = .awaiting_auth, }; defer app.submission.pending.?.deinit(app.alloc); try std.testing.expect(!try runtime.recoverCredentialFailure(&app, .fx_login, error.OAuthRequestFailed)); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count); try std.testing.expectEqualStrings( "fx login credential refresh failed.\npress enter to retry. Your prompt is saved.", app.transcript.items, ); try std.testing.expectEqualStrings("keep this ordinary prompt", app.submission.pending.?.draft.prompt); try std.testing.expectEqual(@as(u64, 1), app.submission.pending.?.draft.turn_id); try std.testing.expectEqual(.awaiting_auth, app.submission.pending.?.phase); try std.testing.expectEqual(failure, app.auth.credentialFailure().?); try std.testing.expect(app.shell.render_requests.footer_requested);
try std.testing.expect(!try runtime.recoverCredentialFailure(&app, .fx_login, error.OAuthRequestFailed)); try std.testing.expectEqual(@as(usize, 1), app.notice_write_count);}
test "prompt credential refresh falls back when its task cannot start" { var app: TestApp = .{}; defer app.deinit(); app.auth.active_source = .fx_login; app.auth.prompt_refresh_start = .failed; app.auth.gateway_ready = false; app.auth.gateway_ready_after_refresh_count = 1;
try std.testing.expectEqual( PendingPromptCredentialReadiness.current, try Runtime(TestApp).collectPendingPromptCredential(&app), ); try std.testing.expectEqual(@as(usize, 1), app.auth.refresh_count);
app.auth.gateway_ready = false; app.auth.gateway_ready_after_refresh_count = 2; try std.testing.expectEqual( PendingPromptCredentialReadiness.current, try Runtime(TestApp).retryPendingPromptCredential(&app), ); try std.testing.expectEqual(@as(usize, 2), app.auth.refresh_count);}