Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a6f153b
Integrate out-of-process Rust runtime wrapper
roji Aug 16, 2026
7c807db
Validate runtime wrapper across SDK harnesses
roji Aug 16, 2026
a1fdae8
Use residual CLI for Python FFI test
roji Aug 17, 2026
6251ba1
Honor per-client runtime environment in .NET
roji Aug 17, 2026
e07c87d
Handle Rust session requests during creation
roji Aug 17, 2026
77c4cbd
Skip Go telemetry callback test in-process
roji Aug 17, 2026
8f3a6cc
Add legacy CLI launch escape hatch
roji Aug 17, 2026
9b8a4e6
Remove residual Node runtime compatibility
roji Aug 19, 2026
0a7ab08
Remove COPILOT_RUNTIME_PATH override
roji Aug 19, 2026
35f60c4
fix(rust): materialize runtime launch contract
roji Aug 19, 2026
4db46b9
Remove residual runtime host contract
roji Aug 19, 2026
6b8ccf9
fix(rust): materialize sibling CLI host
roji Aug 19, 2026
353018f
fix(rust): create runtime install directory
roji Aug 19, 2026
b719f19
Complete managed runtime bundle materialization
roji Aug 20, 2026
63db602
Remove managed SEA staging for hostless runtime
roji Aug 21, 2026
63917f9
Stage auxiliary runtime assets from npm packages
roji Aug 25, 2026
98a6f5e
Exclude runtime package documentation from staging
roji Aug 25, 2026
c335d6b
Finalize runtime wrapper integration after rebase
roji Aug 26, 2026
edc5cff
Use executable cache for Node runtime wrapper
roji Aug 26, 2026
501d6e6
Temporarily skip extension-host E2E coverage
roji Aug 26, 2026
c77b0e4
Address runtime wrapper review feedback
roji Aug 26, 2026
a3be0f8
Stop resolving SEA for in-process hosting
roji Aug 27, 2026
1797fbf
Keep default runtime bundles SEA-free
roji Aug 27, 2026
2420ccf
Fix cross-platform runtime integration tests
roji Aug 27, 2026
eced766
Fix runtime CI harness portability
roji Aug 27, 2026
fd2f5ae
Relax closed-stream startup assertion
roji Aug 27, 2026
fb5abdb
Fix failed-start runtime test handling
roji Aug 27, 2026
0e1de07
Format Python runtime test setup
roji Aug 27, 2026
6f22f9e
Clean up failed Node runtime startup
roji Aug 27, 2026
ab564f1
Fix Java token provider sample
roji Aug 27, 2026
eb17190
Fix runtime integration CI checks
roji Aug 27, 2026
aaf39dc
Make Java relative path test drive-safe
roji Aug 27, 2026
5f2a6f0
Use generated logging for permission failures
roji Aug 27, 2026
441db45
Suppress Node pipe writes after runtime exit
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 @@
/// </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