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
18 changes: 0 additions & 18 deletions .github/lsp.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,6 @@
".go": "go"
},
"rootUri": "go"
},
"rust-analyzer": {
"command": "rust-analyzer",
"fileExtensions": {
".rs": "rust"
},
"initializationOptions": {
"cargo": {
"buildScripts": {
"enable": true
},
"allFeatures": true
},
"checkOnSave": true,
"check": {
"command": "clippy"
}
}
}
}
}
16 changes: 10 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,17 @@ jobs:
- 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 cli-version.txt exists
run: |
if [[ ! -f cli-version.txt ]]; then
echo "::error::cli-version.txt was not generated. The Snapshot step must run before packaging."
exit 1
fi
bash scripts/snapshot-bundled-cli-version.sh
bash scripts/snapshot-bundled-in-process-version.sh
- name: Verify CLI version snapshots exist
run: |
for snapshot in cli-version.txt cli-version-in-process.txt; do
if [[ ! -f "${snapshot}" ]]; then
echo "::error::${snapshot} was not generated. The Snapshot step must run before packaging."
exit 1
fi
done
- name: Package (dry run)
run: cargo publish --dry-run --allow-dirty
- name: Upload artifact
Expand Down
94 changes: 87 additions & 7 deletions .github/workflows/rust-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ permissions:

jobs:
test:
name: "Rust SDK Tests"
name: "Rust SDK Tests (${{ matrix.os }}, default)"
if: github.event.repository.fork == false
env:
POWERSHELL_UPDATECHECK: Off
Expand Down Expand Up @@ -84,7 +84,7 @@ jobs:
# Share the bundled-CLI archive cache with the `bundle` job: build.rs
# now downloads in both modes (embed for `bundle`, extract-to-cache
# for this `test` job's `--no-default-features` build).
- name: Cache bundled CLI tarball
- name: Cache bundled CLI archives
uses: actions/cache@v4
with:
path: ./rust/.bundled-cli-cache
Expand All @@ -98,7 +98,7 @@ jobs:
if: runner.os == 'Linux'
env:
BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache
run: cargo clippy --all-targets --features test-support -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type
run: cargo clippy --all-targets --features test-support,bundled-in-process -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type

- name: cargo doc
if: runner.os == 'Linux'
Expand Down Expand Up @@ -129,14 +129,92 @@ jobs:
# The dedicated `bundle` job below exercises the embed pipeline.
run: cargo test --no-default-features --features test-support -- --test-threads=4 --nocapture

# Exercises the in-process FFI transport (`Transport::InProcess`, the Rust
# analogue of the .NET `RuntimeConnection.ForInProcess()`), mirroring the
# `inprocess` transport cell in dotnet-sdk-tests.yml. Sets
# COPILOT_SDK_DEFAULT_CONNECTION=inprocess so the client hosts the runtime
# cdylib in-process instead of spawning a stdio child, then runs the whole
# E2E suite over the in-process transport. The suite runs serially in-process
# (the harness forces concurrency to 1) because it mirrors each test's
# environment onto the shared process environment the in-process worker inherits.
# Runs the whole E2E suite over the in-process transport on supported hosts.
test-inprocess:
name: "Rust SDK Tests (${{ matrix.os }}, inprocess)"
if: github.event.repository.fork == false
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
strategy:
fail-fast: false
matrix:
# TODO: Re-enable Windows after fixing the napi-oop peer shutdown crash.
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
working-directory: ./rust
steps:
- uses: actions/checkout@v6.0.2

- uses: ./.github/actions/setup-copilot
id: setup-copilot

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
toolchain: "1.94.0"

- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
with:
workspaces: "rust"
prefix-key: v1-rust-no-bin
cache-bin: false

- name: Read pinned @github/copilot CLI version
id: cli-version
working-directory: ./nodejs
run: |
version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version")
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Pinned CLI version: $version"

- name: Cache bundled CLI archives
uses: actions/cache@v4
with:
path: ./rust/.bundled-cli-cache
key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }}

- name: Install test harness dependencies
working-directory: ./test/harness
run: npm ci --ignore-scripts

- name: Warm up PowerShell
if: runner.os == 'Windows'
run: pwsh.exe -Command "Write-Host 'PowerShell ready'"

- name: Select in-process transport
run: echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV"

- name: cargo test (in-process transport, full E2E suite)
timeout-minutes: 60
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
COPILOT_CLI_PATH: ${{ steps.setup-copilot.outputs.cli-path }}
BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache
# The harness forces serial execution in-process (both the async semaphore and
# libtest via --test-threads=1) because it mirrors each test's environment onto
# the shared process environment, so RUST_E2E_CONCURRENCY is not set here.
run: cargo test --no-default-features --features test-support,bundled-in-process --test e2e -- --test-threads=1 --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"
name: "Rust SDK Bundled CLI Build (${{ matrix.os }})"
if: github.event.repository.fork == false
env:
CARGO_TERM_COLOR: always
Expand Down Expand Up @@ -180,13 +258,15 @@ jobs:
# ~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
- name: Cache bundled CLI archives
uses: actions/cache@v4
with:
path: ./rust/.bundled-cli-cache
key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }}

- name: cargo build (bundled-cli is the default feature)
- name: Test bundled CLI build paths
env:
BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache
run: cargo build
run: |
cargo build
cargo test --features bundled-in-process --lib embedded_archive_contains_only_expected_files
1 change: 1 addition & 0 deletions rust/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
Cargo.lock.bak
cli-version.txt
cli-version-in-process.txt
11 changes: 11 additions & 0 deletions rust/Cargo.lock

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

12 changes: 8 additions & 4 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ readme = "README.md"
license = "MIT"
include = [
"src/**/*",
"build/**/*",
"examples/**/*",
"tests/**/*",
"build.rs",
"Cargo.toml",
"README.md",
"LICENSE",
"cli-version.txt",
"cli-version-in-process.txt",
]

[lib]
Expand All @@ -28,6 +30,7 @@ name = "github_copilot_sdk"
[features]
default = ["bundled-cli"]
bundled-cli = ["dep:tar", "dep:flate2", "dep:zip"]
bundled-in-process = ["bundled-cli", "dep:libloading"]
derive = ["dep:schemars"]
test-support = []

Expand All @@ -49,10 +52,13 @@ tokio-stream = { version = "0.1", features = ["sync"] }
tokio-util = { version = "0.7", default-features = false }
tracing = "0.1"
dirs = "5"
libloading = { version = "0.8", optional = true }
parking_lot = "0.12"
regex = "1"
getrandom = "0.2"
uuid = { version = "1", default-features = false, features = ["v4"] }
flate2 = { version = "1", optional = true }
tar = { version = "0.4", optional = true }
# LLM inference callback transport: idiomatic HTTP/WebSocket forwarding for the
# `CopilotRequestHandler`, plus base64/byte/stream plumbing for the chunk protocol.
base64 = "0.22"
Expand All @@ -65,10 +71,6 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c
[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"
Expand All @@ -89,8 +91,10 @@ name = "protocol_version_test"
required-features = ["test-support"]

[build-dependencies]
base64 = "0.22"
dirs = "5"
flate2 = "1"
serde_json = "1"
sha2 = "0.10"
tar = "0.4"
ureq = { version = "2", default-features = false, features = ["tls"] }
Expand Down
52 changes: 33 additions & 19 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ client.stop().await?;

**`ClientOptions`:**

| Field | Type | Description |
| ------------- | --------------------------- | --------------------------------------------------------------- |
| `program` | `CliProgram` | `Resolve` (default: auto-detect) or `Path(PathBuf)` (explicit) |
| `prefix_args` | `Vec<OsString>` | Args before `--server` (e.g. script path for node) |
| `cwd` | `PathBuf` | Working directory for CLI process |
| `env` | `Vec<(OsString, OsString)>` | Environment variables for CLI process |
| `env_remove` | `Vec<OsString>` | Environment variables to remove |
| `extra_args` | `Vec<String>` | Extra CLI flags |
| `transport` | `Transport` | `Stdio` (default), `Tcp { port }`, or `External { host, port }` |
| Field | Type | Description |
| ------------------- | --------------------------- | ----------------------------------------------------------------- |
| `program` | `CliProgram` | `Resolve` (default: auto-detect) or `Path(PathBuf)` (explicit) |
| `prefix_args` | `Vec<OsString>` | Args before `--server` (e.g. script path for node) |
| `working_directory` | `PathBuf` | Working directory for CLI process (empty = host process's cwd) |
| `env` | `Vec<(OsString, OsString)>` | Environment variables for CLI process |
| `env_remove` | `Vec<OsString>` | Environment variables to remove |
| `extra_args` | `Vec<String>` | Extra CLI flags |
| `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` |

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`.

Expand Down Expand Up @@ -749,7 +749,7 @@ none of them are scheduled for removal.
caller-supplied `AsyncRead` / `AsyncWrite`. Useful for testing,
in-process embedding, or custom transports. Other SDKs are spawn-only
or fixed-stdio.
- **`enum Transport { Stdio, Tcp, External }`** — explicit, exhaustive
- **`enum Transport { Default, Stdio, InProcess, Tcp, External }`** — explicit
transport selector on `ClientOptions::transport`. Node/Python/Go rely
on conditional config field combinations instead.
- **Split `prefix_args` / `extra_args`** on `ClientOptions` — separate
Expand All @@ -776,7 +776,14 @@ none of them are scheduled for removal.

## Embedded CLI

The SDK provisions the Copilot CLI binary at build time. By default the `bundled-cli` feature embeds the verified binary directly in your compiled crate, so end-user binaries are self-contained — no env var setup, no separate install, just `cargo build`.
The SDK provisions the Copilot CLI binary at build time. By default the
`bundled-cli` feature embeds only the verified CLI executable in your compiled
crate. Enable `bundled-in-process` to additionally embed the native
runtime library and use `Transport::InProcess`:

```toml
github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] }
```

For builds that prefer a smaller artifact, disable the `bundled-cli` feature:

Expand All @@ -795,7 +802,7 @@ github-copilot-sdk = { version = "0.1", default-features = false }
> together.
>
> **Convenience on the build machine only.** As a special case,
> `build.rs` downloads and SHA-verifies the compatible CLI version and
> `build.rs` downloads and integrity-verifies the compatible CLI version and
> drops it into the build machine's per-user cache; the runtime
> resolver on that same machine will pick it up automatically. This
> makes local development and CI ergonomic, but it does **not** carry
Expand All @@ -812,8 +819,11 @@ github-copilot-sdk = { version = "0.1", default-features = false }

The resolved version is baked into the crate via `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` regardless of mode. The runtime resolver consumes it to recompute the on-disk path by convention, so no absolute paths leak into the rlib.

2. **Build time:** `build.rs` 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), live-fetches the matching `SHA256SUMS.txt`, and verifies the archive hash. Then:
- **`bundled-cli` on (default, release):** embeds the raw archive bytes via `include_bytes!()`. Runtime extracts on first `Client::start()`.
2. **Build time:** `build.rs` downloads the platform-specific npm package and
verifies its `sha512` integrity against the lockfile or publish snapshot.
Then:
- **`bundled-cli` on (default):** creates and embeds a minimal archive containing only the CLI executable.
- **`bundled-in-process` on:** the minimal archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`); no other npm package files are embedded.
- **`bundled-cli` off:** extracts the binary directly into the platform cache (staging file + atomic rename), idempotent across rebuilds. If the extracted binary is already present at the expected path, the download is skipped entirely — the extracted binary *is* the cache.

3. **Runtime:** in both modes the binary lives at:
Expand Down Expand Up @@ -899,10 +909,11 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`

## Features

| Feature | Default | Description |
| -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bundled-cli` | ✓ | Build-time CLI embedding. Pulls in `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). |
| Feature | Default | Description |
| ------- | ------- | ----------- |
| `bundled-cli` | ✓ | Embeds only the CLI executable. Disable via `default-features = false` when supplying the CLI via `CliProgram::Path` or `COPILOT_CLI_PATH`. |
| `bundled-in-process` | — | Enables `Transport::InProcess`, implies `bundled-cli`, and additionally embeds only the platform-native runtime library. |
| `derive` | — | `schema_for::<T>()` for generating JSON Schema from Rust types (adds `schemars`). |

```toml
# These examples use registry syntax for illustration; until the crate is
Expand All @@ -911,7 +922,10 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`
# Default — bundles the Copilot CLI in your binary.
github-copilot-sdk = "0.1"

# Opt out of bundling — resolve CLI from COPILOT_CLI_PATH or system PATH instead.
# Enable the in-process transport and bundle its native runtime library.
github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] }

# Opt out of bundling — supply the CLI explicitly at runtime.
github-copilot-sdk = { version = "0.1", default-features = false }

# Derive JSON Schema for tool parameters (adds to default bundled-cli).
Expand Down
Loading
Loading