diff --git a/Cargo.lock b/Cargo.lock index 562f507f88..50d510db30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3064,11 +3064,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -4354,22 +4352,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "motosan-ai-oauth" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16994a67367076b08479af83ca05503c4d423fc6631f849fb92fa787956ad557" -dependencies = [ - "base64 0.22.1", - "percent-encoding", - "rand 0.9.4", - "reqwest 0.12.28", - "serde", - "sha2 0.10.9", - "thiserror 2.0.18", - "tokio", -] - [[package]] name = "moxcms" version = "0.8.1" @@ -5000,7 +4982,6 @@ dependencies = [ "log", "mail-parser", "matrix-sdk", - "motosan-ai-oauth", "nu-ansi-term 0.46.0", "objc2 0.6.4", "objc2-contacts", @@ -6153,7 +6134,6 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -6167,7 +6147,6 @@ dependencies = [ "hyper-util", "js-sys", "log", - "mime", "mime_guess", "native-tls", "percent-encoding", @@ -7441,27 +7420,6 @@ dependencies = [ "windows 0.57.0", ] -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.1", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys 0.8.7", - "libc", -] - [[package]] name = "tap" version = "1.0.1" @@ -9106,17 +9064,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-result" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 799c284deb..99d512a919 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,7 +95,6 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["f tracing-appender = "0.2" prometheus = { version = "0.14", default-features = false } urlencoding = "2.1" -motosan-ai-oauth = { version = "0.2", features = ["codex"] } thiserror = "2.0" ring = "0.17" prost = { version = "0.14", default-features = false } diff --git a/app/src/lib/i18n/chunks/de-3.ts b/app/src/lib/i18n/chunks/de-3.ts index b5857a82bd..996a81855a 100644 --- a/app/src/lib/i18n/chunks/de-3.ts +++ b/app/src/lib/i18n/chunks/de-3.ts @@ -123,8 +123,6 @@ const de3: TranslationMap = { 'subconscious.decision.failed': 'Fehlgeschlagen', 'subconscious.decision.cancelled': 'Abgesagt', 'subconscious.decision.skipped': 'Übersprungen', - 'subconscious.providerUnavailableTitle': 'Unterbewusstsein pausiert', - 'subconscious.providerSettings': 'KI-Einstellungen', 'actionable.complete': 'Komplett', 'actionable.dismiss': 'Entlassen', 'actionable.snooze': 'Schlummern', diff --git a/app/src/lib/i18n/chunks/de-5.ts b/app/src/lib/i18n/chunks/de-5.ts index 0e77924028..c9a3abf882 100644 --- a/app/src/lib/i18n/chunks/de-5.ts +++ b/app/src/lib/i18n/chunks/de-5.ts @@ -523,28 +523,6 @@ const de5: TranslationMap = { 'settings.mascot.colorYellow': 'Gelb', 'settings.mascot.libraryUnavailable': 'OpenHuman Bibliothek nicht verfügbar', 'settings.mascot.title': 'OpenHuman', - 'settings.developerMenu.mcpServer.title': 'MCP-Server', - 'settings.developerMenu.mcpServer.desc': - 'Externe MCP-Clients für die Verbindung zu OpenHuman konfigurieren', - 'settings.mcpServer.title': 'MCP-Server', - 'settings.mcpServer.toolsSectionTitle': 'Verfügbare Tools', - 'settings.mcpServer.toolsSectionDesc': - 'Tools, die über den MCP-stdio-Server bereitgestellt werden, wenn openhuman-core mcp läuft', - 'settings.mcpServer.configSectionTitle': 'Client-Konfiguration', - 'settings.mcpServer.configSectionDesc': - 'Wähle deinen MCP-Client, um den passenden Konfigurations-Snippet zu erzeugen', - 'settings.mcpServer.copySnippet': 'In Zwischenablage kopieren', - 'settings.mcpServer.copied': 'Kopiert!', - 'settings.mcpServer.openConfigFile': 'Konfigurationsdatei öffnen', - 'settings.mcpServer.binaryPathNotFound': - 'OpenHuman-Binary nicht gefunden. Bei Quellbau bitte mit `cargo build --bin openhuman-core` bauen.', - 'settings.mcpServer.openConfigError': 'Konfigurationsdatei konnte nicht geöffnet werden', - 'settings.mcpServer.clientClaudeDesktop': 'Claude Desktop', - 'settings.mcpServer.clientCursor': 'Cursor', - 'settings.mcpServer.clientCodex': 'Codex', - 'settings.mcpServer.clientZed': 'Zed', - 'settings.mcpServer.configFilePath': 'Konfigurationsdatei', - 'settings.mcpServer.clientSelectorAriaLabel': 'MCP-Client-Auswahl', }; export default de5; diff --git a/app/src/pages/onboarding/steps/ApiKeysStep.tsx b/app/src/pages/onboarding/steps/ApiKeysStep.tsx index eb305aaf51..5458e5ee3e 100644 --- a/app/src/pages/onboarding/steps/ApiKeysStep.tsx +++ b/app/src/pages/onboarding/steps/ApiKeysStep.tsx @@ -1,10 +1,7 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; import { setCloudProviderKey } from '../../../services/api/aiSettingsApi'; -import { callCoreRpc } from '../../../services/coreRpcClient'; -import { openUrl } from '../../../utils/openUrl'; -import { isTauri } from '../../../utils/tauriCommands/common'; import OnboardingNextButton from '../components/OnboardingNextButton'; interface ApiKeysStepProps { @@ -12,98 +9,17 @@ interface ApiKeysStepProps { onSkip: () => void; } -type OpenAiOAuthStatus = { connected: boolean; authMethod?: string | null }; - -const OPENAI_OAUTH_CONNECTED_LABEL = 'Connected with ChatGPT'; -const OPENAI_OAUTH_CONNECT_LABEL = 'Sign in with ChatGPT'; -const OPENAI_OAUTH_CALLBACK_HINT = - 'After signing in, paste the full redirect URL from your browser (starts with http://127.0.0.1:1455/).'; -const OPENAI_OAUTH_CALLBACK_PLACEHOLDER = 'http://127.0.0.1:1455/auth/callback?code=...&state=...'; - const ApiKeysStep = ({ onNext, onSkip }: ApiKeysStepProps) => { const { t } = useT(); const [openai, setOpenai] = useState(''); const [anthropic, setAnthropic] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); - const [oauthConnected, setOauthConnected] = useState(false); - const [oauthBusy, setOauthBusy] = useState(false); - const [oauthAwaitingCallback, setOauthAwaitingCallback] = useState(false); - const [oauthCallbackUrl, setOauthCallbackUrl] = useState(''); - - const refreshOAuthStatus = useCallback(async () => { - if (!isTauri()) { - return; - } - try { - const res = await callCoreRpc<{ result: OpenAiOAuthStatus }>({ - method: 'openhuman.inference_openai_oauth_status', - params: {}, - }); - setOauthConnected(Boolean(res?.result?.connected)); - } catch (err) { - console.debug('[onboarding:api-keys] oauth status check failed', err); - } - }, []); - - useEffect(() => { - void refreshOAuthStatus(); - }, [refreshOAuthStatus]); - - const handleOpenAiOAuthStart = async () => { - if (!isTauri()) { - setError('ChatGPT sign-in is only available in the desktop app.'); - return; - } - setOauthBusy(true); - setError(null); - try { - const res = await callCoreRpc<{ result: { authUrl: string } }>({ - method: 'openhuman.inference_openai_oauth_start', - params: {}, - }); - const authUrl = res?.result?.authUrl?.trim(); - if (!authUrl) { - throw new Error('missing authUrl'); - } - setOauthAwaitingCallback(true); - await openUrl(authUrl); - } catch (err) { - console.warn('[onboarding:api-keys] oauth start failed', err); - setError('Could not start ChatGPT sign-in. Try again or use an API key.'); - } finally { - setOauthBusy(false); - } - }; - - const handleOpenAiOAuthComplete = async () => { - const callback = oauthCallbackUrl.trim(); - if (!callback) { - setError('Paste the redirect URL from your browser after signing in.'); - return; - } - setOauthBusy(true); - setError(null); - try { - await callCoreRpc({ - method: 'openhuman.inference_openai_oauth_complete', - params: { callback_url: callback }, - }); - setOauthCallbackUrl(''); - setOauthAwaitingCallback(false); - setOauthConnected(true); - } catch (err) { - console.warn('[onboarding:api-keys] oauth complete failed', err); - setError('ChatGPT sign-in did not complete. Check the redirect URL and try again.'); - } finally { - setOauthBusy(false); - } - }; const handleSave = async () => { const trimmedOpenai = openai.trim(); const trimmedAnthropic = anthropic.trim(); - if (!trimmedOpenai && !trimmedAnthropic && !oauthConnected) { + if (!trimmedOpenai && !trimmedAnthropic) { onSkip(); return; } @@ -140,65 +56,12 @@ const ApiKeysStep = ({ onNext, onSkip }: ApiKeysStepProps) => {
-
-
- - {t('onboarding.apiKeys.openaiLabel')} - - {oauthConnected ? ( - - {OPENAI_OAUTH_CONNECTED_LABEL} - - ) : null} -
-

- Use ChatGPT Plus/Pro (subscription) or an OpenAI API key — not both required. -

- - {oauthAwaitingCallback && !oauthConnected ? ( -
-

- {OPENAI_OAUTH_CALLBACK_HINT} -

- { - setOauthCallbackUrl(e.target.value); - setError(null); - }} - className="rounded-lg border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-xs text-stone-900 dark:text-neutral-100 placeholder-stone-400 dark:placeholder-neutral-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" - /> - -
- ) : null} -
-
- - or API key - -
-
+
+ { type="button" onClick={onSkip} disabled={saving} - className="text-xs text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 underline disabled:opacity-50"> + className="text-xs text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 underline disabled:opacity-50"> {t('onboarding.apiKeys.skipForNow')}
diff --git a/app/src/pages/onboarding/steps/__tests__/ApiKeysStep.test.tsx b/app/src/pages/onboarding/steps/__tests__/ApiKeysStep.test.tsx deleted file mode 100644 index 31fb03dc33..0000000000 --- a/app/src/pages/onboarding/steps/__tests__/ApiKeysStep.test.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { setCloudProviderKey } from '../../../../services/api/aiSettingsApi'; -import { callCoreRpc } from '../../../../services/coreRpcClient'; -import { renderWithProviders } from '../../../../test/test-utils'; -import { openUrl } from '../../../../utils/openUrl'; -import { isTauri } from '../../../../utils/tauriCommands/common'; -import ApiKeysStep from '../ApiKeysStep'; - -vi.mock('../../../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() })); - -vi.mock('../../../../utils/openUrl', () => ({ openUrl: vi.fn().mockResolvedValue(undefined) })); - -vi.mock('../../../../utils/tauriCommands/common', () => ({ isTauri: vi.fn(() => true) })); - -vi.mock('../../../../services/api/aiSettingsApi', () => ({ - setCloudProviderKey: vi.fn().mockResolvedValue(undefined), -})); - -describe('ApiKeysStep OpenAI OAuth', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(isTauri).mockReturnValue(true); - vi.mocked(openUrl).mockResolvedValue(undefined); - vi.mocked(setCloudProviderKey).mockResolvedValue(undefined); - }); - - it('shows connected badge when oauth status reports connected', async () => { - vi.mocked(callCoreRpc).mockResolvedValueOnce({ result: { connected: true } }); - - renderWithProviders(); - - expect(await screen.findByTestId('onboarding-openai-oauth-connected')).toBeInTheDocument(); - expect(screen.getByText('Connected with ChatGPT')).toBeInTheDocument(); - }); - - it('starts oauth and accepts pasted callback URL', async () => { - vi.mocked(callCoreRpc) - .mockResolvedValueOnce({ result: { connected: false } }) - .mockResolvedValueOnce({ - result: { - authUrl: 'https://auth.openai.com/oauth/authorize?client_id=test', - state: 'state-1', - redirectUri: 'http://127.0.0.1:1455/auth/callback', - }, - }) - .mockResolvedValueOnce({ result: { connected: true } }); - - renderWithProviders(); - - fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect')); - - await waitFor(() => { - expect(openUrl).toHaveBeenCalledWith( - 'https://auth.openai.com/oauth/authorize?client_id=test' - ); - }); - - const input = await screen.findByTestId('onboarding-openai-oauth-callback-input'); - fireEvent.change(input, { - target: { value: 'http://127.0.0.1:1455/auth/callback?code=abc&state=state-1' }, - }); - fireEvent.click(screen.getByTestId('onboarding-openai-oauth-complete')); - - await waitFor(() => { - expect(callCoreRpc).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'openhuman.inference_openai_oauth_complete', - params: { callback_url: 'http://127.0.0.1:1455/auth/callback?code=abc&state=state-1' }, - }) - ); - }); - - expect(await screen.findByTestId('onboarding-openai-oauth-connected')).toBeInTheDocument(); - }); - - it('shows a desktop-only error without calling core outside Tauri', async () => { - vi.mocked(isTauri).mockReturnValue(false); - - renderWithProviders(); - - fireEvent.click(screen.getByTestId('onboarding-openai-oauth-connect')); - - expect( - await screen.findByText('ChatGPT sign-in is only available in the desktop app.') - ).toBeInTheDocument(); - expect(callCoreRpc).not.toHaveBeenCalled(); - expect(openUrl).not.toHaveBeenCalled(); - }); - - it('reports an oauth start failure when core omits authUrl', async () => { - vi.mocked(callCoreRpc) - .mockResolvedValueOnce({ result: { connected: false } }) - .mockResolvedValueOnce({ result: { authUrl: ' ' } }); - - renderWithProviders(); - - fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect')); - - expect( - await screen.findByText('Could not start ChatGPT sign-in. Try again or use an API key.') - ).toBeInTheDocument(); - expect(openUrl).not.toHaveBeenCalled(); - }); - - it('requires a pasted callback before completing oauth', async () => { - vi.mocked(callCoreRpc) - .mockResolvedValueOnce({ result: { connected: false } }) - .mockResolvedValueOnce({ - result: { authUrl: 'https://auth.openai.com/oauth/authorize?client_id=test' }, - }); - - renderWithProviders(); - - fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect')); - await screen.findByTestId('onboarding-openai-oauth-callback-input'); - fireEvent.click(screen.getByTestId('onboarding-openai-oauth-complete')); - - expect( - await screen.findByText('Paste the redirect URL from your browser after signing in.') - ).toBeInTheDocument(); - expect(callCoreRpc).not.toHaveBeenCalledWith( - expect.objectContaining({ method: 'openhuman.inference_openai_oauth_complete' }) - ); - }); - - it('reports an oauth completion failure and keeps the callback form visible', async () => { - vi.mocked(callCoreRpc) - .mockResolvedValueOnce({ result: { connected: false } }) - .mockResolvedValueOnce({ - result: { authUrl: 'https://auth.openai.com/oauth/authorize?client_id=test' }, - }) - .mockRejectedValueOnce(new Error('state mismatch')); - - renderWithProviders(); - - fireEvent.click(await screen.findByTestId('onboarding-openai-oauth-connect')); - const input = await screen.findByTestId('onboarding-openai-oauth-callback-input'); - fireEvent.change(input, { - target: { value: 'http://127.0.0.1:1455/auth/callback?code=abc&state=wrong' }, - }); - fireEvent.click(screen.getByTestId('onboarding-openai-oauth-complete')); - - expect( - await screen.findByText( - 'ChatGPT sign-in did not complete. Check the redirect URL and try again.' - ) - ).toBeInTheDocument(); - expect(screen.getByTestId('onboarding-openai-oauth-callback-input')).toBeInTheDocument(); - }); - - it('continues without saving API keys when oauth is already connected', async () => { - const onNext = vi.fn(); - vi.mocked(callCoreRpc).mockResolvedValueOnce({ result: { connected: true } }); - - renderWithProviders(); - - await screen.findByTestId('onboarding-openai-oauth-connected'); - fireEvent.click(screen.getByTestId('onboarding-next-button')); - - await waitFor(() => { - expect(onNext).toHaveBeenCalledTimes(1); - }); - expect(setCloudProviderKey).not.toHaveBeenCalled(); - }); -}); diff --git a/src/core/auth.rs b/src/core/auth.rs index a1498d11e8..7e08f36664 100644 --- a/src/core/auth.rs +++ b/src/core/auth.rs @@ -38,7 +38,10 @@ use std::path::Path; use std::sync::OnceLock; #[cfg(unix)] -use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _}; +use std::os::unix::fs::OpenOptionsExt as _; + +#[cfg(all(unix, test))] +use std::os::unix::fs::PermissionsExt as _; use axum::http::{header, Method, StatusCode}; use axum::middleware::Next; diff --git a/src/openhuman/app_state/ops_tests.rs b/src/openhuman/app_state/ops_tests.rs index eab67d560b..be6d61a11e 100644 --- a/src/openhuman/app_state/ops_tests.rs +++ b/src/openhuman/app_state/ops_tests.rs @@ -206,20 +206,32 @@ fn runtime_snapshot_cache_hit_within_ttl() { let _reset = SnapshotCacheResetGuard; let dummy = build_dummy_runtime_snapshot(); - *RUNTIME_SNAPSHOT_CACHE.lock() = Some(CachedRuntimeSnapshot { - snapshot: dummy.clone(), - fetched_at: Instant::now(), - }); - - let cache = RUNTIME_SNAPSHOT_CACHE.lock(); - let entry = cache.as_ref().expect("cache should have entry"); + let fetched_at = Instant::now(); + + // Hold the lock for the entire write-then-read sequence so no concurrent + // test can overwrite RUNTIME_SNAPSHOT_CACHE between the write and the read. + let (elapsed, phase) = { + let mut cache = RUNTIME_SNAPSHOT_CACHE.lock(); + *cache = Some(CachedRuntimeSnapshot { + snapshot: dummy.clone(), + fetched_at, + }); + let entry = cache.as_ref().expect("cache should have entry"); + ( + entry.fetched_at.elapsed(), + entry.snapshot.autocomplete.phase.clone(), + ) + }; assert!( - entry.fetched_at.elapsed() < RUNTIME_SNAPSHOT_TTL, + elapsed < RUNTIME_SNAPSHOT_TTL, "fresh entry should be within TTL" ); - assert_eq!(entry.snapshot.autocomplete.phase, dummy.autocomplete.phase); + assert_eq!(phase, dummy.autocomplete.phase); } +// This test verifies pure Instant arithmetic — no global cache state needed. +// Using the global cache here would race with cache_hit_within_ttl when tests +// run in parallel, causing a flaky "stale entry should be past TTL" failure. #[test] fn runtime_snapshot_cache_miss_after_ttl() { let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock(); diff --git a/src/openhuman/inference/local/mod.rs b/src/openhuman/inference/local/mod.rs index b86e7df291..aef33e8a98 100644 --- a/src/openhuman/inference/local/mod.rs +++ b/src/openhuman/inference/local/mod.rs @@ -37,7 +37,6 @@ pub(crate) mod model_requirements; mod ollama; mod process_util; pub(crate) mod provider; -pub(crate) use model_requirements::{evaluate_context, ContextEligibility, MIN_CONTEXT_TOKENS}; pub(crate) use ollama::{ollama_base_url, OLLAMA_BASE_URL}; pub mod service; pub(crate) mod voice_install_common; diff --git a/src/openhuman/inference/mod.rs b/src/openhuman/inference/mod.rs index 7a04d75ecd..3848ed5873 100644 --- a/src/openhuman/inference/mod.rs +++ b/src/openhuman/inference/mod.rs @@ -17,7 +17,6 @@ pub mod http; pub mod local; pub mod model_context; pub mod model_ids; -pub mod openai_oauth; pub mod ops; pub mod parse; pub mod paths; diff --git a/src/openhuman/inference/openai_oauth/config.rs b/src/openhuman/inference/openai_oauth/config.rs deleted file mode 100644 index 21f6d8aaf1..0000000000 --- a/src/openhuman/inference/openai_oauth/config.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! OpenAI Codex (ChatGPT subscription) OAuth endpoints and client registration. - -use motosan_ai_oauth::providers::codex::codex; -use motosan_ai_oauth::OAuthConfig; - -/// Loopback redirect registered with the Codex public OAuth app. -pub const REDIRECT_URI: &str = "http://127.0.0.1:1455/auth/callback"; - -pub fn codex_oauth_config() -> OAuthConfig { - codex() -} diff --git a/src/openhuman/inference/openai_oauth/flow.rs b/src/openhuman/inference/openai_oauth/flow.rs deleted file mode 100644 index a412bf7fe1..0000000000 --- a/src/openhuman/inference/openai_oauth/flow.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! OAuth start / complete / status for OpenAI Codex (ChatGPT subscription). - -use std::path::PathBuf; -use std::time::Duration; - -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use chrono::{DateTime, Utc}; -use motosan_ai_oauth::StateStrategy; -use rand::RngExt as _; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; - -use crate::openhuman::config::Config; -use crate::openhuman::credentials::state_dir_from_config; - -use super::config::{codex_oauth_config, REDIRECT_URI}; -use super::store::{persist_openai_oauth_token, OPENAI_OAUTH_PROFILE_NAME, OPENAI_PROVIDER_KEY}; - -const LOG_PREFIX: &str = "[inference][openai-oauth]"; -const PENDING_FILENAME: &str = "openai-oauth-pending.json"; -const PENDING_TTL_SECS: u64 = 600; -const OAUTH_HTTP_TIMEOUT_SECS: u64 = 20; - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct PendingOAuth { - state: String, - verifier: String, - redirect_uri: String, - created_at: u64, -} - -#[derive(Debug, Clone, Serialize)] -pub struct OpenAiOAuthStartResult { - pub auth_url: String, - pub state: String, - pub redirect_uri: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct OpenAiOAuthStatusResult { - pub connected: bool, - pub profile_id: Option, - pub expires_at: Option>, - pub auth_method: Option, -} - -fn pending_path(config: &Config) -> PathBuf { - state_dir_from_config(config).join(PENDING_FILENAME) -} - -fn generate_pkce() -> (String, String) { - let mut bytes = [0u8; 64]; - rand::rng().fill(&mut bytes); - let verifier = URL_SAFE_NO_PAD.encode(bytes); - let hash = Sha256::digest(verifier.as_bytes()); - let challenge = URL_SAFE_NO_PAD.encode(hash); - (verifier, challenge) -} - -fn random_state() -> String { - let mut state_bytes = [0u8; 16]; - rand::rng().fill(&mut state_bytes); - URL_SAFE_NO_PAD.encode(state_bytes) -} - -fn write_pending(config: &Config, pending: &PendingOAuth) -> Result<(), String> { - let path = pending_path(config); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; - } - let json = serde_json::to_vec_pretty(pending).map_err(|e| e.to_string())?; - std::fs::write(&path, json).map_err(|e| e.to_string())?; - log::debug!("{LOG_PREFIX} pending session written"); - Ok(()) -} - -fn read_pending(config: &Config) -> Result, String> { - let path = pending_path(config); - if !path.exists() { - return Ok(None); - } - let bytes = std::fs::read(&path).map_err(|e| e.to_string())?; - if bytes.is_empty() { - return Ok(None); - } - let pending: PendingOAuth = serde_json::from_slice(&bytes).map_err(|e| e.to_string())?; - let now = unix_now_secs(); - if now.saturating_sub(pending.created_at) > PENDING_TTL_SECS { - let _ = std::fs::remove_file(&path); - return Ok(None); - } - Ok(Some(pending)) -} - -fn clear_pending(config: &Config) { - let path = pending_path(config); - if path.exists() { - let _ = std::fs::remove_file(path); - } -} - -pub fn start_openai_oauth(config: &Config) -> Result { - let oauth_cfg = codex_oauth_config(); - let (verifier, challenge) = generate_pkce(); - let state = match oauth_cfg.state_strategy { - StateStrategy::Random => random_state(), - StateStrategy::EqualsVerifier => verifier.clone(), - }; - - let pending = PendingOAuth { - state: state.clone(), - verifier, - redirect_uri: REDIRECT_URI.to_string(), - created_at: unix_now_secs(), - }; - write_pending(config, &pending)?; - - let auth_url = build_authorize_url(&oauth_cfg, &challenge, &state, REDIRECT_URI); - log::info!("{LOG_PREFIX} oauth start state_len={}", state.len()); - - Ok(OpenAiOAuthStartResult { - auth_url, - state, - redirect_uri: REDIRECT_URI.to_string(), - }) -} - -pub fn parse_callback_input(input: &str) -> Result<(String, String), String> { - let trimmed = input.trim(); - if trimmed.is_empty() { - return Err("callback URL is required".to_string()); - } - - let query = if let Ok(parsed) = url::Url::parse(trimmed) { - parsed.query().unwrap_or("").to_string() - } else if trimmed.contains('=') { - trimmed.to_string() - } else { - return Err("invalid callback URL".to_string()); - }; - - let mut code: Option = None; - let mut state: Option = None; - for (key, value) in url::form_urlencoded::parse(query.as_bytes()) { - match key.as_ref() { - "code" if !value.is_empty() => code = Some(value.into_owned()), - "state" if !value.is_empty() => state = Some(value.into_owned()), - _ => {} - } - } - - let code = code.ok_or_else(|| "callback URL missing code parameter".to_string())?; - let state = state.ok_or_else(|| "callback URL missing state parameter".to_string())?; - Ok((code, state)) -} - -pub async fn complete_openai_oauth( - config: &Config, - callback_input: &str, -) -> Result { - let pending = read_pending(config)? - .ok_or_else(|| "no pending OAuth session; call openai_oauth_start first".to_string())?; - - let (code, returned_state) = parse_callback_input(callback_input)?; - if returned_state != pending.state { - clear_pending(config); - return Err("OAuth state mismatch — try connecting again".to_string()); - } - - let oauth_cfg = codex_oauth_config(); - let token = - exchange_authorization_code(&oauth_cfg, &code, &pending.verifier, &pending.redirect_uri) - .await?; - - clear_pending(config); - let profile = persist_openai_oauth_token(config, &token)?; - log::info!("{LOG_PREFIX} oauth complete profile_id={}", profile.id); - - Ok(serde_json::json!({ - "connected": true, - "profileId": profile.id, - "provider": OPENAI_PROVIDER_KEY, - "authMethod": "oauth", - })) -} - -pub fn openai_oauth_status(config: &Config) -> Result { - use crate::openhuman::credentials::profiles::AuthProfileKind; - use crate::openhuman::credentials::AuthService; - - let auth = AuthService::from_config(config); - let profile = auth - .get_profile(OPENAI_PROVIDER_KEY, Some(OPENAI_OAUTH_PROFILE_NAME)) - .map_err(|e| e.to_string())?; - - let Some(profile) = profile else { - return Ok(OpenAiOAuthStatusResult { - connected: false, - profile_id: None, - expires_at: None, - auth_method: None, - }); - }; - - if profile.kind != AuthProfileKind::OAuth { - return Ok(OpenAiOAuthStatusResult { - connected: false, - profile_id: Some(profile.id), - expires_at: None, - auth_method: Some("token".to_string()), - }); - } - - Ok(OpenAiOAuthStatusResult { - connected: true, - profile_id: Some(profile.id), - expires_at: profile.token_set.as_ref().and_then(|t| t.expires_at), - auth_method: Some("oauth".to_string()), - }) -} - -pub fn disconnect_openai_oauth(config: &Config) -> Result { - use crate::openhuman::credentials::AuthService; - - let auth = AuthService::from_config(config); - let removed = auth - .remove_profile(OPENAI_PROVIDER_KEY, OPENAI_OAUTH_PROFILE_NAME) - .map_err(|e| e.to_string())?; - clear_pending(config); - Ok(serde_json::json!({ "disconnected": removed })) -} - -pub(super) fn build_authorize_url( - config: &motosan_ai_oauth::OAuthConfig, - challenge: &str, - state: &str, - redirect_uri: &str, -) -> String { - let mut url = reqwest::Url::parse(config.auth_url).expect("auth_url must be valid"); - { - let mut q = url.query_pairs_mut(); - q.append_pair("client_id", config.client_id) - .append_pair("response_type", "code") - .append_pair("redirect_uri", redirect_uri) - .append_pair("scope", &config.scopes.join(" ")) - .append_pair("state", state) - .append_pair("code_challenge", challenge) - .append_pair("code_challenge_method", "S256"); - for (k, v) in config.extra_auth_params { - q.append_pair(k, v); - } - } - url.to_string() -} - -pub(super) async fn exchange_authorization_code( - config: &motosan_ai_oauth::OAuthConfig, - code: &str, - verifier: &str, - redirect_uri: &str, -) -> Result { - // Per RFC 6749 §4.1.3 the token request only requires grant_type, code, - // redirect_uri, code_verifier (PKCE), and client_id. `state` belongs to the - // authorization request / callback validation, not this exchange. - let mut params = vec![ - ("grant_type", "authorization_code"), - ("code", code), - ("redirect_uri", redirect_uri), - ("code_verifier", verifier), - ("client_id", config.client_id), - ]; - if let Some(secret) = config.client_secret { - params.push(("client_secret", secret)); - } - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(OAUTH_HTTP_TIMEOUT_SECS)) - .build() - .map_err(|e| e.to_string())?; - - let resp = client - .post(config.token_url) - .header("Accept", "application/json") - .form(¶ms) - .send() - .await - .map_err(|e| { - log::warn!("{LOG_PREFIX} token exchange request failed: {e}"); - e.to_string() - })?; - - if !resp.status().is_success() { - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - log::warn!( - "{LOG_PREFIX} token exchange http_status={status} body_len={}", - body.len() - ); - return Err(format!("HTTP {status}: {body}")); - } - - #[derive(serde::Deserialize)] - struct RawTokenResponse { - access_token: String, - #[serde(default)] - refresh_token: Option, - #[serde(default)] - id_token: Option, - expires_in: u64, - } - - let raw: RawTokenResponse = resp.json().await.map_err(|e| e.to_string())?; - Ok(motosan_ai_oauth::Token { - access_token: raw.access_token, - refresh_token: raw.refresh_token.unwrap_or_default(), - id_token: raw.id_token, - expires_in: raw.expires_in, - issued_at: unix_now_secs(), - }) -} - -fn unix_now_secs() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() -} diff --git a/src/openhuman/inference/openai_oauth/flow_tests.rs b/src/openhuman/inference/openai_oauth/flow_tests.rs deleted file mode 100644 index 8a6e697a98..0000000000 --- a/src/openhuman/inference/openai_oauth/flow_tests.rs +++ /dev/null @@ -1,465 +0,0 @@ -use super::flow::{build_authorize_url, exchange_authorization_code, parse_callback_input}; -use super::store::persist_openai_oauth_token; -use super::{ - complete_openai_oauth, disconnect_openai_oauth, openai_oauth_status, start_openai_oauth, -}; -use crate::openhuman::config::Config; -use crate::openhuman::credentials::profiles::{ - AuthProfile, AuthProfileKind, AuthProfilesStore, TokenSet, -}; -use crate::openhuman::inference::openai_oauth::lookup_openai_bearer_token; -use crate::openhuman::inference::openai_oauth::store::{ - OPENAI_OAUTH_PROFILE_NAME, OPENAI_PROVIDER_KEY, -}; -use crate::openhuman::inference::provider::factory::lookup_key_for_slug; -use chrono::{Duration, Utc}; -use motosan_ai_oauth::{OAuthConfig, StateStrategy, TokenBodyFormat}; -use tempfile::tempdir; -use wiremock::matchers::{method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -fn test_config(tmp: &tempfile::TempDir) -> Config { - Config { - config_path: tmp.path().join("config.toml"), - ..Config::default() - } -} - -fn runtime() -> tokio::runtime::Runtime { - tokio::runtime::Runtime::new().unwrap() -} - -fn unsigned_jwt(payload: serde_json::Value) -> String { - use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; - - let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#); - let payload = URL_SAFE_NO_PAD.encode(payload.to_string()); - format!("{header}.{payload}.") -} - -fn test_oauth_config(token_url: &'static str) -> OAuthConfig { - OAuthConfig { - client_id: "client-id", - client_secret: Some("client-secret"), - auth_url: "https://auth.example.test/oauth/authorize", - token_url, - scopes: &["scope-a", "scope-b"], - redirect_port: Some(1455), - callback_path: "/auth/callback", - redirect_uri_host: "127.0.0.1", - token_body: TokenBodyFormat::Form, - extra_auth_params: &[("prompt", "consent")], - state_strategy: StateStrategy::Random, - } -} - -#[test] -fn start_openai_oauth_returns_authorize_url() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - - let start = start_openai_oauth(&config).unwrap(); - assert!(start.auth_url.contains("auth.openai.com")); - assert!(start.auth_url.contains("code_challenge=")); - assert_eq!(start.redirect_uri, "http://127.0.0.1:1455/auth/callback"); - assert!(!start.state.is_empty()); - assert!(!openai_oauth_status(&config).unwrap().connected); -} - -#[test] -fn build_authorize_url_includes_codex_pkce_and_extra_params() { - let url = build_authorize_url( - &test_oauth_config("https://token.example.test/oauth/token"), - "challenge-123", - "state-123", - "http://127.0.0.1:1455/auth/callback", - ); - let parsed = reqwest::Url::parse(&url).unwrap(); - let pairs = parsed - .query_pairs() - .into_owned() - .collect::>(); - - assert_eq!( - pairs.get("client_id").map(String::as_str), - Some("client-id") - ); - assert_eq!(pairs.get("response_type").map(String::as_str), Some("code")); - assert_eq!( - pairs.get("scope").map(String::as_str), - Some("scope-a scope-b") - ); - assert_eq!(pairs.get("state").map(String::as_str), Some("state-123")); - assert_eq!( - pairs.get("code_challenge").map(String::as_str), - Some("challenge-123") - ); - assert_eq!( - pairs.get("code_challenge_method").map(String::as_str), - Some("S256") - ); - assert_eq!(pairs.get("prompt").map(String::as_str), Some("consent")); -} - -#[test] -fn parse_callback_input_accepts_full_redirect_url() { - let url = "http://127.0.0.1:1455/auth/callback?code=abc&state=xyz"; - let (code, state) = parse_callback_input(url).unwrap(); - assert_eq!(code, "abc"); - assert_eq!(state, "xyz"); -} - -#[test] -fn parse_callback_input_accepts_raw_query_string() { - let (code, state) = parse_callback_input("code=abc%20123&state=xyz").unwrap(); - assert_eq!(code, "abc 123"); - assert_eq!(state, "xyz"); -} - -#[test] -fn parse_callback_input_rejects_missing_code() { - let err = parse_callback_input("http://127.0.0.1:1455/auth/callback?state=xyz").unwrap_err(); - assert!(err.contains("code")); -} - -#[test] -fn parse_callback_input_rejects_blank_invalid_and_missing_state() { - let blank = parse_callback_input(" ").unwrap_err(); - assert!(blank.contains("required")); - - let invalid = parse_callback_input("not-a-callback").unwrap_err(); - assert!(invalid.contains("invalid")); - - let missing_state = - parse_callback_input("http://127.0.0.1:1455/auth/callback?code=abc").unwrap_err(); - assert!(missing_state.contains("state")); -} - -#[test] -fn complete_openai_oauth_rejects_missing_pending_session() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let err = runtime() - .block_on(complete_openai_oauth( - &config, - "http://127.0.0.1:1455/auth/callback?code=fake&state=state", - )) - .unwrap_err(); - assert!(err.contains("no pending OAuth session")); -} - -#[test] -fn complete_openai_oauth_rejects_expired_pending_session() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - std::fs::write( - tmp.path().join("openai-oauth-pending.json"), - serde_json::json!({ - "state": "state", - "verifier": "verifier", - "redirect_uri": "http://127.0.0.1:1455/auth/callback", - "created_at": 1_u64, - }) - .to_string(), - ) - .unwrap(); - - let err = runtime() - .block_on(complete_openai_oauth( - &config, - "http://127.0.0.1:1455/auth/callback?code=fake&state=state", - )) - .unwrap_err(); - assert!(err.contains("no pending OAuth session")); - assert!(!tmp.path().join("openai-oauth-pending.json").exists()); -} - -#[test] -fn complete_openai_oauth_rejects_state_mismatch() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let start = start_openai_oauth(&config).unwrap(); - let callback = format!( - "http://127.0.0.1:1455/auth/callback?code=fake&state=not-{}", - start.state - ); - let err = runtime() - .block_on(complete_openai_oauth(&config, &callback)) - .unwrap_err(); - assert!(err.contains("state mismatch")); -} - -#[tokio::test] -async fn exchange_authorization_code_parses_successful_token_response() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/token")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "access_token": "access-token", - "refresh_token": "refresh-token", - "id_token": "id-token", - "expires_in": 3600, - }))) - .mount(&server) - .await; - let token_url: &'static str = Box::leak(format!("{}/token", server.uri()).into_boxed_str()); - - let token = exchange_authorization_code( - &test_oauth_config(token_url), - "code-123", - "verifier-123", - "http://127.0.0.1:1455/auth/callback", - ) - .await - .unwrap(); - - assert_eq!(token.access_token, "access-token"); - assert_eq!(token.refresh_token, "refresh-token"); - assert_eq!(token.id_token.as_deref(), Some("id-token")); - assert_eq!(token.expires_in, 3600); - assert!(token.issued_at > 0); -} - -#[tokio::test] -async fn exchange_authorization_code_reports_http_errors() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/token")) - .respond_with(ResponseTemplate::new(400).set_body_string("bad auth code")) - .mount(&server) - .await; - let token_url: &'static str = Box::leak(format!("{}/token", server.uri()).into_boxed_str()); - - let err = exchange_authorization_code( - &test_oauth_config(token_url), - "code-123", - "verifier-123", - "http://127.0.0.1:1455/auth/callback", - ) - .await - .unwrap_err(); - - assert!(err.contains("HTTP 400")); - assert!(err.contains("bad auth code")); -} - -#[test] -fn persist_openai_oauth_token_stores_oauth_profile_with_metadata() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let access_token = unsigned_jwt(serde_json::json!({ "sub": "acct_123" })); - let token = motosan_ai_oauth::Token { - access_token: access_token.clone(), - refresh_token: "refresh-token".into(), - id_token: Some("id-token".into()), - expires_in: 3600, - issued_at: 123, - }; - - let profile = persist_openai_oauth_token(&config, &token).unwrap(); - assert_eq!(profile.kind, AuthProfileKind::OAuth); - assert_eq!( - profile.metadata.get("account_id").map(String::as_str), - Some("acct_123") - ); - assert_eq!( - profile - .token_set - .as_ref() - .map(|set| set.access_token.as_str()), - Some(access_token.as_str()) - ); - assert_eq!( - profile - .token_set - .as_ref() - .and_then(|set| set.refresh_token.as_deref()), - Some("refresh-token") - ); - assert!(profile - .token_set - .as_ref() - .and_then(|set| set.expires_at) - .is_some()); - - let data = AuthProfilesStore::new(tmp.path(), false).load().unwrap(); - let stored = data.profiles.get(&profile.id).unwrap(); - assert_eq!(stored.id, profile.id); -} - -#[test] -fn openai_oauth_status_reports_token_profile_as_disconnected() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let store = AuthProfilesStore::new(tmp.path(), false); - store - .upsert_profile( - AuthProfile::new_token( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - "sk-token-profile".to_string(), - ), - true, - ) - .unwrap(); - - let status = openai_oauth_status(&config).unwrap(); - assert!(!status.connected); - assert_eq!(status.auth_method.as_deref(), Some("token")); - assert!(status.profile_id.is_some()); -} - -#[test] -fn lookup_key_for_slug_prefers_api_key_over_oauth_for_openai() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let store = AuthProfilesStore::new(tmp.path(), false); - - let oauth_profile = AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: "oauth-access".into(), - refresh_token: Some("refresh".into()), - id_token: None, - expires_at: Some(Utc::now() + Duration::hours(1)), - token_type: Some("Bearer".into()), - scope: None, - }, - ); - store.upsert_profile(oauth_profile, true).unwrap(); - - let api_profile = - AuthProfile::new_token("provider:openai", "default", "sk-api-key".to_string()); - store.upsert_profile(api_profile, true).unwrap(); - - // The standard `lookup_key_for_slug` path resolves the API key first; the - // OAuth fallback only fires when no API key is present. - let token = lookup_key_for_slug("openai", &config).unwrap(); - assert_eq!(token, "sk-api-key"); -} - -#[test] -fn lookup_openai_bearer_token_uses_oauth_when_api_key_missing() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let store = AuthProfilesStore::new(tmp.path(), false); - let oauth_profile = AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: "oauth-access".into(), - refresh_token: Some("refresh".into()), - id_token: None, - expires_at: Some(Utc::now() + Duration::hours(1)), - token_type: Some("Bearer".into()), - scope: None, - }, - ); - store.upsert_profile(oauth_profile, true).unwrap(); - - let token = lookup_openai_bearer_token(&config).unwrap(); - assert_eq!(token.as_deref(), Some("oauth-access")); -} - -#[test] -fn lookup_key_for_slug_uses_legacy_openai_api_key_when_new_style_is_empty() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let store = AuthProfilesStore::new(tmp.path(), false); - let oauth_profile = AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: " ".into(), - refresh_token: None, - id_token: None, - expires_at: Some(Utc::now() + Duration::hours(1)), - token_type: Some("Bearer".into()), - scope: None, - }, - ); - store.upsert_profile(oauth_profile, true).unwrap(); - store - .upsert_profile( - AuthProfile::new_token("openai", "default", "sk-legacy-key".to_string()), - true, - ) - .unwrap(); - - // Legacy bare-slug key resolves through the standard path's legacy - // fallback, ahead of the OAuth fallback. - let token = lookup_key_for_slug("openai", &config).unwrap(); - assert_eq!(token, "sk-legacy-key"); -} - -#[test] -fn lookup_openai_bearer_token_keeps_expired_token_when_refresh_fails_without_runtime() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let store = AuthProfilesStore::new(tmp.path(), false); - let oauth_profile = AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: "expired-access".into(), - refresh_token: Some("refresh".into()), - id_token: None, - expires_at: Some(Utc::now() - Duration::minutes(5)), - token_type: Some("Bearer".into()), - scope: None, - }, - ); - store.upsert_profile(oauth_profile, true).unwrap(); - - let token = lookup_openai_bearer_token(&config).unwrap(); - assert_eq!(token.as_deref(), Some("expired-access")); -} - -#[test] -fn lookup_openai_bearer_token_returns_none_without_profiles_or_access_token() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - assert_eq!(lookup_openai_bearer_token(&config).unwrap(), None); - - let store = AuthProfilesStore::new(tmp.path(), false); - let empty_oauth_profile = AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: " ".into(), - refresh_token: None, - id_token: None, - expires_at: Some(Utc::now() - Duration::hours(1)), - token_type: Some("Bearer".into()), - scope: None, - }, - ); - store.upsert_profile(empty_oauth_profile, true).unwrap(); - - assert_eq!(lookup_openai_bearer_token(&config).unwrap(), None); -} - -#[test] -fn disconnect_openai_oauth_clears_profile() { - let tmp = tempdir().unwrap(); - let config = test_config(&tmp); - let store = AuthProfilesStore::new(tmp.path(), false); - let profile = AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: "oauth-access".into(), - refresh_token: None, - id_token: None, - expires_at: None, - token_type: Some("Bearer".into()), - scope: None, - }, - ); - store.upsert_profile(profile, true).unwrap(); - assert!(openai_oauth_status(&config).unwrap().connected); - - disconnect_openai_oauth(&config).unwrap(); - assert!(!openai_oauth_status(&config).unwrap().connected); -} diff --git a/src/openhuman/inference/openai_oauth/mod.rs b/src/openhuman/inference/openai_oauth/mod.rs deleted file mode 100644 index 21b87da1e7..0000000000 --- a/src/openhuman/inference/openai_oauth/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! ChatGPT / OpenAI Codex subscription OAuth for the `openai` cloud provider slug. - -mod config; -mod flow; -mod store; - -#[cfg(test)] -#[path = "flow_tests.rs"] -mod tests; - -pub use flow::{ - complete_openai_oauth, disconnect_openai_oauth, openai_oauth_status, start_openai_oauth, -}; -pub use store::{lookup_openai_bearer_token, OPENAI_OAUTH_PROFILE_NAME, OPENAI_PROVIDER_KEY}; diff --git a/src/openhuman/inference/openai_oauth/store.rs b/src/openhuman/inference/openai_oauth/store.rs deleted file mode 100644 index f80eef6eb9..0000000000 --- a/src/openhuman/inference/openai_oauth/store.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! Persist and resolve OpenAI OAuth tokens for the `openai` cloud provider slug. - -use base64::Engine; -use chrono::{Duration, Utc}; -use motosan_ai_oauth::Token; - -use crate::openhuman::config::Config; -use crate::openhuman::credentials::profiles::{AuthProfile, AuthProfilesStore, TokenSet}; -use crate::openhuman::credentials::{state_dir_from_config, AuthService}; - -use super::config::codex_oauth_config; - -const LOG_PREFIX: &str = "[inference][openai-oauth][store]"; - -pub const OPENAI_PROVIDER_KEY: &str = "provider:openai"; -pub const OPENAI_OAUTH_PROFILE_NAME: &str = "oauth"; - -fn token_set_from_codex(token: &Token) -> TokenSet { - let expires_at = - (token.expires_in > 0).then(|| Utc::now() + Duration::seconds(token.expires_in as i64)); - TokenSet { - access_token: token.access_token.clone(), - refresh_token: (!token.refresh_token.is_empty()).then(|| token.refresh_token.clone()), - id_token: token.id_token.clone(), - expires_at, - token_type: Some("Bearer".to_string()), - scope: None, - } -} - -pub fn persist_openai_oauth_token(config: &Config, token: &Token) -> Result { - let mut profile = AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - token_set_from_codex(token), - ); - if let Some(account_id) = extract_account_id_from_access_token(&token.access_token) { - profile - .metadata - .insert("account_id".to_string(), account_id); - } - - let store = auth_profiles_store(config); - store - .upsert_profile(profile.clone(), true) - .map_err(|e| e.to_string())?; - Ok(profile) -} - -fn auth_profiles_store(config: &Config) -> AuthProfilesStore { - AuthProfilesStore::new(&state_dir_from_config(config), config.secrets.encrypt) -} - -fn try_refresh_oauth_token(refresh: &str) -> Result { - let cfg = codex_oauth_config(); - let refresh = refresh.to_string(); - if let Ok(handle) = tokio::runtime::Handle::try_current() { - // `block_in_place` lets the multi-thread runtime move other tasks off this - // worker before we synchronously drive the refresh future, avoiding a - // deadlock when this lookup is reached from inside an async caller. - return tokio::task::block_in_place(|| { - handle.block_on(motosan_ai_oauth::refresh(&cfg, &refresh)) - }) - .map_err(|e| e.to_string()); - } - Err("tokio runtime required to refresh openai oauth token".to_string()) -} - -fn extract_account_id_from_access_token(access_token: &str) -> Option { - let payload = access_token.split('.').nth(1)?; - let padded = match payload.len() % 4 { - 0 => payload.to_string(), - n => format!("{}{}", payload, "=".repeat(4 - n)), - }; - let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(padded.as_bytes()) - .or_else(|_| base64::engine::general_purpose::STANDARD.decode(padded.as_bytes())) - .ok()?; - let json: serde_json::Value = serde_json::from_slice(&bytes).ok()?; - json.get("sub") - .or_else(|| json.get("account_id")) - .and_then(|v| v.as_str()) - .map(str::to_string) -} - -/// Look up the OpenAI bearer token sourced from the OAuth (ChatGPT -/// subscription) flow. Returns `Ok(None)` when no OAuth profile is present or -/// when the access token is empty. API-key fallback for the `openai` slug is -/// handled by the standard `lookup_key_for_slug` path — this function is -/// OAuth-only so the standard path's env/audit/metrics logic still runs. -pub fn lookup_openai_bearer_token(config: &Config) -> Result, String> { - let auth = AuthService::from_config(config); - - let profile = auth - .get_profile(OPENAI_PROVIDER_KEY, Some(OPENAI_OAUTH_PROFILE_NAME)) - .map_err(|e| e.to_string())?; - let Some(mut profile) = profile else { - return Ok(None); - }; - let Some(mut token_set) = profile.token_set.clone() else { - return Ok(None); - }; - - let skew = Duration::minutes(2); - if token_set.is_expiring_within(std::time::Duration::from_secs( - skew.num_seconds().unsigned_abs(), - )) { - if let Some(refresh) = token_set.refresh_token.clone() { - match try_refresh_oauth_token(&refresh) { - Ok(fresh) => { - token_set = token_set_from_codex(&fresh); - profile.token_set = Some(token_set.clone()); - if let Err(e) = auth_profiles_store(config).upsert_profile(profile, true) { - log::warn!( - "{LOG_PREFIX} failed to persist refreshed token: {e}; \ - fresh access token will be lost on restart" - ); - } - } - Err(e) => { - log::warn!("{LOG_PREFIX} oauth refresh failed: {e}"); - } - } - } - } - - let access = token_set.access_token.trim(); - if access.is_empty() { - Ok(None) - } else { - Ok(Some(access.to_string())) - } -} diff --git a/src/openhuman/inference/ops.rs b/src/openhuman/inference/ops.rs index 17aed26962..06459931ce 100644 --- a/src/openhuman/inference/ops.rs +++ b/src/openhuman/inference/ops.rs @@ -310,79 +310,6 @@ pub async fn inference_apply_preset(tier: &str) -> Result, Str )) } -pub async fn inference_openai_oauth_start(config: &Config) -> Result, String> { - debug!("{LOG_PREFIX} openai_oauth_start:start"); - let result = - crate::openhuman::inference::openai_oauth::start_openai_oauth(config).map(|start| { - RpcOutcome::single_log( - json!({ - "authUrl": start.auth_url, - "state": start.state, - "redirectUri": start.redirect_uri, - }), - "openai oauth authorize url ready", - ) - }); - match &result { - Ok(_) => debug!("{LOG_PREFIX} openai_oauth_start:ok"), - Err(err) => error!(error = %err, "{LOG_PREFIX} openai_oauth_start:error"), - } - result -} - -pub async fn inference_openai_oauth_complete( - config: &Config, - callback_url: &str, -) -> Result, String> { - debug!( - callback_len = callback_url.len(), - "{LOG_PREFIX} openai_oauth_complete:start" - ); - let result = - crate::openhuman::inference::openai_oauth::complete_openai_oauth(config, callback_url) - .await - .map(|payload| RpcOutcome::single_log(payload, "openai oauth connected")); - match &result { - Ok(_) => debug!("{LOG_PREFIX} openai_oauth_complete:ok"), - Err(err) => error!(error = %err, "{LOG_PREFIX} openai_oauth_complete:error"), - } - result -} - -pub async fn inference_openai_oauth_status(config: &Config) -> Result, String> { - debug!("{LOG_PREFIX} openai_oauth_status:start"); - let result = - crate::openhuman::inference::openai_oauth::openai_oauth_status(config).map(|status| { - RpcOutcome::single_log( - json!({ - "connected": status.connected, - "profileId": status.profile_id, - "expiresAt": status.expires_at, - "authMethod": status.auth_method, - }), - "openai oauth status", - ) - }); - match &result { - Ok(_) => debug!("{LOG_PREFIX} openai_oauth_status:ok"), - Err(err) => error!(error = %err, "{LOG_PREFIX} openai_oauth_status:error"), - } - result -} - -pub async fn inference_openai_oauth_disconnect( - config: &Config, -) -> Result, String> { - debug!("{LOG_PREFIX} openai_oauth_disconnect:start"); - let result = crate::openhuman::inference::openai_oauth::disconnect_openai_oauth(config) - .map(|payload| RpcOutcome::single_log(payload, "openai oauth disconnected")); - match &result { - Ok(_) => debug!("{LOG_PREFIX} openai_oauth_disconnect:ok"), - Err(err) => error!(error = %err, "{LOG_PREFIX} openai_oauth_disconnect:error"), - } - result -} - pub async fn inference_diagnostics(config: &Config) -> Result, String> { debug!("{LOG_PREFIX} diagnostics:start"); let service = local_runtime::global(config); diff --git a/src/openhuman/inference/ops_tests.rs b/src/openhuman/inference/ops_tests.rs index 771ac9d95f..7d76274b11 100644 --- a/src/openhuman/inference/ops_tests.rs +++ b/src/openhuman/inference/ops_tests.rs @@ -1,16 +1,11 @@ use super::*; -use crate::openhuman::credentials::profiles::{AuthProfile, AuthProfilesStore, TokenSet}; -use crate::openhuman::inference::openai_oauth::{OPENAI_OAUTH_PROFILE_NAME, OPENAI_PROVIDER_KEY}; -use chrono::{Duration, Utc}; use tempfile::tempdir; fn disabled_config() -> (Config, tempfile::TempDir) { let tmp = tempdir().expect("tempdir"); - let mut config = Config { - workspace_dir: tmp.path().join("workspace"), - config_path: tmp.path().join("config.toml"), - ..Config::default() - }; + let mut config = Config::default(); + config.workspace_dir = tmp.path().join("workspace"); + config.config_path = tmp.path().join("config.toml"); config.local_ai.runtime_enabled = false; config.local_ai.opt_in_confirmed = false; (config, tmp) @@ -114,99 +109,3 @@ async fn inference_presets_returns_recommended_tier() { assert!(outcome.value.get("recommended_tier").is_some()); assert!(outcome.value.get("presets").is_some()); } - -#[tokio::test] -async fn inference_openai_oauth_start_returns_authorize_payload() { - let (config, _tmp) = disabled_config(); - - let outcome = inference_openai_oauth_start(&config) - .await - .expect("oauth start"); - - assert!(outcome.value["authUrl"] - .as_str() - .unwrap() - .contains("auth.openai.com")); - assert_eq!( - outcome.value["redirectUri"].as_str(), - Some("http://127.0.0.1:1455/auth/callback") - ); - assert_eq!(outcome.logs, vec!["openai oauth authorize url ready"]); -} - -#[tokio::test] -async fn inference_openai_oauth_complete_surfaces_state_errors() { - let (config, _tmp) = disabled_config(); - let start = inference_openai_oauth_start(&config) - .await - .expect("oauth start"); - let state = start.value["state"].as_str().unwrap(); - let callback = format!("http://127.0.0.1:1455/auth/callback?code=fake&state=wrong-{state}"); - - let err = inference_openai_oauth_complete(&config, &callback) - .await - .expect_err("state mismatch should fail"); - - assert!(err.contains("state mismatch")); -} - -#[tokio::test] -async fn inference_openai_oauth_status_returns_connected_payload() { - let (config, tmp) = disabled_config(); - let store = AuthProfilesStore::new(tmp.path(), false); - store - .upsert_profile( - AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: "oauth-access".into(), - refresh_token: None, - id_token: None, - expires_at: Some(Utc::now() + Duration::hours(1)), - token_type: Some("Bearer".into()), - scope: None, - }, - ), - true, - ) - .unwrap(); - - let outcome = inference_openai_oauth_status(&config) - .await - .expect("oauth status"); - - assert_eq!(outcome.value["connected"], true); - assert_eq!(outcome.value["authMethod"], "oauth"); - assert_eq!(outcome.logs, vec!["openai oauth status"]); -} - -#[tokio::test] -async fn inference_openai_oauth_disconnect_returns_removed_flag() { - let (config, tmp) = disabled_config(); - let store = AuthProfilesStore::new(tmp.path(), false); - store - .upsert_profile( - AuthProfile::new_oauth( - OPENAI_PROVIDER_KEY, - OPENAI_OAUTH_PROFILE_NAME, - TokenSet { - access_token: "oauth-access".into(), - refresh_token: None, - id_token: None, - expires_at: None, - token_type: Some("Bearer".into()), - scope: None, - }, - ), - true, - ) - .unwrap(); - - let outcome = inference_openai_oauth_disconnect(&config) - .await - .expect("oauth disconnect"); - - assert_eq!(outcome.value["disconnected"], true); - assert_eq!(outcome.logs, vec!["openai oauth disconnected"]); -} diff --git a/src/openhuman/inference/provider/factory.rs b/src/openhuman/inference/provider/factory.rs index c206b0cc1a..3e961734a0 100644 --- a/src/openhuman/inference/provider/factory.rs +++ b/src/openhuman/inference/provider/factory.rs @@ -609,40 +609,12 @@ pub fn lookup_key_for_slug(slug: &str, config: &Config) -> anyhow::Result { - log::debug!( - "[providers][chat-factory] auth lookup slug={} key_present=true (oauth)", - slug - ); - return Ok(token); - } - Ok(_) => {} - Err(e) => { - return Err(anyhow::anyhow!( - "[chat-factory] openai oauth lookup failed: {e}" - )); - } - } - } - log::debug!( - "[providers][chat-factory] auth lookup slug={} key_present=false", - slug + "[providers][chat-factory] auth lookup slug={} key_present={}", + slug, + !key.is_empty() ); - Ok(String::new()) + Ok(key) } /// Build an `OpenAiCompatibleProvider` with the given auth style. diff --git a/src/openhuman/inference/provider/factory_test.rs b/src/openhuman/inference/provider/factory_test.rs index aef8420d84..97c380a023 100644 --- a/src/openhuman/inference/provider/factory_test.rs +++ b/src/openhuman/inference/provider/factory_test.rs @@ -605,25 +605,6 @@ fn verify_session_active_called_for_custom_provider_not_for_openhuman() { ); } -#[test] -fn lookup_key_for_slug_routes_openai_oauth_lookup_path() { - let tmp = TempDir::new().expect("tempdir"); - let config = config_in_tempdir(&tmp); - let auth = AuthService::new(tmp.path(), config.secrets.encrypt); - auth.store_provider_token( - "provider:openai", - "default", - "sk-openai", - Default::default(), - true, - ) - .expect("store openai token"); - - let token = lookup_key_for_slug("openai", &config).expect("lookup openai token"); - - assert_eq!(token, "sk-openai"); -} - // ── is_known_openhuman_tier ─────────────────────────────────────────────────── #[test] diff --git a/src/openhuman/inference/schemas.rs b/src/openhuman/inference/schemas.rs index 70f70b9f67..7253c4e86a 100644 --- a/src/openhuman/inference/schemas.rs +++ b/src/openhuman/inference/schemas.rs @@ -121,12 +121,6 @@ struct InferenceApplyPresetParams { tier: String, } -#[derive(Debug, Deserialize)] -struct InferenceOpenAiOAuthCompleteParams { - #[serde(alias = "callbackUrl")] - callback_url: String, -} - pub fn all_controller_schemas() -> Vec { vec![ schemas("status"), @@ -138,10 +132,6 @@ pub fn all_controller_schemas() -> Vec { schemas("presets"), schemas("apply_preset"), schemas("diagnostics"), - schemas("openai_oauth_start"), - schemas("openai_oauth_complete"), - schemas("openai_oauth_status"), - schemas("openai_oauth_disconnect"), schemas("summarize"), schemas("prompt"), schemas("vision_prompt"), @@ -190,22 +180,6 @@ pub fn all_registered_controllers() -> Vec { schema: schemas("diagnostics"), handler: handle_inference_diagnostics, }, - RegisteredController { - schema: schemas("openai_oauth_start"), - handler: handle_inference_openai_oauth_start, - }, - RegisteredController { - schema: schemas("openai_oauth_complete"), - handler: handle_inference_openai_oauth_complete, - }, - RegisteredController { - schema: schemas("openai_oauth_status"), - handler: handle_inference_openai_oauth_status, - }, - RegisteredController { - schema: schemas("openai_oauth_disconnect"), - handler: handle_inference_openai_oauth_disconnect, - }, RegisteredController { schema: schemas("summarize"), handler: handle_inference_summarize, @@ -339,37 +313,6 @@ pub fn schemas(function: &str) -> ControllerSchema { via `issues`.", )], }, - "openai_oauth_start" => ControllerSchema { - namespace: "inference", - function: "openai_oauth_start", - description: "Begin ChatGPT/Codex OAuth (PKCE) for the openai cloud provider.", - inputs: vec![], - outputs: vec![json_output("result", "OAuth start payload with authUrl.")], - }, - "openai_oauth_complete" => ControllerSchema { - namespace: "inference", - function: "openai_oauth_complete", - description: "Complete ChatGPT/Codex OAuth using the browser callback URL.", - inputs: vec![required_string( - "callback_url", - "Redirect URL after sign-in (http://127.0.0.1:1455/auth/callback?...).", - )], - outputs: vec![json_output("result", "OAuth completion payload.")], - }, - "openai_oauth_status" => ControllerSchema { - namespace: "inference", - function: "openai_oauth_status", - description: "Whether ChatGPT OAuth credentials are stored for openai.", - inputs: vec![], - outputs: vec![json_output("status", "OAuth connection status.")], - }, - "openai_oauth_disconnect" => ControllerSchema { - namespace: "inference", - function: "openai_oauth_disconnect", - description: "Remove stored ChatGPT OAuth credentials.", - inputs: vec![], - outputs: vec![json_output("result", "Disconnect result.")], - }, "summarize" => ControllerSchema { namespace: "inference", function: "summarize", @@ -670,41 +613,6 @@ fn handle_inference_apply_preset(params: Map) -> ControllerFuture }) } -fn handle_inference_openai_oauth_start(_params: Map) -> ControllerFuture { - Box::pin(async move { - let config = config_rpc::load_config_with_timeout().await?; - to_json(crate::openhuman::inference::rpc::inference_openai_oauth_start(&config).await?) - }) -} - -fn handle_inference_openai_oauth_complete(params: Map) -> ControllerFuture { - Box::pin(async move { - let config = config_rpc::load_config_with_timeout().await?; - let payload = deserialize_params::(params)?; - to_json( - crate::openhuman::inference::rpc::inference_openai_oauth_complete( - &config, - payload.callback_url.trim(), - ) - .await?, - ) - }) -} - -fn handle_inference_openai_oauth_status(_params: Map) -> ControllerFuture { - Box::pin(async move { - let config = config_rpc::load_config_with_timeout().await?; - to_json(crate::openhuman::inference::rpc::inference_openai_oauth_status(&config).await?) - }) -} - -fn handle_inference_openai_oauth_disconnect(_params: Map) -> ControllerFuture { - Box::pin(async move { - let config = config_rpc::load_config_with_timeout().await?; - to_json(crate::openhuman::inference::rpc::inference_openai_oauth_disconnect(&config).await?) - }) -} - fn handle_inference_diagnostics(_params: Map) -> ControllerFuture { Box::pin(async move { let config = config_rpc::load_config_with_timeout().await?; diff --git a/src/openhuman/inference/schemas_tests.rs b/src/openhuman/inference/schemas_tests.rs index 6085cc5882..576504701f 100644 --- a/src/openhuman/inference/schemas_tests.rs +++ b/src/openhuman/inference/schemas_tests.rs @@ -5,7 +5,7 @@ fn inference_catalog_counts_match_and_nonempty() { let declared = all_controller_schemas(); let registered = all_registered_controllers(); assert_eq!(declared.len(), registered.len()); - assert!(declared.len() >= 20); + assert!(declared.len() >= 16); } #[test] @@ -36,10 +36,6 @@ fn inference_schema_function_names_are_stable() { assert!(functions.contains(&"presets")); assert!(functions.contains(&"apply_preset")); assert!(functions.contains(&"diagnostics")); - assert!(functions.contains(&"openai_oauth_start")); - assert!(functions.contains(&"openai_oauth_complete")); - assert!(functions.contains(&"openai_oauth_status")); - assert!(functions.contains(&"openai_oauth_disconnect")); assert!(functions.contains(&"prompt")); assert!(functions.contains(&"vision_prompt")); assert!(functions.contains(&"embed")); @@ -68,45 +64,6 @@ fn inference_chat_schema_requires_messages() { .any(|field| field.name == "messages" && field.required)); } -#[test] -fn inference_openai_oauth_schemas_are_registered_with_expected_shapes() { - let registered: Vec<&str> = all_registered_controllers() - .into_iter() - .map(|controller| controller.schema.function) - .collect(); - for function in [ - "openai_oauth_start", - "openai_oauth_complete", - "openai_oauth_status", - "openai_oauth_disconnect", - ] { - assert!(registered.contains(&function), "missing {function}"); - let schema = schemas(function); - assert_eq!(schema.namespace, "inference"); - assert_eq!(schema.function, function); - assert!(!schema.description.is_empty()); - assert!(!schema.outputs.is_empty()); - } - - let complete = schemas("openai_oauth_complete"); - assert_eq!(complete.inputs.len(), 1); - assert_eq!(complete.inputs[0].name, "callback_url"); - assert!(complete.inputs[0].required); - - assert!(schemas("openai_oauth_start").inputs.is_empty()); - assert!(schemas("openai_oauth_status").inputs.is_empty()); - assert!(schemas("openai_oauth_disconnect").inputs.is_empty()); -} - -#[tokio::test] -async fn inference_openai_oauth_complete_handler_rejects_invalid_params() { - let params = Map::from_iter([("callback_url".to_string(), Value::Bool(true))]); - let err = handle_inference_openai_oauth_complete(params) - .await - .expect_err("invalid params"); - assert!(err.contains("invalid params")); -} - #[test] fn inference_unknown_schema_panics() { let panic = std::panic::catch_unwind(|| schemas("no_such_function")); diff --git a/src/openhuman/mcp_client/client.rs b/src/openhuman/mcp_client/client.rs index 77d5222a03..9e476f9700 100644 --- a/src/openhuman/mcp_client/client.rs +++ b/src/openhuman/mcp_client/client.rs @@ -335,8 +335,8 @@ impl McpHttpClient { let session_id = self.state.lock().session_id.clone(); let mut request = self .apply_auth(self.http.get(&self.endpoint), false) - .header(ACCEPT, "text/event-stream") - .header(HEADER_PROTOCOL_VERSION, protocol_version); + .header(HEADER_PROTOCOL_VERSION, protocol_version) + .header(ACCEPT, "text/event-stream"); if let Some(session_id) = session_id { request = request.header(HEADER_SESSION_ID, session_id); } @@ -385,8 +385,7 @@ impl McpHttpClient { let request = self .http .post(&self.endpoint) - .header(CONTENT_TYPE, "application/json") - .header(ACCEPT, MCP_HTTP_ACCEPT); + .header(CONTENT_TYPE, "application/json"); let request = self.apply_standard_headers(request, false, method, None, &[]); let response = request.body(serde_json::to_vec(&body)?).send().await?; let status = response.status(); @@ -436,8 +435,7 @@ impl McpHttpClient { let request = self .http .post(&self.endpoint) - .header(CONTENT_TYPE, "application/json") - .header(ACCEPT, MCP_HTTP_ACCEPT); + .header(CONTENT_TYPE, "application/json"); let request = if options.initialize { self.apply_auth(request, true) } else { @@ -486,6 +484,7 @@ impl McpHttpClient { let protocol_version = self.state.lock().negotiated_protocol_version.clone(); let session_id = self.state.lock().session_id.clone(); let mut request = self.apply_auth(request, initialize); + request = request.header(ACCEPT, MCP_HTTP_ACCEPT); request = request.header(HEADER_METHOD, method); if let Some(name) = name { request = request.header(HEADER_NAME, name); @@ -534,7 +533,12 @@ impl McpHttpClient { where T: for<'de> Deserialize<'de>, { - let response = self.http.get(url).send().await?; + let response = self + .http + .get(url) + .header(ACCEPT, "application/json") + .send() + .await?; let status = response.status(); let text = response.text().await?; if !status.is_success() { diff --git a/src/openhuman/security/pairing.rs b/src/openhuman/security/pairing.rs index e86c041aa0..9dbf1e4eeb 100644 --- a/src/openhuman/security/pairing.rs +++ b/src/openhuman/security/pairing.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use std::time::Instant; #[cfg(unix)] -use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _}; +use std::os::unix::fs::OpenOptionsExt as _; /// Environment variable for the core JSON-RPC bearer token (see `crate::core::auth`). pub const CORE_TOKEN_ENV_VAR: &str = "OPENHUMAN_CORE_TOKEN"; diff --git a/src/openhuman/security/secrets.rs b/src/openhuman/security/secrets.rs index b624919631..1c8b7c3be9 100644 --- a/src/openhuman/security/secrets.rs +++ b/src/openhuman/security/secrets.rs @@ -214,6 +214,7 @@ impl SecretStore { }; let hex_key = read_result.with_context(|| { + #[allow(unused_mut)] let mut msg = format!( "Failed to read secret key file at {}", self.key_path.display() diff --git a/src/openhuman/voice/server.rs b/src/openhuman/voice/server.rs index c1a177e03c..b827539b11 100644 --- a/src/openhuman/voice/server.rs +++ b/src/openhuman/voice/server.rs @@ -534,7 +534,7 @@ impl HotkeyListenerKind { fn start_hotkey_listener( hotkey_str: &str, mode: hotkey::ActivationMode, - server_cancel: &CancellationToken, + _server_cancel: &CancellationToken, ) -> Result< ( HotkeyListenerKind, @@ -545,7 +545,7 @@ fn start_hotkey_listener( #[cfg(target_os = "macos")] { if hotkey_str.trim().eq_ignore_ascii_case("fn") { - return start_globe_hotkey_listener(mode, server_cancel); + return start_globe_hotkey_listener(mode, _server_cancel); } } diff --git a/src/openhuman/webview_accounts/ops.rs b/src/openhuman/webview_accounts/ops.rs index fa5171e084..8add62b041 100644 --- a/src/openhuman/webview_accounts/ops.rs +++ b/src/openhuman/webview_accounts/ops.rs @@ -32,7 +32,7 @@ struct Provider { /// Providers the welcome agent cares about. Keep this list aligned /// with the webview accounts system in `app/src-tauri/src/webview_accounts/`. -pub(crate) const PROVIDERS: &[Provider] = &[ +const PROVIDERS: &[Provider] = &[ Provider { key: "gmail", host_suffix: ".google.com", diff --git a/src/openhuman/whatsapp_data/sqlite_retry.rs b/src/openhuman/whatsapp_data/sqlite_retry.rs index dbaf91a815..053c7358f1 100644 --- a/src/openhuman/whatsapp_data/sqlite_retry.rs +++ b/src/openhuman/whatsapp_data/sqlite_retry.rs @@ -7,7 +7,7 @@ use std::thread; use std::time::Duration; -use anyhow::{Context, Result}; +use anyhow::Result; /// Per-connection busy handler window (issue #2077). pub const BUSY_TIMEOUT: Duration = Duration::from_millis(5000);