mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-06 09:45:51 +00:00
feat(cli): support github-copilot in provider logout
Logout previously claimed to support github-copilot in --help text but had no registered handler, so `provider logout github-copilot` failed with "Logout not implemented". Add the handler, sharing token deletion with the codex flow via `_delete_oauth_files`. Tighten handler-table types, fix the codex test fixture filename, and cover github-copilot plus the unknown provider path.
This commit is contained in:
parent
807b8188e3
commit
3ceabdecd5
@ -5,6 +5,7 @@ import os
|
||||
import select
|
||||
import signal
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from contextlib import nullcontext, suppress
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@ -1487,8 +1488,13 @@ provider_app = typer.Typer(help="Manage providers")
|
||||
app.add_typer(provider_app, name="provider")
|
||||
|
||||
|
||||
_LOGIN_HANDLERS: dict[str, Any] = {}
|
||||
_LOGOUT_HANDLERS: dict[str, Any] = {}
|
||||
_LOGIN_HANDLERS: dict[str, Callable[[], None]] = {}
|
||||
_LOGOUT_HANDLERS: dict[str, Callable[[], None]] = {}
|
||||
|
||||
_PROVIDER_DISPLAY: dict[str, str] = {
|
||||
"openai_codex": "OpenAI Codex",
|
||||
"github_copilot": "GitHub Copilot",
|
||||
}
|
||||
|
||||
|
||||
def _register_login(name: str):
|
||||
@ -1587,20 +1593,46 @@ def _logout_openai_codex() -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
storage = FileTokenStorage(token_filename=OPENAI_CODEX_PROVIDER.token_filename)
|
||||
_delete_oauth_files(storage.get_token_path(), _PROVIDER_DISPLAY["openai_codex"])
|
||||
|
||||
|
||||
@_register_logout("github_copilot")
|
||||
def _logout_github_copilot() -> None:
|
||||
"""Clear local OAuth credentials for GitHub Copilot."""
|
||||
try:
|
||||
from nanobot.providers.github_copilot_provider import get_storage
|
||||
except ImportError:
|
||||
console.print("[red]GitHub Copilot provider unavailable. Ensure oauth-cli-kit is installed.[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
storage = get_storage()
|
||||
_delete_oauth_files(storage.get_token_path(), _PROVIDER_DISPLAY["github_copilot"])
|
||||
|
||||
|
||||
def _delete_oauth_files(token_path: Path, provider_label: str) -> None:
|
||||
"""Delete OAuth token and lock files, reporting the result."""
|
||||
removed_paths: list[Path] = []
|
||||
|
||||
for path in (storage.get_token_path(), storage.get_token_path().with_suffix(".lock")):
|
||||
if path.exists():
|
||||
skipped: list[tuple[Path, OSError]] = []
|
||||
for path in (token_path, token_path.with_suffix(".lock")):
|
||||
try:
|
||||
path.unlink()
|
||||
removed_paths.append(path)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except OSError as exc:
|
||||
skipped.append((path, exc))
|
||||
continue
|
||||
removed_paths.append(path)
|
||||
|
||||
if not removed_paths:
|
||||
console.print("[yellow]! No local OAuth credentials found for OpenAI Codex[/yellow]")
|
||||
if not removed_paths and not skipped:
|
||||
console.print(f"[yellow]! No local OAuth credentials found for {provider_label}[/yellow]")
|
||||
return
|
||||
|
||||
console.print("[green]✓ Logged out from OpenAI Codex[/green]")
|
||||
for path in removed_paths:
|
||||
console.print(f"[dim]Removed: {path}[/dim]")
|
||||
if removed_paths:
|
||||
console.print(f"[green]✓ Logged out from {provider_label}[/green]")
|
||||
for path in removed_paths:
|
||||
console.print(f"[dim]Removed: {path}[/dim]")
|
||||
for path, exc in skipped:
|
||||
console.print(f"[yellow]! Could not remove {path}: {exc}[/yellow]")
|
||||
|
||||
|
||||
@_register_login("github_copilot")
|
||||
|
||||
@ -29,7 +29,7 @@ _EXPIRY_SKEW_SECONDS = 60
|
||||
_LONG_LIVED_TOKEN_SECONDS = 315360000
|
||||
|
||||
|
||||
def _storage() -> FileTokenStorage:
|
||||
def get_storage() -> FileTokenStorage:
|
||||
return FileTokenStorage(
|
||||
token_filename=TOKEN_FILENAME,
|
||||
app_name=TOKEN_APP_NAME,
|
||||
@ -48,7 +48,7 @@ def _copilot_headers(token: str) -> dict[str, str]:
|
||||
|
||||
|
||||
def _load_github_token() -> OAuthToken | None:
|
||||
token = _storage().load()
|
||||
token = get_storage().load()
|
||||
if not token or not token.access:
|
||||
return None
|
||||
return token
|
||||
@ -150,7 +150,7 @@ def login_github_copilot(
|
||||
expires=expires_ms,
|
||||
account_id=str(account_id) if account_id else None,
|
||||
)
|
||||
_storage().save(token)
|
||||
get_storage().save(token)
|
||||
return token
|
||||
|
||||
|
||||
|
||||
@ -221,7 +221,7 @@ def test_config_dump_excludes_oauth_provider_blocks():
|
||||
|
||||
|
||||
def test_provider_logout_openai_codex_removes_local_oauth_files(tmp_path, monkeypatch):
|
||||
token_path = tmp_path / "auth" / "oauth.json"
|
||||
token_path = tmp_path / "auth" / "codex.json"
|
||||
lock_path = token_path.with_suffix(".lock")
|
||||
token_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
token_path.write_text("{}", encoding="utf-8")
|
||||
@ -237,7 +237,7 @@ def test_provider_logout_openai_codex_removes_local_oauth_files(tmp_path, monkey
|
||||
|
||||
|
||||
def test_provider_logout_openai_codex_succeeds_when_no_local_oauth_file(monkeypatch, tmp_path):
|
||||
token_path = tmp_path / "auth" / "oauth.json"
|
||||
token_path = tmp_path / "auth" / "codex.json"
|
||||
monkeypatch.setenv("OAUTH_CLI_KIT_TOKEN_PATH", str(token_path))
|
||||
|
||||
result = runner.invoke(app, ["provider", "logout", "openai-codex"])
|
||||
@ -246,6 +246,63 @@ def test_provider_logout_openai_codex_succeeds_when_no_local_oauth_file(monkeypa
|
||||
assert "No local OAuth credentials found for OpenAI Codex" in result.stdout
|
||||
|
||||
|
||||
def test_provider_logout_github_copilot_removes_local_oauth_files(tmp_path, monkeypatch):
|
||||
token_path = tmp_path / "auth" / "github-copilot.json"
|
||||
lock_path = token_path.with_suffix(".lock")
|
||||
token_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
token_path.write_text("{}", encoding="utf-8")
|
||||
lock_path.write_text("", encoding="utf-8")
|
||||
monkeypatch.setenv("OAUTH_CLI_KIT_TOKEN_PATH", str(token_path))
|
||||
|
||||
result = runner.invoke(app, ["provider", "logout", "github-copilot"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert not token_path.exists()
|
||||
assert not lock_path.exists()
|
||||
assert "Logged out from GitHub Copilot" in result.stdout
|
||||
|
||||
|
||||
def test_provider_logout_github_copilot_succeeds_when_no_local_oauth_file(monkeypatch, tmp_path):
|
||||
token_path = tmp_path / "auth" / "github-copilot.json"
|
||||
monkeypatch.setenv("OAUTH_CLI_KIT_TOKEN_PATH", str(token_path))
|
||||
|
||||
result = runner.invoke(app, ["provider", "logout", "github-copilot"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "No local OAuth credentials found for GitHub Copilot" in result.stdout
|
||||
|
||||
|
||||
def test_provider_logout_rejects_unknown_provider():
|
||||
result = runner.invoke(app, ["provider", "logout", "not-a-real-provider"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "Unknown OAuth provider" in result.stdout
|
||||
|
||||
|
||||
def test_provider_logout_paths_resolve_to_expected_files():
|
||||
from oauth_cli_kit.providers import OPENAI_CODEX_PROVIDER
|
||||
from oauth_cli_kit.storage import FileTokenStorage
|
||||
|
||||
from nanobot.providers.github_copilot_provider import get_storage
|
||||
|
||||
codex_storage = FileTokenStorage(token_filename=OPENAI_CODEX_PROVIDER.token_filename)
|
||||
codex_path = codex_storage.get_token_path()
|
||||
assert codex_path.name == "codex.json"
|
||||
assert codex_path.parent.name == "auth"
|
||||
|
||||
gh_storage = get_storage()
|
||||
gh_path = gh_storage.get_token_path()
|
||||
assert gh_path.name == "github-copilot.json"
|
||||
assert gh_path.parent.name == "auth"
|
||||
|
||||
|
||||
def test_provider_login_rejects_unknown_provider():
|
||||
result = runner.invoke(app, ["provider", "login", "not-a-real-provider"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "Unknown OAuth provider" in result.stdout
|
||||
|
||||
|
||||
def test_config_matches_explicit_ollama_prefix_without_api_key():
|
||||
config = Config()
|
||||
config.agents.defaults.model = "ollama/llama3.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user