Skip to content

Commit 0399963

Browse files
committed
Merge remote-tracking branch 'origin/main' into tclem/rust-sdk-release-prep
2 parents 59557da + 6a0e065 commit 0399963

74 files changed

Lines changed: 2330 additions & 266 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/setup/backend-services.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,40 @@ By default the headless server only accepts connections from loopback (`127.0.0.
7373
copilot --headless --host 0.0.0.0 --port 4321
7474
```
7575

76-
For production, run it as a system service or in a container:
76+
For production, run it as a system service or in a container.
77+
78+
> **Note:** There is no official pre-built Docker image for the Copilot CLI. You can build your own from the [GitHub releases](https://github.com/github/copilot-cli/releases):
79+
80+
```dockerfile
81+
FROM debian:bookworm-slim
82+
ARG COPILOT_VERSION=1.0.7
83+
RUN apt-get update \
84+
&& apt-get install -y --no-install-recommends ca-certificates wget \
85+
&& ARCH=$(dpkg --print-architecture) \
86+
&& case "${ARCH}" in amd64) COPILOT_ARCH="x64" ;; arm64) COPILOT_ARCH="arm64" ;; *) echo "Unsupported: ${ARCH}" && exit 1 ;; esac \
87+
&& wget -q "https://github.com/github/copilot-cli/releases/download/v${COPILOT_VERSION}/copilot-linux-${COPILOT_ARCH}.tar.gz" \
88+
&& tar -xzf "copilot-linux-${COPILOT_ARCH}.tar.gz" \
89+
&& mv copilot /usr/local/bin/ \
90+
&& rm "copilot-linux-${COPILOT_ARCH}.tar.gz" \
91+
&& apt-get purge -y wget && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
92+
ENTRYPOINT ["copilot"]
93+
```
94+
95+
```bash
96+
# Build the image
97+
docker build --build-arg COPILOT_VERSION=1.0.7 -t copilot-cli:latest .
98+
99+
# For remote deployments (Kubernetes, ACI, etc.), push to your registry
100+
docker tag copilot-cli:latest your-registry/copilot-cli:latest
101+
docker push your-registry/copilot-cli:latest
102+
```
77103

78104
```bash
79105
# Docker — must bind to 0.0.0.0 so the container's published port is reachable
80106
docker run -d --name copilot-cli \
81107
-p 4321:4321 \
82108
-e COPILOT_GITHUB_TOKEN="$TOKEN" \
83-
ghcr.io/github/copilot-cli:latest \
109+
copilot-cli:latest \
84110
--headless --host 0.0.0.0 --port 4321
85111

86112
# systemd
@@ -420,7 +446,7 @@ version: "3.8"
420446

421447
services:
422448
copilot-cli:
423-
image: ghcr.io/github/copilot-cli:latest
449+
image: copilot-cli:latest # See "Step 1" above for how to build this image
424450
command: ["--headless", "--host", "0.0.0.0", "--port", "4321"]
425451
environment:
426452
- COPILOT_GITHUB_TOKEN=${COPILOT_GITHUB_TOKEN}

docs/setup/scaling.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ spec:
519519
spec:
520520
containers:
521521
- name: copilot-cli
522-
image: ghcr.io/github/copilot-cli:latest
522+
image: your-registry/copilot-cli:latest # See backend-services.md for how to build and push this image
523523
args: ["--headless", "--host", "0.0.0.0", "--port", "4321"]
524524
env:
525525
- name: COPILOT_GITHUB_TOKEN
@@ -576,7 +576,7 @@ flowchart TB
576576
```yaml
577577
containers:
578578
- name: copilot-cli
579-
image: ghcr.io/github/copilot-cli:latest
579+
image: your-registry/copilot-cli:latest # See backend-services.md for how to build and push this image
580580
command: ["copilot", "--headless", "--host", "0.0.0.0", "--port", "4321"]
581581
volumeMounts:
582582
- name: session-storage

dotnet/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ new CopilotClient(CopilotClientOptions? options = null)
7575
- `LogLevel` - Log level (default: "info")
7676
- `AutoStart` - Auto-start server (default: true)
7777
- `Cwd` - Working directory for the CLI process
78+
- `CopilotHome` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When not set, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using `CliUrl`.
7879
- `Environment` - Environment variables to pass to the CLI process
7980
- `Logger` - `ILogger` instance for SDK logging
8081
- `GitHubToken` - GitHub token for authentication. When provided, takes priority over other auth methods.

dotnet/src/Client.cs

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

dotnet/src/Generated/Rpc.cs

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/JsonRpc.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,5 +831,8 @@ internal sealed class ConnectionLostException() : IOException("The JSON-RPC conn
831831
/// </summary>
832832
internal sealed class RemoteRpcException(string message, int errorCode, Exception? innerException = null) : Exception(message, innerException)
833833
{
834+
/// <summary>JSON-RPC 2.0 reserved error code: requested method does not exist.</summary>
835+
public const int MethodNotFoundErrorCode = -32601;
836+
834837
public int ErrorCode { get; } = errorCode;
835838
}

0 commit comments

Comments
 (0)