Files
buzz/crates/buzz-agent/src/databricks.rs
Pinky ad01b708f8 feat(buzz-agent): persist the OAuth token cache owner-only on Windows
On Windows the token cache was memory-only because std has no way to set
an owner-only DACL, and the Win32 calls need `unsafe`, which buzz-agent
forbids. Each process therefore ran its own sign-in, and Databricks
stayed gated off on Windows in the desktop app.

The new buzz-private-file crate holds that FFI behind a safe API, so
buzz-agent keeps `forbid(unsafe_code)` on every platform:

- `create_new` creates a file with the protected DACL `D:P(A;;FA;;;OW)`
  set atomically at creation, the Windows counterpart of `mode(0o600)`.
- `open_owned` opens without following a final link, refuses anything
  but a regular file owned by the process's default owner, and then
  re-applies that DACL on the open handle, like the Unix `O_NOFOLLOW` +
  `fchmod` repair.

auth.rs now persists and reads the cache on Windows through these, with
the same temp-file + rename lifecycle as Unix. Tests that were Unix-only
only because Windows had no disk cache now run on both, and Windows CI
runs the helper crate (including its elevated planted-owner and symlink
tests) plus the buzz-agent token-cache unit tests.

Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
2026-09-30 11:05:14 -06:00

187 lines
7.0 KiB
Rust

//! Opt-in, workspace-scoped Databricks OAuth and catalog reuse.
//!
//! Legacy OAuth/catalog entry points retain their destination and intent policy.
//! This entry requires an explicit HTTPS workspace, app-owned cache root and
//! opener. Construction reads only that cache; it performs no network or browser
//! work. The caller must not lend this root to legacy APIs or other applications.
use std::{path::Path, sync::Arc, time::Duration};
use reqwest::{redirect::Policy, Client, ClientBuilder};
use url::Url;
use crate::{
auth::{AuthError, AuthIntent, BrowserOpener, PkceOAuthTokenSource},
catalog::{discover_databricks_models_with_client, ModelEntry},
config::{Config, DatabricksModelFilter, Provider},
AgentError,
};
/// An explicitly scoped connection, not an agent runtime or a sign-in side effect.
///
/// Only [`connect`](Self::connect) permits browser opening. Discovery is always
/// headless, including its one 401 refresh. Dropping either operation's future
/// cancels its actual HTTP/callback work; callers should impose their own total
/// deadline and fence stale results. Do not hold a controller lock while awaiting.
/// Tokens remain inside the connection; never serialize this object to a webview.
pub struct DatabricksConnection {
workspace: StrictWorkspace,
source: Arc<PkceOAuthTokenSource>,
http: Client,
}
impl DatabricksConnection {
/// Construct without network/browser work or ambient credential/host lookup.
///
/// `workspace` must be an HTTPS origin (optional trailing slash), without
/// userinfo, query or fragment. `cache_root` must be absolute and owned by
/// this app; a separate `databricks-strict` namespace prevents accidental
/// coalescing with legacy auth. Token files keep owner-only atomic
/// persistence.
/// The opener must not log the authorization URL or its own error details.
pub fn new(
workspace: &str,
cache_root: &Path,
opener: Arc<dyn BrowserOpener>,
) -> Result<Self, AgentError> {
Self::build(workspace, cache_root, opener, Client::builder())
}
fn build(
workspace: &str,
cache_root: &Path,
opener: Arc<dyn BrowserOpener>,
http: ClientBuilder,
) -> Result<Self, AgentError> {
let workspace = StrictWorkspace::new(workspace)?;
if !cache_root.is_absolute() {
return Err(AgentError::InvalidParams(
"an absolute app-owned OAuth cache root is required".into(),
));
}
// Apply policy AFTER the test transport's trust/DNS settings. No caller
// can inject a client that follows redirects or disable production TLS.
let http = http
.redirect(Policy::none())
.timeout(Duration::from_secs(30))
.build()
.map_err(|_| AgentError::Llm("Databricks HTTP client construction failed".into()))?;
let source = PkceOAuthTokenSource::for_workspace(
workspace.clone(),
cache_root,
opener,
http.clone(),
)
.map_err(|_| AgentError::Llm("Databricks private OAuth cache unavailable".into()))?;
Ok(Self {
workspace,
source,
http,
})
}
/// Authenticate after an explicit user action. May call the supplied opener.
/// A valid cached credential short-circuits; this does not fetch models.
pub async fn connect(&self) -> Result<(), AuthError> {
self.source
.acquire_with_intent(AuthIntent::UserInitiated, None)
.await
.map(|_| ())
}
/// Discover v2 models without a browser, using this connection's workspace
/// and credentials only. Preserves filtering, partial success and labelled
/// authenticated-empty/no-filter fallback; this is not proof of completeness.
/// Does not choose or mutate a model. Errors contain no raw provider content.
pub async fn discover_models(
&self,
filter: Option<DatabricksModelFilter>,
) -> Result<Vec<ModelEntry>, AgentError> {
let cfg = Config::for_discovery(
Provider::DatabricksV2,
String::new(),
self.workspace.as_str().into(),
filter,
);
discover_databricks_models_with_client(&cfg, self.source.clone(), &self.http)
.await
.map_err(|error| match error {
AgentError::LlmAuth(_) => {
AgentError::LlmAuth("Databricks authentication required".into())
}
_ => AgentError::Llm("Databricks model discovery unavailable".into()),
})
}
}
/// Unforgeable outside this module: every production value passed to auth was
/// validated as an explicit HTTPS origin. Endpoint validation occurs inside the
/// engine on the SAME discovery response subsequently used for both grants.
#[derive(Clone)]
pub(crate) struct StrictWorkspace(Url);
impl StrictWorkspace {
fn new(raw: &str) -> Result<Self, AgentError> {
let rest = raw.strip_prefix("https://").ok_or_else(invalid_workspace)?;
let authority = rest.strip_suffix('/').unwrap_or(rest);
if authority.contains(['/', '@']) {
return Err(invalid_workspace());
}
let url = Url::parse(raw).map_err(|_| invalid_workspace())?;
if raw.trim() != raw
|| raw.chars().any(char::is_control)
|| raw.contains('\\')
|| url.scheme() != "https"
|| url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
|| url.path() != "/"
{
return Err(invalid_workspace());
}
Ok(Self(url))
}
pub(crate) fn as_str(&self) -> &str {
self.0.as_str().trim_end_matches('/')
}
pub(crate) fn validate_endpoint(&self, raw: &str) -> Result<(), AgentError> {
let authority = raw
.strip_prefix("https://")
.ok_or_else(invalid_endpoint)?
.split('/')
.next()
.ok_or_else(invalid_endpoint)?;
if authority.contains('@') {
return Err(invalid_endpoint());
}
let endpoint = Url::parse(raw).map_err(|_| invalid_endpoint())?;
if endpoint.origin() != self.0.origin()
|| !endpoint.username().is_empty()
|| endpoint.password().is_some()
|| endpoint.query().is_some()
|| endpoint.fragment().is_some()
|| raw.trim() != raw
|| raw.chars().any(char::is_control)
|| raw.contains('\\')
{
return Err(invalid_endpoint());
}
Ok(())
}
}
fn invalid_workspace() -> AgentError {
AgentError::InvalidParams("Databricks workspace must be an explicit HTTPS origin".into())
}
fn invalid_endpoint() -> AgentError {
AgentError::Llm("Databricks OAuth endpoint outside workspace policy".into())
}
#[cfg(test)]
#[path = "databricks_tests.rs"]
mod tests;