fix(commands): plugin provider sign-in moves from /login to /connect, with /disconnect; /login and /logout stay Codewhale-account only

Follow-up to merged PR #6950 (@LIghtJUNction). /login reverts to status|account|key and points plugin sign-in at /connect <provider>; /logout no longer takes a provider. New /connect [provider] opens the provider picker or starts plugin PKCE; /disconnect <provider> removes the local grant. Both are protected built-ins. Picker, OAuth hints and English strings say connected/disconnected; CmdConnectDescription and CmdDisconnectDescription added to all 15 locales; docs/PLUGIN_PROVIDERS.md and features.toml rows updated.

Evidence: nextest of the command, locale and plugin tests 236 run, 236 passed; feature_registry 2 passed; check-tui-locale-parity PASS (2525 keys). Not run: clippy, PTY tests, a real browser OAuth flow, hosted CI.

Refs #6950

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Sonnet 5.5 committed 2026-10-11 02:39:55 -07:00
1 parent 754b46e138
commit 0ff4ffd494
23 files changed
+150 -65

No files matched your search

+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "La mascota funciona amb una versió anterior. Reinicia-la després d’actualitzar Codewhale per triar l’estil de balena i la llengua del text.",
"PluginOAuthBrowser": "Prem Retorn per iniciar sessió al navegador o Esc per tornar. No cal cap clau API.",
"PluginOAuthReady": "{provider}: sessió iniciada. Tria un model.",
"PluginOAuthLocalLogout": "{provider}: sessió local eliminada. L’accés remot no ha canviat."
"PluginOAuthLocalLogout": "{provider}: sessió local eliminada. L’accés remot no ha canviat.",
"CmdConnectDescription": "Connecta un proveïdor de plugins amb inici de sessió al navegador",
"CmdDisconnectDescription": "Elimina l’inici de sessió local d’un proveïdor de plugins"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "Das Haustier verwendet eine ältere Version. Starte es nach dem Codewhale-Update neu, um den Walstil und die Textsprache auszuwählen.",
"PluginOAuthBrowser": "Mit Eingabe im Browser anmelden, mit Esc zurück. Kein API-Schlüssel erforderlich.",
"PluginOAuthReady": "{provider}: angemeldet. Wählen Sie ein Modell.",
"PluginOAuthLocalLogout": "{provider}: lokale Anmeldung entfernt. Der Fernzugriff bleibt unverändert."
"PluginOAuthLocalLogout": "{provider}: lokale Anmeldung entfernt. Der Fernzugriff bleibt unverändert.",
"CmdConnectDescription": "Plugin-Anbieter per Browser-Anmeldung verbinden",
"CmdDisconnectDescription": "Lokale Anmeldung eines Plugin-Anbieters entfernen"
}
+5 -3
View File
@@ -2519,7 +2519,9 @@
"PlanHandoffKeepPlanningDetail": "Stay in Plan. Nothing is changed.",
"PlanHandoffProceed": "Go ahead with the plan.",
"PetAppearanceLegacy": "The pet companion is running an older version. Restart it after updating Codewhale to use whale styles and text languages.",
"PluginOAuthBrowser": "Press Enter to sign in with your browser. Esc returns. No API key is needed.",
"PluginOAuthReady": "{provider}: signed in. Choose a model.",
"PluginOAuthLocalLogout": "{provider}: local sign-in removed. Remote access is unchanged."
"PluginOAuthBrowser": "Press Enter to connect in your browser. Esc returns. No API key is needed.",
"PluginOAuthReady": "{provider}: connected. Choose a model.",
"PluginOAuthLocalLogout": "{provider}: disconnected on this device. Remote access is unchanged.",
"CmdConnectDescription": "Connect a plugin provider with browser sign-in",
"CmdDisconnectDescription": "Remove a plugin provider's local sign-in"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "La mascota usa una versión anterior. Reiníciala después de actualizar Codewhale para elegir el estilo de ballena y el idioma del texto.",
"PluginOAuthBrowser": "Presiona Intro para iniciar sesión en el navegador o Esc para volver. No se necesita una clave API.",
"PluginOAuthReady": "{provider}: sesión iniciada. Elige un modelo.",
"PluginOAuthLocalLogout": "{provider}: se eliminó la sesión local. El acceso remoto no cambió."
"PluginOAuthLocalLogout": "{provider}: se eliminó la sesión local. El acceso remoto no cambió.",
"CmdConnectDescription": "Conecta un proveedor de plugins con inicio de sesión en el navegador",
"CmdDisconnectDescription": "Elimina el inicio de sesión local de un proveedor de plugins"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "Le compagnon utilise une ancienne version. Redémarrez-le après la mise à jour de Codewhale pour choisir le style de baleine et la langue du texte.",
"PluginOAuthBrowser": "Appuyez sur Entrée pour vous connecter dans le navigateur, ou Échap pour revenir. Aucune clé API nécessaire.",
"PluginOAuthReady": "{provider} : connexion établie. Choisissez un modèle.",
"PluginOAuthLocalLogout": "{provider} : connexion locale supprimée. L’accès distant reste inchangé."
"PluginOAuthLocalLogout": "{provider} : connexion locale supprimée. L’accès distant reste inchangé.",
"CmdConnectDescription": "Connecter un fournisseur de plugin par connexion dans le navigateur",
"CmdDisconnectDescription": "Supprimer la connexion locale d’un fournisseur de plugin"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "पेट पुराने संस्करण पर चल रहा है। व्हेल का रूप और टेक्स्ट की भाषा चुनने के लिए Codewhale अपडेट करने के बाद पेट को फिर से शुरू करें।",
"PluginOAuthBrowser": "ब्राउज़र से साइन इन करने के लिए Enter दबाएँ। वापस जाने के लिए Esc दबाएँ। API कुंजी की ज़रूरत नहीं है।",
"PluginOAuthReady": "{provider}: साइन इन हो गया। मॉडल चुनें।",
"PluginOAuthLocalLogout": "{provider}: स्थानीय साइन इन हटाया गया। दूरस्थ पहुँच नहीं बदली है।"
"PluginOAuthLocalLogout": "{provider}: स्थानीय साइन इन हटाया गया। दूरस्थ पहुँच नहीं बदली है।",
"CmdConnectDescription": "ब्राउज़र साइन-इन से किसी प्लगइन प्रदाता को जोड़ें",
"CmdDisconnectDescription": "किसी प्लगइन प्रदाता का स्थानीय साइन-इन हटाएँ"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "Pet masih memakai versi lama. Mulai ulang pet setelah memperbarui Codewhale untuk memilih gaya paus dan bahasa teks.",
"PluginOAuthBrowser": "Tekan Enter untuk masuk melalui peramban atau Esc untuk kembali. Kunci API tidak diperlukan.",
"PluginOAuthReady": "{provider}: berhasil masuk. Pilih model.",
"PluginOAuthLocalLogout": "{provider}: data masuk lokal dihapus. Akses jarak jauh tidak berubah."
"PluginOAuthLocalLogout": "{provider}: data masuk lokal dihapus. Akses jarak jauh tidak berubah.",
"CmdConnectDescription": "Hubungkan penyedia plugin dengan masuk lewat peramban",
"CmdDisconnectDescription": "Hapus data masuk lokal penyedia plugin"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "ペットは以前のバージョンで動作しています。Codewhale を更新してペットを再起動すると、クジラのスタイルと文字の言語を選べます。",
"PluginOAuthBrowser": "Enter でブラウザーからログインします。Esc で戻ります。API キーは不要です。",
"PluginOAuthReady": "{provider}:ログインしました。モデルを選択してください。",
"PluginOAuthLocalLogout": "{provider}:この端末の認証情報を削除しました。サーバーの認可は解除されていません。"
"PluginOAuthLocalLogout": "{provider}:この端末の認証情報を削除しました。サーバーの認可は解除されていません。",
"CmdConnectDescription": "ブラウザーのログインでプラグインプロバイダーに接続します",
"CmdDisconnectDescription": "プラグインプロバイダーのこの端末のログイン情報を削除します"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "펫이 이전 버전으로 실행 중입니다. Codewhale을 업데이트한 뒤 펫을 다시 시작하면 고래 스타일과 텍스트 언어를 선택할 수 있습니다.",
"PluginOAuthBrowser": "Enter를 눌러 브라우저에서 로그인하세요. Esc를 누르면 돌아갑니다. API 키는 필요하지 않습니다.",
"PluginOAuthReady": "{provider}: 로그인했습니다. 모델을 선택하세요.",
"PluginOAuthLocalLogout": "{provider}: 이 기기의 로그인 정보를 삭제했습니다. 서버 권한은 유지됩니다."
"PluginOAuthLocalLogout": "{provider}: 이 기기의 로그인 정보를 삭제했습니다. 서버 권한은 유지됩니다.",
"CmdConnectDescription": "브라우저 로그인으로 플러그인 제공자를 연결합니다",
"CmdDisconnectDescription": "플러그인 제공자의 이 기기 로그인 정보를 삭제합니다"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "O pet está usando uma versão anterior. Reinicie-o após atualizar o Codewhale para escolher o estilo da baleia e o idioma do texto.",
"PluginOAuthBrowser": "Pressione Enter para entrar pelo navegador ou Esc para voltar. Não é necessária uma chave de API.",
"PluginOAuthReady": "{provider}: sessão iniciada. Escolha um modelo.",
"PluginOAuthLocalLogout": "{provider}: sessão local removida. O acesso remoto não foi alterado."
"PluginOAuthLocalLogout": "{provider}: sessão local removida. O acesso remoto não foi alterado.",
"CmdConnectDescription": "Conecta um provedor de plugin com login pelo navegador",
"CmdDisconnectDescription": "Remove o login local de um provedor de plugin"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "Питомец работает на старой версии. После обновления Codewhale перезапустите его, чтобы выбрать стиль кита и язык текста.",
"PluginOAuthBrowser": "Нажмите Enter для входа через браузер или Esc для возврата. Ключ API не нужен.",
"PluginOAuthReady": "{provider}: вход выполнен. Выберите модель.",
"PluginOAuthLocalLogout": "{provider}: локальные данные входа удалены. Удалённый доступ не изменён."
"PluginOAuthLocalLogout": "{provider}: локальные данные входа удалены. Удалённый доступ не изменён.",
"CmdConnectDescription": "Подключить провайдера плагина через вход в браузере",
"CmdDisconnectDescription": "Удалить локальные данные входа провайдера плагина"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "Улюбленець працює на старій версії. Після оновлення Codewhale перезапустіть його, щоб вибрати стиль кита й мову тексту.",
"PluginOAuthBrowser": "Натисніть Enter для входу через браузер або Esc для повернення. Ключ API не потрібен.",
"PluginOAuthReady": "{provider}: вхід виконано. Виберіть модель.",
"PluginOAuthLocalLogout": "{provider}: локальні дані входу видалено. Віддалений доступ не змінено."
"PluginOAuthLocalLogout": "{provider}: локальні дані входу видалено. Віддалений доступ не змінено.",
"CmdConnectDescription": "Підключити провайдера плагіна через вхід у браузері",
"CmdDisconnectDescription": "Видалити локальні дані входу провайдера плагіна"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "Thú cưng đang dùng phiên bản cũ. Khởi động lại sau khi cập nhật Codewhale để chọn kiểu cá voi và ngôn ngữ văn bản.",
"PluginOAuthBrowser": "Nhấn Enter để đăng nhập bằng trình duyệt hoặc Esc để quay lại. Không cần khóa API.",
"PluginOAuthReady": "{provider}: đã đăng nhập. Hãy chọn mô hình.",
"PluginOAuthLocalLogout": "{provider}: đã xóa thông tin đăng nhập cục bộ. Quyền truy cập từ xa không đổi."
"PluginOAuthLocalLogout": "{provider}: đã xóa thông tin đăng nhập cục bộ. Quyền truy cập từ xa không đổi.",
"CmdConnectDescription": "Kết nối nhà cung cấp plugin bằng đăng nhập qua trình duyệt",
"CmdDisconnectDescription": "Xóa đăng nhập cục bộ của nhà cung cấp plugin"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "宠物仍在使用旧版本。更新 Codewhale 后重启宠物,即可选择鲸鱼样式和文字语言。",
"PluginOAuthBrowser": "按 Enter 打开浏览器登录,按 Esc 返回。不需要 API Key。",
"PluginOAuthReady": "{provider}:已登录,请选择模型。",
"PluginOAuthLocalLogout": "{provider}:已删除本机登录凭据,未撤销服务端授权。"
"PluginOAuthLocalLogout": "{provider}:已删除本机登录凭据,未撤销服务端授权。",
"CmdConnectDescription": "通过浏览器登录连接插件提供方",
"CmdDisconnectDescription": "删除插件提供方的本机登录凭据"
}
+3 -1
View File
@@ -2521,5 +2521,7 @@
"PetAppearanceLegacy": "寵物仍在使用舊版本。更新 Codewhale 後重新啟動寵物,即可選擇鯨魚樣式和文字語言。",
"PluginOAuthBrowser": "按 Enter 開啟瀏覽器登入,按 Esc 返回。不需要 API Key。",
"PluginOAuthReady": "{provider}:已登入,請選擇模型。",
"PluginOAuthLocalLogout": "{provider}:已刪除本機登入憑證,未撤銷伺服器授權。"
"PluginOAuthLocalLogout": "{provider}:已刪除本機登入憑證,未撤銷伺服器授權。",
"CmdConnectDescription": "透過瀏覽器登入連接外掛提供者",
"CmdDisconnectDescription": "刪除外掛提供者的本機登入憑證"
}
+4
View File
@@ -548,6 +548,8 @@ pub enum MessageId {
CmdLoadDescription,
CmdLogoutDescription,
CmdLoginDescription,
CmdConnectDescription,
CmdDisconnectDescription,
CmdMcpDescription,
McpRecommendedUnknownId,
McpRecommendationsHeading,
@@ -3189,6 +3191,8 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdLoadDescription,
MessageId::CmdLogoutDescription,
MessageId::CmdLoginDescription,
MessageId::CmdConnectDescription,
MessageId::CmdDisconnectDescription,
MessageId::CmdMcpDescription,
MessageId::McpRecommendedUnknownId,
MessageId::McpRecommendationsHeading,
+45 -15
View File
@@ -3341,12 +3341,9 @@ pub fn lsp_command(app: &mut App, arg: Option<&str>) -> CommandResult {
pub fn login(app: &mut App, arg: Option<&str>) -> CommandResult {
let raw = arg.map(str::trim).unwrap_or("");
if raw.split_whitespace().count() > 1 {
return CommandResult::error("Usage: /login [status|account|key|<provider>]");
}
match raw {
"" => CommandResult::action(AppAction::OpenProviderPicker),
"status" => CommandResult::message(login_status_text(app)),
let token = raw.split_whitespace().next().unwrap_or("");
match token {
"" | "status" => CommandResult::message(login_status_text(app)),
"key" | "provider" => CommandResult::with_message_and_action(
"Open the provider picker to store an API key. Account sign-in is `codewhale login`.",
AppAction::OpenProviderPicker,
@@ -3354,26 +3351,59 @@ pub fn login(app: &mut App, arg: Option<&str>) -> CommandResult {
"account" => CommandResult::message(
"TUI cannot start the browser device flow without freezing the session.\n\
Run `codewhale login` (same as `codewhale account login`) in a terminal.\n\
Then `/login status` to confirm the session landed."
Then `/login` to confirm the session landed."
.to_string(),
),
provider
if app
.plugin_registry
.active_plugins()
.into_iter()
.any(|plugin| plugin.manifest.providers.contains_key(provider)) =>
{
other => CommandResult::error(format!(
"Usage: /login [status|account|key]\nUnknown argument: {other}\n\
To sign in to a plugin provider, use `/connect <provider>`."
)),
}
}
fn is_enabled_plugin_provider(app: &App, provider: &str) -> bool {
app.plugin_registry
.active_plugins()
.into_iter()
.any(|plugin| plugin.manifest.providers.contains_key(provider))
}
pub fn connect(app: &mut App, arg: Option<&str>) -> CommandResult {
let raw = arg.map(str::trim).unwrap_or("");
if raw.split_whitespace().count() > 1 {
return CommandResult::error("Usage: /connect [<provider>]");
}
match raw {
"" => CommandResult::action(AppAction::OpenProviderPicker),
provider if is_enabled_plugin_provider(app, provider) => {
CommandResult::action(AppAction::StartPluginLogin {
provider: provider.to_owned(),
})
}
other => CommandResult::error(format!(
"Usage: /login [status|account|key|<provider>]\nUnknown or inactive provider: {other}"
"Usage: /connect [<provider>]\nUnknown or inactive plugin provider: {other}\n\
Codewhale account sign-in is `/login`."
)),
}
}
pub fn disconnect(app: &mut App, arg: Option<&str>) -> CommandResult {
let raw = arg.map(str::trim).unwrap_or("");
if raw.is_empty() || raw.split_whitespace().count() > 1 {
return CommandResult::error("Usage: /disconnect <provider>");
}
if is_enabled_plugin_provider(app, raw) {
CommandResult::action(AppAction::StartPluginLogout {
provider: raw.to_owned(),
})
} else {
CommandResult::error(format!(
"Usage: /disconnect <provider>\nUnknown or inactive plugin provider: {raw}\n\
Signing out of Codewhale is `/logout`."
))
}
}
fn login_status_text(app: &App) -> String {
use codewhale_secrets::account::{
ACCOUNT_API_BASE_ENV, AccountSessionState, AccountSessionStore, DEFAULT_ACCOUNT_API_BASE,
+25 -17
View File
@@ -22,6 +22,8 @@ impl CommandGroup for ConfigCommands {
Box::new(FunctionCommand::new(&IMPORT_CLAUDE_INFO, run_import_claude)),
crate::commands::config_policy_host::permissions_registration(),
Box::new(FunctionCommand::new(&LOGIN_INFO, run_login)),
Box::new(FunctionCommand::new(&CONNECT_INFO, run_connect)),
Box::new(FunctionCommand::new(&DISCONNECT_INFO, run_disconnect)),
Box::new(FunctionCommand::new(&AUTH_INFO, run_auth)),
Box::new(FunctionCommand::new(&RAIL_INFO, run_rail)),
Box::new(FunctionCommand::new(&PET_INFO, run_pet)),
@@ -54,9 +56,21 @@ static IMPORT_CLAUDE_INFO: CommandInfo = CommandInfo {
static LOGIN_INFO: CommandInfo = CommandInfo {
name: "login",
aliases: &[],
usage: "/login [status|account|key|<provider>]",
usage: "/login [status|account|key]",
description_id: MessageId::CmdLoginDescription,
};
static CONNECT_INFO: CommandInfo = CommandInfo {
name: "connect",
aliases: &[],
usage: "/connect [<provider>]",
description_id: MessageId::CmdConnectDescription,
};
static DISCONNECT_INFO: CommandInfo = CommandInfo {
name: "disconnect",
aliases: &[],
usage: "/disconnect <provider>",
description_id: MessageId::CmdDisconnectDescription,
};
static AUTH_INFO: CommandInfo = CommandInfo {
name: "auth",
aliases: &[],
@@ -128,7 +142,7 @@ static TRUST_INFO: CommandInfo = CommandInfo {
static LOGOUT_INFO: CommandInfo = CommandInfo {
name: "logout",
aliases: &[],
usage: "/logout [<provider>]",
usage: "/logout",
description_id: MessageId::CmdLogoutDescription,
};
fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
@@ -180,6 +194,12 @@ fn run_trust(app: &mut App, arg: Option<&str>) -> CommandResult {
fn run_logout(app: &mut App, arg: Option<&str>) -> CommandResult {
run_registered(app, "logout", arg)
}
fn run_connect(app: &mut App, arg: Option<&str>) -> CommandResult {
run_registered(app, "connect", arg)
}
fn run_disconnect(app: &mut App, arg: Option<&str>) -> CommandResult {
run_registered(app, "disconnect", arg)
}
pub(in crate::commands) fn dispatch(
app: &mut App,
command: &str,
@@ -230,21 +250,9 @@ pub(in crate::commands) fn dispatch(
"theme" => config::theme(app, arg),
"verbose" => config::verbose(app, arg),
"trust" | "xinren" => config::trust(app, arg),
"logout" => match arg.map(str::trim).filter(|arg| !arg.is_empty()) {
None => config::logout(app),
Some(provider)
if app
.plugin_registry
.active_plugins()
.into_iter()
.any(|plugin| plugin.manifest.providers.contains_key(provider)) =>
{
CommandResult::action(crate::tui::app::AppAction::StartPluginLogout {
provider: provider.to_owned(),
})
}
Some(_) => CommandResult::error("Usage: /logout [<enabled-plugin-provider>]"),
},
"logout" => config::logout(app),
"connect" => config::connect(app, arg),
"disconnect" => config::disconnect(app, arg),
_ => return None,
};
Some(result)
+19 -8
View File
@@ -711,11 +711,12 @@ mod tests {
assert!(!message.contains("Daytona"), "{message}");
assert!(!message.contains("set-slot"), "{message}");
assert_eq!(
execute("/login", &mut app).action,
Some(AppAction::OpenProviderPicker)
assert!(
execute("/login", &mut app)
.message
.expect("login status")
.contains("Codewhale login")
);
assert!(execute("/login status extra", &mut app).is_error);
let key = execute("/login key", &mut app);
assert!(!key.is_error);
assert_eq!(key.action, Some(AppAction::OpenProviderPicker));
@@ -723,15 +724,25 @@ mod tests {
let daytona = execute("/login daytona", &mut app);
assert!(daytona.is_error);
let err = daytona.message.expect("usage");
assert!(
err.contains("Usage: /login [status|account|key|<provider>]"),
"{err}"
);
assert!(err.contains("Usage: /login [status|account|key]"), "{err}");
assert!(err.contains("/connect <provider>"), "{err}");
let unknown = execute("/login oauth", &mut app);
assert!(unknown.is_error);
let err = unknown.message.expect("usage");
assert!(err.contains("Usage: /login"), "{err}");
assert_eq!(
execute("/connect", &mut app).action,
Some(AppAction::OpenProviderPicker)
);
let connect = execute("/connect nothing-enabled", &mut app);
assert!(connect.is_error);
let err = connect.message.expect("usage");
assert!(err.contains("Usage: /connect [<provider>]"), "{err}");
assert!(err.contains("`/login`"), "{err}");
assert!(execute("/disconnect", &mut app).is_error);
assert!(execute("/disconnect nothing-enabled", &mut app).is_error);
}
#[test]
+2
View File
@@ -538,7 +538,9 @@ const PROTECTED_BUILTINS: &[&str] = &[
"auth",
"auto",
"config",
"connect",
"constitution",
"disconnect",
"hooks",
"login",
"logout",
+2 -2
View File
@@ -4533,7 +4533,7 @@ impl PluginOAuthConfig {
callback_path: &self.callback_path,
loopback_ports: &[],
relogin_hint: "codewhale auth plugin-login --provider <provider>",
session_login_hint: "/login <provider>",
session_login_hint: "/connect <provider>",
callback_conflict_hint: "",
}
}
@@ -4794,7 +4794,7 @@ fn plugin_oauth_access_token_with_store(
) -> Result<String> {
let resolve = |raw: &mut Option<String>| -> Result<String> {
let stored = raw.as_ref().context(
"Plugin OAuth login missing; use /login <provider> in Codewhale or codewhale auth plugin-login --provider <provider> in a terminal",
"Plugin OAuth login missing; use /connect <provider> in Codewhale or codewhale auth plugin-login --provider <provider> in a terminal",
)?;
let mut token: PluginOAuthTokens = serde_json::from_str(stored)
.map_err(|_| anyhow::anyhow!("Plugin OAuth credential store contains invalid data"))?;
+4 -4
View File
@@ -49,18 +49,18 @@ the extension host is enabled. Receipts from the previous policy require an
explicit review again, including bundles without provider declarations. An
old review is never silently upgraded.
## Sign in inside Codewhale
## Connect inside Codewhale
After installation, review and enable the bundle, then start a new session.
Run `/login` to open the provider picker, or `/login <provider-id>` to authorize
Run `/connect` to open the provider picker, or `/connect <provider-id>` to authorize
an enabled plugin directly. The host runs its existing PKCE flow and then
refreshes the provider’s standard `/models` roster. Choose the model explicitly;
login never chooses the first model, changes billing groups, or copies another
application’s credentials. A provider selected from `/provider` uses the same
flow instead of asking for an API key.
`/login status` retains account status. `/logout <provider-id>` removes only that
plugin’s local grant. Bare `/logout` retains Codewhale account logout.
`/login` stays Codewhale-account sign-in. `/disconnect <provider-id>` removes only that
plugin’s local grant. `/logout` stays Codewhale account logout.
Plugin rosters are account-scoped and not reused from the disk cache.
The terminal is temporarily suspended during browser authorization, like the
built-in PKCE flows; device-code and remote revocation are not added here.
+2 -2
View File
@@ -568,7 +568,7 @@ owner = "crates/tui/src/commands/groups/core/home.rs"
[[feature]]
id = "account-login"
name = "Account login"
summary = "/login selects providers; /login <id> runs reviewed plugin OAuth; status and account remain explicit."
summary = "/login signs in to Codewhale or stores provider keys; plugin providers connect with /connect."
status = "stable"
surfaces = { tui = "stable" }
owner = "crates/tui/src/commands/groups/config/config.rs"
@@ -682,7 +682,7 @@ owner = "crates/tui/src/oauth.rs"
[[feature]]
id = "plugin-oauth-providers"
name = "Reviewed plugin OAuth providers"
summary = "Reviewed plugins appear in /login and /provider; the host owns PKCE, model discovery and per-request approvals."
summary = "Reviewed plugins connect with /connect <id>; the host owns PKCE, model discovery and per-request approvals."
status = "experimental"
surfaces = { tui = "preview", api = "partial", web = "none" }
since = "unreleased"