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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ Local version and pagination-key conversion helpers:
Auth resolution order for commands is:

1. Explicit `--profile`
2. `--api-key` or `BRAINTRUST_API_KEY` (unless `--prefer-profile` is set)
2. `BRAINTRUST_API_KEY` (unless `--prefer-profile` is set)
3. `BRAINTRUST_PROFILE`
4. Compatible profile for the selected app URL and organization
5. Single-profile auto-select (if only one compatible profile exists)
Expand Down
3 changes: 1 addition & 2 deletions scripts/eval-runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,6 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Run evals and emit SSE events for bt.")
parser.add_argument("files", nargs="*", help="Eval files or directories to run.")
parser.add_argument("--local", action="store_true", help="Do not send logs to Braintrust.")
parser.add_argument("--api-key", help="Specify a braintrust API key.")
parser.add_argument("--org-name", help="Organization name.")
parser.add_argument("--app-url", help="Braintrust app URL.")
return parser
Expand All @@ -1442,7 +1441,7 @@ def main(argv: list[str] | None = None) -> int:
try:
try:
if not local:
login(api_key=args.api_key, org_name=args.org_name, app_url=args.app_url)
login(org_name=args.org_name, app_url=args.app_url)
success = asyncio.run(run_once(files, local, sse, config))
except Exception as exc:
stack = traceback.format_exc()
Expand Down
10 changes: 7 additions & 3 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,18 @@ pub struct LoginBaseArgs {
#[arg(skip = false)]
pub profile_explicit: bool,

/// Override stored API key (or via BRAINTRUST_API_KEY)
#[arg(long, env = "BRAINTRUST_API_KEY", global = true, hide = true)]
#[arg(
long = "braintrust-internal-api-key-environment-binding-do-not-use",
env = "BRAINTRUST_API_KEY",
global = true,
hide = true
)]
pub api_key: Option<String>,

#[arg(skip)]
pub api_key_source: Option<ArgValueSource>,

/// Prefer profile credentials even if BRAINTRUST_API_KEY/--api-key is set.
/// Prefer profile credentials even if BRAINTRUST_API_KEY is set.
#[arg(long, global = true)]
pub prefer_profile: bool,

Expand Down
57 changes: 7 additions & 50 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,7 @@ pub async fn fast_login(base: &BaseArgs) -> Result<LoginContext> {
maybe_warn_api_key_override(base);
let auth = resolve_auth(base).await?;
let api_key = auth.api_key.clone().ok_or_else(|| {
anyhow::anyhow!(
"no login credentials found; set BRAINTRUST_API_KEY, pass --api-key, or run `bt login`"
)
anyhow::anyhow!("no login credentials found; set BRAINTRUST_API_KEY or run `bt login`")
})?;
let org_name = auth.org_name.clone().unwrap_or_default();
let api_url = auth
Expand Down Expand Up @@ -494,9 +492,7 @@ pub async fn login(base: &BaseArgs) -> Result<LoginContext> {
maybe_warn_api_key_override(base);
let auth = resolve_auth(base).await?;
let api_key = auth.api_key.clone().ok_or_else(|| {
anyhow::anyhow!(
"no login credentials found; set BRAINTRUST_API_KEY, pass --api-key, or run `bt login`"
)
anyhow::anyhow!("no login credentials found; set BRAINTRUST_API_KEY or run `bt login`")
})?;

let mut builder = BraintrustClient::builder()
Expand Down Expand Up @@ -778,7 +774,7 @@ fn maybe_warn_api_key_override(base: &BaseArgs) {

if let Some(profile_name) = ignored_profile {
eprintln!(
"Info: using --api-key/BRAINTRUST_API_KEY credentials; selected profile '{profile_name}' is ignored for this command. Use --prefer-profile or unset BRAINTRUST_API_KEY to use a profile with OAuth login.",
"Info: using BRAINTRUST_API_KEY credentials; selected profile '{profile_name}' is ignored for this command. Use --prefer-profile or unset BRAINTRUST_API_KEY to use a profile with OAuth login.",
);
}
}
Expand All @@ -792,12 +788,7 @@ fn has_explicit_profile_selection(base: &BaseArgs) -> bool {
}

fn resolve_api_key_override(base: &BaseArgs) -> Option<String> {
if (base.prefer_profile || has_explicit_profile_selection(base))
&& !matches!(
base.api_key_source,
Some(crate::args::ArgValueSource::CommandLine)
)
{
if base.prefer_profile || has_explicit_profile_selection(base) {
return None;
Comment on lines +791 to 792

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 Preserve precedence for command-line API keys

When the still-supported hidden global --api-key (src/args.rs:49-51) is combined with --prefer-profile or an explicit --profile, this unconditional branch now discards the command-line credential and authenticates with the stored profile instead. That can run commands against the wrong identity or organization despite the caller explicitly supplying a key; retain the ArgValueSource::CommandLine exception and keep the CLI-owned flag/env behavior consistent.

AGENTS.md reference: AGENTS.md:L6-L9

Useful? React with 👍 / 👎.

}
let value = base.api_key.as_deref()?.trim();
Expand Down Expand Up @@ -2864,7 +2855,7 @@ fn build_oauth_client(

fn prompt_api_key() -> Result<String> {
let term = ui::prompt_term()
.ok_or_else(|| anyhow::anyhow!("--api-key is required in non-interactive mode"))?;
.ok_or_else(|| anyhow::anyhow!("BRAINTRUST_API_KEY is required in non-interactive mode"))?;
let api_key = Password::new()
.with_prompt("Braintrust API key")
.allow_empty_password(false)
Expand All @@ -2882,7 +2873,7 @@ fn prompt_api_key() -> Result<String> {
fn linux_secret_tool_exec_error(err: std::io::Error) -> anyhow::Error {
if err.kind() == std::io::ErrorKind::NotFound {
anyhow::anyhow!(
"`secret-tool` is not installed. Install `libsecret-tools` (Debian/Ubuntu) or your distro's equivalent package, or use BRAINTRUST_API_KEY/--api-key for non-persistent auth."
"`secret-tool` is not installed. Install `libsecret-tools` (Debian/Ubuntu) or your distro's equivalent package, or use BRAINTRUST_API_KEY for non-persistent auth."
)
} else {
anyhow::anyhow!("failed to execute Linux keychain utility `secret-tool`: {err}")
Expand All @@ -2899,7 +2890,7 @@ fn linux_secret_service_unavailable(stderr: &str) -> bool {
#[cfg(target_os = "linux")]
fn linux_secret_service_error() -> anyhow::Error {
anyhow::anyhow!(
"no Secret Service provider is running. Start a Secret Service daemon (for example gnome-keyring or keepassxc with Secret Service enabled), or use BRAINTRUST_API_KEY/--api-key for non-persistent auth."
"no Secret Service provider is running. Start a Secret Service daemon (for example gnome-keyring or keepassxc with Secret Service enabled), or use BRAINTRUST_API_KEY for non-persistent auth."
)
}

Expand Down Expand Up @@ -4012,40 +4003,6 @@ mod tests {
assert_eq!(resolved.org_name.as_deref(), Some("Example Org"));
}

#[test]
fn resolve_auth_prefers_cli_api_key_even_with_prefer_profile() {
let mut base = make_base();
base.api_key = Some("explicit-key".to_string());
base.api_key_source = Some(crate::args::ArgValueSource::CommandLine);
base.prefer_profile = true;
base.profile = Some("work".to_string());

let mut store = AuthStore::default();
store.profiles.insert(
"work".to_string(),
AuthProfile {
auth_kind: AuthKind::ApiKey,
oauth_api_url: Some("https://api.example.com".to_string()),
app_url: None,
org_name: Some("Example Org".to_string()),
org_bound: Some(true),
oauth_client_id: None,
oauth_access_expires_at: None,
..Default::default()
},
);

let resolved = resolve_auth_from_store_with_secret_lookup(
&base,
&store,
|_| Ok(Some("profile-key".to_string())),
&None,
)
.expect("resolve");
assert_eq!(resolved.api_key.as_deref(), Some("explicit-key"));
assert_eq!(resolved.org_name, None);
}

#[test]
fn resolve_auth_explicit_profile_ignores_env_api_key_override() {
let mut base = make_base();
Expand Down
2 changes: 1 addition & 1 deletion src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3115,7 +3115,7 @@ impl EvalUi {
self.record_deferred_error(message);
}
if show_hint {
let hint = "Hint: pass --api-key, set BRAINTRUST_API_KEY, run `bt login`/`bt login --oauth`, or use --no-send-logs for local evals.";
let hint = "Hint: set BRAINTRUST_API_KEY, run `bt login`/`bt login --oauth`, or use --no-send-logs for local evals.";
if self.verbose {
let _ = self.progress.println(hint.dark_grey().to_string());
} else {
Expand Down
26 changes: 5 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,11 @@ fn apply_base_arg_sources(matches: &ArgMatches, base: &mut LoginBaseArgs) {
base.verbose_source = find_value_source(matches, "verbose").and_then(map_value_source);
base.quiet_source = find_value_source(matches, "quiet").and_then(map_value_source);
base.api_key_source = find_value_source(matches, "api_key").and_then(map_value_source);

if !matches!(base.api_key_source, Some(ArgValueSource::EnvVariable)) {
base.api_key = None;
base.api_key_source = None;
}
}

fn apply_base_output_defaults(command: &mut Commands) {
Expand Down Expand Up @@ -605,27 +610,6 @@ mod tests {
}
}

#[test]
fn apply_base_arg_sources_tracks_cli_api_key() {
let _guard = env_test_lock().lock().expect("env test lock");
let previous_api_key = env::var_os("BRAINTRUST_API_KEY");
env::remove_var("BRAINTRUST_API_KEY");

let matches = Cli::command()
.try_get_matches_from(["bt", "status", "--api-key", "secret"])
.expect("matches");
let mut cli = Cli::from_arg_matches(&matches).expect("cli");

apply_base_arg_sources(&matches, cli.command.base_mut());

restore_env_var("BRAINTRUST_API_KEY", previous_api_key);

assert_eq!(
cli.command.base().api_key_source,
Some(ArgValueSource::CommandLine)
);
}

#[test]
fn apply_base_arg_sources_leaves_api_key_source_empty_when_unset() {
let _guard = env_test_lock().lock().expect("env test lock");
Expand Down
7 changes: 1 addition & 6 deletions src/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2200,12 +2200,7 @@ async fn build_setup_auth_context(
}

fn should_create_api_key_for_setup(is_oauth: bool, base: &BaseArgs, needs_api_key: bool) -> bool {
needs_api_key
&& is_oauth
&& !matches!(
base.api_key_source,
Some(ArgValueSource::CommandLine | ArgValueSource::EnvVariable)
)
needs_api_key && is_oauth && !matches!(base.api_key_source, Some(ArgValueSource::EnvVariable))
}

async fn maybe_create_api_key_for_oauth(base: &BaseArgs, client: &ApiClient) -> Result<String> {
Expand Down
Loading