Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
93217b1
Integrate out-of-process Rust runtime wrapper
roji Aug 16, 2026
a977f67
Validate runtime wrapper across SDK harnesses
roji Aug 16, 2026
c3042c5
Use residual CLI for Python FFI test
roji Aug 17, 2026
343a9c0
Honor per-client runtime environment in .NET
roji Aug 17, 2026
3536cc2
Handle Rust session requests during creation
roji Aug 17, 2026
0e1a0de
Skip Go telemetry callback test in-process
roji Aug 17, 2026
7ff6ef7
Add legacy CLI launch escape hatch
roji Aug 17, 2026
24fae5a
Remove residual Node runtime compatibility
roji Aug 19, 2026
1419a61
Remove COPILOT_RUNTIME_PATH override
roji Aug 19, 2026
879adb0
fix(rust): materialize runtime launch contract
roji Aug 19, 2026
5494bcc
Remove residual runtime host contract
roji Aug 19, 2026
4cc6daf
fix(rust): materialize sibling CLI host
roji Aug 19, 2026
25330b4
fix(rust): create runtime install directory
roji Aug 19, 2026
9f57392
Complete managed runtime bundle materialization
roji Aug 20, 2026
f6d9224
Remove managed SEA staging for hostless runtime
roji Aug 21, 2026
7944d66
Stage auxiliary runtime assets from npm packages
roji Aug 25, 2026
fdc8e14
Exclude runtime package documentation from staging
roji Aug 25, 2026
d0f4091
Finalize runtime wrapper integration after rebase
roji Aug 26, 2026
64a5d14
Use executable cache for Node runtime wrapper
roji Aug 26, 2026
1073040
Temporarily skip extension-host E2E coverage
roji Aug 26, 2026
1447fd5
Address runtime wrapper review feedback
roji Aug 26, 2026
bb46df3
Stop resolving SEA for in-process hosting
roji Aug 27, 2026
9829383
Keep default runtime bundles SEA-free
roji Aug 27, 2026
7c23f64
Fix cross-platform runtime integration tests
roji Aug 27, 2026
dbf2cbb
Fix runtime CI harness portability
roji Aug 27, 2026
490d7dc
Relax closed-stream startup assertion
roji Aug 27, 2026
424433c
Fix failed-start runtime test handling
roji Aug 27, 2026
36b0fc2
Format Python runtime test setup
roji Aug 27, 2026
1e90499
Clean up failed Node runtime startup
roji Aug 27, 2026
ea26014
Fix Java token provider sample
roji Aug 27, 2026
ea703fb
Fix runtime integration CI checks
roji Aug 27, 2026
11bad76
Make Java relative path test drive-safe
roji Aug 27, 2026
b3afd79
Use generated logging for permission failures
roji Aug 27, 2026
f5aeef6
Suppress Node pipe writes after runtime exit
roji Aug 27, 2026
9f7d94b
Preserve Node startup transport errors
roji Aug 27, 2026
d539e12
Add Rust extension launch provider
roji Aug 28, 2026
37249fe
Clarify extension launch environment
roji Aug 28, 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 @@
/// </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 @@
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 @@ -2215,17 +2226,19 @@
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 @@ -2407,7 +2420,11 @@

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 @@ -2416,6 +2433,57 @@
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 @@ -2439,26 +2507,22 @@
return arch != null ? $"{os}-{arch}" : null;
}

private string ResolveCliPathForFfi()
private static string ResolveRuntimePathForExplicitCli(string cliPath)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
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());
Comment thread
roji marked this conversation as resolved.
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");
Comment thread
roji marked this conversation as resolved.
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
5 changes: 4 additions & 1 deletion dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission
}
catch (Exception ex)
{
_logger.LogError(ex, "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}", SessionId, requestId);
LogPermissionHandlerOrDeliveryFailed(ex, SessionId, requestId);
try
{
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, PermissionDecision.UserNotAvailable());
Expand Down Expand Up @@ -1975,6 +1975,9 @@ await InvokeRpcAsync<object>(
[LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch tool metadata for {toolName}")]
private partial void LogToolMetadataFetchFailed(Exception exception, string toolName);

[LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")]
private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId);

internal record SendMessageRequest
{
public string SessionId { get; init; } = string.Empty;
Expand Down
Loading
Loading