Skip to content

SDK 0.3.0 fails to connect to remote headless CLI 1.0.36 over JSON-RPC #1153

@mhartvig

Description

@mhartvig

Summary

GitHub.Copilot.SDK 0.3.0 fails to connect to a remote @github/copilot 1.0.36 CLI running in headless JSON-RPC mode (--headless --port 4321). The same setup works against SDK 0.2.2 / CLI 1.0.21. The pairing in each case is the matching SDK→CLI version that ships with that SDK release, so this is not a deliberately
mismatched combination — it only manifests when the SDK talks to a CLI it does not spawn itself.

Environment

  • SDK: GitHub.Copilot.SDK 0.3.0 (NuGet)
  • CLI: @github/copilot 1.0.36, installed globally inside node:20-bookworm-slim
  • CLI launch: copilot --no-auto-update --headless --port 4321
  • Client config: CopilotClientOptions { CliUrl = "localhost:4322", UseStdio = false } (host port 4322 mapped to container 4321)
  • Host OS: Windows 11, .NET 10
  • Auth: COPILOT_GITHUB_TOKEN PAT with Copilot access, passed into the container via -e

Expected

CopilotClient.StartAsync connects, CreateSessionAsync succeeds, and SendAndWaitAsync({ Prompt = "Hi" }) returns a response — same as the 0.2.2 / 1.0.21 baseline.

Actual

StartAsync (or the first session call — whichever is hit first) throws a JSON-RPC error:

Unhandled exception. System.IO.IOException: Communication error with Copilot CLI: The JSON-RPC connection with the remote party was lost before the request could complete.
---> StreamJsonRpc.ConnectionLostException: The JSON-RPC connection with the remote party was lost before the request could complete.
---> System.OperationCanceledException: The operation was canceled.
  at System.Threading.CancellationToken.ThrowOperationCanceledException()
  at System.Threading.CancellationToken.ThrowIfCancellationRequested()
  at StreamJsonRpc.MessageHandlerBase.WriteAsync(JsonRpcMessage content, CancellationToken cancellationToken)
  at StreamJsonRpc.JsonRpc.SendAsync(JsonRpcMessage message, CancellationToken cancellationToken)
  at StreamJsonRpc.JsonRpc.InvokeCoreAsync(JsonRpcRequest request, Type expectedResultType, CancellationToken cancellationToken)
  --- End of inner exception stack trace ---
  at StreamJsonRpc.JsonRpc.InvokeCoreAsync(JsonRpcRequest request, Type expectedResultType, CancellationToken cancellationToken)
  at StreamJsonRpc.JsonRpc.InvokeCoreAsync[TResult](RequestId id, String targetName, IReadOnlyList`1 arguments, IReadOnlyList`1 positionalArgumentDeclaredTypes, IReadOnlyDictionary`2 namedArgumentDeclaredTypes, CancellationToken cancellationToken, Boolean isParameterObject)
  at GitHub.Copilot.SDK.CopilotClient.InvokeRpcAsync[T](JsonRpc rpc, String method, Object[] args, StringBuilder stderrBuffer, CancellationToken cancellationToken)
  --- End of inner exception stack trace ---
  at GitHub.Copilot.SDK.CopilotClient.InvokeRpcAsync[T](JsonRpc rpc, String method, Object[] args, StringBuilder stderrBuffer, CancellationToken cancellationToken)
  at GitHub.Copilot.SDK.CopilotClient.VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken)
  at GitHub.Copilot.SDK.CopilotClient.<StartAsync>g__StartCoreAsync|25_0(CancellationToken ct)
  at GitHub.Copilot.SDK.CopilotClient.CleanupConnectionAsync(List`1 errors)
  at GitHub.Copilot.SDK.CopilotClient.ForceStopAsync()
  at GitHub.Copilot.SDK.CopilotClient.DisposeAsync()
  at Program.<Main>$(String[] args) in C:\Code\copilot-sdk-docker-remote\src\BrokenRepro\Program.cs:line 52
  at Program.<Main>(String[] args)

The container is reachable on the mapped port (TCP connect succeeds before the SDK call) and the CLI process inside the container is running and listening. The same client code against CLI 1.0.21 with SDK 0.2.2 succeeds against an otherwise identical container.

Repro

Minimal repro (Dockerfile + two side-by-side console projects, one per SDK/CLI pairing):
https://github.com/mhartvig/copilot-sdk-docker-remote

./run.sh working --token # SDK 0.2.2 + CLI 1.0.21 — succeeds
./run.sh broken --token # SDK 0.3.0 + CLI 1.0.36 — fails

Each launcher builds the pinned CLI image, starts the container with --headless --port 4321, waits for the port, then runs:

var clientOptions = new CopilotClientOptions { CliUrl = cliUrl, UseStdio = false };
await using var client = new CopilotClient(clientOptions);
await client.StartAsync(ct);

var session = await client.CreateSessionAsync(new SessionConfig
{
SessionId = "user-123-task-456",
OnPermissionRequest = PermissionHandler.ApproveAll,
});

await session.SendAndWaitAsync(new MessageOptions { Prompt = "Hi" });

Notes

  • UseStdio = false + CliUrl is used, so the SDK is acting purely as a JSON-RPC client; it is not spawning the bundled CLI binary that ships with the NuGet package.
  • The CLI version installed in the container matches the version the SDK release ships with, so the SDK/CLI pair is the intended pairing — just not the one being launched in-process.
  • Container is stock node:20-bookworm-slim plus git and ca-certificates; no proxy or custom networking.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions