Skip to content

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

Draft
SteveSandersonMS wants to merge 3 commits into
mainfrom
stevesa/go-ffi
Draft

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

Conversation

@SteveSandersonMS

Copy link
Copy Markdown
Contributor

Summary

Brings the Go SDK to parity with the .NET, Rust, TypeScript, and Python SDKs by adding an in-process (FFI) transport. Instead of spawning a CLI child and talking JSON-RPC over stdio/TCP, the SDK loads the native runtime library (runtime.node) and pumps LSP-framed JSON-RPC across its C ABI. The runtime's host_start export spawns the worker itself (<entrypoint> --embedded-host --no-auto-update).

The only intentional divergence from the other SDKs is the Go-specific FFI mechanism: ebitengine/purego, which loads the cdylib at runtime with CGO disabled — preserving Go's pure build and cross-compilation.

What's included

  • go/internal/ffihost/ — purego FFI host: C ABI binding, single-load-per-process guard, cdylib resolution (flat name next to the CLI binary, with a dev prebuilds/<platform>/runtime.node fallback), plus musl and Windows loaders.
  • API — new InProcessConnection{Path, Args} (marked Experimental) implementing RuntimeConnection; connection-level Env on StdioConnection/TCPConnection via a shared childProcessConnection interface.
  • client.gostartInProcess wired into Start/Stop/ForceStop/killProcess, with no PATH fallback for entrypoint resolution (InProcessConnection.Path > COPILOT_CLI_PATH > embedded), --no-auto-update argv, and host ownership before the blocking handshake for cancellation safety. validateEnvironmentOptions rejects Env/WorkingDirectory/Telemetry in-process, and rejects env set in both client- and connection-level for child transports.
  • Bundler + embeddedcli — extract and embed runtime.node (conditional per platform, like .NET's Condition="Exists(...)"), install it next to the CLI binary with SHA-256 verification (fail closed), and expose RuntimeLibPath().
  • E2E harnessCOPILOT_SDK_DEFAULT_CONNECTION=inprocess support that swaps only the default stdio connection, mirrors the merged env + cwd onto the process, and leaves TCP/URI/custom-stdio/telemetry tests on their original transport. New in-process ping smoke test.
  • CI.github/workflows/go-sdk-tests.yml gains a transport: [default, inprocess] matrix dimension and a "Select inprocess transport" step (mirrors the Python/Node/.NET workflows).
  • Docs + unit tests for the new connection type and validation semantics.

Consistency with other SDKs

  • --embedded-host --no-auto-update worker argv
  • No PATH resolution for the in-process entrypoint (matches Python Python SDK: copilot.session.create() raises AttributeError with streaming enabled #1467)
  • Reject client-level env / working-directory / telemetry in-process (fail loud)
  • Reject env set in both client + connection for child transports
  • Single-load-per-process guard; error on a second/different cdylib path
  • Fail closed on missing/mismatched cdylib integrity
  • Host owned before blocking start (cancellation safety)
  • Experimental doc markers on new public API
  • Flat cdylib name next to the binary, dev prebuilds fallback

Validation

  • go build ./... clean, gofmt clean, golangci-lint clean on changed packages.
  • Root unit tests pass (including new validation-semantics tests).
  • In-process ping smoke e2e passes.
  • Representative e2e tests pass under both default and inprocess: snapshot session, cwd-override, env-append/provider-endpoint, telemetry (stays stdio), TCP connection-token (stays TCP).

Notes / follow-ups

  • The full e2e suite under inprocess on all three OSes is validated locally only via representative tests (CI now runs the full matrix). Individual tests that assert child-process-specific behavior may later need a testharness.IsInProcessTransport() skip.

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

Copy link
Copy Markdown
Contributor

SDK Consistency Review

This PR maintains and improves cross-SDK consistency. It brings the Go SDK to parity with the Node.js, Python, .NET, and Rust SDKs by adding the in-process (FFI) transport.

Feature coverage after this PR

Feature Node.js Python .NET Rust Go Java
In-process transport ✅ (this PR) ❌ pre-existing gap
Connection-level env override ✅ (Environment) ❌ client-level only ✅ (this PR) ❌ client-level only

Notes

  • Java in-process transport: Java remains the only SDK without in-process transport support. This is a pre-existing gap — not introduced by this PR. The PR actually reduces the total gap by bringing Go into alignment.
  • Naming conventions: The Go Env []string (key=value pairs, matching os/exec idioms) is appropriately idiomatic compared to .NET's IReadOnlyDictionary<string,string> and Record<string,string> in TypeScript — all semantically equivalent.
  • Experimental annotation: InProcessConnection is correctly marked Experimental to match .NET's [Experimental] and the // Experimental: doc comments in other SDKs.
  • Validation behavior: Rejecting Env/WorkingDirectory/Telemetry with InProcessConnection matches the validation in Node.js, Python, and .NET.

No action required from a consistency standpoint — this PR is a net improvement.

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

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.

1 participant