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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/smoke-codex.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ jobs:
BRAINTRUST_API_KEY: smoke-key
BRAINTRUST_API_URL: http://127.0.0.1:53999
BRAINTRUST_APP_URL: http://127.0.0.1:53999
BT_DAEMON_CONFIG: ${{ runner.temp }}/bt-daemon-config.json
BRAINTRUST_DAEMON_CONFIG: ${{ runner.temp }}/bt-daemon-config.json
MOCK_COLLECTOR_OUT: ${{ runner.temp }}/mock-summary.json
run: |
printf '%s\n' '{"trace_to_braintrust":true,"route":{"destination":{"type":"project_logs","project_name":"trace-codex-smoke"},"flush_mode":"flush_on_turn_end"}}' > "$BT_DAEMON_CONFIG"
printf '%s\n' '{"trace_to_braintrust":true,"route":{"destination":{"type":"project_logs","project_name":"trace-codex-smoke"},"flush_mode":"flush_on_turn_end"}}' > "$BRAINTRUST_DAEMON_CONFIG"
python3 .github/scripts/mock-braintrust.py > "${{ runner.temp }}/mock-collector.log" 2>&1 &
collector_pid=$!
trap 'kill "$collector_pid" 2>/dev/null || true' EXIT
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/smoke-cursor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ jobs:
- name: Run a real traced Cursor session
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
BT_DAEMON_CONFIG: ${{ runner.temp }}/bt-daemon-config.json
BT_DAEMON_SOCKET: ${{ runner.temp }}/cursor-daemon.sock
BRAINTRUST_DAEMON_CONFIG: ${{ runner.temp }}/bt-daemon-config.json
BRAINTRUST_DAEMON_SOCKET: ${{ runner.temp }}/cursor-daemon.sock
BT_DAEMON_BIN: ${{ github.workspace }}/bt-daemon/target/debug/bt-daemon
BT_BIN: ${{ github.workspace }}/src/plugins/cursor/test/bt-standalone-wrapper.sh
BRAINTRUST_BT_BIN: ${{ github.workspace }}/src/plugins/cursor/test/bt-standalone-wrapper.sh
BRAINTRUST_API_KEY: smoke-key
BRAINTRUST_API_URL: http://127.0.0.1:53999
BRAINTRUST_APP_URL: http://127.0.0.1:53999
Expand All @@ -117,13 +117,13 @@ jobs:
set -euo pipefail
cargo build --manifest-path bt-daemon/Cargo.toml --features cli
mkdir -p "${RUNNER_TEMP}/cursor-bin"
install -m 755 "$BT_BIN" "${RUNNER_TEMP}/cursor-bin/bt"
install -m 755 "$BRAINTRUST_BT_BIN" "${RUNNER_TEMP}/cursor-bin/bt"
export PATH="${RUNNER_TEMP}/cursor-bin:$PATH"
printf '%s\n' '{"trace_to_braintrust":true,"route":{"destination":{"type":"project_logs","project_name":"trace-cursor-smoke"},"flush_mode":"flush_on_turn_end"}}' > "$BT_DAEMON_CONFIG"
printf '%s\n' '{"trace_to_braintrust":true,"route":{"destination":{"type":"project_logs","project_name":"trace-cursor-smoke"},"flush_mode":"flush_on_turn_end"}}' > "$BRAINTRUST_DAEMON_CONFIG"
python3 .github/scripts/mock-braintrust.py > "${RUNNER_TEMP}/mock-collector.log" 2>&1 &
collector_pid=$!
trap 'kill "$collector_pid" 2>/dev/null || true' EXIT
"$BT_DAEMON_BIN" serve --socket "$BT_DAEMON_SOCKET" --data-dir "${RUNNER_TEMP}/cursor-state" --idle-timeout-secs 0 &
"$BT_DAEMON_BIN" serve --socket "$BRAINTRUST_DAEMON_SOCKET" --data-dir "${RUNNER_TEMP}/cursor-state" --idle-timeout-secs 0 &
daemon_pid=$!
trap 'kill "$daemon_pid" "$collector_pid" 2>/dev/null || true' EXIT
for attempt in $(seq 1 50); do
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ process APIs. These helpers prevent Windows console flashes. Foreground commands
and daemon detachment have explicit exceptions; do not reuse them for background
work. Clippy and Oxlint enforce use of these helpers in CI.

User-facing environment variables must use the `BRAINTRUST_` prefix (for
example `BRAINTRUST_DAEMON_SOCKET`), matching `bt` and the Braintrust SDKs. Do
not add new `BT_*` names. Read them through `bt-daemon/src/env.rs`;
`DEPRECATED_BT_ENV_VARS` there only keeps old names working.
`BT_TRACE_MANAGED_RUN_ID` and `BT_TRACE_INVOCATION_SETTINGS` remain the
internal protocol between `bt trace run` and its child processes.

## Versioning and distribution

Versioning is per distribution. Claude, Codex, Cursor, and Grok plugins carry
Expand Down
13 changes: 9 additions & 4 deletions bt-daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ Each coding agent reads an independent non-credential `braintrust.json` file:
- Grok: `~/.grok/braintrust.json`
- Antigravity: `~/.gemini/config/braintrust.json`

`BT_DAEMON_CONFIG` can override the path for isolated tests and managed hosts.
`BRAINTRUST_DAEMON_CONFIG` can override the path for isolated tests and managed
hosts.

Tracing env vars use the `BRAINTRUST_` prefix. The old `BT_` names (for example
`BT_DAEMON_CONFIG`) are deprecated but still work; the `BRAINTRUST_` name wins
when both are set.

See [`config.json.example`](config.json.example). `trace_to_braintrust` controls
enablement and `route` stores the selected profile, organization, typed
Expand Down Expand Up @@ -256,13 +261,13 @@ default suite; see the [test harness guide](tests/support/README.md).
From the monorepo root, in a Unix shell:

```bash
export BT_DAEMON_SOCKET=/tmp/btd.sock BT_DAEMON_DATA_DIR=/tmp/btd BT_DAEMON_CONFIG=/tmp/btd-config.json
printf '%s\n' '{"trace_to_braintrust":true,"route":{"destination":{"type":"project_logs","project_name":"debug"}}}' > "$BT_DAEMON_CONFIG"
export BRAINTRUST_DAEMON_SOCKET=/tmp/btd.sock BRAINTRUST_DAEMON_DATA_DIR=/tmp/btd BRAINTRUST_DAEMON_CONFIG=/tmp/btd-config.json
printf '%s\n' '{"trace_to_braintrust":true,"route":{"destination":{"type":"project_logs","project_name":"debug"}}}' > "$BRAINTRUST_DAEMON_CONFIG"
cargo build --manifest-path bt-daemon/Cargo.toml --features cli --locked --bin bt-daemon
echo '{"session_id":"s1","hook_event_name":"SessionStart"}' | ./bt-daemon/target/debug/bt-daemon hook --source debug
echo '{"session_id":"s1","hook_event_name":"Stop"}' | ./bt-daemon/target/debug/bt-daemon hook --source debug
./bt-daemon/target/debug/bt-daemon status
# Inspect journal/ and spans/ under $BT_DAEMON_DATA_DIR
# Inspect journal/ and spans/ under $BRAINTRUST_DAEMON_DATA_DIR
```

The first `hook` spawns the daemon detached; it idles out after 5 minutes.
Expand Down
16 changes: 8 additions & 8 deletions bt-daemon/docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ front-end and the standalone test binary.

Hook clients use an agent-specific non-credential `braintrust.json` file. Codex,
Claude Code, OpenCode, and Pi therefore retain independent persistent profile,
organization, and destination selections. `$BT_DAEMON_CONFIG` may explicitly
override the file path. The hook front-end applies `trace_to_braintrust` and the
stored `route` before sending an event. A managed run sets
`BT_TRACE_INVOCATION_SETTINGS` for only its child process tree, keeping global
agent configuration and generated hook commands unchanged.
organization, and destination selections. `$BRAINTRUST_DAEMON_CONFIG` may
explicitly override the file path. The hook front-end applies
`trace_to_braintrust` and the stored `route` before sending an event. A managed
run sets `BT_TRACE_INVOCATION_SETTINGS` for only its child process tree, keeping
global agent configuration and generated hook commands unchanged.
The profile is optional and defaults through `bt`. Credentials and backend
URLs are resolved and refreshed inside the daemon and never enter this file.

Expand All @@ -19,8 +19,8 @@ URLs are resolved and refreshed inside the daemon and never enter this file.
## Transport

- **Unix domain socket (Linux/macOS).** Default path resolution (first match wins):
1. `--socket <path>` flag / `BT_DAEMON_SOCKET` env (explicit override; used by
tests to sandbox a daemon per test).
1. `--socket <path>` flag / `BRAINTRUST_DAEMON_SOCKET` env (explicit
override; used by tests to sandbox a daemon per test).
2. `$XDG_RUNTIME_DIR/braintrust/daemon.sock` if `XDG_RUNTIME_DIR` is set.
3. `$HOME/.braintrust/run/daemon.sock`.
The containing directory is created mode `0700`. macOS caps `sockaddr_un`
Expand All @@ -30,7 +30,7 @@ URLs are resolved and refreshed inside the daemon and never enter this file.
so `\n` is an unambiguous frame delimiter. Max line length is bounded
(default 64 MiB) to cap memory on a malformed/huge payload; over-length lines
are a protocol error and close the connection.
- **Windows named pipe.** `--socket` / `BT_DAEMON_SOCKET` may provide an
- **Windows named pipe.** `--socket` / `BRAINTRUST_DAEMON_SOCKET` may provide an
explicit full pipe name. Otherwise the daemon uses
`\\.\pipe\braintrust-bt-daemon-<user-hash>`, where the suffix is derived
from the Windows domain and user name so concurrent users do not share a
Expand Down
132 changes: 132 additions & 0 deletions bt-daemon/src/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! User-facing environment variables use the `BRAINTRUST_` prefix, matching
//! `bt` and the Braintrust SDKs. Read them through [`var_os`] so that the
//! deprecated `BT_` names keep working until support is removed.

use std::ffi::{OsStr, OsString};

/// Pre-`BRAINTRUST_` names of tracing env vars. Each `BT_<NAME>` is still
/// honored as an alias for `BRAINTRUST_<NAME>` until support is removed in a
/// major release. Do not add entries: new env vars must use the `BRAINTRUST_`
/// prefix.
///
/// `BT_TRACE_MANAGED_RUN_ID` and `BT_TRACE_INVOCATION_SETTINGS` are not listed:
/// they are the internal protocol `bt trace run` uses to configure its child
/// process tree, read by separately released plugins, and are unchanged.
pub(crate) const DEPRECATED_BT_ENV_VARS: &[&str] = &[
"BT_ANTIGRAVITY_CONFIG_DIR",
"BT_CURSOR_CONFIG_DIR",
"BT_CURSOR_PLUGIN_DIR",
"BT_DAEMON_CONFIG",
"BT_DAEMON_DATA_DIR",
"BT_DAEMON_SOCKET",
"BT_TRACE_OPENCODE_PLUGIN_SPEC",
"BT_TRACE_PI_PLUGIN_SPEC",
];

pub(crate) fn canonical_env_name(deprecated: &str) -> String {
let name = deprecated.strip_prefix("BT_").unwrap_or(deprecated);
format!("BRAINTRUST_{name}")
}

/// The deprecated `BT_` alias of `canonical`, if it has one.
pub(crate) fn deprecated_env_name(canonical: &str) -> Option<&'static str> {
DEPRECATED_BT_ENV_VARS
.iter()
.copied()
.find(|deprecated| canonical_env_name(deprecated) == canonical)
}

/// Read `canonical`, falling back to its deprecated `BT_` alias. The canonical
/// name wins when both are set.
pub(crate) fn var_os(canonical: &str) -> Option<OsString> {
std::env::var_os(canonical)
.or_else(|| deprecated_env_name(canonical).and_then(std::env::var_os))
}

pub(crate) fn var(canonical: &str) -> Option<String> {
var_os(canonical).and_then(|value| value.into_string().ok())
}

/// Pass every resolved value to a child process under both names, so plugins
/// released before the rename read the same value instead of a stale or
/// missing deprecated one.
pub(crate) fn mirror_aliases(command: &mut tokio::process::Command) {
for deprecated in DEPRECATED_BT_ENV_VARS {
let canonical = canonical_env_name(deprecated);
if let Some(value) = var_os(&canonical) {
command.env(&canonical, &value).env(deprecated, &value);
}
}
}

/// Set `canonical` and its deprecated alias to the same value on a child
/// process, so plugins released before the rename still read it.
pub(crate) fn set_with_alias(
command: &mut tokio::process::Command,
canonical: &str,
value: impl AsRef<OsStr>,
) {
command.env(canonical, value.as_ref());
if let Some(deprecated) = deprecated_env_name(canonical) {
command.env(deprecated, value.as_ref());
}
}

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

#[test]
fn canonical_env_name_swaps_prefix() {
assert_eq!(
canonical_env_name("BT_DAEMON_SOCKET"),
"BRAINTRUST_DAEMON_SOCKET"
);
assert_eq!(
deprecated_env_name("BRAINTRUST_DAEMON_SOCKET"),
Some("BT_DAEMON_SOCKET")
);
assert_eq!(deprecated_env_name("BRAINTRUST_API_KEY"), None);
}

/// New user-facing env vars must use the `BRAINTRUST_` prefix. Every
/// `BT_*` name in the crate must be a deprecated alias or internal protocol,
/// and every deprecated alias must map to a name the crate reads.
#[test]
fn env_vars_use_braintrust_prefix() {
const INTERNAL: &[&str] = &[
"BT_TRACE_INVOCATION_SETTINGS",
"BT_TRACE_MANAGED_RUN_ID",
// Fallback for `BRAINTRUST_BT_BIN` in the Cursor hook launchers.
"BT_BIN",
];
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut sources = String::new();
let mut pending = vec![root];
while let Some(path) = pending.pop() {
if path.is_dir() {
pending.extend(std::fs::read_dir(&path).unwrap().map(|e| e.unwrap().path()));
} else if path.extension().is_some_and(|ext| ext == "rs") && !path.ends_with("env.rs") {
sources.push_str(&std::fs::read_to_string(&path).unwrap());
}
}
let pattern = regex::Regex::new(r"\bBT_[A-Z0-9_]+\b").unwrap();
let unexpected: BTreeSet<_> = pattern
.find_iter(&sources)
.map(|m| m.as_str())
.filter(|name| !DEPRECATED_BT_ENV_VARS.contains(name) && !INTERNAL.contains(name))
.collect();
assert!(
unexpected.is_empty(),
"env vars must use the BRAINTRUST_ prefix: {unexpected:?}"
);
for deprecated in DEPRECATED_BT_ENV_VARS {
let canonical = canonical_env_name(deprecated);
assert!(
sources.contains(&format!("\"{canonical}\"")),
"{deprecated} maps to {canonical}, which nothing reads"
);
}
}
}
1 change: 1 addition & 0 deletions bt-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod command_output;
mod correlation;
mod delivery_ledger;
mod dispatch;
mod env;
mod hook;
mod ids;
mod journal;
Expand Down
10 changes: 5 additions & 5 deletions bt-daemon/src/managed_run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::client::{flush_managed_run_in, shutdown_daemon};
use crate::hook::MANAGED_RUN_ID_ENV;
use crate::route::resolve_span_plugin_paths;
use crate::wire::{self, SessionRoute};
use crate::{paths, settings, subprocess};
use crate::{env, paths, settings, subprocess};

const MANAGED_RUN_FLUSH_TIMEOUT_MS: u64 = 10_000;

Expand Down Expand Up @@ -94,7 +94,7 @@ pub async fn run_traced(
// Only create our own boundary when environment auth would otherwise use
// the ambient shared daemon.
let isolate_daemon = route.auth.effective_source() == wire::AuthSource::Environment
&& std::env::var_os(paths::SOCKET_ENV).is_none();
&& env::var_os(paths::SOCKET_ENV).is_none();
let isolated_runtime = isolate_daemon
.then(|| ManagedRunRuntime::new(&managed_run_id))
.transpose()?;
Expand All @@ -106,10 +106,10 @@ pub async fn run_traced(
.env("_BT_TRACE_MANAGED_RUN", "1")
.env(MANAGED_RUN_ID_ENV, &managed_run_id)
.env(settings::INVOCATION_SETTINGS_ENV, invocation_settings);
env::mirror_aliases(&mut command);
if let Some(runtime) = &isolated_runtime {
command
.env(paths::SOCKET_ENV, &runtime.socket)
.env(paths::DATA_DIR_ENV, runtime.temp_dir.path());
env::set_with_alias(&mut command, paths::SOCKET_ENV, &runtime.socket);
env::set_with_alias(&mut command, paths::DATA_DIR_ENV, runtime.temp_dir.path());
Comment on lines 110 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mirror explicit sockets to the legacy child variable

When a non-default BRAINTRUST_DAEMON_SOCKET is supplied, isolate_daemon is false, so this block never mirrors the resolved socket into BT_DAEMON_SOCKET. An already-installed Pi or OpenCode plugin from before the rename reads only the legacy variable and therefore connects to the default or stale socket, while the managed runner flushes the explicitly configured socket, silently losing the run's trace. Mirror an inherited explicit socket into both names on the child just as this block does for an isolated runtime.

AGENTS.md reference: AGENTS.md:L53-L58

Useful? React with 👍 / 👎.

@AbhiPrasad Abhijeet Prasad (AbhiPrasad) Oct 9, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in 7432fa0. bt trace run now passes every resolved BRAINTRUST_* value to the child under both names (env::mirror_aliases), so an explicit BRAINTRUST_DAEMON_SOCKET reaches pre-rename plugins as BT_DAEMON_SOCKET too, and a stale BT_DAEMON_SOCKET is overwritten. The isolated-daemon override still applies on top. New test standalone_run_mirrors_an_explicit_socket_to_the_deprecated_name covers both the missing and stale cases; it failed before the fix (child saw an empty BT_DAEMON_SOCKET).

}
command.envs(injection.env);
let _guards = injection.guards;
Expand Down
4 changes: 2 additions & 2 deletions bt-daemon/src/managed_run/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ fn opencode_managed_config(existing: Option<&str>) -> anyhow::Result<String> {
.or_insert_with(|| serde_json::Value::Array(Vec::new()))
.as_array_mut()
.ok_or_else(|| anyhow::anyhow!("OPENCODE_CONFIG_CONTENT.plugin must be an array"))?;
let plugin = std::env::var("BT_TRACE_OPENCODE_PLUGIN_SPEC")
.unwrap_or_else(|_| "@braintrust/trace-opencode/tracing".to_string());
let plugin = crate::env::var("BRAINTRUST_TRACE_OPENCODE_PLUGIN_SPEC")
.unwrap_or_else(|| "@braintrust/trace-opencode/tracing".to_string());
if !plugins.iter().any(|value| value.as_str() == Some(&plugin)) {
plugins.push(serde_json::Value::String(plugin));
}
Expand Down
2 changes: 1 addition & 1 deletion bt-daemon/src/managed_run/pi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl ManagedRun for Pi {
_hook_command: &RunHookCommand,
_managed_run_id: &str,
) -> anyhow::Result<Injection> {
let extension = match std::env::var_os("BT_TRACE_PI_PLUGIN_SPEC") {
let extension = match crate::env::var_os("BRAINTRUST_TRACE_PI_PLUGIN_SPEC") {
Some(extension) => extension,
None => OsString::from(crate::setup::pi::plugin_spec()),
};
Expand Down
Loading
Loading