Skip to content

Commit a5f0dbe

Browse files
committed
fix(auth): hide persistence diagnostics from events
1 parent d810a43 commit a5f0dbe

8 files changed

Lines changed: 200 additions & 9 deletions

File tree

src/pythinker_code/auth/copilot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ async def login_copilot(config: Config, *, open_browser: bool = True) -> AsyncIt
272272
)
273273
except Exception as exc:
274274
logger.warning("Failed to persist GitHub Copilot login: {exc}", exc=exc)
275-
yield OAuthEvent("error", f"Failed to save GitHub Copilot login: {exc}")
275+
yield OAuthEvent("error", "Failed to save GitHub Copilot login.")
276276
return
277277
yield OAuthEvent(
278278
"success",
@@ -300,6 +300,6 @@ def _remove(cfg: Config) -> None:
300300
await persist_logout(config, _copilot_oauth_ref(), _remove)
301301
except Exception as exc:
302302
logger.warning("Failed to persist GitHub Copilot logout: {exc}", exc=exc)
303-
yield OAuthEvent("error", f"Failed to log out of GitHub Copilot: {exc}")
303+
yield OAuthEvent("error", "Failed to log out of GitHub Copilot.")
304304
return
305305
yield OAuthEvent("success", "Logged out of GitHub Copilot successfully.")

src/pythinker_code/auth/digitalocean.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ async def login_digitalocean(
189189
)
190190
except Exception as exc:
191191
logger.warning("Failed to persist DigitalOcean login: {exc}", exc=exc)
192-
yield OAuthEvent("error", f"Failed to save DigitalOcean login: {exc}")
192+
yield OAuthEvent("error", "Failed to save DigitalOcean login.")
193193
return
194194

195195
message = _router_status_message(catalog.status)
@@ -222,6 +222,6 @@ def _remove(cfg: Config) -> None:
222222
await persist_config_change(config, _remove)
223223
except Exception as exc:
224224
logger.warning("Failed to persist DigitalOcean logout: {exc}", exc=exc)
225-
yield OAuthEvent("error", f"Failed to log out of DigitalOcean: {exc}")
225+
yield OAuthEvent("error", "Failed to log out of DigitalOcean.")
226226
return
227227
yield OAuthEvent("success", "Logged out of DigitalOcean successfully.")

src/pythinker_code/auth/snowflake.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ async def login_snowflake(
330330
)
331331
except Exception as exc:
332332
logger.warning("Failed to persist Snowflake Cortex login: {exc}", exc=exc)
333-
yield OAuthEvent("error", f"Failed to save Snowflake Cortex login: {exc}")
333+
yield OAuthEvent("error", "Failed to save Snowflake Cortex login.")
334334
return
335335

336336
yield OAuthEvent(
@@ -367,6 +367,6 @@ def _remove(cfg: Config) -> None:
367367
await persist_config_change(config, _remove)
368368
except Exception as exc:
369369
logger.warning("Failed to persist Snowflake Cortex logout: {exc}", exc=exc)
370-
yield OAuthEvent("error", f"Failed to log out of Snowflake Cortex: {exc}")
370+
yield OAuthEvent("error", "Failed to log out of Snowflake Cortex.")
371371
return
372372
yield OAuthEvent("success", "Logged out of Snowflake Cortex successfully.")

src/pythinker_code/auth/xai.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ async def login_xai_browser(
250250
)
251251
except Exception as exc:
252252
logger.warning("Failed to persist xAI Grok login: {exc}", exc=exc)
253-
yield OAuthEvent("error", f"Failed to save xAI Grok login: {exc}")
253+
yield OAuthEvent("error", "Failed to save xAI Grok login.")
254254
return
255255
yield OAuthEvent("success", f"xAI Grok configured with model {config.default_model}.")
256256

@@ -310,7 +310,7 @@ async def login_xai_headless(config: Config) -> AsyncIterator[OAuthEvent]:
310310
)
311311
except Exception as exc:
312312
logger.warning("Failed to persist xAI Grok login: {exc}", exc=exc)
313-
yield OAuthEvent("error", f"Failed to save xAI Grok login: {exc}")
313+
yield OAuthEvent("error", "Failed to save xAI Grok login.")
314314
return
315315
yield OAuthEvent("success", f"xAI Grok configured with model {config.default_model}.")
316316

@@ -335,6 +335,6 @@ def _remove(cfg: Config) -> None:
335335
await persist_logout(config, _xai_oauth_ref(), _remove)
336336
except Exception as exc:
337337
logger.warning("Failed to persist xAI Grok logout: {exc}", exc=exc)
338-
yield OAuthEvent("error", f"Failed to log out of xAI Grok: {exc}")
338+
yield OAuthEvent("error", "Failed to log out of xAI Grok.")
339339
return
340340
yield OAuthEvent("success", "Logged out of xAI Grok successfully.")

tests/auth/test_copilot_auth.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,55 @@ async def fake_refresh_copilot_token(github_token: str) -> OAuthToken:
116116
assert {model.provider for model in config.models.values()} == {"managed:copilot"}
117117

118118

119+
@pytest.mark.asyncio
120+
async def test_copilot_persistence_errors_hide_internal_details(
121+
monkeypatch: pytest.MonkeyPatch,
122+
) -> None:
123+
from pythinker_code.auth import copilot
124+
125+
diagnostic = "permission denied at /private/credentials/github-copilot.json"
126+
config = Config(is_from_default_location=True)
127+
128+
async def fake_request_device_code(**_kwargs: Any) -> DeviceCode:
129+
return DeviceCode(
130+
user_code="ABCD-EFGH",
131+
verification_uri="https://github.com/login/device",
132+
device_code="device-secret",
133+
interval=5,
134+
expires_in=900,
135+
)
136+
137+
async def fake_poll_device_token(**_kwargs: Any) -> dict[str, Any]:
138+
return {"access_token": "github-oauth-token"}
139+
140+
async def fake_refresh_copilot_token(_github_token: str) -> OAuthToken:
141+
return OAuthToken.from_response(
142+
{
143+
"access_token": "copilot-bearer",
144+
"refresh_token": "github-oauth-token",
145+
"expires_in": 1500,
146+
}
147+
)
148+
149+
async def fail_persistence(*_args: object, **_kwargs: object) -> None:
150+
raise OSError(diagnostic)
151+
152+
monkeypatch.setattr(copilot, "request_device_code", fake_request_device_code)
153+
monkeypatch.setattr(copilot, "poll_device_token", fake_poll_device_token)
154+
monkeypatch.setattr(copilot, "refresh_copilot_token", fake_refresh_copilot_token)
155+
monkeypatch.setattr(copilot, "persist_login", fail_persistence)
156+
monkeypatch.setattr(copilot, "persist_logout", fail_persistence)
157+
_mock_unavailable_catalog(monkeypatch)
158+
159+
login_events = [event async for event in copilot.login_copilot(config, open_browser=False)]
160+
logout_events = [event async for event in copilot.logout_copilot(config)]
161+
162+
assert login_events[-1].message == "Failed to save GitHub Copilot login."
163+
assert logout_events[-1].message == "Failed to log out of GitHub Copilot."
164+
assert diagnostic not in login_events[-1].json
165+
assert diagnostic not in logout_events[-1].json
166+
167+
119168
class _ExchangeResponse:
120169
def __init__(self, status: int, payload: object) -> None:
121170
self.status = status

tests/auth/test_digitalocean_auth.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,38 @@ async def fake_implicit_flow(**kwargs: Any) -> ImplicitAuthorization:
213213
assert (tmp_path / "config.toml").exists()
214214

215215

216+
@pytest.mark.asyncio
217+
async def test_digitalocean_persistence_errors_hide_internal_details(
218+
monkeypatch: pytest.MonkeyPatch,
219+
) -> None:
220+
from pythinker_code.auth import digitalocean as do
221+
from pythinker_code.auth.oauth_flows import ImplicitAuthorization
222+
223+
diagnostic = "permission denied at /private/config.toml"
224+
config = Config(is_from_default_location=True)
225+
226+
async def fake_implicit_flow(**_kwargs: Any) -> ImplicitAuthorization:
227+
return ImplicitAuthorization("access-token", None, "state")
228+
229+
async def fake_catalog(_access_token: str) -> do.RouterCatalog:
230+
return do.RouterCatalog(do.RouterDiscovery.OK, ("primary",))
231+
232+
async def fail_persistence(*_args: object, **_kwargs: object) -> None:
233+
raise OSError(diagnostic)
234+
235+
monkeypatch.setattr(do, "run_loopback_implicit_flow", fake_implicit_flow)
236+
monkeypatch.setattr(do, "_fetch_router_catalog", fake_catalog)
237+
monkeypatch.setattr(do, "persist_config_change", fail_persistence)
238+
239+
login_events = [event async for event in do.login_digitalocean(config)]
240+
logout_events = [event async for event in do.logout_digitalocean(config)]
241+
242+
assert login_events[-1].message == "Failed to save DigitalOcean login."
243+
assert logout_events[-1].message == "Failed to log out of DigitalOcean."
244+
assert diagnostic not in login_events[-1].json
245+
assert diagnostic not in logout_events[-1].json
246+
247+
216248
@pytest.mark.asyncio
217249
@pytest.mark.parametrize(
218250
"response",

tests/auth/test_snowflake_auth.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,51 @@ async def fake_exchange(
294294
assert "snowflake-refresh-secret" not in rendered_events
295295

296296

297+
@pytest.mark.asyncio
298+
async def test_snowflake_persistence_errors_hide_internal_details(
299+
monkeypatch: pytest.MonkeyPatch,
300+
) -> None:
301+
from pythinker_code.auth import snowflake
302+
303+
diagnostic = "permission denied at /private/credentials/snowflake.json"
304+
config = Config(is_from_default_location=True)
305+
306+
async def fake_loopback(**_kwargs: Any) -> LoopbackAuthorization:
307+
return LoopbackAuthorization("auth-code", "verifier", "http://127.0.0.1:49231/")
308+
309+
async def fake_exchange(
310+
_account: str,
311+
_code: str,
312+
_code_verifier: str,
313+
_redirect_uri: str,
314+
) -> dict[str, Any]:
315+
return {
316+
"access_token": "snowflake-access",
317+
"refresh_token": "snowflake-refresh",
318+
"expires_in": 600,
319+
}
320+
321+
async def fake_models() -> tuple[snowflake.SnowflakeModel, ...]:
322+
return snowflake.SNOWFLAKE_MODELS
323+
324+
async def fail_persistence(*_args: object, **_kwargs: object) -> None:
325+
raise OSError(diagnostic)
326+
327+
monkeypatch.setattr(snowflake, "run_loopback_pkce_flow", fake_loopback)
328+
monkeypatch.setattr(snowflake, "_exchange_code_for_tokens", fake_exchange)
329+
monkeypatch.setattr(snowflake, "_discover_snowflake_models", fake_models)
330+
monkeypatch.setattr(snowflake, "persist_login", fail_persistence)
331+
monkeypatch.setattr(snowflake, "persist_config_change", fail_persistence)
332+
333+
login_events = [event async for event in snowflake.login_snowflake(config, "myorg-acct")]
334+
logout_events = [event async for event in snowflake.logout_snowflake(config)]
335+
336+
assert login_events[-1].message == "Failed to save Snowflake Cortex login."
337+
assert logout_events[-1].message == "Failed to log out of Snowflake Cortex."
338+
assert diagnostic not in login_events[-1].json
339+
assert diagnostic not in logout_events[-1].json
340+
341+
297342
@pytest.mark.asyncio
298343
async def test_login_snowflake_fails_when_refresh_token_is_missing(
299344
monkeypatch: pytest.MonkeyPatch,

tests/auth/test_xai_auth.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,71 @@ async def fake_exchange(code: str, code_verifier: str, redirect_uri: str) -> dic
107107
assert config.default_model == "xai/grok-4"
108108

109109

110+
@pytest.mark.asyncio
111+
async def test_xai_persistence_errors_hide_internal_details(
112+
monkeypatch: pytest.MonkeyPatch,
113+
) -> None:
114+
from pythinker_code.auth import xai
115+
116+
diagnostic = "permission denied at /private/credentials/xai.json"
117+
config = Config(is_from_default_location=True)
118+
119+
async def fake_loopback(**_kwargs: Any) -> LoopbackAuthorization:
120+
return LoopbackAuthorization("auth-code", "verifier", "http://127.0.0.1:56121/callback")
121+
122+
async def fake_exchange(
123+
_code: str,
124+
_code_verifier: str,
125+
_redirect_uri: str,
126+
) -> dict[str, Any]:
127+
return {
128+
"access_token": "xai-access",
129+
"refresh_token": "xai-refresh",
130+
"expires_in": 3600,
131+
}
132+
133+
async def fake_request_device_code(**_kwargs: Any) -> DeviceCode:
134+
return DeviceCode(
135+
user_code="GROK-CODE",
136+
verification_uri="https://auth.x.ai/activate",
137+
device_code="device-secret",
138+
interval=5,
139+
expires_in=900,
140+
)
141+
142+
async def fake_poll_device_token(**_kwargs: Any) -> dict[str, Any]:
143+
return {
144+
"access_token": "xai-access",
145+
"refresh_token": "xai-refresh",
146+
"expires_in": 3600,
147+
}
148+
149+
async def fake_models() -> tuple[xai.XAIModel, ...]:
150+
return xai.XAI_MODELS
151+
152+
async def fail_persistence(*_args: object, **_kwargs: object) -> None:
153+
raise OSError(diagnostic)
154+
155+
monkeypatch.setattr(xai, "run_loopback_pkce_flow", fake_loopback)
156+
monkeypatch.setattr(xai, "_exchange_code_for_tokens", fake_exchange)
157+
monkeypatch.setattr(xai, "request_device_code", fake_request_device_code)
158+
monkeypatch.setattr(xai, "poll_device_token", fake_poll_device_token)
159+
monkeypatch.setattr(xai, "_discover_xai_models", fake_models)
160+
monkeypatch.setattr(xai, "persist_login", fail_persistence)
161+
monkeypatch.setattr(xai, "persist_logout", fail_persistence)
162+
163+
browser_events = [event async for event in xai.login_xai_browser(config)]
164+
headless_events = [event async for event in xai.login_xai_headless(config)]
165+
logout_events = [event async for event in xai.logout_xai(config)]
166+
167+
assert browser_events[-1].message == "Failed to save xAI Grok login."
168+
assert headless_events[-1].message == "Failed to save xAI Grok login."
169+
assert logout_events[-1].message == "Failed to log out of xAI Grok."
170+
assert diagnostic not in browser_events[-1].json
171+
assert diagnostic not in headless_events[-1].json
172+
assert diagnostic not in logout_events[-1].json
173+
174+
110175
@pytest.mark.asyncio
111176
async def test_login_xai_headless_uses_device_flow(
112177
monkeypatch: pytest.MonkeyPatch,

0 commit comments

Comments
 (0)