feat(code): toast on saved /auth API key (#4558)

The `/auth` prompt now shows a confirmation toast when an API key is
saved.

---

Pressing Enter in the `/auth` key prompt persisted the credential and
silently closed the modal, giving no confirmation. This adds a
"Successfully saved key for PROVIDER" toast on a successful save so the
action has visible feedback. The provider label reuses
`provider_display_name`, so it honors configured/built-in display names.

Made by [Open
SWE](https://openswe.vercel.app/agents/cd83cc67-e5bc-6845-8bbe-dc0c8fc8d4bc)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-07-08 21:07:53 -04:00
committed by GitHub
parent 841cf9c54d
commit ee3c26415b
8 changed files with 436 additions and 21 deletions
+6 -1
View File
@@ -14507,8 +14507,13 @@ class DeepAgentsApp(App):
)
if result == AuthResult.SAVED:
self._notice_registry.remove(entry.key)
# The modal's own success toast already confirms the save and names
# the provider. This path can't activate the key in-session (unlike
# the Tavily flow, which calls `apply_stored_service_credentials`),
# so surface only the restart hint the modal can't — repeating the
# "saved" confirmation here would just stack a duplicate toast.
self.notify(
f"Saved {service} API key. Restart to apply.",
"Restart to apply your new key.",
severity="information",
timeout=6,
markup=False,
+41 -6
View File
@@ -110,6 +110,39 @@ class WriteOutcome:
"""User-visible warning strings (e.g., chmod failures). Empty on success."""
@dataclass(frozen=True, slots=True)
class DeleteOutcome:
"""Result of a credential delete that may have warnings to surface.
A delete rewrites the whole store, so it can hit the same chmod failures as
a write. `warnings` carries them symmetrically with `WriteOutcome` so a
caller's "removed" confirmation doesn't paper over a store the delete
failed to lock down to owner-only.
"""
removed: bool
"""`True` if a credential was removed, `False` if none was stored."""
warnings: tuple[str, ...] = field(default_factory=tuple)
"""chmod-failure warnings from the rewrite. Always empty for a no-op delete
(`removed=False`), which performs no write."""
def __post_init__(self) -> None:
"""Reject the one field combination the docstring forbids.
A no-op delete performs no write, so it can raise no chmod warnings.
Enforcing it here makes "no warnings when nothing was removed" a
construction-time guarantee rather than a producer-side convention a
future edit (or a second producer) could quietly break.
Raises:
ValueError: If `warnings` is non-empty while `removed` is `False`.
"""
if self.warnings and not self.removed:
msg = "DeleteOutcome cannot carry warnings when removed=False"
raise ValueError(msg)
def auth_path() -> Path:
"""Return the resolved path to the credential store (`auth.json`).
@@ -473,14 +506,16 @@ def set_stored_key(
return WriteOutcome(warnings=warnings)
def delete_stored_key(provider: str) -> bool:
def delete_stored_key(provider: str) -> DeleteOutcome:
"""Remove a stored credential for `provider`.
Args:
provider: Provider identifier.
Returns:
`True` if a credential was removed, `False` if none was stored.
A `DeleteOutcome` whose `removed` flag reports whether a credential was
present, and whose `warnings` tuple lists chmod failures from the
rewrite (empty for a no-op delete, which performs no write).
Raises:
RuntimeError: If the credential file is corrupt and cannot be read, or
@@ -489,16 +524,16 @@ def delete_stored_key(provider: str) -> bool:
""" # noqa: DOC502 - re-raised from `_read_raw`/`_write_raw_or_raise`
data = _read_raw()
if data is None:
return False
return DeleteOutcome(removed=False)
creds = data.get("credentials")
if not isinstance(creds, dict) or provider not in creds:
return False
return DeleteOutcome(removed=False)
del creds[provider]
data["version"] = _STORAGE_VERSION
data["credentials"] = creds
_write_raw_or_raise(data)
warnings = _write_raw_or_raise(data)
logger.debug("Deleted credential for provider %s", provider)
return True
return DeleteOutcome(removed=True, warnings=warnings)
def list_configured_providers() -> list[str]:
@@ -18,8 +18,8 @@ Security notes:
reading from stdin so the key never lands in shell history or argv, and it
refuses an interactive TTY (use `--from-env` instead) so an accidental
invocation cannot hang waiting on input.
- `set` routes through `auth_store.set_stored_key`, so chmod warnings from the
same `WriteOutcome` path the TUI uses are surfaced on stderr.
- `set` and `remove` route through `auth_store`, so chmod warnings from the
store rewrite (the same path the TUI uses) are surfaced on stderr.
Help rendering for a bare `auth` invocation is served by `ui.show_auth_help`,
which does not import this module. The heavy `model_config` imports here are
@@ -515,11 +515,14 @@ def _run_remove(provider: str) -> int:
from deepagents_code import auth_store
try:
removed = auth_store.delete_stored_key(provider)
result = auth_store.delete_stored_key(provider)
except RuntimeError as exc:
print(f"Error: {exc}", file=sys.stderr) # noqa: T201
return 1
if removed:
# Surface chmod warnings on the rewritten store, symmetric with `set`.
for warning in result.warnings:
print(f"Warning: {warning}", file=sys.stderr) # noqa: T201
if result.removed:
print(f"Removed stored credential for {provider}.") # noqa: T201
else:
print(f"No stored credential for {provider}.") # noqa: T201
+36 -4
View File
@@ -1328,6 +1328,20 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
if self._is_langsmith:
apply_stored_langsmith_auth(replace_project=True)
clear_caches()
if not outcome.warnings:
# Only claim a clean save when the store locked the file down. When
# chmod warnings fired above, they *are* the outcome message — an
# extra "success" toast on top would compete with (and visually
# bury) the one signal that the key isn't secured.
provider_label = provider_display_name(self._provider, self._config)
# `markup=False`: a configured display name can contain markup
# metacharacters (e.g. `[`) that must not be interpreted here. The
# same guard applies to every interpolated toast below.
self.app.notify(
f"Successfully saved key for {provider_label}.",
severity="information",
markup=False,
)
self.dismiss(AuthResult.SAVED)
def action_cancel(self) -> None:
@@ -1420,23 +1434,41 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
if not confirmed:
return
try:
removed = auth_store.delete_stored_key(self._provider)
result = auth_store.delete_stored_key(self._provider)
except RuntimeError as exc:
logger.warning(
"Failed to delete credential for %s: %s", self._provider, exc
)
self._show_error("Could not delete credential: $exc", exc=str(exc))
return
if not removed:
for warning in result.warnings:
# The rewritten store still holds other providers' secrets, so a
# chmod failure here is the same security regression as on save —
# surface it rather than dropping it on the floor.
self.app.notify(warning, severity="warning", markup=False)
# Toast after `clear_caches` (like the save path) so the confirmation
# reflects fully-settled state rather than firing before the cache is
# invalidated.
clear_caches()
if not result.removed:
# The entry was gone — likely a concurrent delete from another
# app instance. Surface that fact so "delete" UX doesn't lie when
# nothing actually happened on disk.
provider_label = provider_display_name(self._provider, self._config)
self.app.notify(
f"No stored credential for {self._provider} — already removed.",
f"No stored credential for {provider_label} — already removed.",
severity="information",
markup=False,
)
elif not result.warnings:
# Mirror the save path: a silent successful delete gives no
# confirmation, and the toast is suppressed when warnings fired.
provider_label = provider_display_name(self._provider, self._config)
self.app.notify(
f"Successfully removed key for {provider_label}.",
severity="information",
markup=False,
)
clear_caches()
self.dismiss(AuthResult.DELETED)
def _show_error(self, template: str, /, **substitutions: str) -> None:
@@ -548,6 +548,29 @@ class TestRemove:
assert code == 0
assert "No stored credential for anthropic." in capsys.readouterr().out
def test_remove_surfaces_chmod_warning(
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""A chmod failure on the delete rewrite is surfaced on stderr, like `set`."""
auth_store.set_stored_key("anthropic", "sk-ant")
original_chmod = Path.chmod
def _deny_chmod(self: Path, mode: int) -> None:
if self.name == "auth.json":
msg = "simulated chmod denial"
raise OSError(msg)
original_chmod(self, mode)
# Deny chmod only for the delete rewrite, not the seeding write above.
monkeypatch.setattr(Path, "chmod", _deny_chmod)
code = run_auth_command(_ns(auth_command="remove", provider="anthropic"))
assert code == 0
captured = capsys.readouterr()
assert auth_store.get_stored_key("anthropic") is None
assert "Warning:" in captured.err
assert "world-readable" in captured.err
assert "Removed stored credential for anthropic." in captured.out
def test_remove_corrupt_store_errors(
self, capsys: pytest.CaptureFixture[str]
) -> None:
+5 -2
View File
@@ -15507,9 +15507,12 @@ class TestNotificationCenterIntegration:
assert isinstance(screen, AuthPromptScreen)
assert screen._provider == "tavily"
assert screen._env_var == "TAVILY_API_KEY"
# ... and on save the stale notice is gone and the user is told to restart.
# ... and on save the stale notice is gone and the user is told to
# restart. The modal owns the "saved" confirmation now, so this path
# emits only the restart hint — no duplicate "Saved ... API key" toast.
assert app._notice_registry.get("dep:tavily") is None
assert any("Restart to apply." in m for m in messages)
assert any(m == "Restart to apply your new key." for m in messages)
assert not any("Saved" in m and "API key" in m for m in messages)
async def test_enter_api_key_unknown_service_is_a_noop(
self, caplog: pytest.LogCaptureFixture
+43 -4
View File
@@ -182,14 +182,22 @@ class TestRoundTrip:
assert any("malformed base_url" in r.getMessage() for r in caplog.records)
def test_delete_returns_true_when_removed(self) -> None:
"""Deleting an existing entry returns `True` and clears the value."""
"""Deleting an existing entry reports `removed=True` and clears the value."""
auth_store.set_stored_key("openai", "k")
assert auth_store.delete_stored_key("openai") is True
assert auth_store.delete_stored_key("openai").removed is True
assert auth_store.get_stored_key("openai") is None
def test_delete_missing_returns_false(self) -> None:
"""Deleting an unknown provider is a no-op."""
assert auth_store.delete_stored_key("anthropic") is False
"""Deleting an unknown provider is a no-op that reports `removed=False`."""
outcome = auth_store.delete_stored_key("anthropic")
assert outcome.removed is False
# A no-op performs no write, so it can carry no chmod warnings.
assert outcome.warnings == ()
def test_delete_outcome_rejects_warnings_without_removal(self) -> None:
"""The type refuses the illegal `removed=False` + warnings combination."""
with pytest.raises(ValueError, match="cannot carry warnings"):
auth_store.DeleteOutcome(removed=False, warnings=("boom",))
@pytest.mark.usefixtures("fake_home")
@@ -251,6 +259,37 @@ class TestPermissions:
outcome = auth_store.set_stored_key("anthropic", "k")
assert outcome.warnings == ()
def test_delete_chmod_failure_returned_as_warning(
self,
fake_home: Path, # noqa: ARG002 - fixture activates the temp state dir
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A chmod that can't lock down the delete rewrite shows up in DeleteOutcome."""
from pathlib import Path as _Path
auth_store.set_stored_key("anthropic", "k")
original_chmod = _Path.chmod
def deny_file_chmod(self: _Path, mode: int) -> None:
if self.name == "auth.json":
msg = "simulated chmod denial"
raise OSError(msg)
original_chmod(self, mode)
# Deny chmod only for the delete rewrite, not the seeding write above.
monkeypatch.setattr(_Path, "chmod", deny_file_chmod)
outcome = auth_store.delete_stored_key("anthropic")
assert outcome.removed is True
assert any("0600" in w for w in outcome.warnings)
assert any("simulated chmod denial" in w for w in outcome.warnings)
def test_clean_delete_returns_no_warnings(self, fake_home: Path) -> None: # noqa: ARG002 - fixture activates the temp state dir
"""A successful delete reports an empty warnings tuple."""
auth_store.set_stored_key("anthropic", "k")
outcome = auth_store.delete_stored_key("anthropic")
assert outcome.removed is True
assert outcome.warnings == ()
@pytest.mark.usefixtures("fake_home")
class TestCorruption:
@@ -637,6 +637,86 @@ api_key_url = "javascript:alert(1)"
assert app.prompt_result is AuthResult.SAVED
assert auth_store.get_stored_key("anthropic") == "sk-ant-test-12345"
async def test_successful_save_notifies_with_provider_name(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A successful save surfaces a confirmation toast naming the provider."""
# `openai` resolves to "OpenAI" via the built-in display-name map but
# would title-case to "Openai" via the bare fallback, so asserting on it
# proves the label is resolved through `provider_display_name` rather
# than the raw provider key.
notices: list[tuple[str, str | None, bool]] = []
def _capture_notify(
message: str,
*_args: object,
severity: str | None = None,
markup: bool = True,
**_kwargs: object,
) -> None:
notices.append((str(message), severity, markup))
app = _AuthHostApp()
async with app.run_test() as pilot:
monkeypatch.setattr(app, "notify", _capture_notify)
app.show_prompt("openai", "OPENAI_API_KEY")
await pilot.pause()
app.screen.query_one("#auth-prompt-input", Input).value = "sk-test"
await pilot.press("enter")
await pilot.pause()
assert app.prompt_result is AuthResult.SAVED
# `markup=False` is load-bearing: a user-configured display name can
# contain Textual markup metacharacters (e.g. `[`), so the toast must not
# interpret its interpolated label as markup.
assert (
"Successfully saved key for OpenAI.",
"information",
False,
) in notices
async def test_langsmith_save_notifies_with_service_label(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The save toast fires on the service path with the service's label."""
notices: list[tuple[str, str | None, bool]] = []
def _capture_notify(
message: str,
*_args: object,
severity: str | None = None,
markup: bool = True,
**_kwargs: object,
) -> None:
notices.append((str(message), severity, markup))
# Stub the tracing activation so the test stays hermetic (no process
# env mutation); the toast runs immediately after it on the LangSmith
# branch, so the branch is still exercised end to end.
monkeypatch.setattr(
"deepagents_code.tui.widgets.auth.apply_stored_langsmith_auth",
lambda **_kwargs: None,
)
app = _AuthHostApp()
async with app.run_test() as pilot:
monkeypatch.setattr(app, "notify", _capture_notify)
app.show_prompt("langsmith", "LANGSMITH_API_KEY")
await pilot.pause()
app.screen.query_one("#auth-prompt-input", Input).value = "lsv2_live"
await pilot.press("enter")
await pilot.pause()
assert app.prompt_result is AuthResult.SAVED
# "LangSmith (tracing)" carries a parenthetical the bare provider key
# never would, proving the service branch resolves its label through
# `provider_display_name` rather than echoing "langsmith".
assert (
"Successfully saved key for LangSmith (tracing).",
"information",
False,
) in notices
async def test_langsmith_submit_applies_tracing_env_immediately(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
@@ -1086,6 +1166,12 @@ api_key_url = "javascript:alert(1)"
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A failed credential write shows an inline error and keeps the modal."""
notices: list[tuple[str, str | None]] = []
def _capture_notify(
message: str, *_args: object, severity: str | None = None, **_kwargs: object
) -> None:
notices.append((str(message), severity))
def _raise(*_args: object, **_kwargs: object) -> auth_store.WriteOutcome:
msg = "credential store is not writable"
@@ -1095,6 +1181,7 @@ api_key_url = "javascript:alert(1)"
app = _AuthHostApp()
async with app.run_test() as pilot:
monkeypatch.setattr(app, "notify", _capture_notify)
app.show_prompt("tavily", "TAVILY_API_KEY", allow_empty_submit=True)
await pilot.pause()
app.screen.query_one("#auth-prompt-input", Input).value = "tvly-key"
@@ -1108,6 +1195,9 @@ api_key_url = "javascript:alert(1)"
assert "Could not save credential" in error_text
assert "credential store is not writable" in error_text
assert app.prompt_dismissed is False
# The success toast must never fire when the write raised — it would
# tell the user the key was saved when it wasn't.
assert not any(msg.startswith("Successfully saved key") for msg, _ in notices)
async def test_optional_prompt_notifies_store_warnings(
self, monkeypatch: pytest.MonkeyPatch
@@ -1136,6 +1226,9 @@ api_key_url = "javascript:alert(1)"
assert ("credential file is not private", "warning") in notices
assert app.prompt_result is AuthResult.SAVED
# When the store warns, the plain "success" toast is suppressed so it
# can't visually bury (or seem to contradict) the security warning.
assert not any(msg.startswith("Successfully saved key") for msg, _ in notices)
async def test_escape_cancels(self) -> None:
"""Escape dismisses with `CANCELLED` and writes nothing."""
@@ -1414,6 +1507,188 @@ api_key_url = "javascript:alert(1)"
assert app.prompt_result is AuthResult.DELETED
assert auth_store.get_stored_key("openai") is None
async def test_successful_delete_notifies_with_provider_name(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A completed delete surfaces a confirmation toast naming the provider."""
from deepagents_code.tui.widgets.auth import DeleteCredentialConfirmScreen
# `openai` resolves to "OpenAI" via the built-in display-name map but
# would title-case to "Openai" via the bare fallback, so asserting on it
# proves the label is resolved through `provider_display_name` rather
# than the raw provider key.
notices: list[tuple[str, str | None, bool]] = []
def _capture_notify(
message: str,
*_args: object,
severity: str | None = None,
markup: bool = True,
**_kwargs: object,
) -> None:
notices.append((str(message), severity, markup))
auth_store.set_stored_key("openai", "to-be-removed")
app = _AuthHostApp()
async with app.run_test() as pilot:
monkeypatch.setattr(app, "notify", _capture_notify)
app.show_prompt("openai", "OPENAI_API_KEY")
await pilot.pause()
await pilot.press("ctrl+d")
await pilot.pause()
assert isinstance(app.screen, DeleteCredentialConfirmScreen)
await pilot.press("enter")
await pilot.pause()
assert app.prompt_result is AuthResult.DELETED
# `markup=False` is load-bearing: a user-configured display name can
# contain Textual markup metacharacters (e.g. `[`).
assert (
"Successfully removed key for OpenAI.",
"information",
False,
) in notices
async def test_delete_failure_does_not_notify_success(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A failed delete shows an inline error and fires no success toast."""
from deepagents_code.tui.widgets.auth import DeleteCredentialConfirmScreen
notices: list[tuple[str, str | None]] = []
def _capture_notify(
message: str, *_args: object, severity: str | None = None, **_kwargs: object
) -> None:
notices.append((str(message), severity))
def _raise(*_args: object, **_kwargs: object) -> auth_store.DeleteOutcome:
msg = "credential store is not writable"
raise RuntimeError(msg)
# Seed a key so Ctrl+D opens the confirm modal, then fail the delete
# itself — the mirror of `test_optional_prompt_surfaces_save_failure`.
auth_store.set_stored_key("openai", "to-be-removed")
monkeypatch.setattr(auth_store, "delete_stored_key", _raise)
app = _AuthHostApp()
async with app.run_test() as pilot:
monkeypatch.setattr(app, "notify", _capture_notify)
app.show_prompt("openai", "OPENAI_API_KEY")
await pilot.pause()
await pilot.press("ctrl+d")
await pilot.pause()
assert isinstance(app.screen, DeleteCredentialConfirmScreen)
await pilot.press("enter")
await pilot.pause()
err = app.screen.query_one("#auth-prompt-error", Static)
error_text = str(err.content)
# Surfaced in-modal; the prompt stays open rather than dismissing
# DELETED as if the removal had happened.
assert "Could not delete credential" in error_text
assert "credential store is not writable" in error_text
assert app.prompt_dismissed is False
# The success toast must never fire when the delete raised — it would
# claim a removal that did not occur.
assert not any(msg.startswith("Successfully removed key") for msg, _ in notices)
async def test_noop_delete_notifies_already_removed(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A delete that finds nothing surfaces only the 'already removed' toast."""
from deepagents_code.tui.widgets.auth import DeleteCredentialConfirmScreen
notices: list[tuple[str, str | None, bool]] = []
def _capture_notify(
message: str,
*_args: object,
severity: str | None = None,
markup: bool = True,
**_kwargs: object,
) -> None:
notices.append((str(message), severity, markup))
# The key exists when the modal opens (so Ctrl+D confirms) but is gone
# by the time the delete runs — e.g. a concurrent delete from another
# app instance. `removed=False` drives the "already removed" branch.
auth_store.set_stored_key("openai", "to-be-removed")
monkeypatch.setattr(
auth_store,
"delete_stored_key",
lambda *_a, **_k: auth_store.DeleteOutcome(removed=False),
)
app = _AuthHostApp()
async with app.run_test() as pilot:
monkeypatch.setattr(app, "notify", _capture_notify)
app.show_prompt("openai", "OPENAI_API_KEY")
await pilot.pause()
await pilot.press("ctrl+d")
await pilot.pause()
assert isinstance(app.screen, DeleteCredentialConfirmScreen)
await pilot.press("enter")
await pilot.pause()
# The no-op path still dismisses DELETED (shared with the success path),
# surfaces the resolved label, and never claims a real removal.
assert app.prompt_result is AuthResult.DELETED
assert (
"No stored credential for OpenAI — already removed.",
"information",
False,
) in notices
assert not any(
msg.startswith("Successfully removed key") for msg, _, _ in notices
)
async def test_delete_store_warning_suppresses_success(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Store warnings on the delete rewrite fire and suppress the success toast."""
from deepagents_code.tui.widgets.auth import DeleteCredentialConfirmScreen
notices: list[tuple[str, str | None, bool]] = []
def _capture_notify(
message: str,
*_args: object,
severity: str | None = None,
markup: bool = True,
**_kwargs: object,
) -> None:
notices.append((str(message), severity, markup))
warning = "Could not set mode 0600 on auth.json: denied. World-readable."
auth_store.set_stored_key("openai", "to-be-removed")
monkeypatch.setattr(
auth_store,
"delete_stored_key",
lambda *_a, **_k: auth_store.DeleteOutcome(
removed=True, warnings=(warning,)
),
)
app = _AuthHostApp()
async with app.run_test() as pilot:
monkeypatch.setattr(app, "notify", _capture_notify)
app.show_prompt("openai", "OPENAI_API_KEY")
await pilot.pause()
await pilot.press("ctrl+d")
await pilot.pause()
assert isinstance(app.screen, DeleteCredentialConfirmScreen)
await pilot.press("enter")
await pilot.pause()
assert app.prompt_result is AuthResult.DELETED
# The security warning is surfaced...
assert (warning, "warning", False) in notices
# ...and the clean "success" toast is suppressed so it can't bury it.
assert not any(
msg.startswith("Successfully removed key") for msg, _, _ in notices
)
async def test_ctrl_d_then_escape_keeps_credential(self) -> None:
"""Esc on the confirm modal returns to the prompt without deleting."""
from deepagents_code.tui.widgets.auth import DeleteCredentialConfirmScreen