@@ -289,13 +289,20 @@ pub enum Transport {
289289 Tcp {
290290 /// Port to listen on (0 for OS-assigned).
291291 port : u16 ,
292+ /// Optional connection token. When `None` and the SDK is spawning
293+ /// the CLI, the SDK auto-generates a 128-bit hex token so the
294+ /// loopback listener is safe by default.
295+ connection_token : Option < String > ,
292296 } ,
293297 /// Connect to an already-running CLI server (no process spawning).
294298 External {
295299 /// Hostname or IP of the running server.
296300 host : String ,
297301 /// Port of the running server.
298302 port : u16 ,
303+ /// Optional connection token. Required when the external server
304+ /// was started with a token, ignored otherwise.
305+ connection_token : Option < String > ,
299306 } ,
300307}
301308
@@ -393,23 +400,13 @@ pub struct ClientOptions {
393400 /// auth, telemetry buffers). When set, exported as `COPILOT_HOME` to
394401 /// the spawned CLI process. Useful for sandboxing test runs or
395402 /// running multiple isolated SDK instances side-by-side.
396- pub copilot_home : Option < PathBuf > ,
397- /// Optional connection token for TCP transport. Sent to the CLI in
398- /// the `connect` handshake and exported as `COPILOT_CONNECTION_TOKEN`
399- /// to spawned CLI processes. Required when the CLI server was started
400- /// with a token, ignored otherwise.
401- ///
402- /// When the SDK spawns its own CLI in TCP mode and this is left
403- /// `None`, a UUID is generated automatically so the loopback listener
404- /// is safe by default. Combining with [`Transport::Stdio`] is invalid
405- /// and surfaces as an error from [`Client::start`].
406- pub tcp_connection_token : Option < String > ,
403+ pub base_directory : Option < PathBuf > ,
407404 /// Enable remote session support (Mission Control integration).
408405 /// When `true`, the SDK passes `--remote` to the spawned CLI process so
409406 /// sessions in a GitHub repository working directory are accessible from
410407 /// GitHub web and mobile. Ignored when connecting to an external server
411408 /// via [`Transport::External`].
412- pub remote : bool ,
409+ pub enable_remote_sessions : bool ,
413410}
414411
415412impl std:: fmt:: Debug for ClientOptions {
@@ -442,12 +439,8 @@ impl std::fmt::Debug for ClientOptions {
442439 & self . on_get_trace_context . as_ref ( ) . map ( |_| "<set>" ) ,
443440 )
444441 . field ( "telemetry" , & self . telemetry )
445- . field ( "copilot_home" , & self . copilot_home )
446- . field (
447- "tcp_connection_token" ,
448- & self . tcp_connection_token . as_ref ( ) . map ( |_| "<redacted>" ) ,
449- )
450- . field ( "remote" , & self . remote )
442+ . field ( "base_directory" , & self . base_directory )
443+ . field ( "enable_remote_sessions" , & self . enable_remote_sessions )
451444 . finish ( )
452445 }
453446}
@@ -652,9 +645,8 @@ impl Default for ClientOptions {
652645 session_fs : None ,
653646 on_get_trace_context : None ,
654647 telemetry : None ,
655- copilot_home : None ,
656- tcp_connection_token : None ,
657- remote : false ,
648+ base_directory : None ,
649+ enable_remote_sessions : false ,
658650 }
659651 }
660652}
@@ -800,23 +792,15 @@ impl ClientOptions {
800792
801793 /// Override the directory where the CLI persists its state. Set as
802794 /// `COPILOT_HOME` on the spawned CLI process.
803- pub fn with_copilot_home ( mut self , home : impl Into < PathBuf > ) -> Self {
804- self . copilot_home = Some ( home. into ( ) ) ;
805- self
806- }
807-
808- /// Set the connection token for TCP transport. Sent in the `connect`
809- /// handshake and exported as `COPILOT_CONNECTION_TOKEN` to spawned
810- /// CLI processes.
811- pub fn with_tcp_connection_token ( mut self , token : impl Into < String > ) -> Self {
812- self . tcp_connection_token = Some ( token. into ( ) ) ;
795+ pub fn with_base_directory ( mut self , dir : impl Into < PathBuf > ) -> Self {
796+ self . base_directory = Some ( dir. into ( ) ) ;
813797 self
814798 }
815799
816800 /// Enable remote session support (Mission Control). Passes `--remote`
817801 /// to the spawned CLI process.
818- pub fn with_remote ( mut self , enabled : bool ) -> Self {
819- self . remote = enabled;
802+ pub fn with_enable_remote_sessions ( mut self , enabled : bool ) -> Self {
803+ self . enable_remote_sessions = enabled;
820804 self
821805 }
822806}
@@ -930,39 +914,48 @@ impl Client {
930914 ) ) ;
931915 }
932916 }
933- // Validate token + transport combination. Stdio cannot use a
934- // connection token; auto-generate a UUID when the SDK spawns
935- // its own CLI in TCP mode and no explicit token was set.
936- if let Some ( token) = & options. tcp_connection_token {
937- if token. is_empty ( ) {
938- return Err ( Error :: InvalidConfig (
939- "tcp_connection_token must be a non-empty string" . to_string ( ) ,
940- ) ) ;
917+ // Validate token shape. Stdio variants no longer carry a token
918+ // (enforced by the type). For Tcp/External, empty-string is
919+ // rejected eagerly.
920+ match & options. transport {
921+ Transport :: Tcp {
922+ connection_token : Some ( t) ,
923+ ..
941924 }
942- if matches ! ( options. transport, Transport :: Stdio ) {
925+ | Transport :: External {
926+ connection_token : Some ( t) ,
927+ ..
928+ } if t. is_empty ( ) => {
943929 return Err ( Error :: InvalidConfig (
944- "tcp_connection_token cannot be used with Transport::Stdio " . to_string ( ) ,
930+ "connection_token must be a non-empty string " . to_string ( ) ,
945931 ) ) ;
946932 }
933+ _ => { }
947934 }
948- let effective_connection_token: Option < String > = match & options. transport {
949- Transport :: Stdio => None ,
950- Transport :: Tcp { .. } => Some (
951- options
952- . tcp_connection_token
953- . clone ( )
954- . unwrap_or_else ( generate_connection_token) ,
955- ) ,
956- Transport :: External { .. } => options. tcp_connection_token . clone ( ) ,
935+ // Capture (and where needed, auto-generate) the token actually sent
936+ // to the server. For Tcp, the SDK auto-generates one when the
937+ // caller leaves it unset so the loopback listener is safe by
938+ // default.
939+ let ( mut options, effective_connection_token) = {
940+ let mut options = options;
941+ let effective = match & mut options. transport {
942+ Transport :: Stdio => None ,
943+ Transport :: Tcp {
944+ connection_token, ..
945+ } => {
946+ if connection_token. is_none ( ) {
947+ * connection_token = Some ( generate_connection_token ( ) ) ;
948+ }
949+ connection_token. clone ( )
950+ }
951+ Transport :: External {
952+ connection_token, ..
953+ } => connection_token. clone ( ) ,
954+ } ;
955+ ( options, effective)
957956 } ;
958- let mut options = options;
959- if matches ! ( options. transport, Transport :: Tcp { .. } )
960- && options. tcp_connection_token . is_none ( )
961- {
962- // Auto-generated tokens flow to the spawned CLI via env, so
963- // make the field reflect what we'll actually send.
964- options. tcp_connection_token = effective_connection_token. clone ( ) ;
965- }
957+ let _ = & mut options;
958+ let effective_connection_token: Option < String > = effective_connection_token;
966959 let session_fs_config = options. session_fs . clone ( ) ;
967960 let session_fs_sqlite_declared = session_fs_config
968961 . as_ref ( )
@@ -994,7 +987,11 @@ impl Client {
994987 } ;
995988
996989 let client = match options. transport {
997- Transport :: External { ref host, port } => {
990+ Transport :: External {
991+ ref host,
992+ port,
993+ connection_token : _,
994+ } => {
998995 info ! ( host = %host, port = %port, "connecting to external CLI server" ) ;
999996 let connect_start = Instant :: now ( ) ;
1000997 let stream = TcpStream :: connect ( ( host. as_str ( ) , port) ) . await ?;
@@ -1017,7 +1014,10 @@ impl Client {
10171014 effective_connection_token. clone ( ) ,
10181015 ) ?
10191016 }
1020- Transport :: Tcp { port } => {
1017+ Transport :: Tcp {
1018+ port,
1019+ connection_token : _,
1020+ } => {
10211021 let ( mut child, actual_port) = Self :: spawn_tcp ( & program, & options, port) . await ?;
10221022 let connect_start = Instant :: now ( ) ;
10231023 let stream = TcpStream :: connect ( ( "127.0.0.1" , actual_port) ) . await ?;
@@ -1281,10 +1281,14 @@ impl Client {
12811281 ) ;
12821282 }
12831283 }
1284- if let Some ( home ) = & options. copilot_home {
1285- command. env ( "COPILOT_HOME" , home ) ;
1284+ if let Some ( dir ) = & options. base_directory {
1285+ command. env ( "COPILOT_HOME" , dir ) ;
12861286 }
1287- if let Some ( token) = & options. tcp_connection_token {
1287+ if let Transport :: Tcp {
1288+ connection_token : Some ( token) ,
1289+ ..
1290+ } = & options. transport
1291+ {
12881292 command. env ( "COPILOT_CONNECTION_TOKEN" , token) ;
12891293 }
12901294 for ( key, value) in & options. env {
@@ -1343,7 +1347,7 @@ impl Client {
13431347 }
13441348
13451349 fn remote_args ( options : & ClientOptions ) -> Vec < String > {
1346- if options. remote {
1350+ if options. enable_remote_sessions {
13471351 vec ! [ "--remote" . to_string( ) ]
13481352 } else {
13491353 Vec :: new ( )
@@ -1581,8 +1585,8 @@ impl Client {
15811585 ///
15821586 /// # Handshake sequence
15831587 ///
1584- /// 1. Sends the `connect` JSON-RPC method, forwarding
1585- /// [`ClientOptions::tcp_connection_token`] (or the auto-generated
1588+ /// 1. Sends the `connect` JSON-RPC method, forwarding the
1589+ /// [`Transport`]'s `connection_token` (or the auto-generated
15861590 /// token for SDK-spawned TCP servers) as the `token` param. This
15871591 /// is the canonical handshake used by all SDK languages and is
15881592 /// what the CLI uses to enforce loopback authentication when
@@ -1647,7 +1651,7 @@ impl Client {
16471651
16481652 /// Send the `connect` JSON-RPC handshake. Returns the server's
16491653 /// reported protocol version, or `None` if the server omits it.
1650- /// Forwards [`ClientOptions::tcp_connection_token`] (or the
1654+ /// Forwards the [`Transport`]'s `connection_token` (or the
16511655 /// auto-generated token for SDK-spawned TCP servers) as the `token`
16521656 /// param. Server-side, the token is required when the server was
16531657 /// started with `COPILOT_CONNECTION_TOKEN`.
@@ -1981,16 +1985,6 @@ impl Client {
19811985 pub fn subscribe_lifecycle ( & self ) -> LifecycleSubscription {
19821986 LifecycleSubscription :: new ( self . inner . lifecycle_tx . subscribe ( ) )
19831987 }
1984-
1985- /// Return the current [`ConnectionState`].
1986- ///
1987- /// The state advances to [`Connected`](ConnectionState::Connected) once
1988- /// [`Client::start`] / [`Client::from_streams`] returns successfully and
1989- /// drops to [`Disconnected`](ConnectionState::Disconnected) after
1990- /// [`stop`](Self::stop) or [`force_stop`](Self::force_stop).
1991- pub fn state ( & self ) -> ConnectionState {
1992- * self . inner . state . lock ( )
1993- }
19941988}
19951989
19961990impl Drop for ClientInner {
@@ -2050,7 +2044,7 @@ mod tests {
20502044 . with_use_logged_in_user ( false )
20512045 . with_log_level ( LogLevel :: Debug )
20522046 . with_session_idle_timeout_seconds ( 120 )
2053- . with_remote ( true ) ;
2047+ . with_enable_remote_sessions ( true ) ;
20542048 assert ! ( matches!( opts. program, CliProgram :: Path ( _) ) ) ;
20552049 assert_eq ! ( opts. prefix_args, vec![ std:: ffi:: OsString :: from( "node" ) ] ) ;
20562050 assert_eq ! ( opts. cwd, PathBuf :: from( "/tmp" ) ) ;
@@ -2067,7 +2061,7 @@ mod tests {
20672061 assert_eq ! ( opts. use_logged_in_user, Some ( false ) ) ;
20682062 assert ! ( matches!( opts. log_level, Some ( LogLevel :: Debug ) ) ) ;
20692063 assert_eq ! ( opts. session_idle_timeout_seconds, Some ( 120 ) ) ;
2070- assert ! ( opts. remote ) ;
2064+ assert ! ( opts. enable_remote_sessions ) ;
20712065 }
20722066
20732067 #[ test]
@@ -2270,7 +2264,7 @@ mod tests {
22702264
22712265 #[ test]
22722266 fn build_command_sets_copilot_home_env_when_configured ( ) {
2273- let opts = ClientOptions :: new ( ) . with_copilot_home ( PathBuf :: from ( "/custom/copilot" ) ) ;
2267+ let opts = ClientOptions :: new ( ) . with_base_directory ( PathBuf :: from ( "/custom/copilot" ) ) ;
22742268 let cmd = Client :: build_command ( Path :: new ( "/bin/echo" ) , & opts) ;
22752269 assert_eq ! (
22762270 env_value( & cmd, "COPILOT_HOME" ) ,
@@ -2284,7 +2278,10 @@ mod tests {
22842278
22852279 #[ test]
22862280 fn build_command_sets_connection_token_env_when_configured ( ) {
2287- let opts = ClientOptions :: new ( ) . with_tcp_connection_token ( "secret-token" ) ;
2281+ let opts = ClientOptions :: new ( ) . with_transport ( Transport :: Tcp {
2282+ port : 0 ,
2283+ connection_token : Some ( "secret-token" . to_string ( ) ) ,
2284+ } ) ;
22882285 let cmd = Client :: build_command ( Path :: new ( "/bin/echo" ) , & opts) ;
22892286 assert_eq ! (
22902287 env_value( & cmd, "COPILOT_CONNECTION_TOKEN" ) ,
@@ -2297,26 +2294,25 @@ mod tests {
22972294 }
22982295
22992296 #[ tokio:: test]
2300- async fn start_rejects_token_with_stdio_transport ( ) {
2297+ async fn start_rejects_empty_connection_token ( ) {
23012298 let opts = ClientOptions :: new ( )
2302- . with_tcp_connection_token ( "token-123" )
2299+ . with_transport ( Transport :: Tcp {
2300+ port : 0 ,
2301+ connection_token : Some ( String :: new ( ) ) ,
2302+ } )
23032303 . with_program ( CliProgram :: Path ( PathBuf :: from ( "/bin/echo" ) ) ) ;
23042304 let err = Client :: start ( opts) . await . unwrap_err ( ) ;
23052305 assert ! ( matches!( err, Error :: InvalidConfig ( _) ) , "got {err:?}" ) ;
2306- let Error :: InvalidConfig ( msg) = err else {
2307- unreachable ! ( )
2308- } ;
2309- assert ! (
2310- msg. contains( "Stdio" ) ,
2311- "error should explain the stdio incompatibility: {msg}"
2312- ) ;
23132306 }
23142307
23152308 #[ tokio:: test]
2316- async fn start_rejects_empty_connection_token ( ) {
2309+ async fn start_rejects_empty_external_connection_token ( ) {
23172310 let opts = ClientOptions :: new ( )
2318- . with_tcp_connection_token ( "" )
2319- . with_transport ( Transport :: Tcp { port : 0 } )
2311+ . with_transport ( Transport :: External {
2312+ host : "127.0.0.1" . to_string ( ) ,
2313+ port : 1 ,
2314+ connection_token : Some ( String :: new ( ) ) ,
2315+ } )
23202316 . with_program ( CliProgram :: Path ( PathBuf :: from ( "/bin/echo" ) ) ) ;
23212317 let err = Client :: start ( opts) . await . unwrap_err ( ) ;
23222318 assert ! ( matches!( err, Error :: InvalidConfig ( _) ) , "got {err:?}" ) ;
@@ -2392,7 +2388,7 @@ mod tests {
23922388 #[ test]
23932389 fn remote_args_emit_flag_when_enabled ( ) {
23942390 let opts = ClientOptions {
2395- remote : true ,
2391+ enable_remote_sessions : true ,
23962392 ..Default :: default ( )
23972393 } ;
23982394 assert_eq ! ( Client :: remote_args( & opts) , vec![ "--remote" . to_string( ) ] ) ;
0 commit comments