Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
0f33eed
Integrate out-of-process Rust runtime wrapper
roji Aug 16, 2026
ec44962
Validate runtime wrapper across SDK harnesses
roji Aug 16, 2026
4424c9f
Use residual CLI for Python FFI test
roji Aug 17, 2026
d500dcc
Honor per-client runtime environment in .NET
roji Aug 17, 2026
055f94a
Handle Rust session requests during creation
roji Aug 17, 2026
0d3dcf4
Skip Go telemetry callback test in-process
roji Aug 17, 2026
080a857
Add legacy CLI launch escape hatch
roji Aug 17, 2026
3f81f81
Remove residual Node runtime compatibility
roji Aug 19, 2026
b17d256
Remove COPILOT_RUNTIME_PATH override
roji Aug 19, 2026
de8f3c0
fix(rust): materialize runtime launch contract
roji Aug 19, 2026
a63aaca
Remove residual runtime host contract
roji Aug 19, 2026
6a8d29c
fix(rust): materialize sibling CLI host
roji Aug 19, 2026
a9f0d1e
fix(rust): create runtime install directory
roji Aug 19, 2026
5fc5789
Complete managed runtime bundle materialization
roji Aug 20, 2026
c8cc091
Remove managed SEA staging for hostless runtime
roji Aug 21, 2026
c65a203
Stage auxiliary runtime assets from npm packages
roji Aug 25, 2026
982a2f3
Exclude runtime package documentation from staging
roji Aug 25, 2026
a93b7d6
Finalize runtime wrapper integration after rebase
roji Aug 26, 2026
b0971b6
Use executable cache for Node runtime wrapper
roji Aug 26, 2026
71885a1
Temporarily skip extension-host E2E coverage
roji Aug 26, 2026
857dfe8
Address runtime wrapper review feedback
roji Aug 26, 2026
9c834b7
Stop resolving SEA for in-process hosting
roji Aug 27, 2026
eff8c9f
Keep default runtime bundles SEA-free
roji Aug 27, 2026
21a4dba
Fix cross-platform runtime integration tests
roji Aug 27, 2026
0eff77b
Fix runtime CI harness portability
roji Aug 27, 2026
65de4ec
Relax closed-stream startup assertion
roji Aug 27, 2026
e00966b
Fix failed-start runtime test handling
roji Aug 27, 2026
9f57d34
Format Python runtime test setup
roji Aug 27, 2026
2581db9
Clean up failed Node runtime startup
roji Aug 27, 2026
6d8c3fc
Fix Java token provider sample
roji Aug 27, 2026
caba730
Fix runtime integration CI checks
roji Aug 27, 2026
ab3d646
Make Java relative path test drive-safe
roji Aug 27, 2026
abbed69
Use generated logging for permission failures
roji Aug 27, 2026
5c11197
Suppress Node pipe writes after runtime exit
roji Aug 27, 2026
c832b3e
Preserve Node startup transport errors
roji Aug 27, 2026
83bdcdb
Add Rust extension launch provider
roji Aug 28, 2026
75012b0
Clarify extension launch environment
roji Aug 28, 2026
0cf200d
test(rust): cover runtime extension lifecycle
roji Aug 30, 2026
2359d43
fix(java): honor runtime path environment override
roji Aug 31, 2026
def26ff
test(node): skip host-dependent ask user metadata
stephentoub Aug 31, 2026
181b482
fix(node): preserve startup stderr after pipe errors
stephentoub Aug 31, 2026
5729840
refactor(node): separate exit and diagnostic waits
stephentoub Aug 31, 2026
9071c10
test(node): preserve ask user metadata coverage
stephentoub Sep 1, 2026
d3e51ca
refactor(node): simplify startup failure handling
stephentoub Sep 1, 2026
22b06ec
style(node): format startup failure test
stephentoub Sep 1, 2026
b9cb0cc
test(python): set plan mode before handler test
stephentoub Sep 1, 2026
bc2b99e
test: preserve live in-process runtime state
stephentoub Sep 1, 2026
9632e24
test: stabilize transient runtime checks
stephentoub Sep 1, 2026
942f70e
Stabilize Rust processing state E2E
stephentoub Sep 1, 2026
7b2fb35
Stabilize Windows .NET E2E coverage
stephentoub Sep 1, 2026
955cf36
test: stabilize rewind and Rust resume E2E
stephentoub Sep 1, 2026
be05a0f
fix: stabilize runtime disconnect races
stephentoub Sep 1, 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
160 changes: 124 additions & 36 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 @@ -493,6 +504,20 @@
await CleanupCliProcessAsync(cliProcess, stderrPump, errors: null, _logger);
}

if (ex is IOException
&& cliProcess is not null
&& stderrPump is not null
&& !ex.Message.Contains("stderr:", StringComparison.OrdinalIgnoreCase))
{
var stderrOutput = GetStderrOutput(stderrPump.Buffer);
if (!string.IsNullOrEmpty(stderrOutput))
{
throw new IOException(
FormatCliExitedMessage("CLI process exited unexpectedly.", stderrOutput),
ex);
}
}

throw;
}
}
Expand Down Expand Up @@ -673,7 +698,7 @@

private static async Task CleanupCliProcessAsync(Process childProcess, ProcessStderrPump? stderrPump, List<Exception>? errors, ILogger? logger)
{
stderrPump?.Cancel();
var processExited = false;

try
{
Expand Down Expand Up @@ -706,12 +731,19 @@
AddCleanupError(errors, ex, logger);
}
}

processExited = childProcess.HasExited;
}
catch (Exception ex)
{
AddCleanupError(errors, ex, logger);
}

if (!processExited)
{
stderrPump?.Cancel();
}

if (stderrPump is not null)
{
var stderrPumpWaitTimestamp = Stopwatch.GetTimestamp();
Expand All @@ -721,6 +753,7 @@
}
catch (TimeoutException ex)
{
stderrPump.Cancel();
if (logger is not null)
{
LoggingHelpers.LogTiming(logger, LogLevel.Debug, ex,
Expand Down Expand Up @@ -1960,13 +1993,15 @@

private static IOException CreateCliExitedException(string message, StringBuilder stderrBuffer)
{
string stderrOutput;
return new IOException(FormatCliExitedMessage(message, GetStderrOutput(stderrBuffer)));
}

private static string GetStderrOutput(StringBuilder stderrBuffer)
{
lock (stderrBuffer)
{
stderrOutput = stderrBuffer.ToString().Trim();
return stderrBuffer.ToString().Trim();
}

return new IOException(FormatCliExitedMessage(message, stderrOutput));
}

private Task<Connection> EnsureConnectedAsync(CancellationToken cancellationToken)
Expand Down Expand Up @@ -2217,17 +2252,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 @@ -2409,7 +2446,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 @@ -2418,6 +2459,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 @@ -2441,26 +2533,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
Loading
Loading