Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,18 @@ desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1 \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=true \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
echo "Both compiled states verified."
echo "=== Maximum accepted demo name reaches Rust build validation ==="
DEMO_CONFIG="$(node ../scripts/demo-build-config.mjs "$(printf 'x%.0s' {1..31})" /dev/null 1234567812345678)"
DEMO_SLUG="$(node -e 'console.log(JSON.parse(process.argv[1]).slug)' "$DEMO_CONFIG")"
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" \
BUZZ_TEST_EXPECTED_DEMO_SLUG="$DEMO_SLUG" \
cargo test compiled_demo_slug_matches_expected -- --ignored --nocapture
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" cargo test --workspace
if node ../scripts/demo-build-config.mjs "$(printf 'x%.0s' {1..32})" /dev/null 1234567812345678; then
echo "A 32-character demo name unexpectedly passed JavaScript validation" >&2
exit 1
fi
echo "Both compiled states and the accepted/rejected demo-name boundary verified."

# Build the full desktop Tauri app locally (unsigned, for testing)
# Sidecar binary list must stay in sync with _ensure-sidecar-stubs above.
Expand All @@ -277,6 +288,38 @@ desktop-release-build target="aarch64-apple-darwin":
pnpm install
cd {{desktop_dir}} && pnpm tauri build --features mesh-llm --target {{target}}

# Build an unsigned named macOS demo DMG with isolated app and runtime identities.
desktop-demo-build demo_name target="aarch64-apple-darwin":
#!/usr/bin/env bash
set -euo pipefail
TARGET={{target}}
[[ "$(uname -s)" == "Darwin" && "$TARGET" == *-apple-darwin ]] || { echo "Demo DMGs require a macOS Apple target" >&2; exit 2; }
CONFIG_PATH="$(mktemp "${TMPDIR:-/tmp}/buzz-demo-config.XXXXXX")"
trap 'rm -f "$CONFIG_PATH"' EXIT
DEMO_BUILD_ID="$(node -e 'console.log(require("node:crypto").randomBytes(8).toString("hex"))')"
DEMO_CONFIG="$(node desktop/scripts/demo-build-config.mjs {{quote(demo_name)}} "$CONFIG_PATH" "$DEMO_BUILD_ID")"
read_config() { node -e 'console.log(JSON.parse(process.argv[1])[process.argv[2]])' "$DEMO_CONFIG" "$1"; }
PRODUCT_NAME="$(read_config productName)"
DMG_VOLUME_NAME="$(read_config dmgVolumeName)"
DMG_FILE_STEM="$(read_config dmgFileStem)"
DEMO_SLUG="$(read_config slug)"
cargo build --release --target "$TARGET" \
-p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp \
-p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh "$TARGET"
pnpm install
cd {{desktop_dir}}
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" pnpm tauri build --features mesh-llm --target "$TARGET" --bundles app --config "$CONFIG_PATH"
cd ..
VERSION="$(node -p "require('./desktop/package.json').version")"
DMG_ARCH="${TARGET%%-*}"; [[ "$DMG_ARCH" == "x86_64" ]] && DMG_ARCH=x64
APP_PATH="desktop/src-tauri/target/$TARGET/release/bundle/macos/$PRODUCT_NAME.app"
PLIST="$APP_PATH/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $PRODUCT_NAME" "$PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleName $PRODUCT_NAME" "$PLIST"
codesign --force --deep --sign - "$APP_PATH"
VOL_NAME="$DMG_VOLUME_NAME" ./desktop/scripts/package-macos-dmg.sh "$APP_PATH" "desktop/src-tauri/target/$TARGET/release/bundle/dmg/${DMG_FILE_STEM}_${VERSION}_${DMG_ARCH}.dmg"

# Run desktop checks suitable for CI / pre-push
desktop-ci: desktop-check desktop-test desktop-tauri-fmt-check desktop-build desktop-tauri-check desktop-tauri-test

Expand Down
73 changes: 63 additions & 10 deletions crates/buzz-agent/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,17 @@ impl TokenSource for StaticTokenSource {
///
/// The `discovery_url` must return a JSON document with at least
/// `authorization_endpoint` and `token_endpoint` (RFC 8414). The
/// `cache_namespace` is the directory under `~/.config/buzz-agent/oauth/`
/// the token JSON lives in — separates providers' caches cleanly.
/// `cache_namespace` is the directory under the platform config directory's
/// `buzz-agent/oauth/` root where the token JSON lives — separates providers'
/// caches cleanly.
#[derive(Debug, Clone)]
pub struct PkceOAuthConfig {
pub discovery_url: String,
pub client_id: String,
pub scopes: Vec<String>,
pub cache_namespace: String,
/// When `Some`, the engine writes tokens here instead of
/// `~/.config/buzz-agent/oauth/<cache_namespace>/`. Production code
/// `<platform config dir>/buzz-agent/oauth/<cache_namespace>/`. Production code
/// leaves this `None`. Integration tests use it to avoid stomping on
/// a shared `$HOME` when running in parallel.
pub cache_dir_override: Option<PathBuf>,
Expand Down Expand Up @@ -444,6 +445,29 @@ fn is_expired(t: &CachedToken) -> bool {
now + TOKEN_REFRESH_LEEWAY.as_secs() >= exp
}

const BUZZ_AGENT_CONFIG_DIR_ENV: &str = "BUZZ_AGENT_CONFIG_DIR";

fn oauth_cache_root_for(
config_override: Option<PathBuf>,
home_dir: Option<PathBuf>,
) -> Result<PathBuf, AgentError> {
if let Some(root) = config_override {
return Ok(root.join("buzz-agent").join("oauth"));
}
Ok(home_dir
.ok_or_else(|| AgentError::Llm("oauth cache: home directory not found".into()))?
.join(".config")
.join("buzz-agent")
.join("oauth"))
}

fn default_oauth_cache_root() -> Result<PathBuf, AgentError> {
oauth_cache_root_for(
std::env::var_os(BUZZ_AGENT_CONFIG_DIR_ENV).map(PathBuf::from),
dirs::home_dir(),
)
}

fn cache_path_for(cfg: &PkceOAuthConfig) -> Result<PathBuf, AgentError> {
let mut h = sha2::Sha256::new();
h.update(cfg.discovery_url.as_bytes());
Expand All @@ -455,12 +479,7 @@ fn cache_path_for(cfg: &PkceOAuthConfig) -> Result<PathBuf, AgentError> {

let dir = match &cfg.cache_dir_override {
Some(p) => p.join(&cfg.cache_namespace),
None => dirs::home_dir()
.ok_or_else(|| AgentError::Llm("oauth cache: home directory not found".into()))?
.join(".config")
.join("buzz-agent")
.join("oauth")
.join(&cfg.cache_namespace),
None => default_oauth_cache_root()?.join(&cfg.cache_namespace),
};
Ok(dir.join(format!("{hash}.json")))
}
Expand Down Expand Up @@ -862,7 +881,41 @@ mod tests {
}

#[test]
fn cache_path_uses_platform_home_directory() {
fn production_and_demo_oauth_roots_are_concrete_and_distinct() {
let home = PathBuf::from("/Users/demo");
let production = oauth_cache_root_for(None, Some(home.clone())).unwrap();
let first_demo_config = home
.join("Library/Application Support")
.join("buzz-demo-board-1234567812345678");
let second_demo_config = home
.join("Library/Application Support")
.join("buzz-demo-board-8765432187654321");
let first_demo = oauth_cache_root_for(Some(first_demo_config), Some(home.clone())).unwrap();
let second_demo = oauth_cache_root_for(Some(second_demo_config), Some(home)).unwrap();

assert_eq!(
production,
PathBuf::from("/Users/demo/.config/buzz-agent/oauth")
);
assert_eq!(
first_demo,
PathBuf::from(
"/Users/demo/Library/Application Support/buzz-demo-board-1234567812345678/buzz-agent/oauth"
)
);
assert_eq!(
second_demo,
PathBuf::from(
"/Users/demo/Library/Application Support/buzz-demo-board-8765432187654321/buzz-agent/oauth"
)
);
assert_ne!(production, first_demo);
assert_ne!(production, second_demo);
assert_ne!(first_demo, second_demo);
}

#[test]
fn cache_path_preserves_production_home_config_directory() {
let cfg = PkceOAuthConfig {
discovery_url: "https://example.com/.well-known".into(),
client_id: "abc".into(),
Expand Down
23 changes: 21 additions & 2 deletions crates/buzz-agent/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! This helper never opens a browser. Callers choose whether to reject, degrade,
//! or start a separate interactive authentication flow.

use std::{collections::HashSet, sync::Arc, time::Duration};
use std::{collections::HashSet, path::Path, sync::Arc, time::Duration};

use reqwest::Client;
use serde_json::Value;
Expand Down Expand Up @@ -133,7 +133,26 @@ pub(crate) fn is_chat_capable_endpoint(name: &str) -> bool {
/// # Panics
/// Never panics.
pub async fn discover_databricks_models(cfg: &Config) -> Result<Vec<ModelEntry>, AgentError> {
discover_databricks_models_with_token_source(cfg, build_token_source(cfg)?).await
discover_databricks_models_with_cache_dir(cfg, None).await
}

/// Discover Databricks models while storing PKCE credentials under an explicit
/// cache root. `None` preserves buzz-agent's production cache location.
pub async fn discover_databricks_models_with_cache_dir(
cfg: &Config,
cache_dir: Option<&Path>,
) -> Result<Vec<ModelEntry>, AgentError> {
let token_source = if matches!(cfg.provider, Provider::Databricks | Provider::DatabricksV2)
&& cfg.api_key.is_empty()
{
crate::auth::PkceOAuthTokenSource::new(crate::llm::databricks_pkce_config(
&cfg.base_url,
cache_dir.map(Path::to_path_buf),
))?
} else {
build_token_source(cfg)?
};
discover_databricks_models_with_token_source(cfg, token_source).await
}

async fn discover_databricks_models_with_token_source(
Expand Down
22 changes: 18 additions & 4 deletions crates/buzz-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ mod permission;
pub mod types;
mod wire;

pub use catalog::{discover_databricks_models, ModelEntry};
pub use catalog::{
discover_databricks_models, discover_databricks_models_with_cache_dir, ModelEntry,
};
pub use config::Provider;
pub use types::AgentError;

Expand Down Expand Up @@ -161,10 +163,22 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

/// Authenticate to Databricks and store credentials under an optional explicit
/// cache root. `None` preserves buzz-agent's production cache location.
pub async fn authenticate_databricks_with_cache_dir(
host: &str,
cache_dir: Option<&std::path::Path>,
) -> Result<(), AgentError> {
auth::PkceOAuthTokenSource::new(llm::databricks_pkce_config(
host,
cache_dir.map(std::path::Path::to_path_buf),
))?
.interactive_login()
.await
}

pub async fn authenticate_databricks(host: &str) -> Result<(), AgentError> {
auth::PkceOAuthTokenSource::new(llm::databricks_pkce_config(host))?
.interactive_login()
.await
authenticate_databricks_with_cache_dir(host, None).await
}

/// `buzz-agent auth <provider>` — run the interactive auth flow for a
Expand Down
9 changes: 7 additions & 2 deletions crates/buzz-agent/src/llm.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

Expand Down Expand Up @@ -2033,7 +2034,10 @@ where
)))
}

pub(crate) fn databricks_pkce_config(host: &str) -> PkceOAuthConfig {
pub(crate) fn databricks_pkce_config(
host: &str,
cache_dir_override: Option<PathBuf>,
) -> PkceOAuthConfig {
PkceOAuthConfig {
discovery_url: format!(
"{}/oidc/.well-known/oauth-authorization-server",
Expand All @@ -2045,7 +2049,7 @@ pub(crate) fn databricks_pkce_config(host: &str) -> PkceOAuthConfig {
.map(|scope| (*scope).into())
.collect(),
cache_namespace: "databricks".into(),
cache_dir_override: None,
cache_dir_override,
}
}

Expand All @@ -2070,6 +2074,7 @@ pub(crate) fn build_token_source(cfg: &Config) -> Result<Arc<dyn TokenSource>, A
}
Ok(PkceOAuthTokenSource::new(databricks_pkce_config(
&cfg.base_url,
None,
))?)
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/buzz-agent/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const PASSTHROUGH_ENV: &[&str] = &[
"LC_ALL",
"TMPDIR",
"XDG_CONFIG_HOME",
// Explicit Buzz-owned OAuth root for named demo builds. The agent may spawn
// auth-capable child tools after clearing its ambient environment.
"BUZZ_AGENT_CONFIG_DIR",
// SSH — required for git clone/push over SSH (git@github.com:...)
"SSH_AUTH_SOCK",
"SSH_AGENT_PID",
Expand Down
12 changes: 10 additions & 2 deletions crates/buzz-agent/src/model_capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,8 @@ mod tests {
Q::Vector { id: "dbv2-claude-opus-4-7-probe", provider: "databricks_v2", raw_model_id: "claude-opus-4-7", note: None },
Q::Vector { id: "dbv2-databricks-prefix-probe", provider: "databricks_v2", raw_model_id: "databricks-claude-opus-4-7", note: Some("Probes stripping of the databricks- catalog prefix.") },
Q::Vector { id: "dbv2-goose-claude-prefix-probe", provider: "databricks_v2", raw_model_id: "goose-claude-fable-5", note: Some("Probes stripping of the goose- catalog prefix.") },
Q::Vector { id: "dbv2-goose-claude-4-6-sonnet-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-4-6-sonnet", note: Some("Probes the discovered Goose Sonnet 4.6 endpoint spelling and label.") },
Q::Vector { id: "dbv2-goose-claude-4-7-opus-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-4-7-opus", note: Some("Probes the discovered Goose Opus 4.7 endpoint spelling and label.") },
Q::Vector { id: "dbv2-team-prefix-probe", provider: "databricks_v2", raw_model_id: "team-x-claude-opus-4-7", note: Some("Probes stripping of a team-x- catalog prefix.") },
Q::Vector { id: "dbv2-consolidated-llama-substring-probe", provider: "databricks_v2", raw_model_id: "consolidated-llama", note: Some("Probes a name where a code word ('sol') appears only as a substring, not a boundary-aligned segment.") },
Q::Vector { id: "dbv2-terraform-coder-substring-probe", provider: "databricks_v2", raw_model_id: "terraform-coder", note: Some("Probes a name where a code word ('terra') is only a segment prefix, not a full segment.") },
Expand All @@ -638,6 +640,7 @@ mod tests {
Q::Vector { id: "dbv2-goose-claude-opus-5-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-opus-5", note: Some("Probes a prefixed alias of the Databricks Opus 5 endpoint.") },
Q::Vector { id: "dbv2-claude-sonnet-5-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-claude-sonnet-5", note: Some("Probes the canonical Databricks Sonnet 5 endpoint record.") },
Q::Vector { id: "dbv2-goose-claude-sonnet-5-alias-probe", provider: "databricks_v2", raw_model_id: "goose-claude-sonnet-5", note: Some("Probes a prefixed alias of the Databricks Sonnet 5 endpoint.") },
Q::Vector { id: "dbv2-kimi-2-7-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-kimi-2-7", note: Some("Probes the canonical Databricks Kimi 2.7 endpoint record.") },
Q::Vector { id: "dbv2-kimi-k3-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-kimi-k3", note: Some("Probes the canonical Databricks Kimi K3 endpoint record.") },
Q::Vector { id: "dbv2-goose-kimi-k3-alias-probe", provider: "databricks_v2", raw_model_id: "goose-kimi-k3", note: Some("Probes a prefixed alias of the Databricks Kimi K3 endpoint.") },
Q::Vector { id: "resolver-prefixed-alias-probe", provider: "databricks_v2", raw_model_id: "team-x-databricks-gpt-5-4-mini", note: Some("Probes a prefixed alias of an exact-record id (raw exact key differs).") },
Expand Down Expand Up @@ -728,6 +731,7 @@ mod tests {
Q::Vector { id: "dbv2-gemini-3-pro-image-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-gemini-3-pro-image", note: Some("Probes the Gemini 3 Pro Image endpoint record and label.") },
Q::Vector { id: "dbv2-deepseek-v4-flash-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-deepseek-v4-flash-0731", note: Some("Probes the DeepSeek V4 Flash endpoint record and label.") },
Q::Vector { id: "dbv2-deepseek-v4-pro-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-deepseek-v4-pro-0813", note: Some("Probes the DeepSeek V4 Pro endpoint record and label.") },
Q::Vector { id: "dbv2-glm-5-3-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-glm-5-3", note: Some("Probes the GLM-5.3 endpoint record and label.") },
Q::Vector { id: "dbv2-glm-5-3-flash-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-glm-5-3-flash", note: Some("Probes the GLM-5.3 Flash endpoint record and label.") },
Q::Vector { id: "dbv2-grok-4-6-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-grok-4-6", note: Some("Probes the Grok 4.6 endpoint record and label.") },
Q::Vector { id: "dbv2-llama-4-maverick-exact-record-probe", provider: "databricks_v2", raw_model_id: "databricks-llama-4-maverick", note: Some("Probes the Llama 4 Maverick endpoint record and label.") },
Expand Down Expand Up @@ -839,7 +843,7 @@ mod tests {
}

#[test]
fn corpus_has_exactly_135_executable_vectors() {
fn corpus_has_exactly_139_executable_vectors() {
// Locks the vector count so a silent INPUTS edit can't quietly drop
// coverage; must equal the gate in the TS harness
// (modelCapabilitiesCorpus.test.mjs).
Expand All @@ -848,7 +852,7 @@ mod tests {
.filter(|q| matches!(q, Q::Vector { .. }))
.count();
assert_eq!(
vectors, 135,
vectors, 139,
"corpus executable-vector count changed; update this gate deliberately"
);
}
Expand Down Expand Up @@ -1018,9 +1022,12 @@ mod tests {
Some("Claude Fable 5")
);
for (alias, label) in [
("goose-claude-4-6-sonnet", "Claude Sonnet 4.6"),
("goose-claude-4-7-opus", "Claude Opus 4.7"),
("goose-claude-opus-4-8", "Claude Opus 4.8"),
("goose-claude-opus-5", "Claude Opus 5"),
("goose-claude-sonnet-5", "Claude Sonnet 5"),
("goose-kimi-2-7", "Kimi 2.7"),
("goose-kimi-k3", "Kimi K3"),
] {
assert_eq!(
Expand Down Expand Up @@ -1053,6 +1060,7 @@ mod tests {
"data_workflow_tools.goose.goose-deepseek-v4-flash-0731",
"DeepSeek V4 Flash",
),
("data_workflow_tools.goose.goose-glm-5-3", "GLM-5.3"),
(
"data_workflow_tools.goose.goose-glm-5-3-flash",
"GLM-5.3 Flash",
Expand Down
Loading
Loading