@@ -70,6 +70,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
7070 private bool _disposed ;
7171 private readonly int ? _optionsPort ;
7272 private readonly string ? _optionsHost ;
73+ private readonly string ? _effectiveConnectionToken ;
7374 private int ? _actualPort ;
7475 private int ? _negotiatedProtocolVersion ;
7576 private List < ModelInfo > ? _modelsCache ;
@@ -123,23 +124,43 @@ public CopilotClient(CopilotClientOptions? options = null)
123124 _options = options ?? new ( ) ;
124125
125126 // Validate mutually exclusive options
126- if ( ! string . IsNullOrEmpty ( _options . CliUrl ) && _options . CliPath != null )
127+ if ( ! string . IsNullOrEmpty ( _options . CliUrl ) && ( _options . UseStdio == true || _options . CliPath != null ) )
127128 {
128- throw new ArgumentException ( "CliUrl is mutually exclusive with CliPath" ) ;
129+ throw new ArgumentException ( "CliUrl is mutually exclusive with UseStdio and CliPath" ) ;
129130 }
130131
131- // When CliUrl is provided, disable UseStdio (we connect to an external server, not spawn one)
132+ // When CliUrl is provided, force TCP mode (we connect to an external server, not spawn one)
132133 if ( ! string . IsNullOrEmpty ( _options . CliUrl ) )
133134 {
134135 _options . UseStdio = false ;
135136 }
137+ else
138+ {
139+ _options . UseStdio ??= true ;
140+ }
136141
137142 // Validate auth options with external server
138143 if ( ! string . IsNullOrEmpty ( _options . CliUrl ) && ( ! string . IsNullOrEmpty ( _options . GitHubToken ) || _options . UseLoggedInUser != null ) )
139144 {
140145 throw new ArgumentException ( "GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)" ) ;
141146 }
142147
148+ if ( _options . TcpConnectionToken is not null )
149+ {
150+ if ( _options . TcpConnectionToken . Length == 0 )
151+ {
152+ throw new ArgumentException ( "TcpConnectionToken must be a non-empty string" ) ;
153+ }
154+ if ( _options . UseStdio == true )
155+ {
156+ throw new ArgumentException ( "TcpConnectionToken cannot be used with UseStdio = true" ) ;
157+ }
158+ }
159+
160+ var sdkSpawnsCli = _options . UseStdio == false && string . IsNullOrEmpty ( _options . CliUrl ) ;
161+ _effectiveConnectionToken = _options . TcpConnectionToken
162+ ?? ( sdkSpawnsCli ? Guid . NewGuid ( ) . ToString ( ) : null ) ;
163+
143164 _logger = _options . Logger ?? NullLogger . Instance ;
144165 _onListModels = _options . OnListModels ;
145166
@@ -216,7 +237,7 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
216237 else
217238 {
218239 // Child process (stdio or TCP)
219- var ( cliProcess , portOrNull , stderrBuffer ) = await StartCliServerAsync ( _options , _logger , ct ) ;
240+ var ( cliProcess , portOrNull , stderrBuffer ) = await StartCliServerAsync ( _options , _effectiveConnectionToken , _logger , ct ) ;
220241 _actualPort = portOrNull ;
221242 result = ConnectToServerAsync ( cliProcess , portOrNull is null ? null : "localhost" , portOrNull , stderrBuffer , ct ) ;
222243 }
@@ -506,7 +527,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
506527 Traceparent : traceparent ,
507528 Tracestate : tracestate ,
508529 ModelCapabilities : config . ModelCapabilities ,
509- GitHubToken : config . GitHubToken ) ;
530+ GitHubToken : config . GitHubToken ,
531+ InstructionDirectories : config . InstructionDirectories ) ;
510532
511533 var response = await InvokeRpcAsync < CreateSessionResponse > (
512534 connection . Rpc , "session.create" , [ request ] , cancellationToken ) ;
@@ -633,7 +655,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
633655 Tracestate : tracestate ,
634656 ModelCapabilities : config . ModelCapabilities ,
635657 GitHubToken : config . GitHubToken ,
636- ContinuePendingWork : config . ContinuePendingWork ) ;
658+ ContinuePendingWork : config . ContinuePendingWork ,
659+ InstructionDirectories : config . InstructionDirectories ) ;
637660
638661 var response = await InvokeRpcAsync < ResumeSessionResponse > (
639662 connection . Rpc , "session.resume" , [ request ] , cancellationToken ) ;
@@ -1122,30 +1145,42 @@ private void ConfigureSessionFsHandlers(CopilotSession session, Func<CopilotSess
11221145 private async Task VerifyProtocolVersionAsync ( Connection connection , CancellationToken cancellationToken )
11231146 {
11241147 var maxVersion = SdkProtocolVersion . GetVersion ( ) ;
1125- var pingResponse = await InvokeRpcAsync < PingResponse > (
1126- connection . Rpc , "ping" , [ new PingRequest ( ) ] , connection . StderrBuffer , cancellationToken ) ;
1148+ int ? serverVersion ;
1149+ try
1150+ {
1151+ var connectResponse = await InvokeRpcAsync < ConnectResult > (
1152+ connection . Rpc , "connect" , [ new ConnectRequest { Token = _effectiveConnectionToken } ] , connection . StderrBuffer , cancellationToken ) ;
1153+ serverVersion = ( int ) connectResponse . ProtocolVersion ;
1154+ }
1155+ catch ( RemoteRpcException ex ) when ( ex . ErrorCode == RemoteRpcException . MethodNotFoundErrorCode )
1156+ {
1157+ // Legacy server without `connect`; fall back to `ping`. A token, if any,
1158+ // is silently dropped — the legacy server can't enforce one.
1159+ var pingResponse = await InvokeRpcAsync < PingResponse > (
1160+ connection . Rpc , "ping" , [ new PingRequest ( ) ] , connection . StderrBuffer , cancellationToken ) ;
1161+ serverVersion = pingResponse . ProtocolVersion ;
1162+ }
11271163
1128- if ( ! pingResponse . ProtocolVersion . HasValue )
1164+ if ( ! serverVersion . HasValue )
11291165 {
11301166 throw new InvalidOperationException (
11311167 $ "SDK protocol version mismatch: SDK supports versions { MinProtocolVersion } -{ maxVersion } , " +
11321168 $ "but server does not report a protocol version. " +
11331169 $ "Please update your server to ensure compatibility.") ;
11341170 }
11351171
1136- var serverVersion = pingResponse . ProtocolVersion . Value ;
1137- if ( serverVersion < MinProtocolVersion || serverVersion > maxVersion )
1172+ if ( serverVersion . Value < MinProtocolVersion || serverVersion . Value > maxVersion )
11381173 {
11391174 throw new InvalidOperationException (
11401175 $ "SDK protocol version mismatch: SDK supports versions { MinProtocolVersion } -{ maxVersion } , " +
1141- $ "but server reports version { serverVersion } . " +
1176+ $ "but server reports version { serverVersion . Value } . " +
11421177 $ "Please update your SDK or server to ensure compatibility.") ;
11431178 }
11441179
1145- _negotiatedProtocolVersion = serverVersion ;
1180+ _negotiatedProtocolVersion = serverVersion . Value ;
11461181 }
11471182
1148- private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort , StringBuilder StderrBuffer ) > StartCliServerAsync ( CopilotClientOptions options , ILogger logger , CancellationToken cancellationToken )
1183+ private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort , StringBuilder StderrBuffer ) > StartCliServerAsync ( CopilotClientOptions options , string ? connectionToken , ILogger logger , CancellationToken cancellationToken )
11491184 {
11501185 // Use explicit path, COPILOT_CLI_PATH env var (from options.Environment or process env), or bundled CLI - no PATH fallback
11511186 var envCliPath = options . Environment is not null && options . Environment . TryGetValue ( "COPILOT_CLI_PATH" , out var envValue ) ? envValue
@@ -1163,7 +1198,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
11631198
11641199 args . AddRange ( [ "--headless" , "--no-auto-update" , "--log-level" , options . LogLevel ] ) ;
11651200
1166- if ( options . UseStdio )
1201+ if ( options . UseStdio == true )
11671202 {
11681203 args . Add ( "--stdio" ) ;
11691204 }
@@ -1197,7 +1232,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
11971232 FileName = fileName ,
11981233 Arguments = string . Join ( " " , processArgs . Select ( ProcessArgumentEscaper . Escape ) ) ,
11991234 UseShellExecute = false ,
1200- RedirectStandardInput = options . UseStdio ,
1235+ RedirectStandardInput = options . UseStdio == true ,
12011236 RedirectStandardOutput = true ,
12021237 RedirectStandardError = true ,
12031238 WorkingDirectory = options . Cwd ,
@@ -1221,6 +1256,16 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
12211256 startInfo . Environment [ "COPILOT_SDK_AUTH_TOKEN" ] = options . GitHubToken ;
12221257 }
12231258
1259+ if ( ! string . IsNullOrEmpty ( connectionToken ) )
1260+ {
1261+ startInfo . Environment [ "COPILOT_CONNECTION_TOKEN" ] = connectionToken ;
1262+ }
1263+
1264+ if ( ! string . IsNullOrEmpty ( options . CopilotHome ) )
1265+ {
1266+ startInfo . Environment [ "COPILOT_HOME" ] = options . CopilotHome ;
1267+ }
1268+
12241269 // Set telemetry environment variables if configured
12251270 if ( options . Telemetry is { } telemetry )
12261271 {
@@ -1258,7 +1303,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
12581303 } , cancellationToken ) ;
12591304
12601305 var detectedLocalhostTcpPort = ( int ? ) null ;
1261- if ( ! options . UseStdio )
1306+ if ( options . UseStdio != true )
12621307 {
12631308 // Wait for port announcement
12641309 using var cts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
@@ -1324,7 +1369,7 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
13241369 Stream inputStream , outputStream ;
13251370 NetworkStream ? networkStream = null ;
13261371
1327- if ( _options . UseStdio )
1372+ if ( _options . UseStdio == true )
13281373 {
13291374 if ( cliProcess == null )
13301375 {
@@ -1650,7 +1695,8 @@ internal record CreateSessionRequest(
16501695 string ? Traceparent = null ,
16511696 string ? Tracestate = null ,
16521697 ModelCapabilitiesOverride ? ModelCapabilities = null ,
1653- string ? GitHubToken = null ) ;
1698+ string ? GitHubToken = null ,
1699+ IList < string > ? InstructionDirectories = null ) ;
16541700
16551701 internal record ToolDefinition (
16561702 string Name ,
@@ -1707,7 +1753,8 @@ internal record ResumeSessionRequest(
17071753 string ? Tracestate = null ,
17081754 ModelCapabilitiesOverride ? ModelCapabilities = null ,
17091755 string ? GitHubToken = null ,
1710- bool ? ContinuePendingWork = null ) ;
1756+ bool ? ContinuePendingWork = null ,
1757+ IList < string > ? InstructionDirectories = null ) ;
17111758
17121759 internal record ResumeSessionResponse (
17131760 string SessionId ,
0 commit comments