Changes: - Classify clipboard delivery confidence - Add durable session update append - Scope the xAI session bearer to first-party memory embedding endpoints - Persist subagent outputs to disk and bound long-lived agent state - Add MiniSweAgent:bash for mini-swe-agent parity - Revert taking local sessions off the persistent shell - Contextual tip recommending grok wrap on SSH sessions - Voice STT bearer from model BYOK env_key/api_key - Define exact website policies for sandbox - Gate unsafe shell environments - Shared pin hoist; single require_sha gate for marketplace plugins - Server-signed is-managed claim (closes sidecar-removal downgrade) - Optional require_sha pin for remote plugin installs - Show session title and last exchange in the exit resume hint - Gate shell output redirects - Warn when fail_closed is present but not a boolean - Add canonical text editing core (ratatui-textarea) - Keep execution state out of goal scratch - Add acknowledged persistence primitives - Inherit child network restrictions in sandbox - Fail closed when hook matchers fail to recompile - Add MCP setup preferences for plugin MCPs - Gate sourced shell scripts - Gate file-typed project hooks - grok wrap: restore terminal modes on child death - Harden owner-only permissions on auth and MCP credentials - Create crash dump files with owner-only permissions - Write the agent_id cache owner-only (0600) - SessionMetrics mode skips Mixpanel profile sync - Dashboard: slim live-tail peek - Yank full queued prompt text, not (+N lines) - Defeat clock-rollback on the signed managed-config cache - Stop early session/cancel from overtaking the prompt and wedging the turn slot - Self-heal a diverged agent entrypoint on startup - Add matched inference expectations in test-support - Add AuthSingleFlight cancel/successor gap tests - Remove consumer from external OTEL allowlist and pin scrub coverage - Enable /copy in minimal mode - Surface capacity and API-key detail on 429 errors - Single-flight interactive auth - Fix PageUp/PageDown skipping lines behind sticky prompt header
82 lines
2.7 KiB
Rust
82 lines
2.7 KiB
Rust
//! Bearer resolution for voice STT requests.
|
|
//! The voice clients are long-lived: a single voice session opens many STT
|
|
//! WebSocket connections over its lifetime, and an OAuth/session bearer rotates
|
|
//! (~15 min). Capturing a token once at startup would 401 mid-session. So
|
|
//! instead of a static `String`, the clients hold a [`SharedVoiceAuth`] and
|
|
//! resolve a fresh bearer at the point of each connection.
|
|
//!
|
|
//! This crate stays dependency-light: it defines its own minimal async trait
|
|
//! rather than depending on the shell's `AuthManager` / tools' `ApiKeyProvider`.
|
|
//! The pager adapts the shell's refreshing provider onto this trait.
|
|
|
|
use std::future::{Future, ready};
|
|
use std::pin::Pin;
|
|
use std::sync::Arc;
|
|
|
|
#[cfg(feature = "audio")]
|
|
use crate::error::VoiceError;
|
|
|
|
pub trait VoiceAuthProvider: std::fmt::Debug + Send + Sync + 'static {
|
|
fn bearer(&self) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>>;
|
|
}
|
|
|
|
/// Shared provider handed to the voice pipeline.
|
|
pub type SharedVoiceAuth = Arc<dyn VoiceAuthProvider>;
|
|
|
|
#[cfg(feature = "audio")]
|
|
pub(crate) async fn require_bearer(auth: &SharedVoiceAuth) -> Result<String, VoiceError> {
|
|
auth.bearer().await.ok_or_else(|| {
|
|
VoiceError::Auth(
|
|
"not signed in — run `grok login`, set XAI_API_KEY, or set a model api_key/env_key"
|
|
.into(),
|
|
)
|
|
})
|
|
}
|
|
|
|
/// A fixed bearer that never refreshes.
|
|
///
|
|
/// Used by the standalone `voice-probe` binary and tests, where there is no
|
|
/// `AuthManager` — only a raw `XAI_API_KEY`.
|
|
pub struct StaticVoiceAuth(pub String);
|
|
|
|
impl std::fmt::Debug for StaticVoiceAuth {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_tuple("StaticVoiceAuth")
|
|
.field(&"<redacted>")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl VoiceAuthProvider for StaticVoiceAuth {
|
|
fn bearer(&self) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>> {
|
|
Box::pin(ready(Some(self.0.clone())))
|
|
}
|
|
}
|
|
|
|
impl StaticVoiceAuth {
|
|
/// Build a [`SharedVoiceAuth`] from a static key, trimming whitespace and
|
|
/// rejecting an empty value.
|
|
pub fn shared(key: impl Into<String>) -> Option<SharedVoiceAuth> {
|
|
let key = key.into().trim().to_string();
|
|
if key.is_empty() {
|
|
return None;
|
|
}
|
|
Some(Arc::new(Self(key)))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn static_provider_resolves() {
|
|
let provider = StaticVoiceAuth::shared(" sk-test ").unwrap();
|
|
assert_eq!(provider.bearer().await.as_deref(), Some("sk-test"));
|
|
}
|
|
|
|
#[test]
|
|
fn static_provider_rejects_empty() {
|
|
assert!(StaticVoiceAuth::shared(" ").is_none());
|
|
}
|
|
}
|