Add in-process (FFI) transport to the Go SDK#1976
Conversation
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
This comment has been minimized.
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
This comment has been minimized.
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
This comment has been minimized.
This comment has been minimized.
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
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 447feac4-35df-4c0b-ab34-00789ad78da7
This comment has been minimized.
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
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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
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
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
|
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.-tags copilot_inprocess. Ordinary builds do not compilepurego, the FFI host, or the native runtime payload.COPILOT_CLI_PATHis supported only for an externally provisioned compatible runtime package; noPATHlookup or per-connection path/arguments are exposed.Env,WorkingDirectory, andTelemetryare rejected for in-process clients because the loaded runtime shares the host process's ambient state.Start,Stop, andForceStop.StdioConnectionandTCPConnectionnow support connection-levelEnv, with validation preventing simultaneous client- and connection-level environment blocks.COPILOT_SDK_DEFAULT_CONNECTION=inprocessandGOFLAGS=-tags=copilot_inprocess.Python consistency changes
RuntimeConnection.for_inprocess()so it accepts nopathor rawargs.pathandargsfromInProcessRuntimeConnection;COPILOT_CLI_PATHis now the only override for an externally provisioned runtime package.Rust consistency changes
ClientOptions::programandClientOptions::extra_argswithTransport::InProcess; these remain child-process-only options.COPILOT_CLI_PATHas the sole externally provisioned runtime override, matching Go and Python.--yoloargument with the equivalent permissions RPC, allowing the same tests to run on transports that do not accept process arguments.GH_TOKEN/GITHUB_TOKENauthentication.Additional cross-language parity fixes
Shared semantics
COPILOT_CLI_PATHis reserved for externally provisioned compatible runtime packages.