Skip to content

Conversation

@qmuntal
Copy link
Member

@qmuntal qmuntal commented Feb 9, 2026

This pull request introduces support for embedding the GitHub Copilot CLI binary directly within Go applications using the SDK, removing the need for users to have the CLI installed separately. It adds a robust mechanism for embedding, unpacking, and managing the Copilot CLI binary, including versioning, file locking for concurrency, and hash validation. The changes also include comprehensive tests and documentation updates.

@github-actions
Copy link

github-actions bot commented Feb 9, 2026

Cross-SDK Consistency Review

Summary

This PR introduces CLI bundling/embedding capability exclusively to the Go SDK, creating a significant feature disparity across the SDK implementations. While this is an excellent feature for Go users, the same capability would be valuable for Node.js, Python, and .NET SDKs.


Current State

Go SDK (this PR):

  • ✅ Supports embedding CLI binary using Go's embed package
  • ✅ Automatic CLI installation when no explicit path is provided
  • ✅ Build tooling via go tool bundler
  • ✅ Eliminates need for users to install CLI separately

Node.js SDK:

  • ❌ Requires CLI installed and in PATH (or custom cliPath)
  • ❌ No bundling/embedding support

Python SDK:

  • ❌ Requires CLI installed and accessible (or custom cli_path)
  • ❌ No bundling/embedding support

.NET SDK:

  • ❌ Requires CLI installed and in PATH (or custom CliPath)
  • ❌ No bundling/embedding support

Consistency Impact

Requirements sections now differ:

Go SDK (after this PR):

## Distributing your application with an embedded GitHub Copilot CLI

The SDK supports bundling, using Go's `embed` package, the Copilot CLI binary within your application's distribution.
This allows you to bundle a specific CLI version and avoid external dependencies on the user's system.

All other SDKs:

## Requirements
- [Language runtime]
- GitHub Copilot CLI installed and in PATH (or provide custom path)

Recommendations

Option 1: Track feature parity (Recommended)

Accept this PR as-is since it's Go-specific infrastructure, but create tracking issues to implement equivalent functionality in other SDKs:

Node.js: Could use npm's optional dependencies or postinstall scripts to download platform-specific binaries
Python: Could use package data + custom installers (similar to packages like playwright or chromedriver-py)
.NET: Could use NuGet package assets and runtime-specific dependencies

Option 2: Document the difference

If bundling is intentionally Go-only (due to Go's superior embedding capabilities), explicitly document this as a Go-specific advantage in the main README to set user expectations.


Why This Matters

Distribution simplification: This feature allows Go SDK users to ship applications without external CLI dependencies, significantly improving the deployment experience. Users of other SDKs would benefit equally from this capability.

Version pinning: Embedding specific CLI versions ensures consistent behavior across deployments, which is valuable regardless of language.

Offline environments: Bundled CLIs work in airgapped/restricted environments where downloading dependencies isn't possible.


Suggested Next Steps

  1. Approve this PR - The Go implementation is well-designed and language-appropriate
  2. 📝 Create follow-up issues for bundling support in Node.js, Python, and .NET SDKs
  3. 📚 Update main README to mention bundling capability and which SDKs support it

Would you like me to help create those tracking issues or provide implementation guidance for the other languages?

AI generated by SDK Consistency Review Agent

@github-actions
Copy link

github-actions bot commented Feb 9, 2026

Cross-SDK Consistency Review: CLI Bundling Feature

I've reviewed PR #414 which adds CLI embedding/bundling functionality to the Go SDK. Here's the consistency analysis across all four SDK implementations:

Good News: Feature Parity Already Exists!

All four SDKs already have CLI bundling/embedding functionality, though implemented using different approaches appropriate to each ecosystem:

SDK Bundling Approach Status
Go (this PR) Runtime extraction from Go embed package ✅ Being added
Node.js/TypeScript npm dependency (@github/copilot package) ✅ Existing
Python Platform-specific wheels with bundled binaries ✅ Existing
.NET MSBuild targets download at build time ✅ Existing

📋 Implementation Comparison

Go SDK (this PR)

  • Build tool: go tool bundler command downloads and prepares CLI binary
  • Embedding: Uses Go's embed package to include binary in application
  • Runtime: Extracts to temp directory on first use with file locking
  • Documentation: ✅ New README section explains the workflow

Node.js/TypeScript SDK

  • Distribution: CLI is npm dependency, bundled via @github/copilot package
  • Resolution: import.meta.resolve() locates bundled CLI at runtime
  • Runtime: No extraction needed—points to JavaScript entry point
  • Documentation: ⚠️ Mentions bundled CLI but lacks detailed bundling docs

Python SDK

  • Distribution: Platform-specific wheels can include copilot/bin/ binaries
  • Resolution: _get_bundled_cli_path() checks for bundled binary
  • Runtime: Direct execution, no extraction needed
  • Documentation: ⚠️ Bundling infrastructure exists but lacks documentation

.NET SDK

  • Build tool: MSBuild .targets file downloads platform-specific binaries
  • Embedding: Copies to runtimes/{RID}/native/ in output directory
  • Runtime: GetBundledCliPath() locates platform-specific binary
  • Documentation: ⚠️ Automatic discovery not documented

🎯 Consistency Assessment

API Consistency: ✅ All SDKs follow the same priority order:

  1. Explicit user-provided path (cliPath/cli_path/CLIPath/CliPath)
  2. Environment variable (COPILOT_CLI_PATH)
  3. Bundled/embedded CLI (automatic fallback)

Naming Consistency: ✅ Follows language conventions:

  • TypeScript: cliPath (camelCase)
  • Python: cli_path (snake_case)
  • Go: CLIPath (PascalCase for exported)
  • .NET: CliPath (PascalCase)

💡 Suggestions for Improvement

While this PR maintains cross-SDK consistency, consider these non-blocking documentation improvements across all SDKs:

  1. Node.js SDK: Document the bundling mechanism more explicitly in README
  2. Python SDK: Add documentation about creating/using platform-specific wheels
  3. .NET SDK: Document the automatic bundled CLI discovery feature
  4. Go SDK: ✅ This PR already adds excellent documentation!

Conclusion

This PR maintains excellent cross-SDK consistency. The Go implementation uses idiomatic Go patterns (embed package, build tools) while achieving the same user-facing behavior as other SDKs. The feature allows users across all platforms to:

  • Bundle the CLI with their application distribution
  • Avoid requiring end-users to install the CLI separately
  • Control which CLI version their application uses

No blocking issues found. The implementation differences are appropriate ecosystem adaptations rather than inconsistencies.

AI generated by SDK Consistency Review Agent

@qmuntal qmuntal marked this pull request as ready for review February 9, 2026 15:49
@qmuntal qmuntal requested a review from a team as a code owner February 9, 2026 15:49
Copilot AI review requested due to automatic review settings February 9, 2026 15:49
Copy link
Contributor

Copilot AI left a comment

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 SDK support for bundling and automatically installing a GitHub Copilot CLI binary embedded into an application, so users don’t need a separate CLI installation.

Changes:

  • Introduces a small cross-platform file-locking helper (internal/flock) with tests.
  • Adds an embedded CLI installer (internal/embeddedcli) plus a small public wrapper package (embeddedcli) and hooks NewClient to prefer the embedded CLI when available.
  • Adds a go/cmd/bundler tool to download the CLI from npm, zstd-compress it, and generate Go source that embeds the artifacts; updates Go module deps and README.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
go/internal/flock/flock.go Adds Acquire API for lockfiles (currently has an open-flag bug).
go/internal/flock/flock_unix.go Unix flock implementation.
go/internal/flock/flock_windows.go Windows LockFileEx/UnlockFileEx implementation.
go/internal/flock/flock_other.go Stub implementation for unsupported platforms.
go/internal/flock/flock_test.go Concurrency + idempotent release tests for flock.
go/internal/embeddedcli/embeddedcli.go Implements lazy install + lookup of embedded CLI (hashing/dir choice need hardening).
go/internal/embeddedcli/embeddedcli_test.go Tests for setup constraints, install, version sanitization, and hash mismatch behavior.
go/embeddedcli/installer.go Public wrapper exposing embeddedcli.Setup + config type alias.
go/cmd/bundler/main.go New bundler tool to download, compress, and generate embed-ready Go code (has a panic bug + networking/resource concerns).
go/client.go Uses embedded CLI path when COPILOT_CLI_PATH is unset and default CLIPath is used (needs guard for external-server mode).
go/README.md Documents embedding flow via bundler tool.
go/go.mod Adds zstd dependency for generated code.
go/go.sum Adds checksums for new dependency.
Comments suppressed due to low confidence (3)

go/internal/embeddedcli/embeddedcli.go:130

  • After writing config.Cli to disk, the code never verifies the written file matches config.CliHash. This defeats the purpose of shipping a hash and allows silent corruption (or unexpected reader contents). Compute/compare the SHA-256 while writing (or immediately after), and return an error on mismatch (ideally via an atomic temp-file+rename install).
	f, err := os.OpenFile(finalPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
	if err != nil {
		return "", fmt.Errorf("creating binary file: %w", err)
	}
	_, err = io.Copy(f, config.Cli)

go/internal/embeddedcli/embeddedcli.go:123

  • If finalPath exists but has the wrong hash, installAt returns an error and gives up. A previous interrupted install (partial file) would then permanently break embedded CLI usage. Consider a safer install strategy (temp file + rename) and a recovery path for hash mismatches (e.g., delete+reinstall in a trusted per-user dir or surface a clearer remediation).
		}
		if !bytes.Equal(existingHash, config.CliHash) {
			return "", fmt.Errorf("existing binary hash mismatch")
		}
		return finalPath, nil

go/cmd/bundler/main.go:527

  • File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.
				outFile.Close()

func Setup(cfg Config) {
if cfg.Cli == nil {
panic("Cli reader is required")
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

Config docs say CliHash is required, but Setup only validates Cli. If CliHash is empty/wrong length, later comparisons are meaningless and corrupted/tampered installs won't be detected. Validate CliHash in Setup (e.g., non-empty and expected SHA-256 length) and fail fast with a clear panic/message.

This issue also appears in the following locations of the same file:

  • line 126
  • line 119
Suggested change
}
}
if len(cfg.CliHash) != sha256.Size {
panic(fmt.Sprintf("CliHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.CliHash)))
}

Copilot uses AI. Check for mistakes.
Comment on lines +201 to +205
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("failed to fetch: %w", err)
}
defer resp.Body.Close()
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

This tool uses http.Get, which has no request timeout and can hang indefinitely on stalled connections. Prefer an http.Client{Timeout: ...} and/or context-aware requests, and apply that consistently to all downloads in this tool (package-lock fetch, CLI tarball, and license tarball).

Copilot uses AI. Check for mistakes.
Comment on lines 183 to +188
// Check environment variable for CLI path
if cliPath := os.Getenv("COPILOT_CLI_PATH"); cliPath != "" {
opts.CLIPath = cliPath
} else if embeddedPath := embeddedcli.Path(); embeddedPath != "" && opts.CLIPath == "copilot" {
// Use the unpacked embedded CLI if available and no custom path was set
opts.CLIPath = embeddedPath
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

embeddedcli.Path() is evaluated even when CLIUrl is set (external server mode). In that case the client won’t spawn a local CLI process, so this can trigger an unnecessary embedded CLI install/unpack. Consider guarding this branch with !client.isExternalServer (or opts.CLIUrl == "") so embedded installation only happens when CLIPath will actually be used.

Copilot uses AI. Check for mistakes.
// Acquire opens (or creates) the lock file at path and blocks until the lock is acquired.
// It returns a release function to unlock and close the file.
func Acquire(path string) (func() error, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC, 0644)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

Acquire opens the lock file with os.O_CREATE|os.O_TRUNC but without an access mode (O_RDWR/O_WRONLY). In Go this means read-only (O_RDONLY) and combining that with O_TRUNC can fail on many platforms, causing locking to never work. Open the file with a write-capable flag (typically O_RDWR|O_CREATE) and consider dropping O_TRUNC since lockfiles generally shouldn't be truncated on each acquisition.

Suggested change
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC, 0644)
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +85
installDir := config.Dir
if installDir == "" {
installDir = os.TempDir()
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

When Dir is empty, the binary is installed directly into os.TempDir() (often a shared, world-writable location like /tmp). Installing the executable directly there increases collision/DoS risk between different apps/users and makes it easier for other local users to interfere. Consider defaulting to a per-user directory (e.g. os.UserCacheDir()/os.UserConfigDir()) and/or a dedicated subdirectory with restrictive permissions.

Copilot uses AI. Check for mistakes.
Comment on lines +409 to +411
stat, _ := os.Stat(binaryPath)
sizeMB := float64(stat.Size()) / 1024 / 1024
fmt.Printf("Downloaded %s (%.1f MB)\n", binaryName, sizeMB)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

downloadCLIBinary ignores the error from os.Stat(binaryPath) and then unconditionally uses stat.Size(). If os.Stat fails, stat is nil and this will panic. Handle the error (or reuse the earlier successful os.Stat result) before computing size.

Copilot uses AI. Check for mistakes.
Comment on lines +336 to +340
r, err := zstd.NewReader(bytes.NewReader(localEmbeddedCopilotCLI))
if err != nil {
panic("failed to create zstd reader: " + err.Error())
}
return r
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The generated cliReader() creates a zstd decoder via zstd.NewReader(...) but never closes it. Since embeddedcli.Config only accepts an io.Reader, there’s currently no way for the installer to call Close, which can leak resources. Consider changing the config to accept an io.ReadCloser (or closing Cli when it implements io.Closer) and update the generated code accordingly.

Copilot uses AI. Check for mistakes.
return fmt.Errorf("failed to create output file: %w", err)
}
if _, err := io.Copy(outFile, r); err != nil {
outFile.Close()
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.

This issue also appears on line 527 of the same file.

Copilot uses AI. Check for mistakes.
//
// --platform: Target platform using Go conventions (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64). Defaults to current platform.
// --output: Output directory for embedded artifacts. Defaults to the current directory.
// --cli-version: CLI version to download. If not specified, automatically detects from the copilot-sdk version in go.mod.
Copy link
Contributor

@SteveSandersonMS SteveSandersonMS Feb 9, 2026

Choose a reason for hiding this comment

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

Is there something that forces developers to realise if their local bundled version is out of date?

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