Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ jobs:
workspaces: "rust"
- name: Set version
run: sed -i -E 's/^version = ".*"$/version = "${{ needs.version.outputs.version }}"/' Cargo.toml
- name: Snapshot CLI version + hashes for build.rs
run: bash scripts/snapshot-bundled-cli-version.sh
- name: Verify snapshot file exists
run: |
if [[ ! -f bundled_cli_version.txt ]]; then
echo "::error::bundled_cli_version.txt was not generated. The Snapshot step must run before packaging."
exit 1
fi
- name: Package (dry run)
run: cargo publish --dry-run --allow-dirty
- name: Upload artifact
Expand Down
34 changes: 17 additions & 17 deletions .github/workflows/rust-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,17 @@ jobs:
RUST_E2E_CONCURRENCY: 4
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }}
run: cargo test --features test-support -- --test-threads=4 --nocapture

# Validates the `embedded-cli` build path on all three supported
# platforms. This is the only place `build.rs` actually runs (the
# default `cargo test` job above has `COPILOT_CLI_VERSION` unset, so
# `build.rs` returns immediately). Catches regressions in the
# download / verify / extract / embed pipeline before they ship to
# crates.io and before bundling consumers (e.g. github-app's
# bundled-CLI release pipeline) hit them downstream.
# `--no-default-features` disables the bundled-cli download; the
# tests use the CLI provided by setup-copilot via COPILOT_CLI_PATH.
# The dedicated `bundle` job below exercises the bundling pipeline.
run: cargo test --no-default-features --features test-support -- --test-threads=4 --nocapture

# Validates the bundled-CLI build path on all three supported
# platforms. While the regular `cargo test` job above also exercises
# build.rs (bundling is on by default now), this matrix job is the
# dedicated cross-platform smoke test for the download / verify /
# extract / embed pipeline. Catches regressions before they ship to
# crates.io and before bundling consumers hit them downstream.
bundle:
name: "Rust SDK Bundled CLI Build"
if: github.event.repository.fork == false
Expand Down Expand Up @@ -144,23 +146,21 @@ jobs:
id: cli-version
working-directory: ./nodejs
run: |
version=$(node -p "require('./package.json').dependencies['@github/copilot'].replace(/^[\^~]/, '')")
version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version")
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Pinned CLI version: $version"

# Cache the downloaded archive across runs so we don't refetch
# ~130 MB on every CI invocation. Keyed by OS + CLI version; on
# cache miss the bundle job exercises the full ureq download +
# SHA-256 + retry path, which is exactly the regression surface
# we want validated.
# ~130 MB on every CI invocation. Keyed by OS + CLI version so old
# archives drop out when the pinned version bumps, keeping the
# cache bounded.
- name: Cache bundled CLI tarball
uses: actions/cache@v4
with:
path: ./rust/.bundled-cli-cache
key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }}

Comment thread
SteveSandersonMS marked this conversation as resolved.
- name: cargo build --features embedded-cli
- name: cargo build (bundled-cli is the default feature)
env:
COPILOT_CLI_VERSION: ${{ steps.cli-version.outputs.version }}
BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache
run: cargo build --features embedded-cli
run: cargo build
1 change: 1 addition & 0 deletions rust/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
Cargo.lock.bak
bundled_cli_version.txt
61 changes: 1 addition & 60 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 19 additions & 15 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,23 @@ homepage = "https://github.com/github/copilot-sdk"
documentation = "https://docs.rs/github-copilot-sdk"
readme = "README.md"
license = "MIT"
exclude = [
"RELEASING.md",
"rust-toolchain.toml",
".rustfmt.toml",
".rustfmt.nightly.toml",
"clippy.toml",
".gitignore",
include = [
"src/**/*",
"examples/**/*",
"tests/**/*",
"build.rs",
"Cargo.toml",
"README.md",
"LICENSE",
"bundled_cli_version.txt",
]

[lib]
name = "github_copilot_sdk"

[features]
default = []
embedded-cli = ["dep:sha2", "dep:zstd"]
default = ["bundled-cli"]
bundled-cli = ["dep:dirs", "dep:tar", "dep:flate2", "dep:zip"]
derive = ["dep:schemars"]
test-support = []

Expand All @@ -46,27 +48,29 @@ tokio = { version = "1", features = ["io-util", "sync", "rt", "process", "net",
tokio-stream = { version = "0.1", features = ["sync"] }
tokio-util = { version = "0.7", default-features = false }
tracing = "0.1"
dirs = "5"
dirs = { version = "5", optional = true }
parking_lot = "0.12"
regex = "1"
sha2 = { version = "0.10", optional = true }
getrandom = "0.2"
uuid = { version = "1", default-features = false, features = ["v4"] }
zstd = { version = "0.13", optional = true }

[target.'cfg(windows)'.dependencies]
zip = { version = "2", default-features = false, features = ["deflate"], optional = true }

[target.'cfg(not(windows))'.dependencies]
flate2 = { version = "1", optional = true }
tar = { version = "0.4", optional = true }

[dev-dependencies]
rusqlite = { version = "0.35", features = ["bundled"] }
schemars = "1"
serial_test = "3"
tempfile = "3"
sha2 = "0.10"
tokio = { version = "1", features = ["rt-multi-thread"] }
zstd = "0.13"

[build-dependencies]
flate2 = "1"
sha2 = "0.10"
tar = "0.4"
ureq = { version = "2", default-features = false, features = ["tls"] }
zip = { version = "2", default-features = false, features = ["deflate"] }
zstd = "0.13"
61 changes: 39 additions & 22 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ client.stop().await?;
| `extra_args` | `Vec<String>` | Extra CLI flags |
| `transport` | `Transport` | `Stdio` (default), `Tcp { port }`, or `External { host, port }` |

With the default `CliProgram::Resolve`, `Client::start()` automatically resolves the binary via `github_copilot_sdk::resolve::copilot_binary()` — checking `COPILOT_CLI_PATH`, the [embedded CLI](#embedded-cli), and then the system PATH. Use `CliProgram::Path(path)` to skip resolution.
With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in this order: an explicit `CliProgram::Path(path)`, the `COPILOT_CLI_PATH` env var, then the bundled CLI that was embedded at build time. There is no PATH scanning — if you've opted out of bundling (`default-features = false`) you must supply either `CliProgram::Path` or `COPILOT_CLI_PATH`.

### Session

Expand Down Expand Up @@ -740,61 +740,78 @@ none of them are scheduled for removal.
| `transforms.rs` | `SystemMessageTransform` trait, section-level system message customization |
| `tool.rs` | `ToolHandler` trait, `define_tool`, `schema_for::<T>()` (with `derive` feature) |
| `types.rs` | CLI protocol types (`SessionId`, `SessionEvent`, `SessionConfig`, `Tool`, etc.) |
| `resolve.rs` | Binary resolution (`copilot_binary`, `node_binary`, `extended_path`) |
| `embeddedcli.rs` | Embedded CLI extraction (`embedded-cli` feature) |
| `resolve.rs` | Bundled-CLI resolution (`copilot_binary`) |
| `embeddedcli.rs` | Embedded CLI extraction (gated on the default `bundled-cli` feature) |
| `router.rs` | Internal per-session event demux |
| `jsonrpc.rs` | Internal Content-Length framed JSON-RPC transport |

## Embedded CLI

By default, `copilot_binary()` searches `COPILOT_CLI_PATH`, the system PATH, and common install locations. To **ship with a specific CLI version** embedded in the binary, set `COPILOT_CLI_VERSION` at build time:
The SDK bundles the Copilot CLI binary inside the consumer's compiled crate by default. No env var setup, no separate install — just `cargo build` and you get a self-contained binary.

```bash
COPILOT_CLI_VERSION=1.0.15 cargo build
To opt out (e.g. for binary-size-sensitive consumers, or environments that provide the CLI via PATH), set `default-features = false`:

```toml
github-copilot-sdk = { version = "0.1", default-features = false }
```

### How it works

1. **Build time:** The SDK's `build.rs` detects `COPILOT_CLI_VERSION`, downloads the platform-appropriate archive from the [`github/copilot-cli` GitHub Releases](https://github.com/github/copilot-cli/releases) (`copilot-{platform}.tar.gz` on macOS/Linux, `.zip` on Windows), verifies the archive's SHA-256 against the release's `SHA256SUMS.txt`, extracts the `copilot` binary, compresses it with zstd, and embeds via `include_bytes!()`. No extra steps or tools needed — just the env var.
1. **Pinned at publish time.** When the rust crate is published, a workflow step writes `bundled_cli_version.txt` (CLI version + per-platform SHA-256 hashes) into the crate from the in-effect `nodejs/package-lock.json` and the matching GitHub Release's `SHA256SUMS.txt`. This file is gitignored locally; it only exists in the published crate tarball.

2. **Build time:** The SDK's `build.rs` resolves the version + per-platform SHA-256:
- `COPILOT_CLI_VERSION` env var (advanced override; fetches live `SHA256SUMS.txt`).
- Otherwise, `bundled_cli_version.txt` from the published crate.
- Otherwise (mono-repo contributor build), live read from `../nodejs/package-lock.json` + live fetch of `SHA256SUMS.txt`.

It then downloads the platform-appropriate archive from the [`github/copilot-cli` GitHub Releases](https://github.com/github/copilot-cli/releases) (`copilot-{platform}.tar.gz` on macOS/Linux, `.zip` on Windows), verifies the SHA-256, extracts the `copilot` binary, compresses it with zstd, and embeds via `include_bytes!()`.

2. **Runtime:** On the first call to `github_copilot_sdk::resolve::copilot_binary()`, the embedded binary is lazily extracted to `~/.cache/github-copilot-sdk-{version}/copilot` (or `copilot.exe` on Windows), SHA-256 verified, and cached. Subsequent calls return the cached path.
3. **Runtime:** On the first call to `github_copilot_sdk::Client::start()`, the embedded archive is lazily extracted to the platform cache dir (`%LOCALAPPDATA%\github-copilot-sdk-{version}\` on Windows, `~/Library/Caches/github-copilot-sdk-{version}/` on macOS, `$XDG_CACHE_HOME/github-copilot-sdk-{version}/` (or `~/.cache/...`) on Linux). Subsequent runs reuse the extracted binary.

3. **Dev builds:** Without the env var, `build.rs` does nothing. The binary is resolved from PATH as usual — zero friction.
### Overriding the extraction location

Use [`ClientOptions::with_bundled_cli_extract_dir`] when you need to place the extracted binary somewhere other than the platform cache dir (CI runners with ephemeral homes, sandboxes that disallow cache paths, etc.):

```rust,ignore
use std::path::PathBuf;
use github_copilot_sdk::{Client, ClientOptions};

let options = ClientOptions::new()
.with_bundled_cli_extract_dir(PathBuf::from("/var/run/my-app/copilot"));
let client = Client::start(options).await?;
```

### Resolution priority

`copilot_binary()` checks these sources in order:

1. `COPILOT_CLI_PATH` environment variable
2. Embedded CLI (build-time, via `COPILOT_CLI_VERSION`)
3. System PATH + common install locations
1. Explicit `CliProgram::Path(path)` on `ClientOptions::program`
2. `COPILOT_CLI_PATH` environment variable
3. Embedded CLI (when the `bundled-cli` feature is enabled, which it is by default)

There is no PATH scanning. If both 1+2 are unset and the SDK was built with `default-features = false`, `Client::start` returns `Error::BinaryNotFound`.

### Platforms

Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`. The target platform is auto-detected from `CARGO_CFG_TARGET_OS` and `CARGO_CFG_TARGET_ARCH` (cross-compilation works).

## Features

No features are enabled by default — the bare SDK resolves the CLI from `COPILOT_CLI_PATH` or the system PATH without pulling in additional feature-gated dependencies.

| Feature | Default | Description |
| -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `embedded-cli` | — | Build-time CLI embedding via `COPILOT_CLI_VERSION` (adds `sha2`, `zstd`). Enable when you need to ship a self-contained binary with a pinned CLI version. |
| `bundled-cli` | ✓ | Build-time CLI embedding. Pulls in `dirs`, `tar`+`flate2` (Linux/macOS), or `zip` (Windows). Disable via `default-features = false` to opt out (e.g. when shipping a smaller binary or when always supplying the CLI via `CliProgram::Path` / `COPILOT_CLI_PATH`). |
| `derive` | — | `schema_for::<T>()` for generating JSON Schema from Rust types (adds `schemars`). Enable when defining [tool parameters](#tool-registration). |

```toml
# These examples use registry syntax for illustration; until the crate is
# published, use a path or git dependency instead.

# Minimalresolve CLI from PATH
# Defaultbundles the Copilot CLI in your binary.
github-copilot-sdk = "0.1"

# Ship a pinned CLI version in your binary
github-copilot-sdk = { version = "0.1", features = ["embedded-cli"] }
# Opt out of bundling — resolve CLI from COPILOT_CLI_PATH or system PATH instead.
github-copilot-sdk = { version = "0.1", default-features = false }

# Derive JSON Schema for tool parameters
# Derive JSON Schema for tool parameters (adds to default bundled-cli).
github-copilot-sdk = { version = "0.1", features = ["derive"] }

# Both
github-copilot-sdk = { version = "0.1", features = ["embedded-cli", "derive"] }
```
Loading
Loading