4141from surfaces .cli .wizard .azure_openai import (
4242 choose_provider_model ,
4343)
44- from surfaces .cli .wizard .config import PROJECT_ENV_PATH , ProviderOption
44+ from surfaces .cli .wizard .config import PROJECT_ENV_PATH , ProviderOption , WizardCredentialKind
4545from surfaces .cli .wizard .endpoint_prompt import (
4646 ensure_endpoint_settings as ensure_provider_endpoint_settings ,
4747)
5151#: What became of the credential the wizard just collected. ``unsaved`` outranks
5252#: ``unverified``: a credential that never landed anywhere is the more urgent thing
5353#: to say on the summary screen.
54- CredentialState = Literal ["ok" , "unverified" , "unsaved" ]
55- CredentialOutcome = Literal ["ok" , "unverified" , "unsaved" , "repick" , "cancel" ]
54+ CredentialState = Literal ["ok" , "unverified" , "unsaved" , "deferred" ]
55+ CredentialOutcome = Literal ["ok" , "unverified" , "unsaved" , "deferred" , " repick" , "cancel" ]
5656
5757# Recovery/outcome vocabulary. These are the byte-identical string values the wizard
5858# menus, ``_choose`` defaults, and ``run_wizard`` branches all resolve against; named
6868OK : Final = "ok"
6969UNSAVED : Final = "unsaved"
7070UNVERIFIED : Final = "unverified"
71+ #: The user has no key to hand. Onboarding finishes; the key is added later with
72+ #: ``opensre auth login <provider>`` or the provider env var. One value for both
73+ #: the prompt outcome and the recorded state, as with ``unsaved``/``unverified``.
74+ DEFERRED : Final = "deferred"
7175
7276_LLM_CREDENTIAL_MAX_ATTEMPTS = 10 # mirrors _run_cli_llm_onboarding's retry budget
7377
@@ -113,14 +117,16 @@ def _credential_line_for_saved_summary(
113117 if provider .value == "openai" :
114118 return "OpenAI OAuth tokens (Codex CLI)"
115119 return f"{ _provider_choice_label (provider )} OAuth session"
116- if provider .credential_kind == "host" :
120+ if provider .credential_kind == WizardCredentialKind . HOST :
117121 # A ``host`` credential (e.g. the Ollama host URL) is written to the project
118122 # ``.env`` by ``_persist_llm_credential``, never the keyring — the summary must
119123 # name .env, not the system keychain, in both the verified and unverified cases.
120124 if credential_state == UNVERIFIED :
121125 return "project .env (unverified)"
122126 return "project .env"
123- if provider .credential_kind != "cli" :
127+ if provider .credential_kind != WizardCredentialKind .CLI :
128+ if credential_state == DEFERRED :
129+ return f"not set yet — run `opensre auth login { provider .value } ` when you have a key"
124130 if credential_state == UNSAVED :
125131 return "not saved — re-enter next run"
126132 if credential_state == UNVERIFIED :
@@ -135,7 +141,7 @@ def _credential_line_for_saved_summary(
135141def _persist_llm_credential (provider : ProviderOption , value : str ) -> bool :
136142 """Persist one prompted credential where the runtime will actually read it.
137143
138- ``credential_kind == "host" `` values (e.g. the Ollama host URL) are plain
144+ ``credential_kind == WizardCredentialKind.HOST `` values (e.g. the Ollama host URL) are plain
139145 runtime configuration, not secrets: the runtime resolves them from the
140146 environment only, never the keyring, so they belong in the project ``.env``.
141147 Everything else keeps the keyring path.
@@ -145,7 +151,7 @@ def _persist_llm_credential(provider: ProviderOption, value: str) -> bool:
145151 (permission denied, read-only fs, full disk) fails soft exactly like a keyring
146152 failure — it never propagates and crashes onboarding (#3591).
147153 """
148- if provider .credential_kind == "host" :
154+ if provider .credential_kind == WizardCredentialKind . HOST :
149155 try :
150156 sync_env_values ({provider .api_key_env : value })
151157 except OSError as exc :
@@ -249,7 +255,7 @@ def _persist_llm_credential_with_recovery(
249255 why this is also correct at the legacy-key migration site, where there is no
250256 prompt to return to.
251257
252- Reachable for ``credential_kind == "host" `` only when the ``.env`` write fails:
258+ Reachable for ``credential_kind == WizardCredentialKind.HOST `` only when the ``.env`` write fails:
253259 ``_persist_llm_credential`` returns ``False`` on an ``OSError`` write error, so the
254260 host lands on the same recovery menu the keyring path uses (#3591). A successful
255261 host write returns ``True`` and never reaches this menu.
@@ -353,15 +359,22 @@ def _prompt_validated_llm_credential(
353359 for _attempt in range (_LLM_CREDENTIAL_MAX_ATTEMPTS ):
354360 try :
355361 value = _prompt_value (
356- f"{ credential_display } ({ env_key } )" ,
362+ f"{ credential_display } ({ env_key } ) — leave blank to set up later " ,
357363 # Only a ``host`` credential may be pre-filled. _prompt_value returns the
358364 # default on empty input, and a secret provider's credential_default is a
359365 # placeholder (Azure's is an endpoint URL) — offering it would let a bare
360366 # Enter persist a URL as the API key.
361- default = provider .credential_default if provider .credential_kind == "host" else "" ,
367+ default = provider .credential_default
368+ if provider .credential_kind == WizardCredentialKind .HOST
369+ else "" ,
362370 secret = provider .credential_secret ,
371+ # A blank answer is "I do not have this yet", not a mistake to
372+ # re-prompt: this was the only step that could end onboarding.
373+ allow_empty = provider .credential_kind != WizardCredentialKind .HOST ,
363374 back_on_cancel = True ,
364375 )
376+ if not value :
377+ return DEFERRED , model
365378 except WizardBack : # must precede KeyboardInterrupt: WizardBack subclasses it
366379 return REPICK , model
367380 except KeyboardInterrupt :
0 commit comments