Skip to content

Honor inprocess transport in C# E2E harness and fix in-process auth#1920

Merged
SteveSandersonMS merged 14 commits into
mainfrom
stevesa/csharp-e2e-connection
Jul 7, 2026
Merged

Honor inprocess transport in C# E2E harness and fix in-process auth#1920
SteveSandersonMS merged 14 commits into
mainfrom
stevesa/csharp-e2e-connection

Conversation

@SteveSandersonMS

Copy link
Copy Markdown
Contributor

What

Makes the C# E2E harness genuinely honor COPILOT_SDK_DEFAULT_CONNECTION=inprocess, and fixes the in-process auth failures that surfaced once it does.

Two commits:

  1. Honor COPILOT_SDK_DEFAULT_CONNECTION in the harness. E2ETestContext.CreateClient previously always assigned an explicit ForStdio connection for the default (useStdio == null) case, which made options.Connection non-null and short-circuited CopilotClient's ResolveDefaultConnection — so the env var was never read. As a result the inprocess CI cell secretly ran the whole suite over stdio; only one hard-coded ForInProcess smoke test actually exercised the FFI transport. Now the default case leaves Connection null so the env var is honored.

  2. Fix in-process per-session auth (401 Bad credentials). Some runtime code paths run host-side in the SDK's own process (the loaded cdylib) and read the ambient process environment rather than the environment passed to copilot_runtime_host_start:

    • native fetch_copilot_user reads COPILOT_DEBUG_GITHUB_API_URL via std::env::var
    • the gh-CLI auth fallback spawns gh auth token, which inherits GH_TOKEN / GITHUB_TOKEN / GH_CONFIG_DIR

    So the harness's per-test redirects and cleared tokens (which live only in options.Environment) were invisible in-process and auth escaped the replay proxy → 401. The harness now mirrors just those auth-relevant vars onto the real process environment (via libc setenv on Unix, since .NET's managed SetEnvironmentVariable doesn't reach getenv). It is gated to run only when the session actually uses the in-process transport, and limited to vars that GetEnvironment() re-sets every call, so it can't pollute stdio/tcp tests or leak stale state across the serially-run suite.

Why this is harness-only

For real in-process consumers the parent process already holds the real token and real GitHub URL, so the host-side getenv reads resolve correctly. The gap only bites the E2E harness because it redirects those values to a replay proxy. The proper long-term fix belongs in the runtime: thread the host_start environment into these host-side reads instead of consulting the global process env — noted in the code comment.

Verification

Local (Linux), CLI pointed at a from-source dist-cli:

  • PerSessionAuth + ModeHandlers + account login/quota: 8/8 pass in-process (were failing with 401 before).
  • Mixed default-mode batch (force-stop / dispose / no-permission-handler / telemetry / per-session-auth): 11/11 pass — confirms the gating prevents cross-test env pollution.

Known remaining in-process gap (not addressed here)

TelemetryExportE2ETests.Should_Export_File_Telemetry_For_Sdk_Interactions fails in-process (telemetry .jsonl not written) — a separate transport gap in the OTLP file exporter, unrelated to auth. Left for CI to surface / a follow-up.

SteveSandersonMS and others added 2 commits July 6, 2026 16:45
The E2E harness's CreateClient always assigned options.Connection =
ForStdio for the default (no-Connection, no-useStdio) case, which made
CopilotClient skip ResolveDefaultConnection — the only place
COPILOT_SDK_DEFAULT_CONNECTION is read. As a result the "inprocess"
test matrix cell silently ran the whole suite over stdio; only the one
test that hard-codes ForInProcess exercised the in-process transport.

Leave Connection null when useStdio is null so the default transport is
resolved from COPILOT_SDK_DEFAULT_CONNECTION (stdio by default, or
in-process). useStdio still pins stdio/tcp explicitly. The CLI path
flows via options.Environment["COPILOT_CLI_PATH"] (setup-copilot sets it
in CI), so no path needs to be injected on the connection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Host-side native code in the loaded cdylib (fetch_copilot_user, and the
gh-CLI auth fallback) reads the ambient process environment rather than
the environment passed to copilot_runtime_host_start, so the E2E harness's
per-test redirects (COPILOT_DEBUG_GITHUB_API_URL) and cleared tokens were
invisible in-process, causing 401 Bad credentials.

Mirror only the auth-relevant vars (and only when the session actually uses
the in-process transport) onto the real process environment, using libc
setenv on Unix since .NET's managed setter does not reach getenv. Gating
avoids polluting stdio/tcp tests; serial test execution makes it safe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
SteveSandersonMS and others added 2 commits July 6, 2026 18:27
The COPILOT_OTEL_* telemetry variables derived from CopilotClientOptions.Telemetry
were only applied to the stdio/tcp child process environment, so in-process FFI
sessions never enabled OTLP export. Factor the derivation into a shared helper and
apply it when building the in-process host environment, giving telemetry parity
across transports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI sets COPILOT_HMAC_KEY as an ambient credential, but in-process auth resolution
runs host-side in the test process and ranks HMAC above the GitHub token, so
provider.getEndpoint and login tests wrongly resolved to HMAC. Clear both HMAC
trigger vars in the default E2E environment and mirror them (empty) onto the host
process for in-process runs. Also simplify the in-process env-mirror gate to the
default-connection env var.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

SteveSandersonMS and others added 2 commits July 6, 2026 19:35
The in-process auth env mirror mutated shared process-env vars (HMAC keys,
GitHub API URL redirect, tokens) without restoring them, so a later test in a
different fixture inherited a cleared HMAC key and a stale/disposed replay-proxy
URL. That broke ClientE2ETests' direct-construction stdio/tcp cases, which rely
on the ambient CI HMAC credential (order-dependent, so only some OS cells went
red). Back up each var's pre-test value on first mutation and restore (or unset)
it in CleanupAfterTestAsync so mirroring cannot leak across tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The HostSideAuthEnvVars allowlist existed to keep the shared-process-env
mutation minimal so stale values couldn't leak into later tests. Now that
RestoreMirroredEnvironment reverts every mutation after each test, that
rationale is obsolete, and a curated allowlist is a maintenance trap: it
silently breaks in-process auth whenever the runtime starts reading a new var
host-side (as happened with COPILOT_HMAC_KEY). Since in-process mode mutates the
shared host env regardless, filtering to a subset buys nothing. Mirror the whole
options.Environment so every host-side read (auth, HMAC, COPILOT_HOME/XDG,
gh-CLI config) observes the intended test environment, and rely on restore for
isolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
The per-test env restore could be defeated on CI: the transient-client
force-stop loop only caught transient exceptions, so a non-transient failure
skipped RestoreMirroredEnvironment, leaving the shared process env cleared
(empty HMAC + redirected GitHub API URL). Worse, the next test then captured
that polluted value as its own 'pristine' baseline, so every later restore
preserved the pollution permanently -- which is why direct-construction
ClientE2ETests (Force_Stop / Allow_CreateSession / Not_Throw_When_Disposing)
failed on CI with a malformed-URL user fetch while passing locally (cleanup
never throws locally).

Record each mirrored var's pristine value once, process-wide, and never
overwrite it, so restore always reverts to the true ambient value regardless of
ordering or a missed restore. Run RestoreMirroredEnvironment in a finally and
also at fixture teardown as a backstop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
Comment thread dotnet/test/Harness/E2ETestContext.cs Fixed
… inprocess mode

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

SteveSandersonMS and others added 2 commits July 6, 2026 22:59
…dentials per-test

Move all in-process process-env hackery into InProcessEnvIsolation.cs and add
an assembly-level BeforeAfterTest attribute that neutralizes ambient CI
credentials (HMAC keys) around every test in the in-process job, independent of
how the CopilotClient is constructed. This stops directly-constructed clients
(e.g. ClientE2ETests) from authenticating for real and reaching the live API in
the in-process cell. The per-test proxy/home mirror stays in CreateClient but
now routes through the same single, deletable helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Replace the manual copy loop with a LINQ projection and drop the dead
null-filter (neither options.Environment nor ApplyTelemetryEnvironment ever
produces null values).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Comment thread dotnet/src/Client.cs
// The worker reads its configuration (telemetry export, etc.) from
// the environment passed here, so apply the same telemetry-derived
// vars the child-process path sets on its startInfo.Environment.
var ffiEnvironment = _options.Environment?.ToDictionary(kvp => kvp.Key, kvp => (string?)kvp.Value)

[DllImport("libc", EntryPoint = "setenv", CharSet = CharSet.Ansi,
BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern int NativeSetEnv(string name, string value, int overwrite);

[DllImport("libc", EntryPoint = "unsetenv", CharSet = CharSet.Ansi,
BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern int NativeUnsetEnv(string name);
Environment.SetEnvironmentVariable(name, value);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_ = NativeSetEnv(name, value, 1);
Comment on lines +167 to +174
if (value is null)
{
_ = NativeUnsetEnv(name);
}
else
{
_ = NativeSetEnv(name, value, 1);
}
{
if (value is null)
{
_ = NativeUnsetEnv(name);
}
else
{
_ = NativeSetEnv(name, value, 1);
In in-process (FFI) mode the runtime is hosted inside the test process and
holds the session-store SQLite handle open. ForceStopAsync only fires the
abrupt native host_shutdown, which does not release that handle, so the
temp-dir delete fails on Windows. Use graceful StopAsync for inproc so the
runtime shutdown RPC closes the DB before the directory is removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

SteveSandersonMS and others added 2 commits July 7, 2026 08:52
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

All changes in this PR are confined to the .NET SDK (dotnet/) and its CI workflow. No cross-SDK consistency concerns were identified.

Analysis summary:

Area Finding
In-process FFI transport (InProcessRuntimeConnection) .NET-only feature — no other SDK has an equivalent, and none need to add one
COPILOT_SDK_DEFAULT_CONNECTION env var .NET-only concept; not present in Node.js, Python, Go, Rust, or Java
ApplyTelemetryEnvironment refactoring Internal .NET plumbing change; other SDKs already independently implement telemetry env var wiring
[Experimental] on ForInProcess() / InProcessRuntimeConnection Language-idiomatic (.NET [Experimental] attribute); no cross-SDK action needed
InProcessEnvIsolation.cs Test-harness-only, .NET-specific; correctly scoped and marked as temporary

The other SDKs (Node.js, Python, Go, Rust, Java) each have TelemetryConfig support but are unaffected by this change since they don't have an in-process transport path where the same env-var gap would apply.

Generated by SDK Consistency Review Agent for issue #1920 · sonnet46 769.1K ·

@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review July 7, 2026 08:14
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner July 7, 2026 08:14
Copilot AI review requested due to automatic review settings July 7, 2026 08:14
@SteveSandersonMS SteveSandersonMS merged commit f250a9e into main Jul 7, 2026
36 of 37 checks passed
@SteveSandersonMS SteveSandersonMS deleted the stevesa/csharp-e2e-connection branch July 7, 2026 08:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the .NET/C# SDK E2E harness so the test suite truly exercises the in-process FFI transport when COPILOT_SDK_DEFAULT_CONNECTION=inprocess, and adds harness-side environment isolation to prevent in-process host-side code paths from escaping the replay proxy and authenticating against live endpoints.

Changes:

  • Adjusts E2ETestContext.CreateClient so the default (unset) connection leaves options.Connection null, allowing CopilotClient to honor COPILOT_SDK_DEFAULT_CONNECTION.
  • Introduces a temporary, centralized in-process environment mirroring/restoration mechanism and an assembly-level xUnit hook to neutralize ambient credentials during in-process runs.
  • Unifies telemetry environment variable application across child-process and in-process transports, and updates CI to exclude Windows in-process until shutdown locking is resolved.
Show a summary per file
File Description
dotnet/test/Harness/InProcessEnvIsolation.cs Adds temporary process-env mirroring/restoration and an assembly-level xUnit hook for in-process hermeticity.
dotnet/test/Harness/E2ETestContext.cs Ensures default connection resolution honors COPILOT_SDK_DEFAULT_CONNECTION; mirrors env for in-process; improves cleanup behavior.
dotnet/test/AssemblyInfo.cs Registers the new assembly-level InProcessEnvIsolation hook for tests.
dotnet/src/Types.cs Marks in-process connection APIs as experimental.
dotnet/src/Client.cs Applies telemetry-derived environment variables consistently for the in-process FFI host path and refactors child-process telemetry env setup.
.github/workflows/dotnet-sdk-tests.yml Temporarily excludes Windows in-process transport from the test matrix.

Review details

  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines +151 to +169
private static void SetProcessEnvironmentVariable(string name, string value)
{
Environment.SetEnvironmentVariable(name, value);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_ = NativeSetEnv(name, value, 1);
}
}

// Restores (or unsets) an environment variable on both the managed cache and
// (on Unix) the libc environment block.
private static void RestoreProcessEnvironmentVariable(string name, string? value)
{
Environment.SetEnvironmentVariable(name, value);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_ = value is null ? NativeUnsetEnv(name) : NativeSetEnv(name, value, 1);
}
}
Comment on lines +292 to +298
if (InProcessEnvIsolation.IsActive(options.Environment))
{
foreach (var (name, value) in options.Environment)
{
InProcessEnvIsolation.Mirror(name, value);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants