Files
buzz/crates/buzz-agent/src/databricks.rs
Wes 4cd82f5132 feat(buzz-agent): add opt-in workspace-scoped Databricks reuse (#7606)
Posted by Brain, Wes Billman’s AI assistant, on behalf of `wesbillman`.

## Summary

Add an opt-in `databricks::DatabricksConnection` over the existing
PKCE/coordinator and v2 catalog machinery. It accepts an explicit HTTPS
workspace origin, absolute caller-owned cache root, and browser opener;
it does not start an agent or infer credentials/host from the
environment.

- Validate both OAuth endpoints on the actual discovery response used
for grants; native HTTP redirects are disabled and requests have a
30-second timeout.
- Keep strict cache/single-flight state in a separate namespace. Connect
is explicit/user-initiated; catalog lookup and its one 401 refresh stay
headless. Tokens are not exposed by the connection API.
- Preserve existing legacy constructors, endpoint/redirect policy, cache
layout, intents, and catalog filtering/pagination/partial/default
semantics. No internal host default, credential migration, app
integration or release configuration changes.

### Intentional shared changes

OAuth success/discovery JSON is capped at 1 MiB; error JSON at 16 KiB.
Oversized/malformed responses are infrastructure failures, while bounded
client-error `invalid_grant` retains its existing classification. OAuth
diagnostics omit raw bodies/URLs/opener/callback details. Overflowing
token expiry returns an infrastructure failure. These changes affect
legacy callers too and have synthetic regression coverage.

See [the reuse contract](crates/buzz-agent/DATABRICKS_REUSE.md) for API,
compatibility and caller obligations. Total operation deadlines,
stale-result fencing, private root ownership, nonlogging opener behavior
and credential-removal UX remain the caller’s responsibility.

### Related issue

Related discovery work: #6918. Searched existing Databricks PRs and
OAuth issues; no duplicate of this strict opt-in API was found among
those results. This is a new reuse boundary, not a claim of a live
credential leak.

### Testing

Published head: `cf33eb2f4f267d5808e068e35127d5504f0bc674`, based on
main `78618804ec86a014524ad7d1fb55928e8f5c3edf`.
Two implementation/doc commits were transplanted from a release-derived
local base to exclude unrelated release files. `git range-diff` reports
both patches unchanged; independent source review of the transplant
found no blocker.

**At the published head:** normal pre-push hooks passed: branch skew,
push-head scope, file-size checks, Rust test lane and desktop Tauri
Clippy/tests. The fallback Rust lane actually ran `buzz-agent --lib`:
542 passed / 1 ignored, NOT the full package integration suite. Hosted
full package/repository CI remains pending. The first push attempt was
interrupted by the command runner’s 300-second limit; the second
completed normally with all hooks enabled.

**At reviewed implementation checkpoint `8cb3b72d4`:** 16 strict, 27
auth, 32 catalog and 63 coordinator/OAuth/CLI tests passed,
independently rerun; 18 author-selected mutations and four independent
reviewer mutations failed tests with passing restored controls. The
subsequent `539319411` delta changed only two doc-comment blocks. These
earlier test/mutation results are not silently attributed to the rebased
head.

Synthetic coverage includes HTTPS grants/catalog, off-origin endpoint
and redirect rejection, cancellation/socket/listener cleanup,
host/root/legacy namespace isolation, bounded declared/chunked bodies
and exact-limit controls, redaction with positive log capture, malformed
expiry, legacy endpoint/redirect controls, and real CLI cached-auth
aliases.

Desktop native checks used fail-closed sidecar stubs: compile/test
evidence, **not** a runnable or packaged app. No real provider
credentials, browser sign-in, app launch, or agent cutover was
performed. Cross-platform execution and real-provider acceptance remain
separate gates. No merge or downstream adoption is requested by this PR.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
2026-09-13 08:04:56 -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. Unix token files retain owner-only atomic
/// persistence; on non-Unix the existing engine keeps credentials in memory.
/// 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;