@@ -1772,6 +1772,13 @@ async fn maybe_create_api_key_for_oauth(base: &BaseArgs, client: &ApiClient) ->
17721772 key : String ,
17731773 }
17741774
1775+ let org_id = client. org_id ( ) . trim ( ) ;
1776+ if org_id. is_empty ( ) {
1777+ bail ! (
1778+ "setup could not determine the current org_id for API key creation; rerun with a direct API key or re-authenticate so setup can resolve the selected organization"
1779+ ) ;
1780+ }
1781+
17751782 let existing: Vec < String > = client
17761783 . get :: < ApiKeyList > ( "/v1/api_key" )
17771784 . await
@@ -1790,7 +1797,7 @@ async fn maybe_create_api_key_for_oauth(base: &BaseArgs, client: &ApiClient) ->
17901797 . expect ( "name sequence is infinite" )
17911798 } ;
17921799
1793- let body = serde_json:: json!( { "name" : name, "org_name " : client . org_name ( ) } ) ;
1800+ let body = serde_json:: json!( { "name" : name, "org_id " : org_id } ) ;
17941801 let created: CreatedKey = client. post ( "/v1/api_key" , & body) . await ?;
17951802
17961803 let explicitly_quiet = base. quiet && base. quiet_source . is_some ( ) ;
@@ -4916,8 +4923,11 @@ fn print_mcp_human_report(
49164923#[ cfg( test) ]
49174924mod tests {
49184925 use super :: * ;
4926+ use crate :: auth:: LoginContext ;
49194927 use std:: env;
49204928 use std:: ffi:: OsString ;
4929+ use std:: io:: { Read , Write } ;
4930+ use std:: net:: TcpListener ;
49214931 use std:: sync:: { Mutex , OnceLock } ;
49224932 use std:: time:: { SystemTime , UNIX_EPOCH } ;
49234933
@@ -4970,6 +4980,146 @@ mod tests {
49704980 }
49714981 }
49724982
4983+ fn make_login_context (
4984+ api_url : String ,
4985+ app_url : String ,
4986+ org_id : & str ,
4987+ org_name : & str ,
4988+ ) -> LoginContext {
4989+ let login = braintrust_sdk_rust:: LoginState :: new ( ) ;
4990+ let _ = login. set (
4991+ "test-api-key" . to_string ( ) ,
4992+ org_id. to_string ( ) ,
4993+ org_name. to_string ( ) ,
4994+ api_url. clone ( ) ,
4995+ app_url. clone ( ) ,
4996+ ) ;
4997+
4998+ LoginContext {
4999+ login,
5000+ api_url,
5001+ app_url,
5002+ }
5003+ }
5004+
5005+ #[ tokio:: test]
5006+ async fn maybe_create_api_key_for_oauth_uses_org_id_in_request_body ( ) {
5007+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . expect ( "bind listener" ) ;
5008+ let addr = listener. local_addr ( ) . expect ( "listener addr" ) ;
5009+ let server = std:: thread:: spawn ( move || {
5010+ let ( mut stream, _) = listener. accept ( ) . expect ( "accept list request" ) ;
5011+ let mut buffer = [ 0u8 ; 4096 ] ;
5012+ let read = stream. read ( & mut buffer) . expect ( "read list request" ) ;
5013+ let request = String :: from_utf8_lossy ( & buffer[ ..read] ) ;
5014+ assert ! ( request. starts_with( "GET /v1/api_key HTTP/1.1" ) ) ;
5015+ let response = concat ! (
5016+ "HTTP/1.1 200 OK\r \n " ,
5017+ "Content-Type: application/json\r \n " ,
5018+ "Content-Length: 14\r \n " ,
5019+ "Connection: close\r \n " ,
5020+ "\r \n " ,
5021+ "{\" objects\" :[]}"
5022+ ) ;
5023+ stream
5024+ . write_all ( response. as_bytes ( ) )
5025+ . expect ( "write list response" ) ;
5026+ stream. flush ( ) . expect ( "flush list response" ) ;
5027+ drop ( stream) ;
5028+
5029+ let ( mut stream, _) = listener. accept ( ) . expect ( "accept create request" ) ;
5030+ let mut header_buf = Vec :: new ( ) ;
5031+ let mut temp = [ 0u8 ; 1024 ] ;
5032+ let header_end;
5033+ loop {
5034+ let read = stream. read ( & mut temp) . expect ( "read create request" ) ;
5035+ assert ! ( read > 0 , "request closed before headers" ) ;
5036+ header_buf. extend_from_slice ( & temp[ ..read] ) ;
5037+ if let Some ( pos) = header_buf. windows ( 4 ) . position ( |w| w == b"\r \n \r \n " ) {
5038+ header_end = pos + 4 ;
5039+ break ;
5040+ }
5041+ }
5042+ let headers = String :: from_utf8_lossy ( & header_buf[ ..header_end] ) ;
5043+ assert ! ( headers. starts_with( "POST /v1/api_key HTTP/1.1" ) ) ;
5044+ let content_length = headers
5045+ . split ( "\r \n " )
5046+ . find_map ( |line| {
5047+ let ( name, value) = line. split_once ( ':' ) ?;
5048+ name. eq_ignore_ascii_case ( "content-length" )
5049+ . then ( || value. trim ( ) . parse :: < usize > ( ) . expect ( "content length" ) )
5050+ } )
5051+ . expect ( "content-length header" ) ;
5052+ let mut body = header_buf[ header_end..] . to_vec ( ) ;
5053+ while body. len ( ) < content_length {
5054+ let read = stream. read ( & mut temp) . expect ( "read request body" ) ;
5055+ assert ! ( read > 0 , "request closed before body completed" ) ;
5056+ body. extend_from_slice ( & temp[ ..read] ) ;
5057+ }
5058+ let json: serde_json:: Value =
5059+ serde_json:: from_slice ( & body[ ..content_length] ) . expect ( "parse request body" ) ;
5060+ assert_eq ! ( json. get( "org_id" ) . and_then( |v| v. as_str( ) ) , Some ( "org_123" ) ) ;
5061+ assert ! ( json. get( "org_name" ) . is_none( ) ) ;
5062+ assert ! ( json. get( "name" ) . and_then( |v| v. as_str( ) ) . is_some( ) ) ;
5063+
5064+ let response = concat ! (
5065+ "HTTP/1.1 200 OK\r \n " ,
5066+ "Content-Type: application/json\r \n " ,
5067+ "Content-Length: 17\r \n " ,
5068+ "Connection: close\r \n " ,
5069+ "\r \n " ,
5070+ "{\" key\" :\" new-key\" }"
5071+ ) ;
5072+ stream
5073+ . write_all ( response. as_bytes ( ) )
5074+ . expect ( "write create response" ) ;
5075+ stream. flush ( ) . expect ( "flush create response" ) ;
5076+ } ) ;
5077+
5078+ let mut base = make_base_args ( ) ;
5079+ base. quiet = true ;
5080+ base. quiet_source = Some ( ArgValueSource :: CommandLine ) ;
5081+
5082+ let api_url = format ! ( "http://{addr}" ) ;
5083+ let ctx = make_login_context (
5084+ api_url,
5085+ "https://app.example.test" . to_string ( ) ,
5086+ "org_123" ,
5087+ "Acme" ,
5088+ ) ;
5089+ let client = ApiClient :: new ( & ctx) . expect ( "client" ) ;
5090+
5091+ let key = maybe_create_api_key_for_oauth ( & base, & client)
5092+ . await
5093+ . expect ( "create api key" ) ;
5094+ assert_eq ! ( key, "new-key" ) ;
5095+
5096+ server. join ( ) . expect ( "server join" ) ;
5097+ }
5098+
5099+ #[ tokio:: test]
5100+ async fn maybe_create_api_key_for_oauth_requires_org_id ( ) {
5101+ let mut base = make_base_args ( ) ;
5102+ base. quiet = true ;
5103+ base. quiet_source = Some ( ArgValueSource :: CommandLine ) ;
5104+
5105+ let ctx = make_login_context (
5106+ "https://api.example.test" . to_string ( ) ,
5107+ "https://app.example.test" . to_string ( ) ,
5108+ "" ,
5109+ "Acme" ,
5110+ ) ;
5111+ let client = ApiClient :: new ( & ctx) . expect ( "client" ) ;
5112+
5113+ let err = maybe_create_api_key_for_oauth ( & base, & client)
5114+ . await
5115+ . expect_err ( "missing org_id should fail" ) ;
5116+ let err_text = format ! ( "{err:#}" ) ;
5117+ assert ! (
5118+ err_text. contains( "org_id" ) && err_text. contains( "API key creation" ) ,
5119+ "unexpected error: {err_text}"
5120+ ) ;
5121+ }
5122+
49735123 #[ test]
49745124 fn single_path_agent_is_selected_by_default ( ) {
49755125 let detected = vec ! [ DetectionSignal {
0 commit comments