From 9ab75519cd2f6d544f23aec7885bfd6a1ee793c4 Mon Sep 17 00:00:00 2001 From: Sam Lijin Date: Thu, 5 Nov 2026 11:20:27 -0800 Subject: [PATCH 1/6] fix: share local trace stores across bridge and CLI --- baml_language/Cargo.lock | 5 +- baml_language/ENV_VARS.md | 59 +++- baml_language/crates/baml_cli/Cargo.toml | 2 + .../crates/baml_cli/src/query_command.rs | 53 ++- .../crates/baml_cli/src/runtime_telemetry.rs | 2 +- .../baml_cli/tests/boundary_telemetry_e2e.rs | 4 +- .../baml_cli/tests/shared_trace_store_e2e.rs | 301 ++++++++++++++++++ .../baml_cli/tests/telemetry_recording_e2e.rs | 7 + baml_language/crates/baml_db/src/manifest.rs | 12 +- .../crates/baml_query_btel/src/lib.rs | 6 +- baml_language/crates/bex_engine/Cargo.toml | 1 - .../crates/bex_engine/src/telemetry.rs | 111 +++---- .../src/telemetry/user_files_tests.rs | 47 +-- .../crates/btel_reader/src/layout.rs | 74 ++++- baml_language/crates/btel_settings/Cargo.toml | 4 + baml_language/crates/btel_settings/src/lib.rs | 2 + .../crates/btel_settings/src/local_files.rs | 6 +- .../crates/btel_settings/src/local_store.rs | 131 ++++++++ 18 files changed, 705 insertions(+), 122 deletions(-) create mode 100644 baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs create mode 100644 baml_language/crates/btel_settings/src/local_store.rs diff --git a/baml_language/Cargo.lock b/baml_language/Cargo.lock index 4d664cbb502..c82767c4034 100644 --- a/baml_language/Cargo.lock +++ b/baml_language/Cargo.lock @@ -460,6 +460,7 @@ dependencies = [ "baml_query_btel", "baml_release", "baml_sdkgen_types", + "baml_test_support", "baml_tests", "baml_type", "baml_version", @@ -471,6 +472,7 @@ dependencies = [ "bex_project", "bex_vm_types", "borsh", + "bridge_cffi", "btel_bcs", "btel_file", "btel_recorder", @@ -1190,7 +1192,6 @@ dependencies = [ "async-trait", "baml_db", "baml_env", - "baml_project_root", "baml_test_support", "baml_type", "baml_type_runtime", @@ -1879,10 +1880,12 @@ name = "btel_settings" version = "0.0.0-beta" dependencies = [ "baml_env", + "baml_project_root", "borsh", "serde", "sha2", "strsim", + "tempfile", "thiserror 2.0.18", "toml 0.8.23", "tracing", diff --git a/baml_language/ENV_VARS.md b/baml_language/ENV_VARS.md index 57c779f5c96..c4eda27b5f9 100644 --- a/baml_language/ENV_VARS.md +++ b/baml_language/ENV_VARS.md @@ -34,7 +34,7 @@ Windows keeps an empty `login/cache/.lock` beside each credential | Variable | What it does | |---|---| -| `BAML_HOME` | Move all BAML state (CI workspace, container, read-only home). | +| `BAML_HOME` | Move user state (CI workspace, container, read-only home). Also selects the local trace store unless the project sets `[tracing].store`; see below. Empty means unset. | | `BAML_TOOLCHAIN` | Which toolchain `baml` runs: a channel, a version, or a path to a local `baml-cli`. Default `canary`. | | `BAML_MANIFEST_BASE_URL` | Release manifest mirror, used by the wrapper and the bridges. | | `BAML_BUILD_CACHE` | `false` turns off the build cache. | @@ -42,7 +42,7 @@ Windows keeps an empty `login/cache/.lock` beside each credential | `BAML_LOG` | Level for `log.*` output from `baml run` and `baml test` (same as `--log`): `off`, `error`, `warn`, `info` (default), `debug`, `trace`. | | `BAML_TELEMETRY` | Runtime tracing level: `off`, `low`, `medium` (default), `high`. | | `BOUNDARY_API_URL` | Boundary API gateway for login, cloud queries and telemetry. Overrides `[boundary].api_url`; default `https://api.prod.bcs.boundaryml.com`. | -| `BOUNDARY_API_KEY` | Non-interactive Boundary credential for cloud queries and telemetry. Takes precedence over saved user login. | +| `BOUNDARY_API_KEY` | Non-interactive Boundary credential for cloud queries and telemetry. Takes precedence over saved user login. Set to `local` to record and query locally even when a saved cloud login exists. | | `BOUNDARY_PROJECT` | Cloud target as `org_handle/project_name`. Overrides `[boundary].project`; query's `--project` flag takes precedence. | | `BAML_CLI_ALLOW_DIRECT` | Hides the "don't run baml-cli directly" warning. The wrapper sets it. | | `BAML_FEEDBACK_HOST` | Where `baml feedback` sends reports. Staging and tests. | @@ -80,3 +80,58 @@ Go, Rust, C++, Java, C# and TypeScript load a native library. Python and Swift l `NAPI_RS_NATIVE_LIBRARY_PATH`, `NAPI_RS_FORCE_WASI` and `NAPI_RS_ENFORCE_VERSION_CHECK` belong to the generated napi-rs loader, not to us. Not listed: test, benchmark and CI-only variables, Ruby, and the old `engine/`. + +## Shared local trace store + +Set one local store for SDK bridge recordings, `baml run` / `baml test`, and `baml query --local` in the project's `baml.toml`: + +```toml +[package] +name = "computer-use" + +[tracing] +store = ".baml" +``` + +`store` names a state directory containing `btel/recordings/`, shared `btel/cas/` blobs, and the disposable `btel/query.sqlite` index. Relative paths are resolved against the directory containing `baml.toml`, including when you launch from a nested directory. Absolute paths can share a store across projects. Paths are literal: `~` and environment variables are not expanded inside TOML strings. Add the store directory to `.gitignore` if it is inside your repository. + +For local recording and querying, the precedence is: + +1. An explicit traces directory passed to `baml query --from` (query only). +2. The resolved project's `[tracing].store`. +3. Nonempty `BAML_HOME`. +4. `/.baml` when a project is available. +5. `~/.baml` without a project (`.baml` relative to the working directory if the OS cannot provide a home directory). + +The CLI uses its resolved project root, including an explicit project passed to `--from`. The SDK bridge discovers the project enclosing the host process's working directory using the same `baml.toml` / `baml_src/` markers as the CLI. A nonempty `BAML_HOME` can be relative to the process working directory; use an absolute path when launching processes from different directories. `[tracing].store` takes precedence over `BAML_HOME` for traces while authentication, build caches, and toolchains continue using `BAML_HOME`. An invalid store setting produces a configuration error; an unwritable store disables recording with a diagnostic rather than silently writing into another store. + +The store setting chooses a filesystem location, independently of cloud/local destination selection and recording level. Use `BOUNDARY_API_KEY=local` to select local recording and query even with a saved Boundary login; `baml query --local` selects local reads without changing how your application records. The store setting does not enable recording that the runtime telemetry policy has disabled. This setting is read at runtime from the local manifest; it is not embedded into generated SDK artifacts. A bridge launched outside the project cannot discover that manifest: set an absolute `BAML_HOME` in both processes in that case. Packed executables always use `BAML_HOME` (otherwise the user store), so launching one inside a project does not adopt that project's store. + +For example, launch your computer-use demo from its project and query its recordings with the same environment. CLI entrypoints use this store too (`baml run main` below assumes the project defines `main`): + +```sh +cd /path/to/demo-computer-use +export BOUNDARY_API_KEY=local +export BAML_TELEMETRY=high +# Run your application using baml_bridge from this project or a child directory. +baml run main +baml query --local 'SELECT COUNT(*) FROM processes' +``` + +`baml query --from` also accepts an existing state directory, a directory containing `recordings/` and `cas/`, or the `recordings/` directory itself. A recognized traces directory selects local querying directly, even with cloud credentials set. It takes precedence over enclosing project configuration and `BAML_HOME`; it cannot be combined with cloud `--project` or `--environment` selectors. These commands all read the same store: + +```sh +baml query --from /path/to/demo-computer-use/.baml 'SELECT COUNT(*) FROM processes' +baml query --from /path/to/demo-computer-use/.baml/btel 'SELECT COUNT(*) FROM processes' +baml query --from /path/to/demo-computer-use/.baml/btel/recordings 'SELECT COUNT(*) FROM processes' +# Query the global user store from anywhere, including inside a project: +baml query --from "$HOME/.baml" 'SELECT COUNT(*) FROM processes' +``` + +Compatibility: without `[tracing].store` or `BAML_HOME`, existing project stores keep their location. With `BAML_HOME`, CLI local writes and reads now follow the same user store as the bridge. Existing recordings are not moved or combined automatically. To inspect an older project store while `BAML_HOME` is set, pass its `.baml` directory explicitly, or add `store = ".baml"` to keep that project local. + +### Querying large stores + +The first query builds an index of the selected store; later refreshes reconcile completed files using metadata and decode new or changed files. Refresh still lists every recording and stats completed files, so a global store accumulating many projects has more discovery and reconciliation work than a project store. SQL `LIMIT` and `--max-wall` constrain query execution, not this preceding refresh. `--format json` exposes `outcome.refresh` counters/timings and `outcome.query` timings to distinguish those costs. + +Use a per-project store to limit how much unrelated history each refresh visits. After building the index, `baml query --from /path/to/store --no-refresh 'SELECT COUNT(*) FROM processes'` skips file reconciliation and queries the existing index; it does not include recordings added since the last refresh. Queries can still scan indexed rows and report extents for all recordings, and captured-value expressions can read CAS blobs. This path/configuration change does not claim a speedup for SQL over the same global dataset; global-store query performance is being investigated separately. diff --git a/baml_language/crates/baml_cli/Cargo.toml b/baml_language/crates/baml_cli/Cargo.toml index ae857b9a825..8f37970f7cc 100644 --- a/baml_language/crates/baml_cli/Cargo.toml +++ b/baml_language/crates/baml_cli/Cargo.toml @@ -86,6 +86,8 @@ internal = [ ] skip-integ-tests = [ ] [dev-dependencies] +baml_test_support = { workspace = true } +bridge_cffi = { workspace = true } baml_tests = { workspace = true } btel_bcs = { workspace = true } btel_file = { workspace = true } diff --git a/baml_language/crates/baml_cli/src/query_command.rs b/baml_language/crates/baml_cli/src/query_command.rs index 4fe165c2c2a..eab523a70f9 100644 --- a/baml_language/crates/baml_cli/src/query_command.rs +++ b/baml_language/crates/baml_cli/src/query_command.rs @@ -1,6 +1,6 @@ -//! `baml query`: SQL over the project's local Btel recordings. +//! `baml query`: SQL over local Btel recordings or the Boundary cloud. //! -//! The first query creates `/.baml/btel/query.sqlite`; each query +//! The first local query creates `query.sqlite` in the selected Btel root; each query //! then applies only newly completed recording files before running. Rows //! and a terminal outcome (indexed extents, evidence diagnostics) are //! printed; JSON keeps diagnostics out of row data. @@ -12,7 +12,9 @@ use std::{io::Read as _, path::PathBuf, time::Duration}; -use baml_query_btel::{Budgets, Error, Index, IndexOptions, QueryRequest, Status, catalog, format}; +use baml_query_btel::{ + Budgets, Error, Index, IndexOptions, QueryRequest, SourceLayout, Status, catalog, format, +}; use clap::{Parser, ValueEnum}; use crate::project_load::find_project_root_from; @@ -56,7 +58,9 @@ pub struct QueryArgs { #[arg(long, conflicts_with = "local")] pub environment: Option, - /// Project directory (defaults to the current directory's project). + /// Project or traces directory: a state directory (.baml or BAML_HOME), + /// btel directory, or recordings directory. A traces directory selects local + /// querying directly. Defaults to the shared local store for this project. #[arg(long, value_name = "PATH")] pub from: Option, @@ -178,14 +182,29 @@ impl QueryArgs { Some(sql) => sql.to_owned(), None => anyhow::bail!("provide a SELECT statement, or --schema to list relations"), }; - let root = match find_project_root_from(self.from.as_deref())? { - Some(root) => root, + // Explicit trace folders must win over ancestor project discovery and + // saved cloud credentials. Canonicalization also rejects missing paths. + let trace_layout = self + .from + .as_deref() + .map(std::fs::canonicalize) + .transpose()? + .as_deref() + .and_then(SourceLayout::from_trace_directory); + let local = self.local || trace_layout.is_some(); + anyhow::ensure!( + trace_layout.is_none() || (self.project.is_none() && self.environment.is_none()), + "--from selects a local traces directory; use a project directory for a cloud query" + ); + let project_root = find_project_root_from(self.from.as_deref())?; + let root = match project_root.as_ref() { + Some(root) => root.clone(), None => match &self.from { Some(from) => from.clone(), None => std::env::current_dir()?, }, }; - let api_key = if self.local { + let api_key = if local { None } else { baml_env::string_var("BOUNDARY_API_KEY")? @@ -195,7 +214,7 @@ impl QueryArgs { !local_from_env || (self.project.is_none() && self.environment.is_none()), "BOUNDARY_API_KEY=local selects local recordings; unset BOUNDARY_API_KEY to use --project or --environment for a cloud query" ); - if !self.local && !local_from_env { + if !local && !local_from_env { let settings = crate::cloud_config::Boundary::read(&root)?; let endpoint = settings.endpoint().map_err(|error| { bcs_api::diagnostics::Context { @@ -259,7 +278,19 @@ impl QueryArgs { }, explain: self.explain, }; - let outcome = Index::for_project(&root, options).and_then(|mut index| { + let layout = match trace_layout { + Some(layout) => layout, + None => { + let store = btel_settings::local_store::resolve( + project_root.as_deref().or(self.from.as_deref()), + baml_env::os_var("BAML_HOME"), + std::env::home_dir(), + )?; + SourceLayout::for_btel(store.join("btel")) + } + }; + let recordings = layout.recordings.clone(); + let outcome = Index::open(layout, options).and_then(|mut index| { if self.no_refresh { index.query(&request) } else { @@ -287,8 +318,8 @@ impl QueryArgs { } if result.outcome.source_missing && self.format == QueryFormat::Table { eprintln!( - "note: no recordings at {}; run BAML code in this project to record some", - root.join(".baml/btel/recordings").display() + "note: no recordings at {}; run BAML code with this local store to record some", + recordings.display() ); } Ok(match result.outcome.status { diff --git a/baml_language/crates/baml_cli/src/runtime_telemetry.rs b/baml_language/crates/baml_cli/src/runtime_telemetry.rs index 7358bc112c3..856243ea44b 100644 --- a/baml_language/crates/baml_cli/src/runtime_telemetry.rs +++ b/baml_language/crates/baml_cli/src/runtime_telemetry.rs @@ -37,7 +37,7 @@ pub(crate) fn create_engine_with_context( boundary.api_url.as_deref(), ) .unwrap_or_else(|| { - TelemetryRecording::local_files( + TelemetryRecording::project_files( project_root, btel_settings::publisher::RecordingConfig::default(), ) diff --git a/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs b/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs index e54ac6633af..640ad4ee6ca 100644 --- a/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs +++ b/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs @@ -379,7 +379,9 @@ async fn local_sentinel_records_and_queries_locally_without_boundary_requests() let server = MockServer::start().await; let output = execute_case(temp.path(), &server.uri(), "local", &["run", "main"]); assert!(output.status.success(), "{output:?}"); - assert!(temp.path().join(RECORDINGS).exists()); + // Local writers and readers now honor this fixture's BAML_HOME. + assert!(temp.path().join("home/btel/recordings").exists()); + assert!(!temp.path().join(RECORDINGS).exists()); let output = execute_case( temp.path(), &server.uri(), diff --git a/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs new file mode 100644 index 00000000000..013d4ca6e78 --- /dev/null +++ b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs @@ -0,0 +1,301 @@ +//! The real bridge writes, the CLI writes, and fresh CLI processes read one store. +use std::{fmt::Write as _, path::Path, process::Command}; + +use bridge_cffi::baml_bridge::cffi::{ + BamlOutboundResult, CallFunctionArgs, InvocationOptions, baml_outbound_result, + call_function_args::CallTarget, +}; +use prost::Message as _; +use serde_json::{Value, json}; + +fn cli(cwd: &Path, args: &[&str]) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_baml-cli")) + .current_dir(cwd) + .args(args) + .env("DEV_BAML_CLI_DISABLE_AGENT_DETECTION", "1") + .env("BAML_CLI_ALLOW_DIRECT", "1") + .env("BOUNDARY_API_KEY", "local") + .env("DO_NOT_TRACK", "1") + .output() + .unwrap() +} + +fn query(cwd: &Path, from: Option<&Path>) -> Value { + let mut args = vec![ + "query", + "--format", + "json", + "SELECT COUNT(*) FROM processes", + ]; + if let Some(from) = from { + args.extend(["--from", from.to_str().unwrap()]); + } + let output = cli(cwd, &args); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap() +} + +#[tokio::test] +async fn bridge_cli_and_query_share_the_resolved_store() { + const STORE: &str = "BAML_TEST_SHARED_STORE"; + let Some(store) = std::env::var_os(STORE) else { + // Isolate the bridge singleton, cwd, auth, and configuration per scenario. + for scenario in ["project", "manifest", "absolute", "home"] { + let tmp = tempfile::tempdir().unwrap(); + let project = tmp.path().join("app"); + let nested = project.join("baml_src/nested"); + std::fs::create_dir_all(&nested).unwrap(); + std::fs::write( + project.join("baml_src/main.baml"), + "function main() -> int { 7 }", + ) + .unwrap(); + let state = match scenario { + "manifest" => project.join("traces"), + "absolute" => tmp.path().join("external-traces"), + "home" => tmp.path().join("state"), + _ => project.join(".baml"), + }; + let mut manifest = "[package]\nname = 'shared-store'\n".to_owned(); + if scenario == "manifest" { + manifest.push_str("[tracing]\nstore = 'traces'\n"); + } else if scenario == "absolute" { + write!( + manifest, + "[tracing]\nstore = {:?}\n", + state.to_str().unwrap() + ) + .unwrap(); + } + // The default case also verifies discovery using only baml_src/. + if scenario != "project" { + std::fs::write(project.join("baml.toml"), manifest).unwrap(); + } + let home = tmp.path().join("user"); + std::fs::create_dir(&home).unwrap(); + let mut child = Command::new(std::env::current_exe().unwrap()); + child + .args([ + "--exact", + "bridge_cli_and_query_share_the_resolved_store", + "--nocapture", + ]) + .current_dir(nested) + .env(STORE, &state) + .env("BAML_TEST_SHARED_PROJECT", &project) + .env( + "BAML_TEST_SHARED_ARTIFACT", + if scenario == "absolute" { + "generated" + } else { + "legacy" + }, + ) + .env("HOME", &home) + .env("USERPROFILE", &home) + .env("BOUNDARY_API_KEY", "local") + .env("BAML_TELEMETRY", "high") + .env("DO_NOT_TRACK", "1") + .env_remove("BOUNDARY_PROJECT") + .env_remove("BOUNDARY_API_URL") + .env_remove("BAML_HOME"); + if scenario != "project" { + child.env("BAML_HOME", tmp.path().join("state")); + } + let output = child.output().unwrap(); + assert!( + output.status.success(), + "{scenario}: {}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + if scenario == "manifest" || scenario == "absolute" { + assert!( + !tmp.path().join("state/btel").exists(), + "manifest wins over BAML_HOME" + ); + } + } + return; + }; + let store = std::path::PathBuf::from(store); + let project = std::path::PathBuf::from(std::env::var_os("BAML_TEST_SHARED_PROJECT").unwrap()); + let cwd = std::env::current_dir().unwrap(); + bridge_cffi::register_bridge(bridge_cffi::BridgeInfo { + language: bridge_cffi::BridgeLanguage::Python, + bridge_runtime_name: "shared-store-test".into(), + bridge_runtime_version: baml_version::CANONICAL_VERSION.into(), + toolchain_version: baml_version::CANONICAL_VERSION.into(), + }) + .unwrap(); + let program = baml_test_support::compile_source("function main() -> int { 7 }"); + let artifact = baml_artifact::encode(baml_artifact::ArtifactKind::Program, &program).unwrap(); + let blob = baml_artifact::encode_embedded(&artifact); + // Exercise both legacy blobs and the generated SDK metadata path. + let metadata = + (std::env::var("BAML_TEST_SHARED_ARTIFACT").unwrap() == "generated").then(|| { + let policy = btel_settings::artifact::ArtifactTelemetry::default(); + let codegen = toml::Table::from_iter([ + ("metadata_version".into(), 1.into()), + ("telemetry".into(), toml::Value::try_from(&policy).unwrap()), + ( + "toolchain".into(), + toml::Value::Table(toml::Table::from_iter([( + "version".into(), + baml_version::CANONICAL_VERSION.into(), + )])), + ), + ]); + toml::to_string(&toml::Table::from_iter([( + "__baml_codegen".into(), + toml::Value::Table(codegen), + )])) + .unwrap() + }); + bridge_cffi::initialize_runtime_from_blob(blob.as_bytes(), metadata.as_deref()).unwrap(); + let args = CallFunctionArgs { + call_id: bridge_cffi::new_function_call_id(), + call_target: Some(CallTarget::FunctionName("main".into())), + invocation: Some(InvocationOptions::default()), + ..Default::default() + } + .encode_to_vec(); + let request = bridge_cffi::decode_invocation_request(&args).unwrap(); + let response = bridge_cffi::execute_invocation(request).await; + let response = BamlOutboundResult::decode(response.as_slice()).unwrap(); + assert!(matches!( + response.result, + Some(baml_outbound_result::Result::Ok(_)) + )); + bridge_cffi::shutdown_runtime(None).await.unwrap(); + assert!(store.join("btel/recordings").is_dir()); + let written = query(&cwd, None); + assert_eq!(written["rows"], json!([[1]])); + + // CLI --from resolves its project independently of the launch directory. + let run = cli( + store.parent().unwrap(), + &["run", "main", "--from", project.to_str().unwrap()], + ); + assert!( + run.status.success(), + "{}", + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!(query(&cwd, Some(&project))["rows"], json!([[2]])); + if store.file_name().is_some_and(|name| name == "state") { + // Without a project marker, query still follows the same BAML_HOME. + assert_eq!(query(store.parent().unwrap(), None)["rows"], json!([[2]])); + } + + // Trace directories override both enclosing projects and cloud credentials. + // Point BAML_HOME elsewhere too: explicit --from must be authoritative. + for from in [ + store.clone(), + store.join("btel"), + store.join("btel/recordings"), + ] { + let output = Command::new(env!("CARGO_BIN_EXE_baml-cli")) + .current_dir(&cwd) + .args([ + "query", + "--from", + from.to_str().unwrap(), + "--format", + "json", + "SELECT COUNT(*) FROM processes", + ]) + .env("BOUNDARY_API_KEY", "bdry_secret_should_not_contact_cloud") + .env("BAML_HOME", store.join("other-home")) + .env("DEV_BAML_CLI_DISABLE_AGENT_DETECTION", "1") + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["rows"], json!([[2]])); + assert_eq!(result["outcome"]["refresh"]["files_decoded"], 0); + } +} + +#[test] +fn bad_store_configuration_fails_without_creating_a_different_store() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("baml.toml"), "[tracing]\nstore = 42").unwrap(); + let output = cli( + tmp.path(), + &["query", "--local", "SELECT COUNT(*) FROM processes"], + ); + assert!(!output.status.success()); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + error.contains("baml.toml") && error.contains("tracing"), + "{error}" + ); + assert!(!tmp.path().join(".baml").exists()); +} + +#[test] +fn empty_trace_folder_bypasses_enclosing_project_configuration() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("baml.toml"), "[tracing]\nstore = 42").unwrap(); + let traces = tmp.path().join(".baml"); + std::fs::create_dir(&traces).unwrap(); + let result = query(tmp.path(), Some(&traces)); + assert_eq!(result["rows"], json!([[0]])); + assert_eq!(result["outcome"]["source_missing"], true); + assert!(!traces.join("btel").exists()); + let output = cli( + tmp.path(), + &[ + "query", + "--from", + traces.to_str().unwrap(), + "--project", + "org/project", + "SELECT 1", + ], + ); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("local traces directory")); +} + +#[test] +fn telemetry_off_does_not_read_local_store_configuration() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::create_dir(tmp.path().join("baml_src")).unwrap(); + std::fs::write( + tmp.path().join("baml_src/main.baml"), + "function main() -> int { 7 }", + ) + .unwrap(); + std::fs::write( + tmp.path().join("baml.toml"), + "[package]\nname = 'app'\n[tracing]\nstore = 42", + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_baml-cli")) + .current_dir(tmp.path()) + .args(["run", "main"]) + .env("BAML_TELEMETRY", "off") + .env("BOUNDARY_API_KEY", "local") + .env("BAML_HOME", tmp.path().join("home")) + .env("DO_NOT_TRACK", "1") + .env("DEV_BAML_CLI_DISABLE_AGENT_DETECTION", "1") + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(!tmp.path().join(".baml").exists()); + assert!(!tmp.path().join("home/btel").exists()); +} diff --git a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs index 9111c3141cf..da7fd16409c 100644 --- a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs +++ b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs @@ -78,6 +78,13 @@ function fail() -> int { assert.is_true(false); 0 } testset "smoke" { test "ok" { assert.is_true(true) } } "#, ); + // Explicit project tracing keeps the CLI in its project while BAML_HOME + // isolates toolchain/auth state and packed-program recordings. + std::fs::write( + project.join("baml.toml"), + "[package]\nname = 'test'\n[tracing]\nstore = '.baml'\n", + ) + .unwrap(); let home = home(temp.path()); let nested = project.join("baml_src/nested"); std::fs::create_dir(&nested).unwrap(); diff --git a/baml_language/crates/baml_db/src/manifest.rs b/baml_language/crates/baml_db/src/manifest.rs index 896ba22d1d3..49ae764807f 100644 --- a/baml_language/crates/baml_db/src/manifest.rs +++ b/baml_language/crates/baml_db/src/manifest.rs @@ -83,8 +83,10 @@ pub struct TestProfileManifest { /// Top-level keys owned outside the compiler's manifest model, preserved in /// [`BamlToml::unknown`]. /// The wrapper reads `toolchain` to choose the executable; the CLI's cloud -/// configuration reads `boundary`. Matching is by top-level key name. -const KNOWN_UNHANDLED_TOP_LEVEL_KEYS: &[&str] = &["toolchain", "boundary", "pack", "bridge"]; +/// configuration reads `boundary`; runtime trace-store settings read `tracing`. +/// Matching is by top-level key name. +const KNOWN_UNHANDLED_TOP_LEVEL_KEYS: &[&str] = + &["toolchain", "boundary", "pack", "bridge", "tracing"]; #[derive(Debug, Deserialize)] pub struct Package { @@ -379,6 +381,12 @@ mod tests { assert!(warns.iter().any(|w| w.contains("nmae")), "got: {warns:?}"); } + #[test] + fn tracing_settings_are_owned_by_the_runtime() { + let manifest = parse("[package]\nname = 'app'\n[tracing]\nstore = '.baml'\n").unwrap(); + assert!(unknown_field_warnings(&manifest).is_empty()); + } + #[test] fn parses_generator_section_with_spans() { let m = parse( diff --git a/baml_language/crates/baml_query_btel/src/lib.rs b/baml_language/crates/baml_query_btel/src/lib.rs index 3ffb4e67146..01e3bfef779 100644 --- a/baml_language/crates/baml_query_btel/src/lib.rs +++ b/baml_language/crates/baml_query_btel/src/lib.rs @@ -27,10 +27,8 @@ mod usage; use std::{path::Path, sync::Arc, time::Duration}; -use btel_reader::{ - cas::{CasLimits, CasStore}, - layout::SourceLayout, -}; +use btel_reader::cas::{CasLimits, CasStore}; +pub use btel_reader::layout::SourceLayout; pub use ingest::{RefreshMetrics, RefreshOptions}; pub use query::{Budgets, Outcome, QueryRequest, QueryResult, Status}; use rusqlite::Connection; diff --git a/baml_language/crates/bex_engine/Cargo.toml b/baml_language/crates/bex_engine/Cargo.toml index 56d62cb0a67..72e35b12866 100644 --- a/baml_language/crates/bex_engine/Cargo.toml +++ b/baml_language/crates/bex_engine/Cargo.toml @@ -8,7 +8,6 @@ license.workspace = true [dependencies] baml_env = { workspace = true } -baml_project_root = { workspace = true } baml_type = { workspace = true } baml_type_runtime = { workspace = true } bex_events = { workspace = true } diff --git a/baml_language/crates/bex_engine/src/telemetry.rs b/baml_language/crates/bex_engine/src/telemetry.rs index 4a19585bce3..da18126df0b 100644 --- a/baml_language/crates/bex_engine/src/telemetry.rs +++ b/baml_language/crates/bex_engine/src/telemetry.rs @@ -37,6 +37,9 @@ enum Destination { recordings: PathBuf, cas: PathBuf, }, + ProjectFiles { + root: PathBuf, + }, UserFiles { project_from_cwd: bool, }, @@ -414,6 +417,16 @@ impl TelemetryRecording { } } + /// Use the shared project store setting, then `BAML_HOME`, then `.baml`. + /// Configuration is resolved only when enabled recording starts. + pub fn project_files(project_root: impl Into, config: RecordingConfig) -> Self { + let mut recording = Self::user_files(config); + recording.destination = Destination::ProjectFiles { + root: project_root.into(), + }; + recording + } + /// Override the root: recordings use `/` and shared /// CAS uses `/cas/v4`. No I/O or worker /// starts until engine construction; `BAML_TELEMETRY=off` ignores this output. @@ -453,12 +466,9 @@ impl TelemetryRecording { } } - /// When recording to the BAML home without an explicit `BAML_HOME`, prefer - /// the BAML project enclosing the working directory, where - /// `baml query --local` reads. Falls back to the BAML home when there is no - /// project or its directory cannot be created. For SDK hosts, which run - /// from their project; a packed program launched elsewhere must not write - /// into an unrelated project. + /// Resolve the shared local store using the project enclosing the working + /// directory. For SDK hosts; packed programs keep using the BAML home so + /// launching one elsewhere does not adopt an unrelated project's settings. #[must_use] pub fn preferring_cwd_project(mut self) -> Self { if let Destination::UserFiles { project_from_cwd } = &mut self.destination { @@ -522,7 +532,9 @@ impl TelemetryRecording { // Cloud source uploads are a separate feature; local recordings retain their code. sources: (matches!( self.destination, - Destination::LocalFiles { .. } | Destination::UserFiles { .. } + Destination::LocalFiles { .. } + | Destination::ProjectFiles { .. } + | Destination::UserFiles { .. } ) && !self.sources.is_empty()) .then(|| { let pool = btel_snapshot::SnapshotPool::unbounded(btel_snapshot::Limits::default()); @@ -554,7 +566,7 @@ impl TelemetryRecording { self.config .validate_transport(&transport) .map_err(|error| EngineError::Other(error.to_owned()))?; - let roots = match self.destination { + let (root, cas) = match self.destination { Destination::InvalidConfiguration(diagnostic) => { return Err(EngineError::CloudAuthorization(diagnostic)); } @@ -584,42 +596,34 @@ impl TelemetryRecording { .and_then(Result::ok) .as_deref(), std::env::home_dir(), - ), - Destination::LocalFiles { recordings, cas } => vec![(recordings, cas)], + ) + .map_err(|error| EngineError::Other(error.to_string()))?, + Destination::ProjectFiles { root } => { + let store = btel_settings::local_store::resolve( + Some(&root), + baml_env::os_var("BAML_HOME"), + std::env::home_dir(), + ) + .map_err(|error| EngineError::Other(error.to_string()))?; + let btel = store.join("btel"); + (btel.join("recordings"), btel.join("cas")) + } + Destination::LocalFiles { recordings, cas } => (recordings, cas), }; let mut delivery = None; let runtime = btel_processor::TelemetryRuntime::with_publisher_factory(transport, |control| { - let mut writer = Err(std::io::Error::other("no telemetry directory")); - let last = roots.len().saturating_sub(1); - for (index, (root, cas)) in roots.into_iter().enumerate() { - let failure = control.clone(); - // The CAS is first written with the first snapshot, after a - // fallback is no longer possible. - let cas_ready = if index < last { - cas_accepts_writes(&cas, self.id) - } else { - Ok(()) - }; - writer = cas_ready.and_then(|()| { - btel_file::LocalDelivery::create_with_cas( - &root, - &cas, - self.id, - btel_file::LocalDeliveryConfig::default(), - move |error| { - failure.disable(btel_processor::RuntimeError(error.to_string())); - tracing::warn!(%error, "telemetry recording disabled"); - }, - ) - }); - match &writer { - Ok(_) => break, - Err(error) => { - tracing::debug!(%error, root = %root.display(), "telemetry directory unavailable"); - } - } - } + let failure = control.clone(); + let writer = btel_file::LocalDelivery::create_with_cas( + &root, + &cas, + self.id, + btel_file::LocalDeliveryConfig::default(), + move |error| { + failure.disable(btel_processor::RuntimeError(error.to_string())); + tracing::warn!(%error, "telemetry recording disabled"); + }, + ); let builder = RecordingBuilder::new(self.id, self.config) .map_err(std::io::Error::other)? .with_source_snapshot(source_snapshot) @@ -756,32 +760,15 @@ impl TelemetryRecording { } } -/// Recording and CAS directories for [`TelemetryRecording::user_files`], most -/// preferred first. `cwd` is set only when the recording prefers the project -/// enclosing it. +/// Resolve the same store as CLI writers and readers, without silently +/// falling back to another directory when the selected store is unwritable. fn user_file_roots( baml_home: Option, cwd: Option<&Path>, home_dir: Option, -) -> Vec<(PathBuf, PathBuf)> { - use btel_settings::local_files::{CAS_DIRECTORY, RECORDINGS_DIRECTORY}; - let baml_home = baml_home.filter(|value| !value.is_empty()); - let project = cwd - .filter(|_| baml_home.is_none()) - .and_then(baml_project_root::find_baml_project_root); - let btel = baml_env::baml_home_from(baml_home, home_dir).join("btel"); - project - .map(|root| (root.join(RECORDINGS_DIRECTORY), root.join(CAS_DIRECTORY))) - .into_iter() - .chain([(btel.join("recordings"), btel.join("cas"))]) - .collect() -} - -fn cas_accepts_writes(cas: &Path, id: RecordingId) -> std::io::Result<()> { - std::fs::create_dir_all(cas)?; - let probe = btel_file::recording_directory(cas, id); - std::fs::create_dir(&probe)?; - std::fs::remove_dir(&probe) +) -> std::io::Result<(PathBuf, PathBuf)> { + let btel = btel_settings::local_store::resolve_for_cwd(cwd, baml_home, home_dir)?.join("btel"); + Ok((btel.join("recordings"), btel.join("cas"))) } fn artifact_recovery(policy: &ArtifactTelemetry) -> RecordingOverride { diff --git a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs index a6ae6ec4873..57d9e8400fc 100644 --- a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs +++ b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs @@ -30,8 +30,8 @@ fn project_enclosing_cwd_precedes_home() { let home = dir.path().join("home"); assert_eq!( - user_file_roots(None, Some(&nested), Some(home.clone())), - vec![project(&root), btel(&home.join(".baml"))] + user_file_roots(None, Some(&nested), Some(home)).unwrap(), + project(&root) ); } @@ -42,8 +42,8 @@ fn baml_home_overrides_project() { let baml_home = dir.path().join("state"); assert_eq!( - user_file_roots(Some(baml_home.clone().into()), Some(dir.path()), None), - vec![btel(&baml_home)] + user_file_roots(Some(baml_home.clone().into()), Some(dir.path()), None).unwrap(), + btel(&baml_home) ); } @@ -54,8 +54,8 @@ fn empty_baml_home_is_unset() { let home = dir.path().join("home"); assert_eq!( - user_file_roots(Some("".into()), Some(dir.path()), Some(home.clone())), - vec![project(dir.path()), btel(&home.join(".baml"))] + user_file_roots(Some("".into()), Some(dir.path()), Some(home)).unwrap(), + project(dir.path()) ); } @@ -65,12 +65,12 @@ fn no_project_uses_home() { let home = dir.path().join("home"); assert_eq!( - user_file_roots(None, Some(dir.path()), Some(home.clone())), - vec![btel(&home.join(".baml"))] + user_file_roots(None, Some(dir.path()), Some(home.clone())).unwrap(), + btel(&home.join(".baml")) ); assert_eq!( - user_file_roots(None, None, Some(home.clone())), - vec![btel(&home.join(".baml"))] + user_file_roots(None, None, Some(home.clone())).unwrap(), + btel(&home.join(".baml")) ); } @@ -139,25 +139,10 @@ fn recording_starts_in_cwd_project() { // Packed programs keep recording to the user store. run(&fallback, true); - // An unwritable project store falls back to the user store. - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt as _; - let readonly = std::fs::Permissions::from_mode(0o555); - std::fs::set_permissions(&recordings, readonly).unwrap(); - let writable = std::fs::File::create(recordings.join("probe")).is_ok(); - if !writable { - run(&fallback, false); - } - std::fs::set_permissions(&recordings, std::fs::Permissions::from_mode(0o755)).unwrap(); - - // So does a writable project store whose CAS is not. - let cas = root.join(".baml/btel/cas"); - std::fs::create_dir_all(&cas).unwrap(); - std::fs::set_permissions(&cas, std::fs::Permissions::from_mode(0o555)).unwrap(); - if !writable { - run(&fallback, false); - } - std::fs::set_permissions(&cas, std::fs::Permissions::from_mode(0o755)).unwrap(); - } + // An explicit project store is shared even when BAML_HOME is set. + std::fs::write(root.join("baml.toml"), "[tracing]\nstore = 'custom'\n").unwrap(); + let custom = root.join("custom/btel/recordings"); + std::fs::create_dir_all(&custom).unwrap(); + run(&custom, false); + run(&fallback, true); } diff --git a/baml_language/crates/btel_reader/src/layout.rs b/baml_language/crates/btel_reader/src/layout.rs index 6ce65b12270..d55c32ce8a5 100644 --- a/baml_language/crates/btel_reader/src/layout.rs +++ b/baml_language/crates/btel_reader/src/layout.rs @@ -1,11 +1,11 @@ -//! Where one project's recordings and CAS blobs live. The caller resolves -//! the project; nothing here searches for it. +//! Where a store's recordings and CAS blobs live. The caller resolves +//! project configuration; explicit trace directories need no project. use std::path::{Path, PathBuf}; use btel_settings::local_files::{CAS_DIRECTORY, RECORDINGS_DIRECTORY}; use btel_snapshot::CasId; -/// `/.baml/btel`: `recordings//.btel` plus shared CAS. +/// A Btel root: `recordings//.btel` plus shared CAS. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SourceLayout { /// The directory holding `recordings/` and `cas/`, and derived state. @@ -16,6 +16,35 @@ pub struct SourceLayout { } impl SourceLayout { + /// Open the directory containing `recordings/`, `cas/` and the query index. + pub fn for_btel(root: impl Into) -> Self { + let root = root.into(); + Self { + recordings: root.join("recordings"), + cas: root.join("cas"), + root, + } + } + + /// Recognize an explicitly selected trace directory before project walk-up. + /// Accept a state directory (`.baml` or `BAML_HOME`), its `btel` child, + /// or the `recordings` directory. Never scan recording contents or the CAS. + pub fn from_trace_directory(path: &Path) -> Option { + if !path.is_dir() { + return None; + } + if path.join("btel").is_dir() || path.file_name().is_some_and(|name| name == ".baml") { + return Some(Self::for_btel(path.join("btel"))); + } + if path.join("recordings").is_dir() || path.file_name().is_some_and(|name| name == "btel") { + return Some(Self::for_btel(path)); + } + if path.file_name().is_some_and(|name| name == "recordings") { + return path.parent().map(Self::for_btel); + } + None + } + pub fn for_project(project_root: &Path) -> Self { let recordings = project_root.join(RECORDINGS_DIRECTORY); let cas = project_root.join(CAS_DIRECTORY); @@ -34,3 +63,42 @@ impl SourceLayout { btel_file::cas_path(&self.cas, id) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_trace_directories_keep_their_layout_even_when_empty() { + let tmp = tempfile::tempdir().unwrap(); + let state = tmp.path().join(".baml"); + std::fs::create_dir(&state).unwrap(); + let expected = SourceLayout::for_project(tmp.path()); + assert_eq!( + SourceLayout::from_trace_directory(&state), + Some(expected.clone()) + ); + std::fs::create_dir(state.join("btel")).unwrap(); + assert_eq!( + SourceLayout::from_trace_directory(&state.join("btel")), + Some(expected.clone()) + ); + std::fs::create_dir(state.join("btel/recordings")).unwrap(); + assert_eq!( + SourceLayout::from_trace_directory(&state.join("btel/recordings")), + Some(expected.clone()) + ); + assert_eq!(SourceLayout::from_trace_directory(tmp.path()), None); + assert_eq!( + SourceLayout::from_trace_directory(&state.join("missing")), + None + ); + let custom = tmp.path().join("traces"); + std::fs::rename(state, &custom).unwrap(); + assert_eq!( + SourceLayout::from_trace_directory(&custom), + Some(SourceLayout::for_btel(custom.join("btel"))) + ); + assert!(!expected.root.join("query.sqlite").exists()); + } +} diff --git a/baml_language/crates/btel_settings/Cargo.toml b/baml_language/crates/btel_settings/Cargo.toml index 47d2b8f6d1b..1b9ab0b74a5 100644 --- a/baml_language/crates/btel_settings/Cargo.toml +++ b/baml_language/crates/btel_settings/Cargo.toml @@ -19,6 +19,7 @@ workspace = true [dependencies] baml_env = { workspace = true } +baml_project_root = { workspace = true } borsh = { workspace = true } serde = { workspace = true } sha2 = { workspace = true } @@ -26,3 +27,6 @@ strsim = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/baml_language/crates/btel_settings/src/lib.rs b/baml_language/crates/btel_settings/src/lib.rs index 71116631ee1..cba9b3777ae 100644 --- a/baml_language/crates/btel_settings/src/lib.rs +++ b/baml_language/crates/btel_settings/src/lib.rs @@ -77,6 +77,8 @@ pub mod encoding; pub mod identity; pub mod layout; pub mod local_files; +#[cfg(not(target_arch = "wasm32"))] +pub mod local_store; pub mod mode; pub mod network; pub mod overage; diff --git a/baml_language/crates/btel_settings/src/local_files.rs b/baml_language/crates/btel_settings/src/local_files.rs index a0c269f5706..c6f50688043 100644 --- a/baml_language/crates/btel_settings/src/local_files.rs +++ b/baml_language/crates/btel_settings/src/local_files.rs @@ -1,8 +1,8 @@ //! Local delivery settings. File output is enabled explicitly per engine. use std::num::NonZeroUsize; -/// Standard recording directory relative to the CLI-resolved BAML project root. -/// User-level recordings instead resolve `btel/recordings` beneath the BAML home. +/// Conventional recording directory relative to a BAML project root. +/// Configured stores instead resolve `btel/recordings` beneath the state directory. /// Each recording gets its own ID directory beneath this path. pub const RECORDINGS_DIRECTORY: &str = ".baml/btel/recordings"; @@ -25,7 +25,7 @@ impl Default for LocalDeliveryConfig { } /// CAS directory relative to the BAML project root, shared across recordings. -/// User-level recordings instead resolve `btel/cas` beneath the BAML home. +/// Configured stores instead resolve `btel/cas` beneath the state directory. /// Local delivery appends the blob format version before the digest shards. pub const CAS_DIRECTORY: &str = ".baml/btel/cas"; /// Writer buffering for scalar-heavy snapshot blobs; does not constrain capture size. diff --git a/baml_language/crates/btel_settings/src/local_store.rs b/baml_language/crates/btel_settings/src/local_store.rs new file mode 100644 index 00000000000..bce622971e1 --- /dev/null +++ b/baml_language/crates/btel_settings/src/local_store.rs @@ -0,0 +1,131 @@ +//! Shared local trace-store resolution for native writers and readers. +use std::{ + ffi::OsString, + io, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +#[derive(Deserialize)] +struct Manifest { + #[serde(default)] + tracing: Tracing, +} + +#[derive(Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct Tracing { + store: Option, +} + +/// Resolve the state directory containing `btel/recordings` and `btel/cas`. +/// A project's `[tracing].store` wins over nonempty `BAML_HOME`, then the +/// project's `.baml`, then the user home. Relative manifest paths belong to +/// the manifest's directory; relative environment paths belong to the cwd. +/// This only reads configuration; it never creates directories or falls back +/// to a different store when the selected destination is unwritable. +pub fn resolve( + project: Option<&Path>, + baml_home: Option, + home_dir: Option, +) -> io::Result { + if let Some(project) = project { + let path = project.join("baml.toml"); + let text = match std::fs::read_to_string(&path) { + Ok(text) => Some(text), + Err(error) if error.kind() == io::ErrorKind::NotFound => None, + Err(error) => { + return Err(io::Error::new( + error.kind(), + format!("{}: {error}", path.display()), + )); + } + }; + if let Some(text) = text { + let manifest: Manifest = toml::from_str(&text).map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("{}: invalid [tracing] settings: {error}", path.display()), + ) + })?; + if let Some(store) = manifest.tracing.store { + if store.as_os_str().is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("{}: tracing.store must not be empty", path.display()), + )); + } + return Ok(project.join(store)); + } + } + } + if let Some(home) = baml_home.filter(|value| !value.is_empty()) { + return Ok(PathBuf::from(home)); + } + Ok(project.map_or_else( + || baml_env::baml_home_from(None, home_dir), + |project| project.join(".baml"), + )) +} + +/// SDK hosts discover the project enclosing their working directory. Packed +/// applications pass no cwd so they do not adopt an unrelated project's store. +pub fn resolve_for_cwd( + cwd: Option<&Path>, + baml_home: Option, + home_dir: Option, +) -> io::Result { + let project = cwd.and_then(baml_project_root::find_baml_project_root); + resolve(project.as_deref(), baml_home, home_dir) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn manifest_home_project_and_user_precedence() { + let tmp = tempfile::tempdir().unwrap(); + let project = tmp.path().join("app"); + let nested = project.join("src/deep"); + std::fs::create_dir_all(&nested).unwrap(); + let home = tmp.path().join("user"); + let state = tmp.path().join("state"); + let resolve = + |baml_home| resolve_for_cwd(Some(&nested), baml_home, Some(home.clone())).unwrap(); + assert_eq!(resolve(None), home.join(".baml")); + std::fs::write(project.join("baml.toml"), "[package]\nname = 'app'\n").unwrap(); + assert_eq!(resolve(None), project.join(".baml")); + assert_eq!(resolve(Some("".into())), project.join(".baml")); + assert_eq!(resolve(Some(state.clone().into())), state); + std::fs::write(project.join("baml.toml"), "[tracing]\nstore = 'traces'\n").unwrap(); + assert_eq!(resolve(Some(state.clone().into())), project.join("traces")); + let config = format!("[tracing]\nstore = {:?}\n", state.to_str().unwrap()); + std::fs::write(project.join("baml.toml"), config).unwrap(); + assert_eq!(resolve(None), state); + // Packed programs ignore the manifest even when launched in a project. + assert_eq!( + super::resolve_for_cwd(None, None, Some(home.clone())).unwrap(), + home.join(".baml") + ); + } + + #[test] + fn invalid_settings_are_actionable_and_do_not_silently_select_another_store() { + let tmp = tempfile::tempdir().unwrap(); + for text in [ + "[tracing]\nstore = ''", + "[tracing]\nstore = 42", + "[tracing]\nstroe = 'traces'", + "tracing = 'traces'", + "not toml", + ] { + std::fs::write(tmp.path().join("baml.toml"), text).unwrap(); + let error = resolve(Some(tmp.path()), Some("elsewhere".into()), None).unwrap_err(); + assert!(error.to_string().contains("baml.toml"), "{error}"); + assert!(error.to_string().contains("tracing"), "{error}"); + } + assert!(!tmp.path().join(".baml").exists()); + } +} From f68b9050693aa45e28e8854d201d7417df39c86e Mon Sep 17 00:00:00 2001 From: Sam Lijin Date: Thu, 5 Nov 2026 11:25:12 -0800 Subject: [PATCH 2/6] test: isolate local query stores and cover unavailable destinations --- baml_language/crates/baml_cli/Cargo.toml | 2 +- .../crates/baml_cli/tests/query_e2e.rs | 6 ++++++ .../src/telemetry/user_files_tests.rs | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/baml_language/crates/baml_cli/Cargo.toml b/baml_language/crates/baml_cli/Cargo.toml index 8f37970f7cc..fb2b6f3d0ea 100644 --- a/baml_language/crates/baml_cli/Cargo.toml +++ b/baml_language/crates/baml_cli/Cargo.toml @@ -87,8 +87,8 @@ skip-integ-tests = [ ] [dev-dependencies] baml_test_support = { workspace = true } -bridge_cffi = { workspace = true } baml_tests = { workspace = true } +bridge_cffi = { workspace = true } btel_bcs = { workspace = true } btel_file = { workspace = true } btel_recorder = { workspace = true } diff --git a/baml_language/crates/baml_cli/tests/query_e2e.rs b/baml_language/crates/baml_cli/tests/query_e2e.rs index 397c49115eb..fa5a3db4072 100644 --- a/baml_language/crates/baml_cli/tests/query_e2e.rs +++ b/baml_language/crates/baml_cli/tests/query_e2e.rs @@ -25,6 +25,9 @@ fn cli(project: &Path, args: &[&str], envs: &[(&str, &str)]) -> std::process::Ou .args(args) .current_dir(project) .env("DEV_BAML_CLI_DISABLE_AGENT_DETECTION", "1") + .env("BAML_HOME", project.join(".baml")) + .env("BOUNDARY_API_KEY", "local") + .env("DO_NOT_TRACK", "1") .env_remove("BAML_TELEMETRY") .envs(envs.iter().copied()) .output() @@ -231,6 +234,9 @@ fn schema_invalid_sql_and_stdin() { .args(["query", "--local", "--format", "jsonl", "-"]) .current_dir(project.path()) .env("DEV_BAML_CLI_DISABLE_AGENT_DETECTION", "1") + .env("BAML_HOME", project.path().join(".baml")) + .env("BOUNDARY_API_KEY", "local") + .env("DO_NOT_TRACK", "1") .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .spawn() diff --git a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs index 57d9e8400fc..5df4009debd 100644 --- a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs +++ b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs @@ -74,6 +74,26 @@ fn no_project_uses_home() { ); } +#[test] +fn an_unwritable_selected_store_does_not_redirect_recordings() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("baml.toml"), + "[tracing]\nstore = 'blocked'\n", + ) + .unwrap(); + // A regular file blocks directory creation on every platform, including + // privileged test runners for which read-only permissions do not suffice. + std::fs::write(dir.path().join("blocked"), "not a directory").unwrap(); + let (runtime, delivery) = + TelemetryRecording::project_files(dir.path(), RecordingConfig::default()) + .start(None, Arc::default(), &Context::default()) + .unwrap(); + assert!(delivery.is_none(), "must not open a different store"); + assert!(runtime.finish().is_err()); + assert!(!dir.path().join(".baml").exists()); +} + /// Starts real recordings in child processes whose working directory is a /// project, since the destination depends on process-wide state. #[test] From 28edab97f47ac5d9f5bd58711f7d2d504270365a Mon Sep 17 00:00:00 2001 From: Sam Lijin Date: Thu, 5 Nov 2026 11:31:01 -0800 Subject: [PATCH 3/6] fix: identify query source paths in resolution errors --- .../crates/baml_cli/src/query_command.rs | 6 +++- .../crates/baml_cli/src/runtime_telemetry.rs | 1 + .../baml_cli/tests/boundary_telemetry_e2e.rs | 1 + .../crates/baml_cli/tests/query_e2e.rs | 2 ++ .../baml_cli/tests/shared_trace_store_e2e.rs | 30 +++++++++++++++++++ .../baml_cli/tests/telemetry_recording_e2e.rs | 1 + baml_language/crates/baml_db/src/manifest.rs | 1 + .../crates/bex_engine/src/telemetry.rs | 2 ++ .../src/telemetry/user_files_tests.rs | 1 + .../crates/btel_reader/src/layout.rs | 1 + .../crates/btel_settings/src/local_store.rs | 2 ++ 11 files changed, 47 insertions(+), 1 deletion(-) diff --git a/baml_language/crates/baml_cli/src/query_command.rs b/baml_language/crates/baml_cli/src/query_command.rs index eab523a70f9..64e9499159c 100644 --- a/baml_language/crates/baml_cli/src/query_command.rs +++ b/baml_language/crates/baml_cli/src/query_command.rs @@ -169,6 +169,7 @@ impl QueryArgs { crate::cloud_query::run(self, endpoint, credential, source, project, sql) } + /// Select a source, refresh local files when requested, and render rows and outcomes. pub fn run(&self) -> anyhow::Result { if self.schema { return self.print_schema(); @@ -187,7 +188,10 @@ impl QueryArgs { let trace_layout = self .from .as_deref() - .map(std::fs::canonicalize) + .map(|from| { + std::fs::canonicalize(from) + .map_err(|error| anyhow::anyhow!("--from {}: {error}", from.display())) + }) .transpose()? .as_deref() .and_then(SourceLayout::from_trace_directory); diff --git a/baml_language/crates/baml_cli/src/runtime_telemetry.rs b/baml_language/crates/baml_cli/src/runtime_telemetry.rs index 856243ea44b..b7cccec6824 100644 --- a/baml_language/crates/baml_cli/src/runtime_telemetry.rs +++ b/baml_language/crates/baml_cli/src/runtime_telemetry.rs @@ -15,6 +15,7 @@ pub(crate) fn create_engine( create_engine_with_context(program, argv, project_root, sources, Default::default()) } +/// Create the CLI engine with its launch context and shared local-store fallback. pub(crate) fn create_engine_with_context( program: bex_vm_types::Program, argv: Vec, diff --git a/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs b/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs index 640ad4ee6ca..8df22408b2c 100644 --- a/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs +++ b/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs @@ -373,6 +373,7 @@ async fn initial_connection_failures_warn_without_changing_execution() { ); } +/// The local sentinel must keep both recording and querying away from the cloud. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn local_sentinel_records_and_queries_locally_without_boundary_requests() { let temp = fixture("function main() -> int { 7 }\n"); diff --git a/baml_language/crates/baml_cli/tests/query_e2e.rs b/baml_language/crates/baml_cli/tests/query_e2e.rs index fa5a3db4072..82afd8ee884 100644 --- a/baml_language/crates/baml_cli/tests/query_e2e.rs +++ b/baml_language/crates/baml_cli/tests/query_e2e.rs @@ -20,6 +20,7 @@ function main() -> int { } "; +/// Run with isolated local state so saved credentials cannot change the data source. fn cli(project: &Path, args: &[&str], envs: &[(&str, &str)]) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_baml-cli")) .args(args) @@ -207,6 +208,7 @@ fn fresh_query_processes_index_once_and_then_only_new_files() { assert_eq!(calls["outcome"]["status"], "complete"); } +/// Schema, SQL rejection, and stdin framing must also work with an empty local store. #[test] fn schema_invalid_sql_and_stdin() { let project = tempfile::tempdir().unwrap(); diff --git a/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs index 013d4ca6e78..467eb5d4c1d 100644 --- a/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs +++ b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs @@ -8,6 +8,7 @@ use bridge_cffi::baml_bridge::cffi::{ use prost::Message as _; use serde_json::{Value, json}; +/// Run a local-only CLI subprocess without using the developer's cloud login. fn cli(cwd: &Path, args: &[&str]) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_baml-cli")) .current_dir(cwd) @@ -20,6 +21,7 @@ fn cli(cwd: &Path, args: &[&str]) -> std::process::Output { .unwrap() } +/// Count recorded processes through the public query command and JSON output. fn query(cwd: &Path, from: Option<&Path>) -> Value { let mut args = vec![ "query", @@ -39,6 +41,7 @@ fn query(cwd: &Path, from: Option<&Path>) -> Value { serde_json::from_slice(&output.stdout).unwrap() } +/// Bridge and CLI recordings must remain visible through every supported query source. #[tokio::test] async fn bridge_cli_and_query_share_the_resolved_store() { const STORE: &str = "BAML_TEST_SHARED_STORE"; @@ -225,6 +228,7 @@ async fn bridge_cli_and_query_share_the_resolved_store() { } } +/// A malformed manifest must fail visibly without selecting a fallback store. #[test] fn bad_store_configuration_fails_without_creating_a_different_store() { let tmp = tempfile::tempdir().unwrap(); @@ -242,6 +246,7 @@ fn bad_store_configuration_fails_without_creating_a_different_store() { assert!(!tmp.path().join(".baml").exists()); } +/// An explicit empty store bypasses enclosing configuration and rejects cloud selectors. #[test] fn empty_trace_folder_bypasses_enclosing_project_configuration() { let tmp = tempfile::tempdir().unwrap(); @@ -267,6 +272,7 @@ fn empty_trace_folder_bypasses_enclosing_project_configuration() { assert!(String::from_utf8_lossy(&output.stderr).contains("local traces directory")); } +/// Disabled source execution must not inspect local-store settings or create traces. #[test] fn telemetry_off_does_not_read_local_store_configuration() { let tmp = tempfile::tempdir().unwrap(); @@ -299,3 +305,27 @@ fn telemetry_off_does_not_read_local_store_configuration() { assert!(!tmp.path().join(".baml").exists()); assert!(!tmp.path().join("home/btel").exists()); } + +/// Missing source paths must identify both the flag and the supplied directory. +#[test] +fn missing_from_path_is_named_in_the_error() { + let tmp = tempfile::tempdir().unwrap(); + let missing = tmp.path().join("missing-traces"); + let output = cli( + tmp.path(), + // Reach query's source resolver rather than the preceding skill check. + &[ + "--agent-skill-check", + "off", + "query", + "--from", + missing.to_str().unwrap(), + "SELECT 1", + ], + ); + assert!(!output.status.success()); + let error = String::from_utf8_lossy(&output.stderr); + assert!(error.contains("--from"), "{error}"); + assert!(error.contains(missing.to_str().unwrap()), "{error}"); + assert!(!missing.exists()); +} diff --git a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs index da7fd16409c..0956d69c5e4 100644 --- a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs +++ b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs @@ -65,6 +65,7 @@ fn home(root: &Path) -> std::path::PathBuf { home } +/// Explicit project storage must survive nested launches and bytecode-cache hits. #[test] fn cli_uses_project_root_for_cold_cached_nested_expression_and_test_execution() { let temp = tempfile::tempdir().unwrap(); diff --git a/baml_language/crates/baml_db/src/manifest.rs b/baml_language/crates/baml_db/src/manifest.rs index 49ae764807f..78d2b7b1dc5 100644 --- a/baml_language/crates/baml_db/src/manifest.rs +++ b/baml_language/crates/baml_db/src/manifest.rs @@ -381,6 +381,7 @@ mod tests { assert!(warns.iter().any(|w| w.contains("nmae")), "got: {warns:?}"); } + /// Runtime-owned tracing configuration must not produce compiler typo warnings. #[test] fn tracing_settings_are_owned_by_the_runtime() { let manifest = parse("[package]\nname = 'app'\n[tracing]\nstore = '.baml'\n").unwrap(); diff --git a/baml_language/crates/bex_engine/src/telemetry.rs b/baml_language/crates/bex_engine/src/telemetry.rs index da18126df0b..eb735eef8ff 100644 --- a/baml_language/crates/bex_engine/src/telemetry.rs +++ b/baml_language/crates/bex_engine/src/telemetry.rs @@ -508,6 +508,7 @@ impl TelemetryRecording { Arc::clone(&self.exit) } + /// Capture process metadata and local sources while omitting them from cloud uploads. fn process( &self, launch_context: &btel_types::context::Context, @@ -549,6 +550,7 @@ impl TelemetryRecording { } } + /// Resolve the selected destination and construct its recording processor and delivery. pub(crate) fn start( self, source_snapshot: Option<[u8; 32]>, diff --git a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs index 5df4009debd..12501e7ef37 100644 --- a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs +++ b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs @@ -74,6 +74,7 @@ fn no_project_uses_home() { ); } +/// A blocked destination disables delivery instead of selecting an unrelated home. #[test] fn an_unwritable_selected_store_does_not_redirect_recordings() { let dir = tempfile::tempdir().unwrap(); diff --git a/baml_language/crates/btel_reader/src/layout.rs b/baml_language/crates/btel_reader/src/layout.rs index d55c32ce8a5..7d70c5d78e2 100644 --- a/baml_language/crates/btel_reader/src/layout.rs +++ b/baml_language/crates/btel_reader/src/layout.rs @@ -68,6 +68,7 @@ impl SourceLayout { mod tests { use super::*; + /// Trace-directory recognition must not require files or create a query index. #[test] fn explicit_trace_directories_keep_their_layout_even_when_empty() { let tmp = tempfile::tempdir().unwrap(); diff --git a/baml_language/crates/btel_settings/src/local_store.rs b/baml_language/crates/btel_settings/src/local_store.rs index bce622971e1..b70b9532ab7 100644 --- a/baml_language/crates/btel_settings/src/local_store.rs +++ b/baml_language/crates/btel_settings/src/local_store.rs @@ -84,6 +84,7 @@ pub fn resolve_for_cwd( mod tests { use super::*; + /// Each higher-priority location must mask the lower-priority candidates. #[test] fn manifest_home_project_and_user_precedence() { let tmp = tempfile::tempdir().unwrap(); @@ -111,6 +112,7 @@ mod tests { ); } + /// Invalid configuration must identify the manifest and never fall through to `BAML_HOME`. #[test] fn invalid_settings_are_actionable_and_do_not_silently_select_another_store() { let tmp = tempfile::tempdir().unwrap(); From f129766d15c84ebad29fa821a39f09259a689256 Mon Sep 17 00:00:00 2001 From: Sam Lijin Date: Thu, 5 Nov 2026 11:42:29 -0800 Subject: [PATCH 4/6] fix: preserve project selection when trace-like folders coexist --- baml_language/ENV_VARS.md | 2 +- .../crates/baml_cli/src/query_command.rs | 16 +++++++++++----- .../crates/baml_cli/tests/run_context_e2e.rs | 4 ++++ .../baml_cli/tests/shared_trace_store_e2e.rs | 7 ++++++- baml_language/crates/btel_reader/src/layout.rs | 2 ++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/baml_language/ENV_VARS.md b/baml_language/ENV_VARS.md index c4eda27b5f9..dfc5065599a 100644 --- a/baml_language/ENV_VARS.md +++ b/baml_language/ENV_VARS.md @@ -118,7 +118,7 @@ baml run main baml query --local 'SELECT COUNT(*) FROM processes' ``` -`baml query --from` also accepts an existing state directory, a directory containing `recordings/` and `cas/`, or the `recordings/` directory itself. A recognized traces directory selects local querying directly, even with cloud credentials set. It takes precedence over enclosing project configuration and `BAML_HOME`; it cannot be combined with cloud `--project` or `--environment` selectors. These commands all read the same store: +`baml query --from` also accepts an existing state directory, a directory containing `recordings/` and `cas/`, or the `recordings/` directory itself. A recognized traces directory selects local querying directly, even with cloud credentials set. It takes precedence over enclosing project configuration and `BAML_HOME`; it cannot be combined with cloud `--project` or `--environment` selectors. An explicit project root uses its project settings even if it contains unrelated `btel/` or `recordings/` directories. These commands all read the same store: ```sh baml query --from /path/to/demo-computer-use/.baml 'SELECT COUNT(*) FROM processes' diff --git a/baml_language/crates/baml_cli/src/query_command.rs b/baml_language/crates/baml_cli/src/query_command.rs index 64e9499159c..309e12f575f 100644 --- a/baml_language/crates/baml_cli/src/query_command.rs +++ b/baml_language/crates/baml_cli/src/query_command.rs @@ -183,24 +183,30 @@ impl QueryArgs { Some(sql) => sql.to_owned(), None => anyhow::bail!("provide a SELECT statement, or --schema to list relations"), }; - // Explicit trace folders must win over ancestor project discovery and - // saved cloud credentials. Canonicalization also rejects missing paths. - let trace_layout = self + // Trace folders beat enclosing projects and saved cloud credentials. + // An explicitly selected project root still uses its own configuration, + // even if it contains unrelated directories named btel or recordings. + let from = self .from .as_deref() .map(|from| { std::fs::canonicalize(from) .map_err(|error| anyhow::anyhow!("--from {}: {error}", from.display())) }) - .transpose()? + .transpose()?; + let project_root = find_project_root_from(from.as_deref())?; + let trace_layout = from .as_deref() + .filter(|from| { + !from.join(baml_db::BAML_TOML).is_file() + && !from.join(baml_db::BAML_SRC_DIR).is_dir() + }) .and_then(SourceLayout::from_trace_directory); let local = self.local || trace_layout.is_some(); anyhow::ensure!( trace_layout.is_none() || (self.project.is_none() && self.environment.is_none()), "--from selects a local traces directory; use a project directory for a cloud query" ); - let project_root = find_project_root_from(self.from.as_deref())?; let root = match project_root.as_ref() { Some(root) => root.clone(), None => match &self.from { diff --git a/baml_language/crates/baml_cli/tests/run_context_e2e.rs b/baml_language/crates/baml_cli/tests/run_context_e2e.rs index 07244d8b5fc..ee0ca6864e4 100644 --- a/baml_language/crates/baml_cli/tests/run_context_e2e.rs +++ b/baml_language/crates/baml_cli/tests/run_context_e2e.rs @@ -11,6 +11,7 @@ use std::{ const CONTEXT: &str = r#"{"distinct_id":"pipe-user","metadata":{"phase":"launch"}}"#; +/// Keep context-query evidence isolated while sharing only the build-cache home. fn project() -> tempfile::TempDir { let project = tempfile::tempdir().unwrap(); common::write_project( @@ -21,6 +22,8 @@ fn project() -> tempfile::TempDir { project.path().join("baml.toml"), r#"[package] name = "run_context" +[tracing] +store = ".baml" [scripts] json = ["--function", "Check", "--", "--json-args", "-"] dash = ["--function", "Check", "--", "--value", "-"] @@ -51,6 +54,7 @@ fn piped_cli_with_env_context( .env("DEV_BAML_CLI_DISABLE_AGENT_DETECTION", "1") .env("BAML_CLI_ALLOW_DIRECT", "1") .env("BAML_TELEMETRY", "high") + .env("BOUNDARY_API_KEY", "local") .env("BAML_HOME", common::shared_baml_home()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs index 467eb5d4c1d..7ffad5d1863 100644 --- a/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs +++ b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs @@ -189,7 +189,12 @@ async fn bridge_cli_and_query_share_the_resolved_store() { "{}", String::from_utf8_lossy(&run.stderr) ); - assert_eq!(query(&cwd, Some(&project))["rows"], json!([[2]])); + // Project roots retain their configured store even when unrelated child + // directories look like traces. Actual trace folders below it still work. + for name in ["recordings", "btel"] { + std::fs::create_dir_all(project.join(name)).unwrap(); + assert_eq!(query(&cwd, Some(&project))["rows"], json!([[2]])); + } if store.file_name().is_some_and(|name| name == "state") { // Without a project marker, query still follows the same BAML_HOME. assert_eq!(query(store.parent().unwrap(), None)["rows"], json!([[2]])); diff --git a/baml_language/crates/btel_reader/src/layout.rs b/baml_language/crates/btel_reader/src/layout.rs index 7d70c5d78e2..46ac1c35eac 100644 --- a/baml_language/crates/btel_reader/src/layout.rs +++ b/baml_language/crates/btel_reader/src/layout.rs @@ -29,6 +29,8 @@ impl SourceLayout { /// Recognize an explicitly selected trace directory before project walk-up. /// Accept a state directory (`.baml` or `BAML_HOME`), its `btel` child, /// or the `recordings` directory. Never scan recording contents or the CAS. + /// The caller must prefer an explicitly selected project root over these + /// shapes: a project may own unrelated `btel` or `recordings` directories. pub fn from_trace_directory(path: &Path) -> Option { if !path.is_dir() { return None; From 862a92bce1b70a48a682448ba96dbcc81cf9110c Mon Sep 17 00:00:00 2001 From: Sam Lijin Date: Thu, 5 Nov 2026 17:22:37 -0800 Subject: [PATCH 5/6] fix: keep project traces under BAML_HOME without manifest configuration --- baml_language/ENV_VARS.md | 44 ++-- .../crates/baml_cli/src/query_command.rs | 2 +- .../baml_cli/tests/boundary_telemetry_e2e.rs | 8 +- .../crates/baml_cli/tests/run_context_e2e.rs | 2 - .../baml_cli/tests/shared_trace_store_e2e.rs | 103 ++++----- .../baml_cli/tests/telemetry_recording_e2e.rs | 35 +-- baml_language/crates/baml_db/src/manifest.rs | 15 +- .../crates/baml_lsp_server/src/engine.rs | 4 +- .../baml_lsp_server/src/playground_btel.rs | 15 +- .../src/playground_btel/tests.rs | 15 +- .../baml_lsp_server/src/playground_seam.rs | 2 +- .../crates/bex_engine/src/telemetry.rs | 4 +- .../src/telemetry/user_files_tests.rs | 68 +++--- .../crates/btel_settings/src/local_store.rs | 210 +++++++++++------- 14 files changed, 281 insertions(+), 246 deletions(-) diff --git a/baml_language/ENV_VARS.md b/baml_language/ENV_VARS.md index dfc5065599a..6b1fb146e09 100644 --- a/baml_language/ENV_VARS.md +++ b/baml_language/ENV_VARS.md @@ -34,7 +34,7 @@ Windows keeps an empty `login/cache/.lock` beside each credential | Variable | What it does | |---|---| -| `BAML_HOME` | Move user state (CI workspace, container, read-only home). Also selects the local trace store unless the project sets `[tracing].store`; see below. Empty means unset. | +| `BAML_HOME` | Move user state (CI workspace, container, read-only home), including per-project local trace stores; see below. Empty means unset. | | `BAML_TOOLCHAIN` | Which toolchain `baml` runs: a channel, a version, or a path to a local `baml-cli`. Default `canary`. | | `BAML_MANIFEST_BASE_URL` | Release manifest mirror, used by the wrapper and the bridges. | | `BAML_BUILD_CACHE` | `false` turns off the build cache. | @@ -83,29 +83,13 @@ Not listed: test, benchmark and CI-only variables, Ruby, and the old `engine/`. ## Shared local trace store -Set one local store for SDK bridge recordings, `baml run` / `baml test`, and `baml query --local` in the project's `baml.toml`: +SDK bridge recordings, playground runs, `baml run` / `baml test`, and `baml query --local` share a fixed per-project store at `/`. `BAML_HOME` is the BAML state directory itself, defaulting to `~/.baml`; no extra `.baml` is appended. No trace-store setting is needed or supported in `baml.toml`, and recording does not create `/.baml`. -```toml -[package] -name = "computer-use" +`` is the project directory's name (sanitized and limited to 64 characters), followed by a hyphen and the first 16 hexadecimal digits of the SHA-256 of its canonical filesystem path. Different checkouts with the same name have separate stores; symlinked paths to the same checkout share a store. Moving a project changes its store identity. Each store contains `btel/recordings/`, shared `btel/cas/` blobs, and the disposable `btel/query.sqlite` index. Store resolution reads no manifest settings and creates no directories until recording or indexing needs them. If neither a nonempty `BAML_HOME` nor an OS home directory is available, the base falls back to `.baml` relative to the working directory. -[tracing] -store = ".baml" -``` - -`store` names a state directory containing `btel/recordings/`, shared `btel/cas/` blobs, and the disposable `btel/query.sqlite` index. Relative paths are resolved against the directory containing `baml.toml`, including when you launch from a nested directory. Absolute paths can share a store across projects. Paths are literal: `~` and environment variables are not expanded inside TOML strings. Add the store directory to `.gitignore` if it is inside your repository. - -For local recording and querying, the precedence is: - -1. An explicit traces directory passed to `baml query --from` (query only). -2. The resolved project's `[tracing].store`. -3. Nonempty `BAML_HOME`. -4. `/.baml` when a project is available. -5. `~/.baml` without a project (`.baml` relative to the working directory if the OS cannot provide a home directory). - -The CLI uses its resolved project root, including an explicit project passed to `--from`. The SDK bridge discovers the project enclosing the host process's working directory using the same `baml.toml` / `baml_src/` markers as the CLI. A nonempty `BAML_HOME` can be relative to the process working directory; use an absolute path when launching processes from different directories. `[tracing].store` takes precedence over `BAML_HOME` for traces while authentication, build caches, and toolchains continue using `BAML_HOME`. An invalid store setting produces a configuration error; an unwritable store disables recording with a diagnostic rather than silently writing into another store. +The CLI uses its resolved project root, including an explicit project passed to `--from`. The SDK bridge discovers the project enclosing the host process's working directory using the same `baml.toml` / `baml_src/` markers as the CLI. Use an absolute `BAML_HOME` when launching processes from different directories; a relative value is resolved against each process's working directory. An unwritable store disables recording with a diagnostic rather than silently writing into another store. -The store setting chooses a filesystem location, independently of cloud/local destination selection and recording level. Use `BOUNDARY_API_KEY=local` to select local recording and query even with a saved Boundary login; `baml query --local` selects local reads without changing how your application records. The store setting does not enable recording that the runtime telemetry policy has disabled. This setting is read at runtime from the local manifest; it is not embedded into generated SDK artifacts. A bridge launched outside the project cannot discover that manifest: set an absolute `BAML_HOME` in both processes in that case. Packed executables always use `BAML_HOME` (otherwise the user store), so launching one inside a project does not adopt that project's store. +Use `BOUNDARY_API_KEY=local` to select local recording and query even with a saved Boundary login; `baml query --local` selects local reads without changing how your application records. The fixed location does not enable recording that the runtime telemetry policy has disabled. SDK hosts launched outside a project and packed executables use `/btel` directly. Packed executables do not adopt a project's store when launched inside it. For example, launch your computer-use demo from its project and query its recordings with the same environment. CLI entrypoints use this store too (`baml run main` below assumes the project defines `main`): @@ -118,20 +102,22 @@ baml run main baml query --local 'SELECT COUNT(*) FROM processes' ``` -`baml query --from` also accepts an existing state directory, a directory containing `recordings/` and `cas/`, or the `recordings/` directory itself. A recognized traces directory selects local querying directly, even with cloud credentials set. It takes precedence over enclosing project configuration and `BAML_HOME`; it cannot be combined with cloud `--project` or `--environment` selectors. An explicit project root uses its project settings even if it contains unrelated `btel/` or `recordings/` directories. These commands all read the same store: +`baml query --from` also accepts an existing state directory, a directory containing `recordings/` and `cas/`, or the `recordings/` directory itself. A recognized traces directory selects local querying directly, even with cloud credentials set. It takes precedence over project discovery and `BAML_HOME`; it cannot be combined with cloud `--project` or `--environment` selectors. An explicit project root uses its fixed project store even if it contains unrelated `btel/` or `recordings/` directories. These commands all read the same store: ```sh -baml query --from /path/to/demo-computer-use/.baml 'SELECT COUNT(*) FROM processes' -baml query --from /path/to/demo-computer-use/.baml/btel 'SELECT COUNT(*) FROM processes' -baml query --from /path/to/demo-computer-use/.baml/btel/recordings 'SELECT COUNT(*) FROM processes' -# Query the global user store from anywhere, including inside a project: -baml query --from "$HOME/.baml" 'SELECT COUNT(*) FROM processes' +# Replace the example suffix with the hash of your project's canonical path. +store="${BAML_HOME:-$HOME/.baml}/demo-computer-use-" +baml query --from "$store" 'SELECT COUNT(*) FROM processes' +baml query --from "$store/btel" 'SELECT COUNT(*) FROM processes' +baml query --from "$store/btel/recordings" 'SELECT COUNT(*) FROM processes' +# Query recordings from packed programs or SDK hosts without a project: +baml query --from "${BAML_HOME:-$HOME/.baml}" 'SELECT COUNT(*) FROM processes' ``` -Compatibility: without `[tracing].store` or `BAML_HOME`, existing project stores keep their location. With `BAML_HOME`, CLI local writes and reads now follow the same user store as the bridge. Existing recordings are not moved or combined automatically. To inspect an older project store while `BAML_HOME` is set, pass its `.baml` directory explicitly, or add `store = ".baml"` to keep that project local. +Compatibility: existing recordings are not moved or combined automatically. Older `/.baml` stores and formerly configured stores remain readable by passing their state directory explicitly to `baml query --from`. `[tracing].store` no longer selects a destination; remove the obsolete table from `baml.toml`. ### Querying large stores The first query builds an index of the selected store; later refreshes reconcile completed files using metadata and decode new or changed files. Refresh still lists every recording and stats completed files, so a global store accumulating many projects has more discovery and reconciliation work than a project store. SQL `LIMIT` and `--max-wall` constrain query execution, not this preceding refresh. `--format json` exposes `outcome.refresh` counters/timings and `outcome.query` timings to distinguish those costs. -Use a per-project store to limit how much unrelated history each refresh visits. After building the index, `baml query --from /path/to/store --no-refresh 'SELECT COUNT(*) FROM processes'` skips file reconciliation and queries the existing index; it does not include recordings added since the last refresh. Queries can still scan indexed rows and report extents for all recordings, and captured-value expressions can read CAS blobs. This path/configuration change does not claim a speedup for SQL over the same global dataset; global-store query performance is being investigated separately. +The default per-project stores limit how much unrelated history each refresh visits. After building the index, `baml query --from /path/to/store --no-refresh 'SELECT COUNT(*) FROM processes'` skips file reconciliation and queries the existing index; it does not include recordings added since the last refresh. Queries can still scan indexed rows and report extents for all recordings, and captured-value expressions can read CAS blobs. This path/configuration change does not claim a speedup for SQL over the same global dataset; global-store query performance is being investigated separately. diff --git a/baml_language/crates/baml_cli/src/query_command.rs b/baml_language/crates/baml_cli/src/query_command.rs index 309e12f575f..841aa109440 100644 --- a/baml_language/crates/baml_cli/src/query_command.rs +++ b/baml_language/crates/baml_cli/src/query_command.rs @@ -58,7 +58,7 @@ pub struct QueryArgs { #[arg(long, conflicts_with = "local")] pub environment: Option, - /// Project or traces directory: a state directory (.baml or BAML_HOME), + /// Project or traces directory: a project store under `BAML_HOME`, a legacy `.baml`, /// btel directory, or recordings directory. A traces directory selects local /// querying directly. Defaults to the shared local store for this project. #[arg(long, value_name = "PATH")] diff --git a/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs b/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs index 8df22408b2c..b0d06cbc141 100644 --- a/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs +++ b/baml_language/crates/baml_cli/tests/boundary_telemetry_e2e.rs @@ -381,7 +381,13 @@ async fn local_sentinel_records_and_queries_locally_without_boundary_requests() let output = execute_case(temp.path(), &server.uri(), "local", &["run", "main"]); assert!(output.status.success(), "{output:?}"); // Local writers and readers now honor this fixture's BAML_HOME. - assert!(temp.path().join("home/btel/recordings").exists()); + let store = btel_settings::local_store::resolve( + Some(temp.path()), + Some(temp.path().join("home").into()), + None, + ) + .unwrap(); + assert!(store.join("btel/recordings").exists()); assert!(!temp.path().join(RECORDINGS).exists()); let output = execute_case( temp.path(), diff --git a/baml_language/crates/baml_cli/tests/run_context_e2e.rs b/baml_language/crates/baml_cli/tests/run_context_e2e.rs index ee0ca6864e4..6e77e671e3b 100644 --- a/baml_language/crates/baml_cli/tests/run_context_e2e.rs +++ b/baml_language/crates/baml_cli/tests/run_context_e2e.rs @@ -22,8 +22,6 @@ fn project() -> tempfile::TempDir { project.path().join("baml.toml"), r#"[package] name = "run_context" -[tracing] -store = ".baml" [scripts] json = ["--function", "Check", "--", "--json-args", "-"] dash = ["--function", "Check", "--", "--value", "-"] diff --git a/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs index 7ffad5d1863..f4d3f41890f 100644 --- a/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs +++ b/baml_language/crates/baml_cli/tests/shared_trace_store_e2e.rs @@ -1,5 +1,5 @@ //! The real bridge writes, the CLI writes, and fresh CLI processes read one store. -use std::{fmt::Write as _, path::Path, process::Command}; +use std::{path::Path, process::Command}; use bridge_cffi::baml_bridge::cffi::{ BamlOutboundResult, CallFunctionArgs, InvocationOptions, baml_outbound_result, @@ -47,7 +47,7 @@ async fn bridge_cli_and_query_share_the_resolved_store() { const STORE: &str = "BAML_TEST_SHARED_STORE"; let Some(store) = std::env::var_os(STORE) else { // Isolate the bridge singleton, cwd, auth, and configuration per scenario. - for scenario in ["project", "manifest", "absolute", "home"] { + for scenario in ["baml_src", "manifest", "home", "generated"] { let tmp = tempfile::tempdir().unwrap(); let project = tmp.path().join("app"); let nested = project.join("baml_src/nested"); @@ -57,28 +57,23 @@ async fn bridge_cli_and_query_share_the_resolved_store() { "function main() -> int { 7 }", ) .unwrap(); - let state = match scenario { - "manifest" => project.join("traces"), - "absolute" => tmp.path().join("external-traces"), - "home" => tmp.path().join("state"), - _ => project.join(".baml"), - }; - let mut manifest = "[package]\nname = 'shared-store'\n".to_owned(); - if scenario == "manifest" { - manifest.push_str("[tracing]\nstore = 'traces'\n"); - } else if scenario == "absolute" { - write!( - manifest, - "[tracing]\nstore = {:?}\n", - state.to_str().unwrap() + // Source-only and manifest projects use the same fixed layout. + if scenario != "baml_src" { + std::fs::write( + project.join("baml.toml"), + "[package]\nname = 'shared-store'\n", ) .unwrap(); } - // The default case also verifies discovery using only baml_src/. - if scenario != "project" { - std::fs::write(project.join("baml.toml"), manifest).unwrap(); - } let home = tmp.path().join("user"); + let baml_home = + matches!(scenario, "home" | "generated").then(|| tmp.path().join("state")); + let state = btel_settings::local_store::resolve( + Some(&project), + baml_home.clone().map(Into::into), + Some(home.clone()), + ) + .unwrap(); std::fs::create_dir(&home).unwrap(); let mut child = Command::new(std::env::current_exe().unwrap()); child @@ -92,7 +87,7 @@ async fn bridge_cli_and_query_share_the_resolved_store() { .env("BAML_TEST_SHARED_PROJECT", &project) .env( "BAML_TEST_SHARED_ARTIFACT", - if scenario == "absolute" { + if scenario == "generated" { "generated" } else { "legacy" @@ -106,8 +101,8 @@ async fn bridge_cli_and_query_share_the_resolved_store() { .env_remove("BOUNDARY_PROJECT") .env_remove("BOUNDARY_API_URL") .env_remove("BAML_HOME"); - if scenario != "project" { - child.env("BAML_HOME", tmp.path().join("state")); + if let Some(baml_home) = baml_home { + child.env("BAML_HOME", baml_home); } let output = child.output().unwrap(); assert!( @@ -116,12 +111,8 @@ async fn bridge_cli_and_query_share_the_resolved_store() { String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - if scenario == "manifest" || scenario == "absolute" { - assert!( - !tmp.path().join("state/btel").exists(), - "manifest wins over BAML_HOME" - ); - } + assert!(!project.join(".baml").exists()); + assert!(!tmp.path().join("state/btel").exists()); } return; }; @@ -189,16 +180,18 @@ async fn bridge_cli_and_query_share_the_resolved_store() { "{}", String::from_utf8_lossy(&run.stderr) ); - // Project roots retain their configured store even when unrelated child + // Project roots retain their fixed store even when unrelated child // directories look like traces. Actual trace folders below it still work. for name in ["recordings", "btel"] { std::fs::create_dir_all(project.join(name)).unwrap(); assert_eq!(query(&cwd, Some(&project))["rows"], json!([[2]])); } - if store.file_name().is_some_and(|name| name == "state") { - // Without a project marker, query still follows the same BAML_HOME. - assert_eq!(query(store.parent().unwrap(), None)["rows"], json!([[2]])); - } + // A query outside a project cannot mix in a project's recordings. + assert_eq!(query(project.parent().unwrap(), None)["rows"], json!([[0]])); + // Another checkout with the same directory name has its own store. + let other = project.parent().unwrap().join("other/app"); + std::fs::create_dir_all(other.join("baml_src")).unwrap(); + assert_eq!(query(&cwd, Some(&other))["rows"], json!([[0]])); // Trace directories override both enclosing projects and cloud credentials. // Point BAML_HOME elsewhere too: explicit --from must be authoritative. @@ -233,29 +226,11 @@ async fn bridge_cli_and_query_share_the_resolved_store() { } } -/// A malformed manifest must fail visibly without selecting a fallback store. -#[test] -fn bad_store_configuration_fails_without_creating_a_different_store() { - let tmp = tempfile::tempdir().unwrap(); - std::fs::write(tmp.path().join("baml.toml"), "[tracing]\nstore = 42").unwrap(); - let output = cli( - tmp.path(), - &["query", "--local", "SELECT COUNT(*) FROM processes"], - ); - assert!(!output.status.success()); - let error = String::from_utf8_lossy(&output.stderr); - assert!( - error.contains("baml.toml") && error.contains("tracing"), - "{error}" - ); - assert!(!tmp.path().join(".baml").exists()); -} - -/// An explicit empty store bypasses enclosing configuration and rejects cloud selectors. +/// An explicit empty store bypasses project discovery and rejects cloud selectors. #[test] -fn empty_trace_folder_bypasses_enclosing_project_configuration() { +fn empty_trace_folder_bypasses_enclosing_project_store() { let tmp = tempfile::tempdir().unwrap(); - std::fs::write(tmp.path().join("baml.toml"), "[tracing]\nstore = 42").unwrap(); + std::fs::write(tmp.path().join("baml.toml"), "[package]\nname = 'app'\n").unwrap(); let traces = tmp.path().join(".baml"); std::fs::create_dir(&traces).unwrap(); let result = query(tmp.path(), Some(&traces)); @@ -277,9 +252,9 @@ fn empty_trace_folder_bypasses_enclosing_project_configuration() { assert!(String::from_utf8_lossy(&output.stderr).contains("local traces directory")); } -/// Disabled source execution must not inspect local-store settings or create traces. +/// Disabled source execution must not create a project trace store. #[test] -fn telemetry_off_does_not_read_local_store_configuration() { +fn telemetry_off_does_not_create_a_project_trace_store() { let tmp = tempfile::tempdir().unwrap(); std::fs::create_dir(tmp.path().join("baml_src")).unwrap(); std::fs::write( @@ -287,11 +262,7 @@ fn telemetry_off_does_not_read_local_store_configuration() { "function main() -> int { 7 }", ) .unwrap(); - std::fs::write( - tmp.path().join("baml.toml"), - "[package]\nname = 'app'\n[tracing]\nstore = 42", - ) - .unwrap(); + std::fs::write(tmp.path().join("baml.toml"), "[package]\nname = 'app'\n").unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_baml-cli")) .current_dir(tmp.path()) .args(["run", "main"]) @@ -308,7 +279,13 @@ fn telemetry_off_does_not_read_local_store_configuration() { String::from_utf8_lossy(&output.stderr) ); assert!(!tmp.path().join(".baml").exists()); - assert!(!tmp.path().join("home/btel").exists()); + let store = btel_settings::local_store::resolve( + Some(tmp.path()), + Some(tmp.path().join("home").into()), + None, + ) + .unwrap(); + assert!(!store.exists()); } /// Missing source paths must identify both the flag and the supplied directory. diff --git a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs index 0956d69c5e4..5bfaccfaa75 100644 --- a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs +++ b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs @@ -36,8 +36,8 @@ fn run(command: &mut Command, success: bool) { ); } -fn assert_recordings(root: &Path, count: usize) { - let directories: Vec<_> = std::fs::read_dir(root.join(RECORDINGS)) +fn assert_recordings_in(recordings: &Path, count: usize) { + let directories: Vec<_> = std::fs::read_dir(recordings) .unwrap() .map(|entry| entry.unwrap().path()) .collect(); @@ -54,6 +54,10 @@ fn assert_recordings(root: &Path, count: usize) { } } +fn assert_recordings(root: &Path, count: usize) { + assert_recordings_in(&root.join(RECORDINGS), count); +} + fn home(root: &Path) -> std::path::PathBuf { let home = root.join("home"); std::fs::create_dir_all(home.join(".baml")).unwrap(); @@ -65,7 +69,7 @@ fn home(root: &Path) -> std::path::PathBuf { home } -/// Explicit project storage must survive nested launches and bytecode-cache hits. +/// A project store beneath `BAML_HOME` must survive nested launches and bytecode-cache hits. #[test] fn cli_uses_project_root_for_cold_cached_nested_expression_and_test_execution() { let temp = tempfile::tempdir().unwrap(); @@ -79,14 +83,10 @@ function fail() -> int { assert.is_true(false); 0 } testset "smoke" { test "ok" { assert.is_true(true) } } "#, ); - // Explicit project tracing keeps the CLI in its project while BAML_HOME - // isolates toolchain/auth state and packed-program recordings. - std::fs::write( - project.join("baml.toml"), - "[package]\nname = 'test'\n[tracing]\nstore = '.baml'\n", - ) - .unwrap(); let home = home(temp.path()); + let store = + btel_settings::local_store::resolve(Some(&project), Some(home.join(".baml").into()), None) + .unwrap(); let nested = project.join("baml_src/nested"); std::fs::create_dir(&nested).unwrap(); // --from must win over both cwd and the baml_src subdirectory. Repeat to @@ -98,31 +98,31 @@ testset "smoke" { test "ok" { assert.is_true(true) } } .arg(&project), true, ); - assert_recordings(&project, count); + assert_recordings_in(&store.join("btel/recordings"), count); } run(cli(&nested, &home).args(["run", "main"]), true); - assert_recordings(&project, 3); + assert_recordings_in(&store.join("btel/recordings"), 3); run( cli(temp.path(), &home) .args(["run", "-e", "1 + 2", "--from"]) .arg(&project), true, ); - assert_recordings(&project, 4); + assert_recordings_in(&store.join("btel/recordings"), 4); run( cli(temp.path(), &home) .args(["test", "--from"]) .arg(&project), true, ); - assert_recordings(&project, 5); + assert_recordings_in(&store.join("btel/recordings"), 5); run( cli(temp.path(), &home) .args(["run", "fail", "--from"]) .arg(&project), false, ); - assert_recordings(&project, 6); + assert_recordings_in(&store.join("btel/recordings"), 6); run( cli(temp.path(), &home) .args(["run", "main", "--from"]) @@ -130,9 +130,10 @@ testset "smoke" { test "ok" { assert.is_true(true) } } .env("BAML_TELEMETRY", "off"), true, ); - assert_recordings(&project, 6); + assert_recordings_in(&store.join("btel/recordings"), 6); assert!(!temp.path().join(RECORDINGS).exists()); assert!(!nested.join(RECORDINGS).exists()); + assert!(!project.join(".baml").exists()); assert!(!home.join(RECORDINGS).exists()); } @@ -322,7 +323,7 @@ fn packed_http_bodies_false_keeps_bodies_out_of_network_spans() { let query = |sql: &str| { let output = cli(temp.path(), &home) .args(["query", "--local", "--format", "json", "--from"]) - .arg(&home) + .arg(home.join(".baml")) .arg(sql) .output() .unwrap(); diff --git a/baml_language/crates/baml_db/src/manifest.rs b/baml_language/crates/baml_db/src/manifest.rs index 78d2b7b1dc5..b284ceb279d 100644 --- a/baml_language/crates/baml_db/src/manifest.rs +++ b/baml_language/crates/baml_db/src/manifest.rs @@ -83,10 +83,9 @@ pub struct TestProfileManifest { /// Top-level keys owned outside the compiler's manifest model, preserved in /// [`BamlToml::unknown`]. /// The wrapper reads `toolchain` to choose the executable; the CLI's cloud -/// configuration reads `boundary`; runtime trace-store settings read `tracing`. +/// configuration reads `boundary`; artifact telemetry reads `pack` and `bridge`. /// Matching is by top-level key name. -const KNOWN_UNHANDLED_TOP_LEVEL_KEYS: &[&str] = - &["toolchain", "boundary", "pack", "bridge", "tracing"]; +const KNOWN_UNHANDLED_TOP_LEVEL_KEYS: &[&str] = &["toolchain", "boundary", "pack", "bridge"]; #[derive(Debug, Deserialize)] pub struct Package { @@ -381,11 +380,15 @@ mod tests { assert!(warns.iter().any(|w| w.contains("nmae")), "got: {warns:?}"); } - /// Runtime-owned tracing configuration must not produce compiler typo warnings. + /// Removed trace-store configuration is no longer a recognized manifest key. #[test] - fn tracing_settings_are_owned_by_the_runtime() { + fn tracing_settings_are_unknown() { let manifest = parse("[package]\nname = 'app'\n[tracing]\nstore = '.baml'\n").unwrap(); - assert!(unknown_field_warnings(&manifest).is_empty()); + assert!( + unknown_field_warnings(&manifest) + .iter() + .any(|warning| warning.contains("tracing")) + ); } #[test] diff --git a/baml_language/crates/baml_lsp_server/src/engine.rs b/baml_language/crates/baml_lsp_server/src/engine.rs index 52a77a76a70..a437ea95954 100644 --- a/baml_language/crates/baml_lsp_server/src/engine.rs +++ b/baml_language/crates/baml_lsp_server/src/engine.rs @@ -533,7 +533,7 @@ impl RuntimeRegistry { } } -/// `recording_root` is the project root whose `.baml/btel` receives this +/// `recording_root` identifies the project store beneath `BAML_HOME` for this /// engine's telemetry recording (`BAML_TELEMETRY=off` disables it); `None` /// records nothing. pub fn construct_engine_candidate( @@ -549,7 +549,7 @@ pub fn construct_engine_candidate( Vec::new(), Some(bex_project::runtime_compiler()), btel_settings::clock::DEFAULT_MODE, - bex_engine::TelemetryRecording::local_files( + bex_engine::TelemetryRecording::project_files( root, btel_settings::publisher::RecordingConfig::default(), ) diff --git a/baml_language/crates/baml_lsp_server/src/playground_btel.rs b/baml_language/crates/baml_lsp_server/src/playground_btel.rs index a74ef5e0913..091371537fe 100644 --- a/baml_language/crates/baml_lsp_server/src/playground_btel.rs +++ b/baml_language/crates/baml_lsp_server/src/playground_btel.rs @@ -20,7 +20,7 @@ use std::{ sync::{Arc, Mutex, TryLockError}, }; -use baml_query_btel::{Index, IndexOptions, QueryRequest, QueryResult, SqlParam}; +use baml_query_btel::{Index, IndexOptions, QueryRequest, QueryResult, SourceLayout, SqlParam}; use serde_json::{Value, json}; /// Rows the list shows; the newest root calls first. Runtime GC pauses are @@ -153,8 +153,17 @@ impl PlaygroundTelemetry { } // Opening can wait on another process's index lock, so other projects // must not wait on the table meanwhile. - let index = Index::for_project(project, IndexOptions::default()) - .map_err(|e| TelemetryError::failed(e.to_string()))?; + let store = btel_settings::local_store::resolve( + Some(project), + baml_env::os_var("BAML_HOME"), + std::env::home_dir(), + ) + .map_err(|e| TelemetryError::failed(e.to_string()))?; + let index = Index::open( + SourceLayout::for_btel(store.join("btel")), + IndexOptions::default(), + ) + .map_err(|e| TelemetryError::failed(e.to_string()))?; // Nothing recorded yet: do not cache an empty in-memory index, so the // first recording is found by the next request. if index.source_missing() { diff --git a/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs b/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs index a115251fc6c..a79e298e162 100644 --- a/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs +++ b/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs @@ -21,7 +21,7 @@ function main(name: string) -> string { } "#; -/// The playground's own engine construction, recording to the project. +/// The playground's own engine construction, recording to the project store. fn engine(project: &Path) -> Arc { engine_for(project, SOURCE) } @@ -262,7 +262,18 @@ function main(name: string) -> string { let execution = main["executionId"].as_str().unwrap().to_owned(); // The request is recorded, under the call that made it. - let mut index = Index::for_project(&project, IndexOptions::default()).unwrap(); + let store = btel_settings::local_store::resolve( + Some(&project), + baml_env::os_var("BAML_HOME"), + std::env::home_dir(), + ) + .unwrap(); + assert!(!project.join(".baml").exists()); + let mut index = Index::open( + SourceLayout::for_btel(store.join("btel")), + IndexOptions::default(), + ) + .unwrap(); let network = index .refresh_and_query(&QueryRequest { sql: "SELECT span_id, parent_span_id, is_complete FROM span_announcements diff --git a/baml_language/crates/baml_lsp_server/src/playground_seam.rs b/baml_language/crates/baml_lsp_server/src/playground_seam.rs index 6b596869940..5eb3752f5d8 100644 --- a/baml_language/crates/baml_lsp_server/src/playground_seam.rs +++ b/baml_language/crates/baml_lsp_server/src/playground_seam.rs @@ -728,7 +728,7 @@ impl PlaygroundSeam { // against this root: the process serves every project at once and // never changes directory. let sys_ops = Arc::new(self.platform.for_root(root)); - // Playground runs record to the project's `.baml/btel`, like `baml run`. + // Playground runs use the project store beneath BAML_HOME, like `baml run`. let recording_root = root.to_path_buf(); let candidate = tokio::task::spawn_blocking(move || { construct_engine_candidate(*program, sys_ops, revision, Some(&recording_root)) diff --git a/baml_language/crates/bex_engine/src/telemetry.rs b/baml_language/crates/bex_engine/src/telemetry.rs index eb735eef8ff..a896a1a8060 100644 --- a/baml_language/crates/bex_engine/src/telemetry.rs +++ b/baml_language/crates/bex_engine/src/telemetry.rs @@ -417,8 +417,8 @@ impl TelemetryRecording { } } - /// Use the shared project store setting, then `BAML_HOME`, then `.baml`. - /// Configuration is resolved only when enabled recording starts. + /// Use the project store beneath `BAML_HOME` (default `~/.baml`). + /// The project path is resolved only when enabled recording starts. pub fn project_files(project_root: impl Into, config: RecordingConfig) -> Self { let mut recording = Self::user_files(config); recording.destination = Destination::ProjectFiles { diff --git a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs index 12501e7ef37..97a370905a4 100644 --- a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs +++ b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs @@ -13,15 +13,12 @@ fn btel(root: &Path) -> (PathBuf, PathBuf) { (root.join("btel/recordings"), root.join("btel/cas")) } -fn project(root: &Path) -> (PathBuf, PathBuf) { - ( - root.join(".baml/btel/recordings"), - root.join(".baml/btel/cas"), - ) +fn project(root: &Path, baml_home: Option, home: Option) -> (PathBuf, PathBuf) { + btel(&btel_settings::local_store::resolve(Some(root), baml_home.map(Into::into), home).unwrap()) } #[test] -fn project_enclosing_cwd_precedes_home() { +fn project_enclosing_cwd_is_namespaced_under_home() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().join("app"); let nested = root.join("src/deep"); @@ -30,20 +27,20 @@ fn project_enclosing_cwd_precedes_home() { let home = dir.path().join("home"); assert_eq!( - user_file_roots(None, Some(&nested), Some(home)).unwrap(), - project(&root) + user_file_roots(None, Some(&nested), Some(home.clone())).unwrap(), + project(&root, None, Some(home)) ); } #[test] -fn baml_home_overrides_project() { +fn baml_home_contains_the_project_store() { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("baml.toml"), "").unwrap(); let baml_home = dir.path().join("state"); assert_eq!( user_file_roots(Some(baml_home.clone().into()), Some(dir.path()), None).unwrap(), - btel(&baml_home) + project(dir.path(), Some(baml_home), None) ); } @@ -54,8 +51,8 @@ fn empty_baml_home_is_unset() { let home = dir.path().join("home"); assert_eq!( - user_file_roots(Some("".into()), Some(dir.path()), Some(home)).unwrap(), - project(dir.path()) + user_file_roots(Some("".into()), Some(dir.path()), Some(home.clone())).unwrap(), + project(dir.path(), None, Some(home)) ); } @@ -77,22 +74,31 @@ fn no_project_uses_home() { /// A blocked destination disables delivery instead of selecting an unrelated home. #[test] fn an_unwritable_selected_store_does_not_redirect_recordings() { + const CHILD: &str = "BAML_TEST_BLOCKED_STORE"; + if let Some(root) = baml_env::os_var(CHILD) { + let root = PathBuf::from(root); + let (runtime, delivery) = + TelemetryRecording::project_files(&root, RecordingConfig::default()) + .start(None, Arc::default(), &Context::default()) + .unwrap(); + assert!(delivery.is_none(), "must not open a different store"); + assert!(runtime.finish().is_err()); + return; + } let dir = tempfile::tempdir().unwrap(); - std::fs::write( - dir.path().join("baml.toml"), - "[tracing]\nstore = 'blocked'\n", - ) - .unwrap(); - // A regular file blocks directory creation on every platform, including - // privileged test runners for which read-only permissions do not suffice. - std::fs::write(dir.path().join("blocked"), "not a directory").unwrap(); - let (runtime, delivery) = - TelemetryRecording::project_files(dir.path(), RecordingConfig::default()) - .start(None, Arc::default(), &Context::default()) - .unwrap(); - assert!(delivery.is_none(), "must not open a different store"); - assert!(runtime.finish().is_err()); + // A regular file blocks directory creation even for privileged test runners. + let blocked = dir.path().join("blocked"); + std::fs::write(&blocked, "not a directory").unwrap(); + let output = Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "telemetry::user_files_tests::an_unwritable_selected_store_does_not_redirect_recordings", "--nocapture"]) + .env(CHILD, dir.path()) + .env("BAML_HOME", blocked) + .env("HOME", dir.path().join("user")) + .env("USERPROFILE", dir.path().join("user")) + .output().unwrap(); + assert!(output.status.success(), "{output:?}"); assert!(!dir.path().join(".baml").exists()); + assert!(!dir.path().join("user").exists()); } /// Starts real recordings in child processes whose working directory is a @@ -152,7 +158,7 @@ fn recording_starts_in_cwd_project() { ); }; - let recordings = root.join(".baml/btel/recordings"); + let recordings = project(&root, None, Some(home.clone())).0; let fallback = home.join(".baml/btel/recordings"); std::fs::create_dir_all(&recordings).unwrap(); std::fs::create_dir_all(&fallback).unwrap(); @@ -160,10 +166,10 @@ fn recording_starts_in_cwd_project() { // Packed programs keep recording to the user store. run(&fallback, true); - // An explicit project store is shared even when BAML_HOME is set. + // Manifest settings cannot redirect recordings away from the fixed project store. std::fs::write(root.join("baml.toml"), "[tracing]\nstore = 'custom'\n").unwrap(); - let custom = root.join("custom/btel/recordings"); - std::fs::create_dir_all(&custom).unwrap(); - run(&custom, false); + run(&recordings, false); run(&fallback, true); + assert!(!root.join("custom").exists()); + assert!(!root.join(".baml").exists()); } diff --git a/baml_language/crates/btel_settings/src/local_store.rs b/baml_language/crates/btel_settings/src/local_store.rs index b70b9532ab7..aa51b6a253e 100644 --- a/baml_language/crates/btel_settings/src/local_store.rs +++ b/baml_language/crates/btel_settings/src/local_store.rs @@ -5,68 +5,52 @@ use std::{ path::{Path, PathBuf}, }; -use serde::Deserialize; - -#[derive(Deserialize)] -struct Manifest { - #[serde(default)] - tracing: Tracing, -} - -#[derive(Default, Deserialize)] -#[serde(deny_unknown_fields)] -struct Tracing { - store: Option, -} +use sha2::{Digest as _, Sha256}; /// Resolve the state directory containing `btel/recordings` and `btel/cas`. -/// A project's `[tracing].store` wins over nonempty `BAML_HOME`, then the -/// project's `.baml`, then the user home. Relative manifest paths belong to -/// the manifest's directory; relative environment paths belong to the cwd. -/// This only reads configuration; it never creates directories or falls back -/// to a different store when the selected destination is unwritable. +/// Project stores live at `/-`; without +/// `BAML_HOME`, the base is `~/.baml`. The canonical project path identifies +/// the store, so aliases share recordings and separate checkouts stay isolated. +/// Without a project, use the BAML home itself (including packed programs). +/// This never reads manifest settings, creates directories, or falls back to +/// a different store when the selected destination is unwritable. pub fn resolve( project: Option<&Path>, baml_home: Option, home_dir: Option, ) -> io::Result { - if let Some(project) = project { - let path = project.join("baml.toml"); - let text = match std::fs::read_to_string(&path) { - Ok(text) => Some(text), - Err(error) if error.kind() == io::ErrorKind::NotFound => None, - Err(error) => { - return Err(io::Error::new( - error.kind(), - format!("{}: {error}", path.display()), - )); - } - }; - if let Some(text) = text { - let manifest: Manifest = toml::from_str(&text).map_err(|error| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("{}: invalid [tracing] settings: {error}", path.display()), - ) - })?; - if let Some(store) = manifest.tracing.store { - if store.as_os_str().is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("{}: tracing.store must not be empty", path.display()), - )); - } - return Ok(project.join(store)); + let home = baml_env::baml_home_from(baml_home, home_dir); + let Some(project) = project else { + return Ok(home); + }; + let project = std::fs::canonicalize(project).map_err(|error| { + io::Error::new( + error.kind(), + format!( + "{}: cannot resolve trace-store project: {error}", + project.display() + ), + ) + })?; + let name: String = project + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("project") + .chars() + .take(64) + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch + } else { + '_' } - } - } - if let Some(home) = baml_home.filter(|value| !value.is_empty()) { - return Ok(PathBuf::from(home)); - } - Ok(project.map_or_else( - || baml_env::baml_home_from(None, home_dir), - |project| project.join(".baml"), - )) + }) + .collect(); + let hash = format!( + "{:x}", + Sha256::digest(project.as_os_str().as_encoded_bytes()) + ); + Ok(home.join(format!("{name}-{}", &hash[..16]))) } /// SDK hosts discover the project enclosing their working directory. Packed @@ -84,50 +68,104 @@ pub fn resolve_for_cwd( mod tests { use super::*; - /// Each higher-priority location must mask the lower-priority candidates. #[test] - fn manifest_home_project_and_user_precedence() { + fn project_stores_live_under_baml_home_and_are_discovered_from_nested_cwd() { let tmp = tempfile::tempdir().unwrap(); let project = tmp.path().join("app"); - let nested = project.join("src/deep"); + let nested = project.join("baml_src/deep"); std::fs::create_dir_all(&nested).unwrap(); let home = tmp.path().join("user"); let state = tmp.path().join("state"); - let resolve = - |baml_home| resolve_for_cwd(Some(&nested), baml_home, Some(home.clone())).unwrap(); - assert_eq!(resolve(None), home.join(".baml")); - std::fs::write(project.join("baml.toml"), "[package]\nname = 'app'\n").unwrap(); - assert_eq!(resolve(None), project.join(".baml")); - assert_eq!(resolve(Some("".into())), project.join(".baml")); - assert_eq!(resolve(Some(state.clone().into())), state); - std::fs::write(project.join("baml.toml"), "[tracing]\nstore = 'traces'\n").unwrap(); - assert_eq!(resolve(Some(state.clone().into())), project.join("traces")); - let config = format!("[tracing]\nstore = {:?}\n", state.to_str().unwrap()); - std::fs::write(project.join("baml.toml"), config).unwrap(); - assert_eq!(resolve(None), state); - // Packed programs ignore the manifest even when launched in a project. + let default = resolve(Some(&project), None, Some(home.clone())).unwrap(); + assert_eq!(default.parent().unwrap(), home.join(".baml")); + assert!( + default + .file_name() + .unwrap() + .to_str() + .unwrap() + .starts_with("app-") + ); + assert_eq!( + resolve(Some(&project), Some("".into()), Some(home.clone())).unwrap(), + default + ); + let configured = resolve(Some(&project), Some(state.clone().into()), None).unwrap(); + assert_eq!(configured, state.join(default.file_name().unwrap())); + assert_eq!( + resolve_for_cwd(Some(&nested), Some(state.into()), None).unwrap(), + configured + ); + // Trace paths do not depend on manifest contents, including old settings. + std::fs::write( + project.join("baml.toml"), + "[tracing]\nstore = 'elsewhere'\n", + ) + .unwrap(); + assert_eq!( + resolve(Some(&project), None, Some(home.clone())).unwrap(), + default + ); + assert_eq!( + resolve_for_cwd(Some(&nested), None, Some(home)).unwrap(), + default + ); + assert!(!default.exists()); + assert!(!project.join(".baml").exists()); + } + + #[test] + fn separate_checkouts_with_the_same_name_have_separate_stores() { + let tmp = tempfile::tempdir().unwrap(); + let first = tmp.path().join("first/app"); + let second = tmp.path().join("second/app"); + std::fs::create_dir_all(&first).unwrap(); + std::fs::create_dir_all(&second).unwrap(); + let home = Some(tmp.path().join("state").into()); + let store = resolve(Some(&first), home.clone(), None).unwrap(); + assert_ne!(store, resolve(Some(&second), home.clone(), None).unwrap()); assert_eq!( - super::resolve_for_cwd(None, None, Some(home.clone())).unwrap(), + store, + resolve(Some(&first.join("../app")), home, None).unwrap() + ); + } + + #[cfg(unix)] + #[test] + fn symlinked_project_uses_the_same_store() { + let tmp = tempfile::tempdir().unwrap(); + let project = tmp.path().join("app"); + let alias = tmp.path().join("alias"); + std::fs::create_dir(&project).unwrap(); + std::os::unix::fs::symlink(&project, &alias).unwrap(); + let home = Some(tmp.path().join("state").into()); + assert_eq!( + resolve(Some(&project), home.clone(), None).unwrap(), + resolve(Some(&alias), home, None).unwrap() + ); + } + + #[test] + fn no_project_uses_baml_home_itself() { + let home = PathBuf::from("user"); + assert_eq!( + resolve_for_cwd(None, None, Some(home.clone())).unwrap(), home.join(".baml") ); + assert_eq!( + resolve(None, Some("state".into()), Some(home)).unwrap(), + PathBuf::from("state") + ); + assert_eq!(resolve(None, None, None).unwrap(), PathBuf::from(".baml")); } - /// Invalid configuration must identify the manifest and never fall through to `BAML_HOME`. #[test] - fn invalid_settings_are_actionable_and_do_not_silently_select_another_store() { + fn missing_project_reports_the_path_instead_of_selecting_a_different_store() { let tmp = tempfile::tempdir().unwrap(); - for text in [ - "[tracing]\nstore = ''", - "[tracing]\nstore = 42", - "[tracing]\nstroe = 'traces'", - "tracing = 'traces'", - "not toml", - ] { - std::fs::write(tmp.path().join("baml.toml"), text).unwrap(); - let error = resolve(Some(tmp.path()), Some("elsewhere".into()), None).unwrap_err(); - assert!(error.to_string().contains("baml.toml"), "{error}"); - assert!(error.to_string().contains("tracing"), "{error}"); - } - assert!(!tmp.path().join(".baml").exists()); + let missing = tmp.path().join("missing"); + let error = + resolve(Some(&missing), Some(tmp.path().join("state").into()), None).unwrap_err(); + assert!(error.to_string().contains(missing.to_str().unwrap())); + assert!(!tmp.path().join("state").exists()); } } From 68097d2a41b87e4a203ccac9db7bed7d627f16ee Mon Sep 17 00:00:00 2001 From: Sam Lijin Date: Thu, 5 Nov 2026 17:35:02 -0800 Subject: [PATCH 6/6] test: align native recording audits with fixed trace stores --- .../baml_cli/tests/embedded_telemetry_e2e.rs | 3 ++- .../baml_cli/tests/telemetry_recording_e2e.rs | 2 ++ .../baml_lsp_server/src/playground_btel.rs | 1 + .../src/playground_btel/tests.rs | 1 + .../src/telemetry/user_files_tests.rs | 6 +++++ .../crates/btel_settings/src/local_store.rs | 5 ++++ .../customizable/test_stream_tracing.py | 27 +++++++++++++------ 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/baml_language/crates/baml_cli/tests/embedded_telemetry_e2e.rs b/baml_language/crates/baml_cli/tests/embedded_telemetry_e2e.rs index ca11ebddca2..9f10bba70f1 100644 --- a/baml_language/crates/baml_cli/tests/embedded_telemetry_e2e.rs +++ b/baml_language/crates/baml_cli/tests/embedded_telemetry_e2e.rs @@ -788,6 +788,7 @@ async fn wrapper_build_embeds_named_api_key_for_both_release_variants() { } } +/// Packed wrapper recordings stay in the user store and keep boundary spans value-free. #[test] fn wrapper_command_boundaries_record_spans_without_values() { let built = common::ensure_built(); @@ -843,7 +844,7 @@ fn wrapper_command_boundaries_record_spans_without_values() { let query = |sql: &str| { let output = Command::new(&built.baml_cli) .args(["query", "--local", "--format", "json", "--from"]) - .arg(&home) + .arg(home.join(".baml")) .arg(sql) .env("BAML_HOME", home.join(".baml")) .env("BAML_CLI_ALLOW_DIRECT", "1") diff --git a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs index 5bfaccfaa75..46778b5d23a 100644 --- a/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs +++ b/baml_language/crates/baml_cli/tests/telemetry_recording_e2e.rs @@ -36,6 +36,7 @@ fn run(command: &mut Command, success: bool) { ); } +/// Validate completed recording contents at an explicit recordings directory. fn assert_recordings_in(recordings: &Path, count: usize) { let directories: Vec<_> = std::fs::read_dir(recordings) .unwrap() @@ -54,6 +55,7 @@ fn assert_recordings_in(recordings: &Path, count: usize) { } } +/// Validate recordings in the conventional user-home layout used by packed programs. fn assert_recordings(root: &Path, count: usize) { assert_recordings_in(&root.join(RECORDINGS), count); } diff --git a/baml_language/crates/baml_lsp_server/src/playground_btel.rs b/baml_language/crates/baml_lsp_server/src/playground_btel.rs index 091371537fe..50e468da344 100644 --- a/baml_language/crates/baml_lsp_server/src/playground_btel.rs +++ b/baml_language/crates/baml_lsp_server/src/playground_btel.rs @@ -147,6 +147,7 @@ impl PlaygroundTelemetry { Ok(path) } + /// Reuse one query index per project at the shared recording destination. fn index(&self, project: &Path) -> Result>>, TelemetryError> { if let Some(index) = self.indexes.lock().expect("index table").get(project) { return Ok(Some(Arc::clone(index))); diff --git a/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs b/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs index a79e298e162..a009be3a774 100644 --- a/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs +++ b/baml_language/crates/baml_lsp_server/src/playground_btel/tests.rs @@ -67,6 +67,7 @@ async fn run( .await } +/// Live refresh finds new playground recordings and preserves their captured values. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn recorded_playground_runs_appear_on_refresh_with_captured_values() { let workspace = tempfile::tempdir().unwrap(); diff --git a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs index 97a370905a4..faa5a62e1a5 100644 --- a/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs +++ b/baml_language/crates/bex_engine/src/telemetry/user_files_tests.rs @@ -9,14 +9,17 @@ use btel_types::context::Context; use super::{TelemetryRecording, user_file_roots}; +/// Recording and CAS roots inside a selected state directory. fn btel(root: &Path) -> (PathBuf, PathBuf) { (root.join("btel/recordings"), root.join("btel/cas")) } +/// Resolve the expected project destination with an injectable home. fn project(root: &Path, baml_home: Option, home: Option) -> (PathBuf, PathBuf) { btel(&btel_settings::local_store::resolve(Some(root), baml_home.map(Into::into), home).unwrap()) } +/// SDK discovery uses the enclosing project identity beneath the user home. #[test] fn project_enclosing_cwd_is_namespaced_under_home() { let dir = tempfile::tempdir().unwrap(); @@ -32,6 +35,7 @@ fn project_enclosing_cwd_is_namespaced_under_home() { ); } +/// An explicit BAML home still isolates each project in its own child store. #[test] fn baml_home_contains_the_project_store() { let dir = tempfile::tempdir().unwrap(); @@ -44,6 +48,7 @@ fn baml_home_contains_the_project_store() { ); } +/// An empty environment override preserves the default user-home project store. #[test] fn empty_baml_home_is_unset() { let dir = tempfile::tempdir().unwrap(); @@ -56,6 +61,7 @@ fn empty_baml_home_is_unset() { ); } +/// Unassociated SDK hosts and packed programs keep the user store. #[test] fn no_project_uses_home() { let dir = tempfile::tempdir().unwrap(); diff --git a/baml_language/crates/btel_settings/src/local_store.rs b/baml_language/crates/btel_settings/src/local_store.rs index aa51b6a253e..67302a004c4 100644 --- a/baml_language/crates/btel_settings/src/local_store.rs +++ b/baml_language/crates/btel_settings/src/local_store.rs @@ -68,6 +68,7 @@ pub fn resolve_for_cwd( mod tests { use super::*; + /// Project identity and nested discovery select one store without manifest configuration or writes. #[test] fn project_stores_live_under_baml_home_and_are_discovered_from_nested_cwd() { let tmp = tempfile::tempdir().unwrap(); @@ -114,6 +115,7 @@ mod tests { assert!(!project.join(".baml").exists()); } + /// Canonical paths separate checkouts while equivalent path spellings share a store. #[test] fn separate_checkouts_with_the_same_name_have_separate_stores() { let tmp = tempfile::tempdir().unwrap(); @@ -131,6 +133,7 @@ mod tests { } #[cfg(unix)] + /// A symlink alias must not create another history for the same checkout. #[test] fn symlinked_project_uses_the_same_store() { let tmp = tempfile::tempdir().unwrap(); @@ -145,6 +148,7 @@ mod tests { ); } + /// Packed and unassociated hosts retain the user store, including existing home fallbacks. #[test] fn no_project_uses_baml_home_itself() { let home = PathBuf::from("user"); @@ -159,6 +163,7 @@ mod tests { assert_eq!(resolve(None, None, None).unwrap(), PathBuf::from(".baml")); } + /// Project-resolution errors identify the requested path and never create a fallback store. #[test] fn missing_project_reports_the_path_instead_of_selecting_a_different_store() { let tmp = tempfile::tempdir().unwrap(); diff --git a/baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_stream_tracing.py b/baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_stream_tracing.py index f77f21f550a..9cb796d003a 100644 --- a/baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_stream_tracing.py +++ b/baml_language/sdk_tests/crates/python_pydantic2/llm_functions/customizable/test_stream_tracing.py @@ -225,6 +225,7 @@ async def test_stream_precancelled_admission_preserves_reservation(): # The ordinary SDK CI does not build the query CLI. Run these audits wherever # baml-cli is built, or set BAML_AUDIT_CLI to a CLI from the same checkout. def _audit_cli(): + """Use a compatible query CLI, skipping inherited binaries from older checkouts.""" import os from pathlib import Path @@ -235,15 +236,18 @@ def _audit_cli(): cli = Path(os.environ.get("BAML_AUDIT_CLI", workspace / "target/debug/baml-cli")) if not cli.is_file(): pytest.skip("Build baml_cli to run the native stream recording audit") - # A CLI older than this checkout's blob format can't read what the SDK - # records here: one left over from another checkout skips, not fails. - blob_format = workspace / "crates/btel_settings/src/snapshot.rs" + # A CLI older than the blob format or store resolver cannot read what + # this SDK records. Warm CI runners can retain an older query binary. + reader_sources = [ + workspace / "crates/btel_settings/src/snapshot.rs", + workspace / "crates/btel_settings/src/local_store.rs", + ] if ( "BAML_AUDIT_CLI" not in os.environ - and cli.stat().st_mtime < blob_format.stat().st_mtime + and cli.stat().st_mtime < max(path.stat().st_mtime for path in reader_sources) ): pytest.skip( - "target/debug/baml-cli predates this checkout's blob format; " + "target/debug/baml-cli predates this checkout's trace format or layout; " "rebuild baml_cli or set BAML_AUDIT_CLI" ) return cli @@ -251,6 +255,7 @@ def _audit_cli(): # SDK_PARITY_LINT(skip): native Python recording lands where `baml query --local` reads def test_recording_lands_in_cwd_project(tmp_path): + """SDK and CLI discover the same project store beneath the user home.""" import json import os from pathlib import Path @@ -291,6 +296,9 @@ def test_recording_lands_in_cwd_project(tmp_path): ) assert child.returncode == 0, child.stderr assert not (home / ".baml/btel/recordings").exists() + assert not (project / ".baml").exists() + stores = list((home / ".baml").glob("app-*/btel/recordings")) + assert len(stores) == 1 and any(stores[0].iterdir()), child.stderr result = subprocess.run( [ @@ -316,6 +324,7 @@ def test_recording_lands_in_cwd_project(tmp_path): # SDK_PARITY_LINT(skip): native Python recording ancestry, duration, values and step-count audit def test_stream_recording_parent_chain(tmp_path): + """Recorded stream ancestry and values remain queryable without a project.""" import json import os from pathlib import Path @@ -328,9 +337,10 @@ def test_stream_recording_parent_chain(tmp_path): import baml_sdk marker = "stream-audit-" + uuid.uuid4().hex - # Local queries are project-relative: the child records beneath - # `/.baml`, its BAML_HOME, and the query reads from there. + # With no project enclosing cwd, the child records in BAML_HOME itself. + # Query that state directory explicitly rather than adopting a project. query_root = tmp_path / "config" + query_root.mkdir() env = { key: value for key, value in os.environ.items() @@ -349,6 +359,7 @@ def test_stream_recording_parent_chain(tmp_path): ) child = subprocess.run( [sys.executable, str(sdk_root / "stream_recording_cases.py"), marker], + cwd=query_root, env=env, capture_output=True, text=True, @@ -375,7 +386,7 @@ def test_stream_recording_parent_chain(tmp_path): "query", "--local", "--from", - str(query_root), + str(query_root / ".baml"), "--format", "json", sql,