Skip to content

Add in-process (FFI) transport to the Go SDK#1976

Merged
SteveSandersonMS merged 14 commits into
mainfrom
stevesa/go-ffi
Jul 14, 2026
Merged

Add in-process (FFI) transport to the Go SDK#1976
SteveSandersonMS merged 14 commits into
mainfrom
stevesa/go-ffi

Conversation

@SteveSandersonMS

@SteveSandersonMS SteveSandersonMS commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an experimental in-process (FFI) transport to the Go SDK, bringing it into semantic parity with the .NET, TypeScript, Python, and Rust SDKs. Instead of spawning a runtime child process and communicating over stdio or TCP, the SDK loads the native runtime library and pumps LSP-framed JSON-RPC over its C ABI.

The Go implementation uses ebitengine/purego, so in-process support does not require CGO or a C toolchain.

This PR also tightens the existing Python and Rust in-process APIs so all three languages expose the same transport boundary: the SDK selects and provisions the runtime, while callers use typed runtime options rather than child-process paths or arbitrary arguments.

Go changes

  • InProcessConnection{} — new experimental runtime connection type.
  • Opt-in build tag — applications must build with -tags copilot_inprocess. Ordinary builds do not compile purego, the FFI host, or the native runtime payload.
  • Bundling — the bundler generates separate tagged and untagged embed files. Tagged builds embed the native runtime; ordinary builds continue to embed only the child-process runtime.
  • Runtime resolution — uses the bundled runtime by default. COPILOT_CLI_PATH is supported only for an externally provisioned compatible runtime package; no PATH lookup or per-connection path/arguments are exposed.
  • Version isolation and integrity — embedded runtime versions use separate cache directories and are installed with SHA-256 verification. Missing or mismatched native libraries fail loudly.
  • Process-wide constraintsEnv, WorkingDirectory, and Telemetry are rejected for in-process clients because the loaded runtime shares the host process's ambient state.
  • Lifecycle safety — the FFI host is owned before the blocking startup handshake and participates in Start, Stop, and ForceStop.
  • Platform handling — includes Windows and musl loading support plus Linux/macOS signal-handler repair required when hosting the native runtime inside a Go process.
  • Child-process cleanupStdioConnection and TCPConnection now support connection-level Env, with validation preventing simultaneous client- and connection-level environment blocks.
  • E2E and CI — the Go workflow runs the full suite on Linux, macOS, and Windows for both default and in-process transports. In-process jobs set both COPILOT_SDK_DEFAULT_CONNECTION=inprocess and GOFLAGS=-tags=copilot_inprocess.

Python consistency changes

  • Simplifies RuntimeConnection.for_inprocess() so it accepts no path or raw args.
  • Removes path and args from InProcessRuntimeConnection; COPILOT_CLI_PATH is now the only override for an externally provisioned runtime package.
  • Keeps runtime provisioning internal and stores the resolved runtime path separately from the public connection object.
  • Updates the E2E harness to use process-global environment/cwd only when the selected client actually uses in-process transport, preserving custom-argument tests on child-process transport.
  • Adds API-shape coverage proving that in-process connections do not expose child-process options.
  • Updates documentation to describe the native runtime abstraction rather than implementation-specific worker details.

Rust consistency changes

  • Rejects ClientOptions::program and ClientOptions::extra_args with Transport::InProcess; these remain child-process-only options.
  • Uses COPILOT_CLI_PATH as the sole externally provisioned runtime override, matching Go and Python.
  • Keeps typed runtime options such as base directory, log level, idle timeout, authentication mode, and remote sessions available in-process.
  • Replaces E2E use of the raw --yolo argument with the equivalent permissions RPC, allowing the same tests to run on transports that do not accept process arguments.
  • Updates the in-process E2E harness to mirror the runtime path and ambient environment onto the host process, remove CI HMAC credentials that conflict with replay authentication, and allow ambient GH_TOKEN/GITHUB_TOKEN authentication.
  • Updates public documentation and diagnostics to describe the native runtime abstraction consistently.

Additional cross-language parity fixes

  • Go, Python, Rust, .NET, and TypeScript now forward the same typed in-process runtime options through managed argv/environment, including token authentication, login mode, base directory/empty mode, log level, idle timeout, and remote sessions.
  • Go, Rust, .NET, and TypeScript now select musl-compatible CLI/runtime packages or prebuilds on musl Linux; Python already had equivalent detection.
  • Go cancellation now waits for synchronous native startup before disposal, preventing leaked workers and start/stop races.

Shared semantics

  • In-process transport is experimental.
  • The SDK loads a native runtime library and communicates through its C ABI.
  • Child-process path and arbitrary argument options are not part of the in-process connection API.
  • COPILOT_CLI_PATH is reserved for externally provisioned compatible runtime packages.
  • Per-client environment, working directory, and telemetry settings are rejected when they cannot be honored.
  • Only one native runtime version may be loaded per process.
  • Missing or invalid native runtime artifacts fail closed.
  • Tests that genuinely require stdio/TCP or custom child-process arguments remain on those transports; the rest of each E2E suite exercises in-process transport through the default-connection selector.

Bring the Go SDK to parity with the .NET, Rust, TypeScript, and Python
SDKs by adding an in-process transport that hosts the runtime via the
native FFI library (runtime.node) instead of spawning a child process
for the runtime server.

- ffihost: pure-Go FFI host using ebitengine/purego (CGO stays off),
  C ABI binding, single-load-per-process guard, and cdylib resolution
  (flat name next to the CLI, or prebuilds/<platform>/runtime.node),
  including musl and Windows loaders.
- API: new InProcessConnection{Path, Args} (Experimental) plus
  connection-level Env on Stdio/TCP via a shared childProcessConnection
  interface.
- client: startInProcess wiring into Start/Stop/ForceStop/killProcess
  with no-PATH entrypoint resolution, --no-auto-update, and host
  ownership before the blocking handshake for cancellation safety.
  validateEnvironmentOptions rejects Env/WorkingDirectory/Telemetry
  in-process and env set in both client and connection for child
  transports.
- bundler + embeddedcli: extract and embed runtime.node (conditional
  per platform) and install it next to the CLI binary with SHA-256
  verification; add RuntimeLibPath().
- e2e: COPILOT_SDK_DEFAULT_CONNECTION=inprocess harness support that
  swaps only the default stdio connection, mirrors the merged env and
  cwd onto the process, and leaves TCP/URI/custom-stdio/telemetry tests
  on their transport; new in-process ping smoke test.
- CI: add transport: [default, inprocess] matrix to the Go workflow.
- Docs and unit tests for the new connection and validation semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a96d1a82-c80e-40f1-a3df-a9708429b68b
@github-actions

This comment has been minimized.

Address three failure modes in the transport:[default,inprocess] matrix:

1. Snapshot path was cwd-relative. ConfigureForTest resolved snapshots via
   filepath.Abs, which broke once the in-process path chdir'd into the temp
   work dir, yielding empty snapshots ("No cached response"). Anchor the path
   to the caller's source directory instead.

2. Process-global LLM inference provider / per-client telemetry. The shared
   in-process runtime refuses a second provider client and ignores per-client
   telemetry. Rather than silently downgrade the transport, the 8 affected
   tests now skip explicitly via testharness.SkipIfInProcess (they still run
   over stdio in the default cell), mirroring Rust's skip_inprocess.

3. macOS SA_ONSTACK crash. libuv installs a SIGCHLD handler without SA_ONSTACK
   on first uv_spawn (during host_start); Go's runtime then aborts when it
   reaps its own os/exec children. Re-arm the foreign handler with SA_ONSTACK
   after host_start (sigonstack_darwin.go; no-op elsewhere), and gate the FFI
   smoke test to the inprocess cell so default cells never load libnode.

Also unset ambient HMAC keys at package init when inprocess (issue #1934).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a96d1a82-c80e-40f1-a3df-a9708429b68b
@github-actions

This comment has been minimized.

Resolve the remaining inprocess-cell failures in the go-sdk-tests matrix:

- Anchor all repo-relative test paths to source dirs, not the process cwd.
  The in-process transport os.Chdir's the whole test process into a per-test
  temp workdir, so cwd-relative resolution of the replay proxy (server.ts) and
  MCP server scripts (*.mjs) broke for every test after the first in-process
  one. Add testharness.RepoPath and use it for the proxy, CLI, and MCP paths.

- Fix a data race in ffihost.Host: `disposed` was written under disposeMu but
  read under callbackMu. Unify all disposed/serverID/connectionID/activeCallbacks
  access under a single mutex, which also fixes a real drain-ordering bug where a
  late onOutbound callback could feed the receive buffer after it was closed.

- Re-arm foreign signal handlers with SA_ONSTACK on linux too (not just darwin).
  libuv's SA_ONSTACK-less SIGCHLD handler makes the Go runtime abort when the
  test process reaps its own os/exec child (e.g. an MCP OAuth server) while
  libnode is loaded. Resolve sigaction through the runtime handle (with a libc
  dlopen fallback).

- Make the OAuth MCP server's stderr buffer thread-safe (syncBuffer): os/exec
  writes to it on a goroutine while the test reads String(), a -race violation.

- Skip only the readCheckpoint "unknown checkpoint" scenario in-process, where
  the native runtime decodes the id as u32 and rejects the large sentinel,
  matching Rust's explicit skip.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a96d1a82-c80e-40f1-a3df-a9708429b68b
@github-actions

This comment has been minimized.

Comment thread go/internal/e2e/testharness/context.go Outdated
Comment thread go/internal/embeddedcli/embeddedcli.go
Comment thread go/internal/ffihost/ffihost.go Outdated
SteveSandersonMS and others added 2 commits July 14, 2026 10:36
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
@github-actions

This comment has been minimized.

Exclude the native runtime payload and purego implementation from normal Go application builds while enabling them with copilot_inprocess.

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

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
Use the Linux rt_sigaction ABI for SA_ONSTACK repair, keep Go plugin test state alive through runtime shutdown, remove HMAC signing keys from Rust in-process tests, and apply the required Rust formatting.

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

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
Apply the in-process build constraint to the private receive buffer so ordinary builds do not lint implementation code whose host is excluded.

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

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
Do not pass --no-auto-login for the in-process harness defaults, so GH_TOKEN and GITHUB_TOKEN can authenticate against the replay proxy. Tests that require unauthenticated behavior still opt out explicitly.

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

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
@github-actions

This comment has been minimized.

@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review July 14, 2026 11:32
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner July 14, 2026 11:32
Copilot AI review requested due to automatic review settings July 14, 2026 11:32

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

Adds Go in-process FFI transport, native-runtime bundling, cross-platform hosting, and CI coverage. It also aligns Python and Rust in-process APIs.

Changes:

  • Implements Go FFI hosting, lifecycle management, runtime resolution, and embedding.
  • Adds transport-aware Go E2E infrastructure and CI matrix coverage.
  • Removes per-connection in-process paths/arguments from Python and Rust tests/APIs.
Show a summary per file
File Description
.github/workflows/go-sdk-tests.yml Adds in-process CI matrix.
go/README.md Documents Go FFI transport.
go/types.go Adds connection APIs and environment options.
go/client.go Integrates FFI lifecycle and validation.
go/client_test.go Tests transport selection and validation.
go/inprocess.go Defines internal host interface.
go/inprocess_enabled.go Enables supported tagged builds.
go/inprocess_disabled.go Handles unavailable builds.
go/go.mod Adds purego dependency.
go/go.sum Records purego checksums.
go/embeddedcli/installer.go Updates public installer documentation.
go/internal/embeddedcli/embeddedcli.go Installs and verifies native runtime.
go/internal/embeddedcli/embeddedcli_test.go Tests versioned runtime installation.
go/cmd/bundler/main.go Bundles and generates runtime embeds.
go/cmd/bundler/main_test.go Tests build-tagged generation.
go/internal/ffihost/ffihost.go Implements C ABI host.
go/internal/ffihost/ffihost_test.go Tests callback cleanup.
go/internal/ffihost/buffer.go Adds callback receive buffer.
go/internal/ffihost/resolve.go Resolves native libraries.
go/internal/ffihost/resolve_test.go Tests library layouts.
go/internal/ffihost/loader_other.go Loads Unix libraries.
go/internal/ffihost/loader_windows.go Loads Windows libraries.
go/internal/ffihost/sigonstack_linux.go Repairs Linux signal handlers.
go/internal/ffihost/sigonstack_linux_test.go Tests Linux signal repair.
go/internal/ffihost/sigonstack_darwin.go Repairs Darwin signal handlers.
go/internal/ffihost/sigonstack_other.go Adds Windows no-op implementation.
go/internal/e2e/testharness/context.go Adds in-process E2E setup.
go/internal/e2e/testharness/helper.go Adds repository-relative paths.
go/internal/e2e/testharness/proxy.go Uses an absolute harness path.
go/internal/e2e/inprocess_ffi_e2e_test.go Adds FFI smoke test.
go/internal/e2e/telemetry_e2e_test.go Skips unsupported matrix cell.
go/internal/e2e/subagent_hooks_e2e_test.go Skips process-global provider test.
go/internal/e2e/session_config_e2e_test.go Skips provider-dependent test.
go/internal/e2e/rpc_workspace_checkpoints_e2e_test.go Skips incompatible checkpoint case.
go/internal/e2e/rpc_server_plugins_e2e_test.go Isolates plugin home under workdir.
go/internal/e2e/pre_mcp_tool_call_hook_e2e_test.go Stabilizes harness path.
go/internal/e2e/mcp_server_helpers_test.go Stabilizes MCP server path.
go/internal/e2e/mcp_oauth_e2e_test.go Stabilizes path and stderr access.
go/internal/e2e/mcp_and_agents_e2e_test.go Stabilizes MCP path.
go/internal/e2e/copilot_request_session_id_e2e_test.go Skips provider-dependent test.
go/internal/e2e/copilot_request_handler_e2e_test.go Skips provider-dependent test.
go/internal/e2e/copilot_request_cancel_error_e2e_test.go Skips provider-dependent tests.
go/internal/e2e/byok_bearer_token_provider_e2e_test.go Skips provider-dependent test.
python/copilot/client.py Removes in-process path and arguments.
python/test_client.py Tests the reduced API.
python/README.md Updates in-process documentation.
python/e2e/testharness/context.py Selects in-process only when compatible.
python/e2e/test_inprocess_ffi_e2e.py Uses environment-based resolution.
rust/src/lib.rs Restricts unsupported in-process options.
rust/README.md Documents updated runtime resolution.
rust/tests/e2e/support.rs Updates in-process environment setup.
rust/tests/e2e/rpc_mcp_and_skills.rs Replaces raw yolo arguments with RPC permissions.

Review details

  • Files reviewed: 51/52 changed files
  • Comments generated: 8
  • Review effort level: Medium

Comment thread go/client.go Outdated
Comment thread go/client.go
Comment thread go/cmd/bundler/main.go
Comment thread go/cmd/bundler/main.go
Comment thread go/internal/e2e/testharness/proxy.go Outdated
Comment thread go/internal/e2e/telemetry_e2e_test.go
Comment thread go/internal/ffihost/ffihost.go Outdated
Comment thread go/internal/embeddedcli/embeddedcli.go
Forward typed runtime options, make Go startup cancellation safe, select musl runtime packages, and correct the Go replay harness working directory.

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

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
Reapply SA_ONSTACK after libuv tears down its child watcher so subsequent Go subprocess cleanup cannot crash.

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

Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

This PR does an excellent job of achieving parity across the five SDKs it touches (Go, .NET, TypeScript, Python, Rust). Here's a summary of the consistency analysis:

✅ What's aligned after this PR

Feature Node.js/TS Python Go .NET Rust Java
In-process (FFI) transport ✅ (new)
No path/args on in-process connection ✅ (tightened) ✅ (tightened) n/a
COPILOT_CLI_PATH as sole external override n/a
Typed options forwarded in-process (token, baseDir, logLevel, etc.) ✅ (fixed) ✅ (fixed) n/a
musl Linux runtime selection ✅ (new) ✅ (new)
Env/WorkingDirectory/Telemetry rejected for in-process n/a

⚠️ Pre-existing Java gap (not introduced by this PR)

The Java SDK does not yet have in-process (FFI) transport or musl Linux detection. This is a pre-existing gap — the PR description explicitly calls out parity with "the .NET, TypeScript, Python, and Rust SDKs", and the Java README itself notes it tracks those five SDKs.

Two items worth tracking for a follow-up Java issue:

  1. In-process transport: Java would need InProcessRuntimeConnection / RuntimeConnection.forInProcess() with the same semantics — no path/args, COPILOT_CLI_PATH as the only override, and typed options forwarded via FFI environment.
  2. musl detection: Java's CLI installer/bundler likely selects linux-x64 unconditionally on Linux; it should detect musl and select the linuxmusl-x64/linuxmusl-arm64 package instead.

Neither of these is a blocker for this PR — they're existing gaps that this PR doesn't worsen. Happy to help track them as follow-on issues if that would be useful.

Summary

The five-SDK surface touched by this PR is internally consistent and the API contracts are well-aligned. 👍

Generated by SDK Consistency Review Agent for issue #1976 · sonnet46 1.8M ·

@SteveSandersonMS SteveSandersonMS merged commit dece20b into main Jul 14, 2026
53 checks passed
@SteveSandersonMS SteveSandersonMS deleted the stevesa/go-ffi branch July 14, 2026 12:56
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.

2 participants