mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
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>
187 lines
7.0 KiB
Rust
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;
|