Skip to content
Open
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ BUZZ_DEV_VIEWER=
# Packaging embeds the value; changing the launched app's environment has no effect.
# Restart Vite for development changes; rebuild/repackage for distributed apps.
# BUZZ_BUILDERLAB_URL=https://builderlab.example.com

# Exact Apps domain paired with that login origin for native viewer SSO.
# Other deployments supply it at build time; empty keeps browser login.
# BUZZ_BUILDERLAB_APPS_DOMAIN=apps.example.com
99 changes: 96 additions & 3 deletions crates/plugin-manager/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
mod build_env;

const BUILDERLAB_URL_KEY: &str = "BUZZ_BUILDERLAB_URL";
const BUILDERLAB_APPS_DOMAIN_KEY: &str = "BUZZ_BUILDERLAB_APPS_DOMAIN";

fn builderlab_origin(local: &Path, process: Option<String>) -> Result<String, &'static str> {
let values = build_env::load(local, &[BUILDERLAB_URL_KEY], |_| process.clone())?;
Expand All @@ -26,30 +27,122 @@ fn builderlab_origin(local: &Path, process: Option<String>) -> Result<String, &'
Ok(url.origin().ascii_serialization())
}

fn builderlab_apps_domain(
local: &Path,
origin: &str,
process: Option<String>,
) -> Result<String, &'static str> {
let values = build_env::load(local, &[BUILDERLAB_APPS_DOMAIN_KEY], |_| process.clone())?;
let domain = values
.get(BUILDERLAB_APPS_DOMAIN_KEY)
.map(String::as_str)
.unwrap_or(if origin == "https://app.builderlab.xyz" {
"apps.builderlab.xyz"
} else {
""
});
if domain.is_empty() {
return Ok(String::new());
}
let url = url::Url::parse(&format!("https://{domain}"))
.map_err(|_| "Invalid Builderlab Apps build domain")?;
if origin.is_empty()
|| !domain.contains('.')
|| domain.len() > 253
|| url.domain() != Some(domain)
|| domain.split('.').any(|label| {
label.is_empty()
|| label.len() > 63
|| label.starts_with('-')
|| label.ends_with('-')
|| !label
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
})
{
return Err("Builderlab Apps build domain must be an exact DNS domain with a login origin");
}
Ok(domain.into())
}

#[cfg_attr(test, allow(dead_code))]
fn main() {
let root = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("../..");
let local = root.join(".env.local");
println!("cargo:rerun-if-changed={}", local.display());
println!("cargo:rerun-if-env-changed={BUILDERLAB_URL_KEY}");
println!("cargo:rerun-if-env-changed={BUILDERLAB_APPS_DOMAIN_KEY}");
println!(
"cargo:rerun-if-changed={}",
root.join("crates/build_env.rs").display()
);
let process = match std::env::var(BUILDERLAB_URL_KEY) {
let process = |key| match std::env::var(key) {
Ok(value) => Some(value),
Err(std::env::VarError::NotPresent) => None,
Err(std::env::VarError::NotUnicode(_)) => panic!("Native build inputs must be UTF-8"),
};
let origin =
builderlab_origin(&local, process).expect("Invalid Builderlab build configuration");
let origin = builderlab_origin(&local, process(BUILDERLAB_URL_KEY))
.expect("Invalid Builderlab build configuration");
let apps_domain = builderlab_apps_domain(&local, &origin, process(BUILDERLAB_APPS_DOMAIN_KEY))
.expect("Invalid Builderlab Apps build configuration");
println!("cargo:rustc-env=BUZZ_BUILDERLAB_ORIGIN={origin}");
println!("cargo:rustc-env=BUZZ_BUILDERLAB_APPS_DOMAIN={apps_domain}");
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn exact_apps_domain_uses_trusted_build_input_and_public_default() {
let directory = tempfile::tempdir().unwrap();
let local = directory.path().join(".env.local");
assert_eq!(
builderlab_apps_domain(&local, "https://app.builderlab.xyz", None).unwrap(),
"apps.builderlab.xyz"
);
assert_eq!(
builderlab_apps_domain(&local, "https://login.example", None).unwrap(),
""
);
std::fs::write(&local, "BUZZ_BUILDERLAB_APPS_DOMAIN=apps.example\n").unwrap();
assert_eq!(
builderlab_apps_domain(&local, "https://login.example", None).unwrap(),
"apps.example"
);
assert_eq!(
builderlab_apps_domain(
&local,
"https://login.example",
Some("apps.other.example".into())
)
.unwrap(),
"apps.other.example"
);
assert_eq!(
builderlab_apps_domain(&local, "https://app.builderlab.xyz", Some(String::new()))
.unwrap(),
""
);
for domain in [
"com",
"https://apps.example",
"apps.example:443",
"apps.example/path",
"apps.example?query",
"apps.example#part",
"user@apps.example",
"127.0.0.1",
".apps.example",
] {
assert!(
builderlab_apps_domain(&local, "https://login.example", Some(domain.into()))
.is_err()
);
}
assert!(builderlab_apps_domain(&local, "", Some("apps.example".into())).is_err());
}

#[test]
fn local_url_and_process_overrides_produce_an_exact_origin() {
let directory = tempfile::tempdir().unwrap();
Expand Down
2 changes: 2 additions & 0 deletions crates/plugin-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ pub fn valid_id(id: &str) -> Result<()> {
}
/// The `BUZZ_BUILDERLAB_URL` build input's origin, or empty when unconfigured.
pub const BUILDERLAB_ORIGIN: &str = env!("BUZZ_BUILDERLAB_ORIGIN");
/// Exact Apps domain paired with the build's login origin, or empty when native SSO is unconfigured.
pub const BUILDERLAB_APPS_DOMAIN: &str = env!("BUZZ_BUILDERLAB_APPS_DOMAIN");
/// The Apps Platform control plane of the tenant whose Builderlab sign-in
/// `origin` is, which the bundled Builderlab plugin deploys apps to. Kept in
/// step with `src/bundled/builderlab/apps/environment.ts`.
Expand Down
14 changes: 14 additions & 0 deletions docs/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ A view with a `storage` also keeps its session for ten minutes after it unmounts

Reloading Buzz's main document ends the website session. An attachment queued before that reload cannot replace a session opened by the new document.

Builderlab's bundled app panel passes a `builderlab` viewer lifetime instead of a
shared website sign-in. The browser host uses its credential only for a native
request to the compile-time Builderlab API origin, then installs the restricted
HttpOnly/Secure viewer cookie on the tenant's fixed Apps domain with the server's
absolute expiry. This is the same cookie as browser login, so login/consent repair
replaces it instead of creating a conflicting second cookie.
If the native exchange is unconfigured or its endpoint returns 404, the same
account-isolated guest opens through browser login. Other errors stay closed.
The guest stays blank until installation or this fallback finishes and its account lifetime is checked. No
credential or generic cookie/header capability is exposed to the guest. Each
sign-in selects separate storage; aborting that lifetime closes its live
and parked guests, including a guest whose attach finishes after cancellation.
Normal website views retain the storage behavior above.

## Native ownership

The controls are ordinary trusted React UI in Buzz's main webview. Website content is a separate raw Wry child view positioned over the panel's website viewport, created without a Tauri IPC handler, custom protocols or host initialization scripts. It uses a nonpersistent data store, or the named persistent store described above. The main window's content-security policy is unchanged.
Expand Down
10 changes: 10 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ not in source. Build defaults are readable binary data, **never secret storage**
| Input | Consumer and boundary |
| --- | --- |
| `BUZZ_BUILDERLAB_URL` | Shared frontend/native build: public login URL and the bundled plugin's exact HTTPS host grant; no runtime override. |
| `BUZZ_BUILDERLAB_APPS_DOMAIN` | Shared frontend/native build: exact Apps cookie domain paired with the login origin for native viewer SSO; no runtime override. |
| `BUZZ_DEFAULT_COMMUNITY_URL` | Packaged frontend build: secure relay origin to select once, after identity setup, only when this viewer has no saved community record. Saved choices, including Personal space, always win. Unset in OSS builds. |
| `BUZZ_DATABRICKS_HOST` | Frontend build and dev server: `https://` Databricks workspace URL passed as `DATABRICKS_HOST` to every bundled `goose-acp`, which then prefers it to the host in goose's config. Unset in OSS builds. See [Databricks host](#databricks-host). |
| `BUZZ_RELAY_URL` | Live development broker's default community, not an agent relay override or a packaged default. |
Expand Down Expand Up @@ -90,6 +91,15 @@ only the configured URL's exact origin. Never put credentials in this input.

The bundled Builderlab login consumes this public URL and appends `/api/goose`.

Native viewer SSO also needs the exact Apps domain in
`BUZZ_BUILDERLAB_APPS_DOMAIN`, for example `apps.example.com` for a configured
login at `https://login.example.com`. Set both inputs in the same local/build
environment. Only the public production login has a default Apps domain; other
deployments supply their mapping through build configuration. An explicit empty
domain disables native SSO. A missing mapping or an exchange endpoint returning
404 keeps the account-isolated browser-login path. Authentication, permission,
transport and other server errors do not fall back.

Restart desktop development after changing this input so Vite and the native
grant rebuild together; browser-only development needs a Vite restart. Production
web and desktop frontend builds substitute the value into compiled code: changing `.env.local`
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ objc2-app-kit = { version = "=0.3.2", default-features = false, features = ["NSS
objc2-foundation = { version = "=0.3.2", default-features = false, features = ["std", "NSObject", "NSThread", "NSBundle", "NSString", "NSError", "NSUserDefaults", "NSProcessInfo"] }
objc2-user-notifications = { version = "=0.3.2", default-features = false, features = ["UNUserNotificationCenter", "UNNotificationSettings", "UNNotification", "UNNotificationContent", "UNNotificationRequest", "UNNotificationResponse", "UNNotificationTrigger", "block2"] }

objc2-web-kit = { version = "=0.3.2", default-features = false, features = ["std", "objc2-app-kit", "block2", "WKWebView", "WKUIDelegate", "WKFrameInfo", "WKSecurityOrigin"] }
objc2-web-kit = { version = "=0.3.2", default-features = false, features = ["std", "objc2-app-kit", "block2", "WKWebView", "WKUIDelegate", "WKFrameInfo", "WKSecurityOrigin", "WKWebViewConfiguration", "WKWebsiteDataStore", "WKHTTPCookieStore"] }

[target.'cfg(target_os = "windows")'.dependencies]
webview2-com = "=0.39.1"
Expand Down
Loading
Loading