diff --git a/bt-daemon/README.md b/bt-daemon/README.md index 9c8b4c0..e5feabb 100644 --- a/bt-daemon/README.md +++ b/bt-daemon/README.md @@ -68,12 +68,12 @@ or provide it for one invocation with `bt trace run`. Both accept `--additional-metadata` or `BRAINTRUST_ADDITIONAL_METADATA`: ```bash -bt trace setup claude --additional-metadata '{"team":"platform"}' +bt trace enable claude --additional-metadata '{"team":"platform"}' BRAINTRUST_ADDITIONAL_METADATA='{"ci":true,"run_id":"'"$JOB_ID"'"}' \ bt trace run codex -- "summarize this change" ``` -`bt trace setup`, `bt trace run`, and `bt trace import` are explicit, +`bt trace enable`, `bt trace run`, and `bt trace import` are explicit, user-invoked commands, so all three accept routing (profile, organization, project, destination) and `additional_metadata` from either a flag or the matching `BRAINTRUST_*` environment variable, with the flag winning if both @@ -84,6 +84,10 @@ run`, from that invocation's settings. An explicit `--additional-metadata` flag or environment variable on `bt trace run` overrides the persisted route for that invocation only, without mutating the file. +Use `bt trace disable ` to remove the installed tracing plugin and its +Braintrust settings. `bt trace setup ` remains an alias for `bt trace +enable ` for backwards compatibility. + ## Build / test ```bash diff --git a/bt-daemon/src/command_output.rs b/bt-daemon/src/command_output.rs index 53998c5..704160d 100644 --- a/bt-daemon/src/command_output.rs +++ b/bt-daemon/src/command_output.rs @@ -71,7 +71,8 @@ pub struct StopCommandOutput { #[serde(tag = "command", rename_all = "snake_case")] pub enum TraceCommandOutput { Status(StatusCommandOutput), - Setup(SetupCommandOutput), + Enable(SetupCommandOutput), + Disable(SetupCommandOutput), Stop(StopCommandOutput), } @@ -85,7 +86,7 @@ impl TraceCommandOutput { display_name: impl Into, settings_path: impl Into, ) -> Self { - Self::Setup(SetupCommandOutput { + Self::Enable(SetupCommandOutput { source: source.into(), display_name: display_name.into(), settings_path: settings_path.into(), @@ -97,6 +98,19 @@ impl TraceCommandOutput { Self::Stop(StopCommandOutput { running, stopped }) } + pub fn disable( + source: impl Into, + display_name: impl Into, + settings_path: impl Into, + ) -> Self { + Self::Disable(SetupCommandOutput { + source: source.into(), + display_name: display_name.into(), + settings_path: settings_path.into(), + restart_required: true, + }) + } + pub fn render(&self, format: OutputFormat) -> anyhow::Result { match format { OutputFormat::Json => Ok(serde_json::to_string(self)?), @@ -112,11 +126,16 @@ impl TraceCommandOutput { uptime_ms: status.uptime_ms.unwrap_or_default(), sessions: status.sessions.clone(), })?), - Self::Setup(setup) => Ok(format!( + Self::Enable(setup) => Ok(format!( "The Braintrust tracing plugin is installed for {} and configured in {}.\nRestart the coding agent to load the tracing plugin.", setup.display_name, setup.settings_path.display() )), + Self::Disable(disable) => Ok(format!( + "The Braintrust tracing plugin and configuration were removed for {} from {}.\nRestart the coding agent to apply the change.", + disable.display_name, + disable.settings_path.display() + )), Self::Stop(stop) if stop.stopped => Ok("Tracing daemon stopped.".into()), Self::Stop(_) => Ok("No tracing daemon is running.".into()), } @@ -140,7 +159,7 @@ mod tests { } #[test] - fn setup_json_contains_stable_selection_fields_without_prose() { + fn enable_json_contains_stable_selection_fields_without_prose() { let output = TraceCommandOutput::setup( "opencode", "OpenCode", @@ -148,7 +167,7 @@ mod tests { ); let rendered = output.render(OutputFormat::Json).unwrap(); let value: serde_json::Value = serde_json::from_str(&rendered).unwrap(); - assert_eq!(value["command"], "setup"); + assert_eq!(value["command"], "enable"); assert_eq!(value["source"], "opencode"); assert_eq!(value["restart_required"], true); assert!(!rendered.contains("installed for")); diff --git a/bt-daemon/src/lib.rs b/bt-daemon/src/lib.rs index a54e326..762a8ad 100644 --- a/bt-daemon/src/lib.rs +++ b/bt-daemon/src/lib.rs @@ -34,9 +34,11 @@ pub use command_output::{ OutputFormat, SetupCommandOutput, StatusCommandOutput, StopCommandOutput, TraceCommandOutput, }; pub use server::{AuthLease, AuthProvider, AuthResolveReason, ServeOptions}; -pub use setup::run_setup; +pub use setup::{run_disable, run_enable, run_setup}; pub use sink::{BraintrustSinkConfig, BraintrustSinkFactory, DebugSinkFactory, Sink, SinkFactory}; -pub use trace_command::{SetupAgent, SetupArgs, StopArgs, TraceArgs, TraceCommand}; +pub use trace_command::{ + DisableArgs, EnableArgs, SetupAgent, SetupArgs, StopArgs, TraceArgs, TraceCommand, +}; pub use trace_runtime::{run_trace, RouteRequirements, TraceHostContext, TraceHostServices}; pub use translate::{ AgentTranslator, Registry, SessionCtx, SpanOp, SpanRow, SpanType, TranslatorFactory, diff --git a/bt-daemon/src/setup.rs b/bt-daemon/src/setup.rs index 50940a8..4792f78 100644 --- a/bt-daemon/src/setup.rs +++ b/bt-daemon/src/setup.rs @@ -1,7 +1,7 @@ //! Persistent installation and configuration for coding-agent tracing plugins. use crate::paths; -use crate::trace_command::{SetupAgent, SetupArgs}; +use crate::trace_command::{EnableArgs, SetupAgent}; use crate::wire::SessionRoute; use crate::TraceCommandOutput; use anyhow::{bail, Context}; @@ -109,6 +109,22 @@ fn setup_codex(runner: &mut impl CommandRunner) -> anyhow::Result<()> { runner.run("codex", &["plugin", "add", CODEX_PLUGIN]) } +fn codex_plugin(value: &Value) -> Option<&Value> { + value + .get("installed") + .and_then(Value::as_array)? + .iter() + .find(|item| item.get("pluginId").and_then(Value::as_str) == Some(CODEX_PLUGIN)) +} + +fn disable_codex(runner: &mut impl CommandRunner) -> anyhow::Result<()> { + let plugins = runner.json("codex", &["plugin", "list", "--json"])?; + if codex_plugin(&plugins).is_some() { + runner.run("codex", &["plugin", "remove", CODEX_PLUGIN, "--json"])?; + } + Ok(()) +} + fn claude_marketplace(value: &Value) -> Option<&Value> { value .as_array()? @@ -180,6 +196,14 @@ fn setup_claude(runner: &mut impl CommandRunner) -> anyhow::Result<()> { } } +fn disable_claude(runner: &mut impl CommandRunner) -> anyhow::Result<()> { + let plugins = runner.json("claude", &["plugin", "list", "--json"])?; + if claude_plugin(&plugins).is_some() { + runner.run("claude", &["plugin", "uninstall", CLAUDE_PLUGIN])?; + } + Ok(()) +} + fn load_object(path: &Path) -> anyhow::Result> { match std::fs::read(path) { Ok(raw) => { @@ -254,10 +278,60 @@ fn setup_opencode() -> anyhow::Result<()> { setup_opencode_at(&path) } +fn remove_opencode_plugin_at(path: &Path) -> anyhow::Result<()> { + let mut config = match std::fs::read(path) { + Ok(raw) => serde_json::from_slice::(&raw) + .with_context(|| format!("invalid JSON configuration: {}", path.display()))? + .as_object() + .cloned() + .ok_or_else(|| { + anyhow::anyhow!("configuration must be a JSON object: {}", path.display()) + })?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) => { + return Err(error) + .with_context(|| format!("failed to read configuration: {}", path.display())) + } + }; + let Some(plugins) = config.get_mut("plugin") else { + return Ok(()); + }; + let plugins = plugins.as_array_mut().ok_or_else(|| { + anyhow::anyhow!( + "OpenCode `plugin` config must be an array: {}", + path.display() + ) + })?; + let original_len = plugins.len(); + plugins.retain(|plugin| { + plugin.as_str().is_none_or(|plugin| { + plugin != "@braintrust/trace-opencode" + && !plugin.starts_with("@braintrust/trace-opencode@") + }) + }); + if plugins.len() != original_len { + write_object_atomic(path, config)?; + } + Ok(()) +} + +fn disable_opencode() -> anyhow::Result<()> { + let settings_path = paths::agent_settings_path("opencode", None); + let path = settings_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join("opencode.json"); + remove_opencode_plugin_at(&path) +} + fn setup_pi(runner: &mut impl CommandRunner) -> anyhow::Result<()> { runner.run("pi", &["install", PI_PLUGIN]) } +fn disable_pi(runner: &mut impl CommandRunner) -> anyhow::Result<()> { + runner.run("pi", &["uninstall", PI_PLUGIN]) +} + fn enable_tracing_at(path: &Path, mut route: SessionRoute) -> anyhow::Result<()> { let mut settings = load_object(path)?; if route.additional_metadata.is_none() { @@ -288,9 +362,46 @@ fn enable_tracing(source: &str, route: SessionRoute) -> anyhow::Result Ok(path) } +fn remove_tracing_settings(path: &Path) -> anyhow::Result<()> { + match std::fs::remove_file(path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error) + .with_context(|| format!("failed to remove tracing settings: {}", path.display())), + } +} + +/// Uninstall an agent's tracing adapter and remove its Braintrust-owned settings. +pub fn run_disable(agent: SetupAgent) -> anyhow::Result { + let mut runner = SystemCommandRunner; + let (source, display_name) = agent_details(agent); + match agent { + SetupAgent::Codex => disable_codex(&mut runner)?, + SetupAgent::Claude => disable_claude(&mut runner)?, + SetupAgent::OpenCode => disable_opencode()?, + SetupAgent::Pi => disable_pi(&mut runner)?, + } + let settings_path = paths::agent_settings_path(source, None); + remove_tracing_settings(&settings_path)?; + Ok(TraceCommandOutput::disable( + source, + display_name, + settings_path, + )) +} + +fn agent_details(agent: SetupAgent) -> (&'static str, &'static str) { + match agent { + SetupAgent::Codex => ("codex", "Codex"), + SetupAgent::Claude => ("claude", "Claude Code"), + SetupAgent::OpenCode => ("opencode", "OpenCode"), + SetupAgent::Pi => ("pi", "Pi"), + } +} + /// Install or refresh one agent's published tracing adapter and persist its /// non-secret route selection. -pub fn run_setup(args: SetupArgs, route: SessionRoute) -> anyhow::Result { +pub fn run_enable(args: EnableArgs, route: SessionRoute) -> anyhow::Result { let mut runner = SystemCommandRunner; let (source, display_name) = match args.agent { SetupAgent::Codex => { @@ -318,6 +429,11 @@ pub fn run_setup(args: SetupArgs, route: SessionRoute) -> anyhow::Result anyhow::Result { + run_enable(args, route) +} + #[cfg(test)] mod tests { use super::*; @@ -575,4 +691,52 @@ mod tests { serde_json::json!({"run_id": "new"}) ); } + + #[test] + fn disabling_removes_only_the_braintrust_settings_file() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("braintrust.json"); + std::fs::write( + &path, + r#"{"traceToBraintrust":true,"route":{"destination":{"project_name":"coding-agents"}},"other":true}"#, + ) + .unwrap(); + + remove_tracing_settings(&path).unwrap(); + assert!(!path.exists()); + } + + #[test] + fn disabling_installed_plugins_uses_each_agents_uninstall_command() { + let mut codex = FakeRunner::new([serde_json::json!({ + "installed": [{"pluginId": CODEX_PLUGIN}] + })]); + disable_codex(&mut codex).unwrap(); + assert!(codex.called("codex plugin remove trace-codex@braintrust-codex-plugins --json")); + + let mut claude = FakeRunner::new([serde_json::json!([{"id": CLAUDE_PLUGIN}])]); + disable_claude(&mut claude).unwrap(); + assert!(claude.called("claude plugin uninstall trace-claude-code@braintrust-claude-plugin")); + + let mut pi = FakeRunner::new([]); + disable_pi(&mut pi).unwrap(); + assert!(pi.called("pi uninstall npm:@braintrust/pi-extension@^1")); + } + + #[test] + fn disabling_opencode_removes_only_the_managed_plugin() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("opencode.json"); + std::fs::write( + &path, + r#"{"plugin":["other","@braintrust/trace-opencode@^1"],"model":"test/model"}"#, + ) + .unwrap(); + + remove_opencode_plugin_at(&path).unwrap(); + + let config: Value = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap(); + assert_eq!(config["plugin"], serde_json::json!(["other"])); + assert_eq!(config["model"], "test/model"); + } } diff --git a/bt-daemon/src/trace_command.rs b/bt-daemon/src/trace_command.rs index 6577b17..235ed44 100644 --- a/bt-daemon/src/trace_command.rs +++ b/bt-daemon/src/trace_command.rs @@ -19,8 +19,11 @@ pub struct TraceArgs { // clearer than boxing individual command variants for stack-size savings. #[allow(clippy::large_enum_variant)] pub enum TraceCommand { - /// Install the published Braintrust tracing plugin for a coding agent. - Setup(SetupArgs), + /// Install or enable the published Braintrust tracing plugin for a coding agent. + #[command(name = "enable", alias = "setup")] + Setup(EnableArgs), + /// Uninstall the Braintrust tracing plugin and remove its saved configuration. + Disable(DisableArgs), /// Run the tracing daemon (foreground). #[command(hide = true)] Daemon(ServeArgs), @@ -47,7 +50,7 @@ pub struct StopArgs { } #[derive(Debug, Clone, Args)] -pub struct SetupArgs { +pub struct EnableArgs { #[command(subcommand)] pub agent: SetupAgent, /// JSON object persisted in this agent's tracing route and merged into root-span metadata. @@ -55,6 +58,15 @@ pub struct SetupArgs { pub additional_metadata: Option, } +/// Backwards-compatible API name for hosts that mounted the former setup command. +pub type SetupArgs = EnableArgs; + +#[derive(Debug, Clone, Args)] +pub struct DisableArgs { + #[command(subcommand)] + pub agent: SetupAgent, +} + #[derive(Debug, Clone, Copy, Subcommand)] pub enum SetupAgent { /// Install the published Codex tracing plugin. @@ -97,6 +109,15 @@ mod tests { }) if value == r#"{"setup":true}"# )); + let legacy_setup = Cli::try_parse_from(["bt", "setup", "codex"]).unwrap(); + assert!(matches!( + legacy_setup.trace.command, + TraceCommand::Setup(SetupArgs { + agent: SetupAgent::Codex, + .. + }) + )); + let hook = Cli::try_parse_from([ "bt", "hook", diff --git a/bt-daemon/src/trace_runtime.rs b/bt-daemon/src/trace_runtime.rs index 461e01d..39a8731 100644 --- a/bt-daemon/src/trace_runtime.rs +++ b/bt-daemon/src/trace_runtime.rs @@ -8,10 +8,10 @@ use crate::trace_command::TraceCommand; use crate::wire::{AuthSelection, SessionConfig, SessionRoute}; use crate::{ - apply_additional_metadata, braintrust_serve_options, paths, run_hook, run_import, run_serve, - run_setup, run_status, run_traced, shutdown_daemon, AuthLease, AuthProvider, AuthResolveReason, - BraintrustSinkConfig, HostInfo, OutputFormat, Registry, RunHookCommand, ServeOptions, - StatusArgs, TraceArgs, TraceCommandOutput, + apply_additional_metadata, braintrust_serve_options, paths, run_disable, run_enable, run_hook, + run_import, run_serve, run_status, run_traced, shutdown_daemon, AuthLease, AuthProvider, + AuthResolveReason, BraintrustSinkConfig, HostInfo, OutputFormat, Registry, RunHookCommand, + ServeOptions, StatusArgs, TraceArgs, TraceCommandOutput, }; use async_trait::async_trait; use std::ffi::OsString; @@ -188,7 +188,7 @@ fn print_output(output: TraceCommandOutput, format: OutputFormat) -> anyhow::Res /// Execute the complete mounted trace command. pub async fn run_trace(args: TraceArgs, host: TraceHostContext) -> anyhow::Result<()> { match args.command { - TraceCommand::Setup(setup_args) => { + TraceCommand::Setup(enable_args) => { let mut route = resolve_command_route( &host, RouteRequirements { @@ -197,8 +197,11 @@ pub async fn run_trace(args: TraceArgs, host: TraceHostContext) -> anyhow::Resul }, ) .await?; - apply_additional_metadata(&mut route, setup_args.additional_metadata.as_deref())?; - print_output(run_setup(setup_args, route)?, host.output_format) + apply_additional_metadata(&mut route, enable_args.additional_metadata.as_deref())?; + print_output(run_enable(enable_args, route)?, host.output_format) + } + TraceCommand::Disable(disable_args) => { + print_output(run_disable(disable_args.agent)?, host.output_format) } TraceCommand::Daemon(serve_args) => { init_daemon_logging(host.verbose); diff --git a/src/plugins/claude/content/README.md b/src/plugins/claude/content/README.md index 08f9c8b..8d3dfed 100644 --- a/src/plugins/claude/content/README.md +++ b/src/plugins/claude/content/README.md @@ -42,7 +42,7 @@ Braintrust daemon. The plugin contains only a fail-open hook forwarder; `bt` owns authentication, trace construction, and delivery. ```bash -bt trace setup claude --project my-coding-agent +bt trace enable claude --project my-coding-agent ``` Use `--profile` or `--org` when needed. Setup stores only non-secret routing @@ -54,7 +54,7 @@ failures never fail a Claude Code turn. #### Additional root metadata -For a persistent route, pass a JSON object to `bt trace setup claude +For a persistent route, pass a JSON object to `bt trace enable claude --additional-metadata ''` to tag the root span of every Claude Code session. Standard session metadata takes precedence if keys conflict. diff --git a/src/plugins/claude/content/plugins/trace-claude-code/README.md b/src/plugins/claude/content/plugins/trace-claude-code/README.md index 3f0a15c..41a0d72 100644 --- a/src/plugins/claude/content/plugins/trace-claude-code/README.md +++ b/src/plugins/claude/content/plugins/trace-claude-code/README.md @@ -6,7 +6,7 @@ This plugin synchronously forwards Claude Code lifecycle payloads to: bt trace hook --source claude-code ``` -Use `bt trace setup claude --project ` to install and configure it. +Use `bt trace enable claude --project ` to install and configure it. The hook first installs the `bt` CLI with the official installer when it is not already available, then forwards the event. The plugin is credential-free and fail-open; the `bt` CLI and shared daemon own authentication, event journaling, @@ -14,7 +14,7 @@ trace construction, and delivery. To add fields to each root trace span, pass a JSON object (as `--additional-metadata` or `BRAINTRUST_ADDITIONAL_METADATA`) to -`bt trace setup claude` for a persistent configuration, or to +`bt trace enable claude` for a persistent configuration, or to `bt trace run claude` for one invocation. The hook itself never reads that -environment variable — only `bt trace setup`, `bt trace run`, and +environment variable — only `bt trace enable`, `bt trace run`, and `bt trace import` do. diff --git a/src/plugins/codex/content/README.md b/src/plugins/codex/content/README.md index b147c9d..9e18fc7 100644 --- a/src/plugins/codex/content/README.md +++ b/src/plugins/codex/content/README.md @@ -22,7 +22,7 @@ codex plugin add braintrust@braintrust-codex-plugins The recommended tracing setup is: ```bash -bt trace setup codex --project my-coding-agent +bt trace enable codex --project my-coding-agent ``` This installs the tracing plugin and stores only non-secret routing settings. diff --git a/src/plugins/codex/content/plugins/trace-codex/README.md b/src/plugins/codex/content/plugins/trace-codex/README.md index 0edc8c4..73ced29 100644 --- a/src/plugins/codex/content/plugins/trace-codex/README.md +++ b/src/plugins/codex/content/plugins/trace-codex/README.md @@ -18,7 +18,7 @@ stored in the plugin. Install and configure the published plugin with the Braintrust CLI: ```bash -bt trace setup codex --project my-coding-agent +bt trace enable codex --project my-coding-agent ``` Use `--profile` or `--org` when needed. Restart Codex after setup so it loads @@ -38,7 +38,7 @@ exits successfully. ## Additional root metadata -For a persistent route, pass a JSON object to `bt trace setup codex +For a persistent route, pass a JSON object to `bt trace enable codex --additional-metadata ''` to tag the root span of every Codex session. Standard session metadata takes precedence if keys conflict. diff --git a/src/plugins/opencode/content/README.md b/src/plugins/opencode/content/README.md index 24baa2e..cf7de22 100644 --- a/src/plugins/opencode/content/README.md +++ b/src/plugins/opencode/content/README.md @@ -16,7 +16,7 @@ running. ```bash bt auth login -bt trace setup opencode +bt trace enable opencode opencode ``` diff --git a/src/plugins/opencode/content/install.sh b/src/plugins/opencode/content/install.sh index 7226b8a..4462a81 100755 --- a/src/plugins/opencode/content/install.sh +++ b/src/plugins/opencode/content/install.sh @@ -37,7 +37,7 @@ echo "1. Authenticate the bt CLI:" echo " bt auth login" echo "" echo "2. (Optional) Configure project name:" -echo " bt trace setup opencode --project my-project" +echo " bt trace enable opencode --project my-project" echo "" echo "3. Run OpenCode:" echo " opencode" diff --git a/src/plugins/pi/content/README.md b/src/plugins/pi/content/README.md index 09ee3b7..bb9c9c0 100644 --- a/src/plugins/pi/content/README.md +++ b/src/plugins/pi/content/README.md @@ -65,7 +65,7 @@ Our GitHub Actions compatibility job automatically resolves and tests that compa ```bash bt auth login -bt trace setup pi +bt trace enable pi pi ```