Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9aabbf6
Integrate out-of-process Rust runtime wrapper
roji Aug 16, 2026
884bac6
Validate runtime wrapper across SDK harnesses
roji Aug 16, 2026
e2e8a49
Use residual CLI for Python FFI test
roji Aug 17, 2026
8456440
Honor per-client runtime environment in .NET
roji Aug 17, 2026
270449e
Handle Rust session requests during creation
roji Aug 17, 2026
83c4c42
Skip Go telemetry callback test in-process
roji Aug 17, 2026
6f208e1
Add legacy CLI launch escape hatch
roji Aug 17, 2026
c4658bc
Remove residual Node runtime compatibility
roji Aug 19, 2026
9fb69ff
Remove COPILOT_RUNTIME_PATH override
roji Aug 19, 2026
703b238
fix(rust): materialize runtime launch contract
roji Aug 19, 2026
d39161a
Remove residual runtime host contract
roji Aug 19, 2026
92a25fd
fix(rust): materialize sibling CLI host
roji Aug 19, 2026
69940f4
fix(rust): create runtime install directory
roji Aug 19, 2026
903390a
Complete managed runtime bundle materialization
roji Aug 20, 2026
54ef6d5
Remove managed SEA staging for hostless runtime
roji Aug 21, 2026
75f0801
Stage auxiliary runtime assets from npm packages
roji Aug 25, 2026
95c7bca
Exclude runtime package documentation from staging
roji Aug 25, 2026
fefe0ff
Finalize runtime wrapper integration after rebase
roji Aug 26, 2026
51e7d15
Use executable cache for Node runtime wrapper
roji Aug 26, 2026
455f17b
Temporarily skip extension-host E2E coverage
roji Aug 26, 2026
c879d17
Address runtime wrapper review feedback
roji Aug 26, 2026
44d3f3e
Stop resolving SEA for in-process hosting
roji Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ new CopilotClient(CopilotClientOptions? options = null)
- `RuntimeConnection.ForTcp(port = 0, connectionToken?, path?, args?)` — spawns the runtime as a child process listening on a TCP port. `port = 0` auto-allocates; if a non-zero port is already in use, startup fails (no fallback). Use `CopilotClient.RuntimePort` after `StartAsync` to read the assigned port. `connectionToken` is required if other clients will connect via `RuntimeConnection.ForUri(...)`.
- `RuntimeConnection.ForUri(url, connectionToken?)` — connects to an already-running runtime at `url` (e.g., `"localhost:8080"`). Does not spawn a process.

Managed stdio and TCP connections use the bundled `copilot-runtime[.exe]` and
adjacent `runtime.node` by default. An explicit connection path or
`COPILOT_CLI_PATH` overrides the bundled runtime.
Managed launch fails if the bundled wrapper pair is unavailable.

#### Methods

##### `StartAsync(): Task`
Expand Down
126 changes: 95 additions & 31 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ namespace GitHub.Copilot;
/// </example>
public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
{
private const string ExplicitBundledCliMarker = ".copilot-explicit-cli";
/// <summary>
/// Minimum protocol version this SDK can communicate with.
/// </summary>
Expand Down Expand Up @@ -416,9 +417,19 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
ffiArgs.Add("--remote");
}

var explicitCliPath = System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
if (string.IsNullOrEmpty(explicitCliPath))
{
explicitCliPath = null;
}
var ffiRuntimePath = explicitCliPath is null
? GetBundledNativePath(FfiRuntimeHost.GetRuntimeLibraryFileName(), out var searchedRuntime)
?? throw new InvalidOperationException(
$"In-process FFI runtime library not found at '{searchedRuntime}'.")
: ResolveRuntimePathForExplicitCli(explicitCliPath);
var ffiHost = FfiRuntimeHost.Create(
ResolveCliPathForFfi(),
GetNapiPrebuildsFolderOrThrow(),
ffiRuntimePath,
explicitCliPath,
ffiEnvironment,
ffiArgs,
_logger);
Expand Down Expand Up @@ -2106,17 +2117,19 @@ private static void ApplyTelemetryEnvironment(IDictionary<string, string?> envir
var tcpConnection = _connection as TcpRuntimeConnection;
var useStdio = _connection is StdioRuntimeConnection;

// Use explicit path, COPILOT_CLI_PATH env var (from the connection's
// Environment, options.Environment, or process env), or bundled runtime - no PATH fallback
var envCliPath =
(childProcessConnection.Environment is not null && childProcessConnection.Environment.TryGetValue("COPILOT_CLI_PATH", out var connEnvValue) ? connEnvValue : null)
?? (options.Environment is not null && options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) ? envValue : null)
?? System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
var cliPath = childProcessConnection.Path
?? envCliPath
?? GetBundledCliPath(out var searchedPath)
?? throw new InvalidOperationException($"Copilot runtime not found at '{searchedPath}'. Ensure the SDK NuGet package was restored correctly or provide an explicit RuntimeConnection.ForStdio(path: ...) / RuntimeConnection.ForTcp(path: ...).");
var cliPathSource = childProcessConnection.Path is not null ? "Options" : envCliPath is not null ? "Environment" : "Bundled";
// Explicit CLI paths preserve the legacy launch contract. Otherwise use
// the bundled native runtime pair.
var configuredEnvironment = childProcessConnection.Environment ?? options.Environment;
var envCliPath = configuredEnvironment is not null
? configuredEnvironment.TryGetValue("COPILOT_CLI_PATH", out var configuredCliPath) ? configuredCliPath : null
: System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
var launch = childProcessConnection.Path is not null
? new RuntimeLaunch(childProcessConnection.Path, "Options")
: envCliPath is not null
? new RuntimeLaunch(envCliPath, "Environment")
: GetBundledRuntimeLaunch();
var cliPath = launch.Executable;
var cliPathSource = launch.Source;
var args = new List<string>();

if (childProcessConnection.Args != null)
Expand Down Expand Up @@ -2298,7 +2311,11 @@ private static void ApplyTelemetryEnvironment(IDictionary<string, string?> envir

private static string? GetBundledCliPath(out string searchedPath)
{
var binaryName = OperatingSystem.IsWindows() ? "copilot.exe" : "copilot";
return GetBundledNativePath(OperatingSystem.IsWindows() ? "copilot.exe" : "copilot", out searchedPath);
}

private static string? GetBundledNativePath(string binaryName, out string searchedPath)
{
// Always use portable RID (e.g., linux-x64) to match the build-time placement,
// since distro-specific RIDs (e.g., ubuntu.24.04-x64) are normalized at build time.
var rid = GetPortableRid()
Expand All @@ -2307,6 +2324,57 @@ private static void ApplyTelemetryEnvironment(IDictionary<string, string?> envir
return File.Exists(searchedPath) ? searchedPath : null;
}

private static RuntimeLaunch GetBundledRuntimeLaunch()
{
_ = GetBundledNativePath(
OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime",
out var searchedWrapper);
var directory = Path.GetDirectoryName(searchedWrapper)!;
var runtimeNode = Path.Combine(directory, "runtime.node");
Comment thread
roji marked this conversation as resolved.
var explicitCliMarker = Path.Combine(directory, ExplicitBundledCliMarker);
Comment thread
roji marked this conversation as resolved.
if (!File.Exists(searchedWrapper)
&& !File.Exists(runtimeNode)
&& File.Exists(explicitCliMarker)
&& GetBundledCliPath(out _) is { } explicitCli)
{
return new RuntimeLaunch(explicitCli, "Bundled explicit CLI");
}
return ValidateRuntimePair(searchedWrapper, "Bundled runtime");
}

private static RuntimeLaunch ValidateRuntimePair(string wrapper, string source)
{
var runtimeNode = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(wrapper))!, "runtime.node");
Comment thread
roji marked this conversation as resolved.
if (!File.Exists(wrapper))
{
throw new InvalidOperationException($"Copilot runtime wrapper not found at '{wrapper}'.");
}
if (!File.Exists(runtimeNode))
{
throw new InvalidOperationException(
$"Copilot runtime wrapper at '{wrapper}' is missing its adjacent runtime.node at '{runtimeNode}'.");
}
if (new FileInfo(wrapper).Length == 0 || new FileInfo(runtimeNode).Length == 0)
{
throw new InvalidOperationException("Copilot runtime wrapper and adjacent runtime.node must both be non-empty.");
}
#if NET8_0_OR_GREATER
if (!OperatingSystem.IsWindows())
{
var mode = File.GetUnixFileMode(wrapper);
const UnixFileMode executeBits =
UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
if ((mode & executeBits) == 0)
{
File.SetUnixFileMode(wrapper, mode | executeBits);
}
}
#endif
return new RuntimeLaunch(wrapper, source);
}

private sealed record RuntimeLaunch(string Executable, string Source);

private static string? GetPortableRid()
{
string os;
Expand All @@ -2330,26 +2398,22 @@ private static void ApplyTelemetryEnvironment(IDictionary<string, string?> envir
return arch != null ? $"{os}-{arch}" : null;
}

private string ResolveCliPathForFfi()
private static string ResolveRuntimePathForExplicitCli(string cliPath)
{
var envCliPath = _options.Environment is not null && _options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue)
? envValue
: System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
if (!string.IsNullOrEmpty(envCliPath))
var fullEntrypoint = Path.GetFullPath(cliPath);
var directory = Path.GetDirectoryName(fullEntrypoint)
?? throw new InvalidOperationException($"Could not determine directory for '{cliPath}'.");
var flatLibraryPath = Path.Combine(directory, FfiRuntimeHost.GetRuntimeLibraryFileName());
if (File.Exists(flatLibraryPath))
{
return envCliPath;
return flatLibraryPath;
}

// Fall back to the bundled single-file CLI the same way stdio discovers it.
// It embeds its own Node and is spawned directly as `copilot --embedded-host`,
// with the sibling cdylib loaded in-process (FfiRuntimeHost.Create prefers the
// flat `libcopilot_runtime.so`/`copilot_runtime.dll` next to the CLI, falling
// back to the dev `prebuilds/<folder>/runtime.node` layout).
var bundled = GetBundledCliPath(out var searchedPath);
return bundled
?? throw new InvalidOperationException(
"In-process FFI hosting requires the Copilot CLI. Set the COPILOT_CLI_PATH "
+ $"environment variable, or ensure the bundled CLI is present (looked in '{searchedPath}').");
var prebuildsLibraryPath = Path.Combine(
directory, "prebuilds", GetNapiPrebuildsFolderOrThrow(), "runtime.node");
return File.Exists(prebuildsLibraryPath)
? prebuildsLibraryPath
: throw new InvalidOperationException(
$"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'.");
}

/// <summary>
Expand Down
84 changes: 32 additions & 52 deletions dotnet/src/FfiRuntimeHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ namespace GitHub.Copilot;
/// and communicating over stdio/TCP.
/// </summary>
/// <remarks>
/// The Rust <c>host_start</c> export spawns the residual TypeScript worker itself —
/// typically the packaged single-file CLI (<c>copilot --embedded-host</c>, which embeds
/// its own Node) or, for dev, <c>node dist-cli/index.js --embedded-host</c> — so the .NET
/// host never launches Node directly. JSON-RPC frames are pumped across the ABI: writes go
/// to <c>connection_write</c>; inbound frames arrive on a native callback that feeds
/// The Rust <c>host_start</c> export constructs the server synchronously in this
/// process. JSON-RPC frames are pumped across the ABI: writes go to
/// <c>connection_write</c>; inbound frames arrive on a native callback that feeds
/// <see cref="ReceiveStream"/>.
/// <para>
/// The native interop layer has two implementations selected by target framework. On
Expand All @@ -41,7 +39,7 @@ internal sealed partial class FfiRuntimeHost : IDisposable
private const string LibraryName = "copilot_runtime";

private readonly ILogger _logger;
private readonly string _cliEntrypoint;
private readonly string? _cliEntrypoint;
private readonly string _libraryPath;
private readonly IReadOnlyDictionary<string, string>? _environment;
private readonly IReadOnlyList<string> _args;
Expand All @@ -53,7 +51,7 @@ internal sealed partial class FfiRuntimeHost : IDisposable
private uint _connectionId;
private bool _disposed;

private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictionary<string, string>? environment, IReadOnlyList<string> args, ILogger logger)
private FfiRuntimeHost(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary<string, string>? environment, IReadOnlyList<string> args, ILogger logger)
{
_libraryPath = libraryPath;
_cliEntrypoint = cliEntrypoint;
Expand All @@ -70,58 +68,42 @@ private FfiRuntimeHost(string libraryPath, string cliEntrypoint, IReadOnlyDictio
?? throw new InvalidOperationException("FfiRuntimeHost has not been started.");

/// <summary>
/// Loads the cdylib next to the given CLI entrypoint and prepares the FFI host.
/// The entrypoint is either the packaged single-file CLI binary (e.g.
/// <c>runtimes/&lt;rid&gt;/native/copilot</c>) or, for dev, a <c>.js</c> file (e.g.
/// <c>dist-cli/index.js</c>) launched via <c>node</c>. The cdylib is resolved
/// relative to the entrypoint directory, preferring the flat, natural
/// shared-library name the .NET build emits (e.g. <c>libcopilot_runtime.so</c>)
/// and falling back to the dev tarball layout
/// <c>prebuilds/&lt;prebuildsFolder&gt;/runtime.node</c>, where
/// <paramref name="prebuildsFolder"/> is the napi-rs
/// <c>&lt;node-platform&gt;-&lt;arch&gt;</c> folder name (e.g. <c>win32-x64</c>).
/// Loads the runtime cdylib and prepares the FFI host.
/// </summary>
public static FfiRuntimeHost Create(string cliEntrypoint, string prebuildsFolder, IReadOnlyDictionary<string, string>? environment, IReadOnlyList<string> args, ILogger logger)
public static FfiRuntimeHost Create(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary<string, string>? environment, IReadOnlyList<string> args, ILogger logger)
{
var fullEntrypoint = Path.GetFullPath(cliEntrypoint);
var distDir = Path.GetDirectoryName(fullEntrypoint)
?? throw new InvalidOperationException($"Could not determine directory for '{cliEntrypoint}'.");

// Bundled .NET layout: flat, natural shared-library name next to the CLI.
var flatLibraryPath = Path.Combine(distDir, GetRuntimeLibraryFileName());
// Dev/tarball layout: dist-cli/prebuilds/<node-platform>-<arch>/runtime.node.
var prebuildsLibraryPath = Path.Combine(distDir, "prebuilds", prebuildsFolder, "runtime.node");

var libraryPath = File.Exists(flatLibraryPath) ? flatLibraryPath
: File.Exists(prebuildsLibraryPath) ? prebuildsLibraryPath
: throw new InvalidOperationException(
$"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'.");

PrepareNativeLibrary(libraryPath);
return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args, logger);
var fullLibraryPath = Path.GetFullPath(libraryPath);
if (!File.Exists(fullLibraryPath))
{
throw new InvalidOperationException($"FFI runtime library not found at '{fullLibraryPath}'.");
}
PrepareNativeLibrary(fullLibraryPath);
return new FfiRuntimeHost(
fullLibraryPath,
cliEntrypoint is null ? null : Path.GetFullPath(cliEntrypoint),
environment,
args,
logger);
}

/// <summary>
/// The natural platform shared-library file name for the runtime cdylib, as
/// emitted by the .NET build (the .node file renamed to what the Rust cdylib
/// would be called on this OS).
/// </summary>
private static string GetRuntimeLibraryFileName()
internal static string GetRuntimeLibraryFileName()
{
if (OperatingSystem.IsWindows()) return "copilot_runtime.dll";
if (OperatingSystem.IsMacOS()) return "libcopilot_runtime.dylib";
return "libcopilot_runtime.so";
}

/// <summary>
/// Starts the in-process runtime: spawns the CLI worker via the Rust host,
/// waits for readiness, and opens the FFI JSON-RPC connection.
/// Starts the in-process Rust runtime and opens the FFI JSON-RPC connection.
/// </summary>
public async Task StartAsync(CancellationToken cancellationToken)
{
// host_start blocks until the worker connects back and signals readiness
// (up to ~30s), and connection_open must run outside any async runtime, so
// perform the blocking FFI handshake on a background thread.
// Keep synchronous native startup off the caller's async context.
await Task.Run(() =>
{
var argvJson = BuildArgvJson(_cliEntrypoint, _args);
Expand All @@ -131,7 +113,7 @@ await Task.Run(() =>
if (_serverId == 0)
{
throw new InvalidOperationException(
$"copilot_runtime_host_start failed (library '{_libraryPath}', entrypoint '{_cliEntrypoint}').");
$"copilot_runtime_host_start failed (library '{_libraryPath}').");
}

_connectionId = NativeOpenConnection(_serverId);
Expand All @@ -154,24 +136,22 @@ await Task.Run(() =>
}
}

private static byte[] BuildArgvJson(string cliEntrypoint, IReadOnlyList<string> args)
private static byte[] BuildArgvJson(string? cliEntrypoint, IReadOnlyList<string> args)
{
// A .js entrypoint (dev / dist-cli) is launched via node; the packaged
// single-file CLI binary embeds its own Node and is invoked directly.
var isJsFile = cliEntrypoint.EndsWith(".js", StringComparison.OrdinalIgnoreCase);
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream))
{
writer.WriteStartArray();
if (isJsFile)
if (cliEntrypoint is not null)
{
writer.WriteStringValue("node");
if (cliEntrypoint.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
{
writer.WriteStringValue("node");
}
writer.WriteStringValue(cliEntrypoint);
writer.WriteStringValue("--embedded-host");
writer.WriteStringValue("--no-auto-update");
}
writer.WriteStringValue(cliEntrypoint);
writer.WriteStringValue("--embedded-host");
// Pin the worker to the bundled pkg matching the loaded cdylib, instead of
// drifting to a newer version under the user's ~/.copilot/pkg (ABI skew).
writer.WriteStringValue("--no-auto-update");
foreach (var arg in args)
{
writer.WriteStringValue(arg);
Expand Down
Loading
Loading