Skip to content

Add Rust SDK (technical preview)#1164

Open
tclem wants to merge 68 commits intomainfrom
tclem/rust-sdk-release-prep
Open

Add Rust SDK (technical preview)#1164
tclem wants to merge 68 commits intomainfrom
tclem/rust-sdk-release-prep

Conversation

@tclem
Copy link
Copy Markdown
Member

@tclem tclem commented Apr 29, 2026

Adds a Rust SDK alongside the existing Node, Python, Go, and .NET SDKs in this repo. Same JSON-RPC client model, same protocol, same session lifecycle — just in Rust.

Important

Technical preview. This is published as github-copilot-sdk = "0.1" (pre-1.0) and the public API is subject to breaking changes as we iterate. Pin to an exact version, expect churn, and please file issues for friction or missing parity.

See rust/README.md for the full overview, examples, and the build/test commands. Generated types follow the same schema-driven flow used by the other SDKs (scripts/codegen/rust.ts).

CI for the new crate runs in .github/workflows/rust-sdk-tests.yml.

  Generated via Copilot (Claude Opus 4.7) on behalf of @tclem

tclem and others added 5 commits April 28, 2026 13:27
Adds the Copilot Rust SDK (`copilot-sdk` crate) under `rust/`,
alongside Rust codegen plumbed into `scripts/codegen/` and CI under
`.github/workflows/rust-sdk-tests.yml`. The crate ships a JSON-RPC
client, session lifecycle management, system message transforms,
permission policy helpers, the `define_tool` adapter, and per-event
`SessionHandler`/`SessionHooks` traits.

Includes:

- 14 ported E2E scenarios under `rust/tests/` driving the replay-proxy
  harness, plus a hand-curated set of unit tests.
- A rust-coding-skill (`.github/skills/rust-coding-skill/`) capturing
  conventions for error handling, async/concurrency, tracing, and the
  intentional trait exceptions in the SDK's public API.
- Release tooling: `rust-publish-release.yml`, `RELEASING.md`, and
  protocol-version generation wired into the existing automation.
- `PermissionResult` extended with `Deferred` and `Custom` variants
  for richer permission decisions.

Public API is held at 0.1.0-pre. Marked protocol-evolving public enums
`#[non_exhaustive]` so additive variants stay non-breaking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Christopher Schleiden <cschleiden@github.com>
Co-authored-by: David Dossett <25163139+daviddossett@users.noreply.github.com>
Co-authored-by: Devraj Mehta <devm33@github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Evan Boyle <EvanBoyle@users.noreply.github.com>
Co-authored-by: Jeremy Moseley <jemoseley@microsoft.com>
Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
- **Broadcast subscriptions for lifecycle and session events.**
  `Client::subscribe_lifecycle()` and `Session::subscribe()` return
  `tokio::sync::broadcast::Receiver`; dropping the receiver
  unsubscribes. Replaces the prior callback-based `Client::on`,
  `Client::on_event_type`, `Session::on`, and `Unsubscribe` API.
  Spawned consumer tasks isolate panics naturally.
- **`PermissionResult` gains `Deferred` and `Custom` variants.**
  `Deferred` lets handlers resolve a request asynchronously via
  `session.permissions.handlePendingPermissionRequest` (notification
  path only — falls back to `Approved` on the direct RPC path).
  `Custom(Value)` lets handlers send arbitrary response payloads
  beyond the standard `approve-once` / `reject` shapes.
- **`#[non_exhaustive]` on protocol-evolving public enums**
  (`PermissionResult`, `SessionLifecycleEventType`,
  `GitHubReferenceType`, others) so additive variants stay
  non-breaking.
- **`ToolHandlerRouter` overrides per-event `SessionHandler` methods**
  so consumers can call `router.on_external_tool(...)` directly
  without unwrapping `HandlerResponse`.
- **`define_tool` accepts bare `async fn` items** in addition to
  closures, matching `tower::service_fn` /
  `hyper::service::service_fn` conventions. Documented in rustdoc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ypes

Generated code emitted `pub session_id: String` for every schema field named
`sessionId` and likewise for `requestId`, leaving consumers with mixed types:
`Session::id()` returned `SessionId` but `session.events_subscribe()` events
exposed `session_id: String`. Same papercut for request IDs in permission and
elicitation event payloads.

The newtypes are `#[serde(transparent)]` so the wire format is unchanged. This
adds a property-name override map to `scripts/codegen/rust.ts` that maps
`sessionId`, `remoteSessionId`, and `requestId` to the hand-authored types in
`crate::types`, and emits the matching `use` statement in both generated
modules. `mc_session_id` (MCP protocol metadata, not a Copilot session) stays
as `String`.

After regeneration: 27 fields converted to `SessionId` (including the handoff
event's `remoteSessionId`) and 25 to `RequestId`. The existing `PartialEq<str>`
/ `PartialEq<String>` impls on both newtypes mean test code like

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
define_tool's Fn(P) -> Fut bound gave closures only the deserialized
arguments, leaving session_id, tool_call_id, and tool_name unreachable.
That blocked the helper for any tool that needs to scope DB lookups to
a session, emit per-tool-call telemetry, or stream UI updates back to
the originating session — patterns that hit dozens of sites across
realistic tool suites.

Change the closure bound to Fn(ToolInvocation, P) -> Fut. The arguments
are moved out via mem::take before deserialization, so there is no
clone cost on the hot path. Closures that don't need the metadata
write |_inv, params|.

Also add ToolInvocation::params<P>() so long-form impl ToolHandler
blocks can deserialize without naming serde_json directly:

    async fn call(&self, inv: ToolInvocation) -> Result<ToolResult, Error> {
        let params: MyParams = inv.params()?;
        // …use inv.session_id alongside params…
    }

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Node, Python, and .NET all expose ping with an optional message.
Go requires it only because Go has no Option type — Rust has one,
so the API should match the languages with the same expressive power
rather than the one without.

Change ping(&self, message: &str) to ping(&self, message: Option<&str>).
When None, the message field is omitted from the request payload
rather than sent as an empty string.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 29, 2026 14:09
@tclem tclem requested a review from a team as a code owner April 29, 2026 14:09
@tclem tclem marked this pull request as draft April 29, 2026 14:11
Copy link
Copy Markdown
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 a new Rust SDK crate (copilot-sdk, library name copilot) to the monorepo, mirroring the existing SDKs’ JSON-RPC client/session model, and wires it into repo workflows and scenario coverage.

Changes:

  • Introduces the Rust SDK crate with protocol types, JSON-RPC transport, session/handler abstractions, examples, and tests.
  • Extends scenario samples and verification scripts to build/run Rust implementations alongside TS/Python/Go/C#.
  • Updates repo automation: codegen, just tasks, scenario-build CI, Rust SDK CI, and release/publish workflows.
Show a summary per file
File Description
test/scenarios/verify.sh Shows Rust status in the aggregated scenario runner UI.
test/scenarios/transport/tcp/verify.sh Builds/runs the Rust TCP transport scenario.
test/scenarios/transport/tcp/rust/Cargo.toml Rust TCP scenario crate manifest.
test/scenarios/transport/tcp/rust/src/main.rs Rust TCP sample using external host:port CLI server.
test/scenarios/transport/stdio/verify.sh Builds/runs the Rust stdio transport scenario.
test/scenarios/transport/stdio/rust/Cargo.toml Rust stdio scenario crate manifest.
test/scenarios/transport/stdio/rust/src/main.rs Rust stdio sample spawning the CLI child process.
test/scenarios/transport/stdio/README.md Documents Rust sample location/package name.
test/scenarios/tools/tool-overrides/verify.sh Builds/runs Rust tool override scenario.
test/scenarios/tools/tool-overrides/rust/Cargo.toml Rust tool-overrides scenario crate manifest (+derive).
test/scenarios/tools/tool-overrides/rust/src/main.rs Rust sample overriding built-in tool behavior.
test/scenarios/tools/tool-filtering/verify.sh Builds/runs Rust tool filtering scenario.
test/scenarios/tools/tool-filtering/rust/Cargo.toml Rust tool-filtering scenario crate manifest.
test/scenarios/tools/tool-filtering/rust/src/main.rs Rust sample limiting available tools.
test/scenarios/tools/skills/verify.sh Builds/runs Rust skills scenario.
test/scenarios/tools/skills/rust/Cargo.toml Rust skills scenario crate manifest.
test/scenarios/tools/skills/rust/src/main.rs Rust sample configuring skill directories + hooks.
test/scenarios/tools/no-tools/verify.sh Builds/runs Rust no-tools scenario.
test/scenarios/tools/no-tools/rust/Cargo.toml Rust no-tools scenario crate manifest.
test/scenarios/tools/no-tools/rust/src/main.rs Rust sample disabling tools + replacing system prompt.
test/scenarios/tools/mcp-servers/verify.sh Builds/runs Rust MCP servers scenario.
test/scenarios/tools/mcp-servers/rust/Cargo.toml Rust mcp-servers scenario crate manifest.
test/scenarios/tools/mcp-servers/rust/src/main.rs Rust sample passing MCP servers config to CLI.
test/scenarios/tools/custom-agents/verify.sh Builds/runs Rust custom agents scenario.
test/scenarios/tools/custom-agents/rust/Cargo.toml Rust custom-agents scenario crate manifest (+derive).
test/scenarios/tools/custom-agents/rust/src/main.rs Rust sample defining custom agents + custom tool.
test/scenarios/sessions/streaming/verify.sh Builds/runs Rust streaming scenario.
test/scenarios/sessions/streaming/rust/Cargo.toml Rust streaming scenario crate manifest.
test/scenarios/sessions/streaming/rust/src/main.rs Rust sample counting streaming delta events.
test/scenarios/sessions/session-resume/verify.sh Builds/runs Rust session resume scenario.
test/scenarios/sessions/session-resume/rust/Cargo.toml Rust session-resume scenario crate manifest.
test/scenarios/sessions/session-resume/rust/src/main.rs Rust sample creating + resuming session by ID.
test/scenarios/sessions/infinite-sessions/verify.sh Builds/runs Rust infinite sessions scenario.
test/scenarios/sessions/infinite-sessions/rust/Cargo.toml Rust infinite-sessions scenario crate manifest.
test/scenarios/sessions/infinite-sessions/rust/src/main.rs Rust sample exercising infinite session thresholds.
test/scenarios/sessions/concurrent-sessions/verify.sh Builds/runs Rust concurrent sessions scenario.
test/scenarios/sessions/concurrent-sessions/rust/Cargo.toml Rust concurrent-sessions scenario crate manifest.
test/scenarios/sessions/concurrent-sessions/rust/src/main.rs Rust sample running two sessions concurrently.
test/scenarios/prompts/system-message/verify.sh Builds/runs Rust system-message scenario.
test/scenarios/prompts/system-message/rust/Cargo.toml Rust system-message scenario crate manifest.
test/scenarios/prompts/system-message/rust/src/main.rs Rust sample replacing system message.
test/scenarios/prompts/reasoning-effort/verify.sh Builds/runs Rust reasoning-effort scenario.
test/scenarios/prompts/reasoning-effort/rust/Cargo.toml Rust reasoning-effort scenario crate manifest.
test/scenarios/prompts/reasoning-effort/rust/src/main.rs Rust sample setting reasoning_effort.
test/scenarios/modes/default/verify.sh Builds/runs Rust default mode scenario.
test/scenarios/modes/default/rust/Cargo.toml Rust default-mode scenario crate manifest.
test/scenarios/modes/default/rust/src/main.rs Rust sample using default tool-enabled mode.
test/scenarios/callbacks/user-input/verify.sh Builds/runs Rust user-input callback scenario.
test/scenarios/callbacks/user-input/rust/Cargo.toml Rust user-input scenario crate manifest.
test/scenarios/callbacks/user-input/rust/src/main.rs Rust sample handling ask_user prompts.
test/scenarios/callbacks/permissions/verify.sh Builds/runs Rust permission callback scenario.
test/scenarios/callbacks/permissions/rust/Cargo.toml Rust permissions scenario crate manifest.
test/scenarios/callbacks/permissions/rust/src/main.rs Rust sample logging/approving permissions.
test/scenarios/callbacks/hooks/verify.sh Builds/runs Rust hooks callback scenario.
test/scenarios/callbacks/hooks/rust/Cargo.toml Rust hooks scenario crate manifest.
test/scenarios/callbacks/hooks/rust/src/main.rs Rust sample implementing SessionHooks logging.
test/scenarios/RUST_COVERAGE.md Documents Rust scenario parity coverage/gaps.
scripts/codegen/package.json Adds Rust generation to codegen scripts.
nodejs/scripts/update-protocol-version.ts Generates Rust SDK protocol version constant.
justfile Adds Rust format/lint/test/codegen tasks.
.gitignore Ignores Rust scenario build artifacts/lockfiles.
.github/workflows/scenario-builds.yml Adds CI job building all Rust scenario crates.
.github/workflows/rust-sdk-tests.yml Adds Rust SDK CI (fmt/clippy/doc/test/semver-checks).
.github/workflows/rust-release-pr.yml Adds release-plz workflow to open Rust release PRs.
.github/workflows/rust-publish-release.yml Adds release-plz workflow to publish Rust crate.
.github/workflows/codegen-check.yml Ensures Rust codegen + protocol version regen in CI.
.github/skills/rust-coding-skill/SKILL.md Adds repo-specific Rust engineering guidance.
.github/skills/rust-coding-skill/examples.md Adds Rust SDK examples/patterns for contributors.
.github/copilot-instructions.md Updates repo guidance to include Rust SDK + Rust skill.
rust/Cargo.toml Defines the new copilot-sdk crate (features, deps, MSRV).
rust/Cargo.lock Locks Rust dependencies for deterministic builds.
rust/README.md Documents Rust SDK usage, architecture, and API surface.
rust/CHANGELOG.md Establishes initial changelog and release-plz plan.
rust/RELEASING.md Documents release/publish operations for maintainers.
rust/LICENSE Rust crate license file.
rust/rust-toolchain.toml Pins Rust toolchain version/components for the crate.
rust/release-plz.toml Configures release-plz behavior for the Rust crate.
rust/clippy.toml Configures Rust clippy rules (e.g., disallowed macros).
rust/.rustfmt.toml Stable rustfmt config (edition 2024).
rust/.rustfmt.nightly.toml Nightly rustfmt config enabling unstable formatting opts.
rust/.gitignore Ignores Rust target dir and backup lock files.
rust/build.rs Build-time CLI bundling/extraction codegen support.
rust/src/sdk_protocol_version.rs Generated SDK protocol version constant for Rust.
rust/src/generated/mod.rs Rust generated-type module root + re-exports.
rust/src/jsonrpc.rs Content-Length framed JSON-RPC transport implementation.
rust/src/router.rs Per-session routing of notifications/requests.
rust/src/handler.rs Session handler traits/events + default handlers.
rust/src/permission.rs Permission policy wrappers over SessionHandler.
rust/src/transforms.rs System message transform extension point + dispatcher.
rust/src/session.rs Session lifecycle/event loop plumbing (core runtime).
rust/tests/jsonrpc_test.rs Tests for JSON-RPC framing/routing (feature-gated).
rust/tests/protocol_version_test.rs Tests protocol version negotiation behavior.
rust/tests/integration_test.rs Ignored integration tests against real CLI.
rust/examples/chat.rs Interactive streaming chat example.
rust/examples/hooks.rs Hooks example for logging/auditing.
rust/examples/tool_server.rs Tool server example (feature-gated on derive).
rust/examples/lifecycle_observer.rs Observer example for lifecycle/session event streams.

Copilot's findings

  • Files reviewed: 102/107 changed files
  • Comments generated: 5

Comment thread rust/src/session.rs Outdated
Comment thread rust/build.rs
Comment thread rust/README.md Outdated
Comment thread test/scenarios/sessions/streaming/verify.sh
Comment thread .github/workflows/scenario-builds.yml
tclem and others added 5 commits April 29, 2026 07:24
cargo doc was running with --features test-support, which left the
derive feature off and made intra-doc links to define_tool and
schema_for resolve to nothing — failing under the crate's
deny(rustdoc::broken_intra_doc_links).

docs.rs already uses all-features (see Cargo.toml's
[package.metadata.docs.rs]); align CI with that so the docs job
matches what users will see on docs.rs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  emitted from the loop correlate to a session in traces. Matches
  the pattern documented in the rust-coding-skill.

- README.md / embeddedcli.rs: correct the embedded-CLI documentation
  to match what build.rs and embeddedcli.rs actually do — archives
  come from the github/copilot-cli GitHub Releases, integrity is
  SHA-256 against SHA256SUMS.txt, and the runtime cache path is
  ~/.cache/copilot-sdk-{version}/copilot.

- test/scenarios/sessions/streaming/verify.sh: drop a duplicate
  '# Go: build' comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picks up the new model.call_failure session event (with its
ModelCallFailureData payload and ModelCallFailureSource enum) and
the new optional 'tip' field on session_info.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes path triggers and the regenerate step for other languages'
protocol-version files. Those drift checks are a pre-existing gap on
main and out of scope for the Rust SDK port.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tclem and others added 2 commits April 29, 2026 07:41
The 23-line setup checklist duplicated content already in
rust/RELEASING.md. One-line pointer is enough.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two scenarios still used the old `Fn(P) -> Fut` shape and broke when
the SDK switched to `Fn(ToolInvocation, P) -> Fut`. They don't use
the invocation field, so just bind it as `_inv`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.5M

Comment thread rust/src/session.rs Outdated
Cross-SDK consistency: every other SDK (Node, Python, Go, .NET) uses
`send`/`Send`/`SendAsync` plus `MessageOptions` as the public
parameter type. Rust was the outlier with `send_message` and
`SendOptions`, and the asymmetry with the existing `send_and_wait`
method made it read awkwardly.

- Rename `Session::send_message` -> `Session::send` (and the private
  helper `send_message_inner` -> `send_inner`).
- Rename the public `SendOptions` type -> `MessageOptions`.
- Delete the previous wire-level `MessageOptions` struct: it had no
  internal callers (the wire payload is hand-rolled in send_inner) and
  freeing the name was the cleanest path to parity.

Pre-1.0 type rename, no protocol or behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread .github/skills/rust-coding-skill/SKILL.md
Comment thread .github/skills/rust-coding-skill/SKILL.md
Comment thread .github/copilot-instructions.md Outdated
Comment thread rust/src/session.rs
Comment thread rust/src/lib.rs
Comment thread rust/src/types.rs Outdated
Comment thread rust/src/session.rs Outdated
Comment thread rust/src/types.rs
Comment thread rust/Cargo.toml Outdated
Comment thread rust/Cargo.toml Outdated
@stephentoub
Copy link
Copy Markdown
Collaborator

Thanks for getting this up.

It'd be helpful to run an agent over the other SDKs and this new one to look for any inconsistencies / gaps / divergences, so that we can then evaluate each and decide whether it's acceptable or should be addressed. The more consistent we can be across the SDKs, the easier it'll be to maintain them moving forward, the more information / docs about one will translate to consumption of the others, the better we'll be able to evolve with reduced concerns for how something we want to add may not fit well in a particular SDK, etc.

@github-actions

This comment has been minimized.

Previously Session::subscribe and Client::subscribe_lifecycle returned
raw tokio::sync::broadcast::Receiver<T> values. A survey of mature Rust
crates (tonic, lapin, rdkafka, redis-rs, tokio-tungstenite, iroh-gossip,
tokio-stream's BroadcastStream itself) found that none of them expose a
raw broadcast::Receiver in their public API; the dominant pattern is a
named newtype implementing futures::Stream, with overflow surfaced
explicitly in the item type.

Introduce a copilot::subscription module with:

  - EventSubscription / LifecycleSubscription newtypes
  - Inherent recv() returning Result<T, RecvError> for existing
    while-let loop ergonomics
  - Stream impl yielding Result<T, Lagged> so callers can use
    tokio_stream::StreamExt or futures::StreamExt combinators
  - Lagged / RecvError types owned by the SDK so consumers no longer
    import tokio's broadcast error types

Net effect: the channel choice is now an internal implementation detail.
We can swap broadcast for async-broadcast / flume / a custom backpressure
policy, or convert lag into an Event::Lagged variant, without a breaking
change to the public surface.

Existing while-let loops in tests and examples continue to compile and
behave identically: close and lag both exit the loop, matching
tokio::sync::broadcast::Receiver.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@tclem tclem marked this pull request as ready for review April 29, 2026 16:19
@tclem tclem marked this pull request as draft April 29, 2026 16:20
Local cargo +nightly fmt --check passed without `--config-path
.rustfmt.nightly.toml`, but CI runs with the explicit config and
flagged two diffs: import group flattening and test-mod import order.
Applied with the same flags CI uses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1164 · ● 3.1M

Comment thread rust/src/lib.rs Outdated
Comment thread rust/src/lib.rs Outdated
Two more wire-shape bugs in the same class as `c58e2f2` (elicitation
`requestedSchema`) and `64541af` (list_sessions filter wrapping):
hand-authored RPC wrappers calling JSON-RPC methods that don't exist
on the CLI runtime, masked by mock-server tests asserting the same
wrong names the implementation sent.

The CLI runtime (verified via `@github/copilot@1.0.39` app.js)
registers two top-level RPCs:

- `status.get` (returns version + protocol_version)
- `auth.getStatus` (returns is_authenticated + auth_type)

Every other public SDK calls those exact names:

- Node `nodejs/src/client.ts:980, 992`
- Go `go/client.go:1239, 1257`
- Python `python/copilot/client.py:1806, 1827`
- .NET `dotnet/src/Client.cs:718, 732`

The Rust hand-authored layer was sending `getStatus` and
`getAuthStatus` — names that don't exist on the wire. Result: both
methods would have returned a method-not-found error (or a
misleading no-such-method log on the CLI side) instead of the
expected payload. The schema doesn't currently typed-define these
top-level RPCs (only `session.auth.getStatus` is in the schema, a
different namespaced endpoint), so the codegen didn't catch the
hand-authored layer's drift.

The mock-server test `client_rpc_methods_send_correct_method_names`
asserted on the same wrong names the implementation used, so the
bugs round-tripped through both ends. Same root cause as the
elicitation and list_sessions tests.

Fix:

- `lib.rs:1301`: `"getStatus"` → `"status.get"`
- `lib.rs:1308`: `"getAuthStatus"` → `"auth.getStatus"`
- `tests/session_test.rs:488-509`: assertions migrated to the
  canonical names AND new explicit-absence assertions
  (`assert_ne!(request["method"], "getStatus")` /
  `"getAuthStatus"`) so the same regression class can't be
  reintroduced silently. Doc-comment on the test names the source-
  of-truth references (CLI runtime registration, four-SDK call
  sites).
- `CHANGELOG.md`: new `### Fixed` entry documenting the wire-name
  fix and the test gap that masked it.

Validation:

- 209 tests pass (no count change — same test, stricter assertions).
- `cargo doc -D warnings` clean.
- `cargo +nightly-2026-04-14 fmt --check` clean.
- `cargo clippy --all-features --all-targets -- -D warnings` clean.

Caught by the github-actions bot on PR #1164 (comments 3164826435
and 3164826441). Verify-before-drafting tally now 14 / 15 for these
two saves.

This brings the wire-shape audit save count to 4 (elicitation,
list_sessions, get_status, get_auth_status) — all four were
hand-authored RPC wrappers asserting on the implementation's wrong
shape rather than the schema/runtime's expected shape. The
structural fix (regression-prevention asserts in tests) is now in
place for all four; the same bug class is no longer silently
reproducible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

tclem and others added 5 commits April 29, 2026 17:15
Closes RFD-400 review findings #1 (writer mutex desync hazard) and #4
(send_request pending-entry leak on cancellation). See cancel-safety
review session db4b1ac8-... for the full report.

The previous design held a `tokio::sync::Mutex<Box<dyn AsyncWrite>>`
across `write_all` + `flush` for Content-Length-framed JSON-RPC
messages. Caller cancellation mid-frame (e.g. wrapping `Session::send`
or `Client::call` in `tokio::time::timeout` or `select!`) could leave
a partial frame on the wire, permanently desyncing the transport for
the rest of the connection. The reader on the CLI side would block
forever waiting for the missing bytes, and the next outbound message
would be interpreted as the tail of the previous body.

Fix: writer-actor task fed by an `mpsc::UnboundedSender<WriteCommand>`.
Public `write` and `send_request` pre-serialize the body, enqueue the
frame + an ack `oneshot`, and await the ack. Caller cancellation
drops the ack receiver but the actor still completes the in-flight
write — the frame either lands atomically or surfaces an io::Error
to the (possibly already-dropped) ack receiver. A partial frame can
never appear on the wire.

Design notes:

- Pre-serialize before enqueue: actor receives `Vec<u8>` (header +
  body, already concatenated). Serde errors fail synchronously at
  enqueue time before touching the actor — cleanest cancel-safety
  story.
- Unbounded sender: RFD 400 explicitly permits this for cancel-safety,
  and the producer-consumer ratio is naturally bounded by request/
  response semantics on the wire.
- Writer actor lives until the JsonRpcClient drops. Channel closes on
  Drop -> actor sees recv None -> flushes and exits.

`pending_requests` cleanup (#4): added a small `PendingGuard` RAII
helper in `send_request` that arms after the map insert and disarms
before the success return. Caller cancellation drops the guard, which
removes the pending-entry synchronously via parking_lot::RwLock (the
map mutex is converted in this commit too — it was never held across
.await anyway, so the conversion is a pure cleanup).

Mutex conversions in this commit:

- `JsonRpcClient::pending_requests`: `tokio::sync::RwLock` ->
  `parking_lot::RwLock`. Lock is never held across .await; conversion
  is also what enables the synchronous PendingGuard drop.
- The previous `Arc<Mutex<Box<dyn AsyncWrite>>>` is gone entirely.
  The actor task owns the writer directly with no shared synchronization.

Tests: two new regression tests in tests/jsonrpc_test.rs

- `write_actor_completes_on_caller_cancel`: tiny duplex (8 bytes) +
  gated reader so the actor's write_all blocks waiting for room. Race
  the caller's future against a sleep; sleep wins, future is dropped
  while suspended on ack_rx.await. Then enqueue a second write and
  release the reader. Both frames must land on the wire intact, in
  order, with no partial frame from the cancelled write.
- `send_request_cancellation_does_not_leak_pending`: cancel an
  in-flight request, then send a (late) response for the cancelled id
  to verify the read loop logs and discards it without colliding,
  then send a fresh request and verify it works normally.

Existing 209 tests continue to pass without modification — the wire
semantics are unchanged. Total now 211.

Validation: cargo test, cargo doc -D warnings, cargo +nightly fmt
--check, cargo clippy --all-features --all-targets -- -D warnings
all clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes RFD-400 review finding #2 (idle_waiter slot leak on external
cancellation). See cancel-safety review session db4b1ac8-... for the
full report.

`Session::send_and_wait` installs an `IdleWaiter` slot under
`self.idle_waiter`, then awaits the response. Internal failure paths
(send_inner error, oneshot closure, internal timeout) clean up the
slot. External cancellation — caller wraps `send_and_wait` in
`tokio::time::timeout`, races it in `select!`, or aborts the
JoinHandle — does not. The cleanup at the event loop's `else` branch
only fires when the channel itself closes (i.e., the entire session
is going away), not when an individual call is cancelled.

Effect: any external cancellation between "install waiter" and
"drain response" leaves `idle_waiter = Some(...)` forever. All
subsequent `send` and `send_and_wait` calls return
`SendWhileWaiting`, permanently bricking the session.

Fix: `WaiterGuard` RAII helper that takes the slot on Drop. Construct
right after install, let RAII handle every exit path. The explicit
`self.idle_waiter.lock().take()` calls inside the function disappear,
and the slot is cleared on every cancellation path automatically.

Mutex conversion (folded in from finding #5):

- `Session::idle_waiter`: `tokio::sync::Mutex<Option<IdleWaiter>>` ->
  `parking_lot::Mutex<Option<IdleWaiter>>`. Lock is never held across
  `.await` in any code path; the conversion is what makes
  WaiterGuard's synchronous Drop work without needing an async-spawn
  fallback.

`event_loop` mutex stays `tokio::sync::Mutex` for now — commit C
converts it as part of cooperative shutdown (so the conversion lands
alongside the matching change to Drop for Session).

Tests: two new regression tests in tests/session_test.rs

- `send_and_wait_outer_cancellation_clears_waiter`: outer
  `tokio::time::timeout(50ms, ...)` around `send_and_wait` with a
  60-second inner timeout. Outer fires, dropping the future. Verify
  the next `send` succeeds (no SendWhileWaiting).
- `send_and_wait_drop_clears_waiter`: explicit `JoinHandle::abort` of
  an in-flight send_and_wait. Verify the next `send` succeeds.

Existing 211 tests continue to pass; total now 213.

Validation: cargo test --all-features, cargo doc -D warnings, cargo
+nightly fmt --check, cargo clippy -- -D warnings all clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes RFD-400 review finding #3 (JoinHandle::abort lands at any
await point in the event loop body) and the remaining bit of #5
(Session::event_loop tokio::sync::Mutex -> parking_lot::Mutex). See
cancel-safety review session db4b1ac8-... for the full report.

The previous design called `JoinHandle::abort()` in two places to
stop the event-loop task: explicitly in `Session::stop_event_loop`
and best-effort in `Drop for Session`. RFD 400's strongest single
recommendation about task aborts:

> With the abort(), the future returned by do_long_running_operation()
> can be cancelled at *any await point*. It is unlikely that arbitrary
> async Rust code is resilient to cancellations anywhere during
> control flow. ... task aborts should be avoided entirely.

The event loop body has many await points:

- The outer `select!` itself
- `handle_notification`'s nested awaits (acquire idle_waiter lock,
  send on broadcast, dispatch RPC callbacks for permission/tool/
  elicitation requests)
- `handle_request`'s inline awaits (deserialize, dispatch handler,
  build response, write back to client)

If `abort()` landed mid-handler — say partway through writing the
permission response — the CLI would keep that requestId in flight
forever. Same hazard for the idle_waiter oneshot: an abort between
"observed session.idle" and "send on waiter.tx" leaves the caller's
`send_and_wait` await blocked until the entire transport tears down.

Fix: cooperative cancellation via `Arc<tokio::sync::Notify>`. The
event-loop body adds a fourth select arm:

    tokio::select! {
        _ = shutdown.notified() => break,
        Some(notification) = notifications.recv() => { ... }
        Some(request) = requests.recv() => { ... }
        else => break,
    }

`Notify::notified()` is cancel-safe per RFD 400's framework: the
unselected branch losing a poll is fine — it's reconstructed on the
next iteration. `notify_one()` (NOT `notify_waiters`) is used because
it buffers a single signal: if the loop is currently inside
`handle_request(...).await` (no `.notified()` future registered), the
signal is held until the handler returns and the loop re-enters
select on the next iteration. `notify_waiters()` would silently lose
the signal in this case.

`stop_event_loop` calls `shutdown.notify_one()` then awaits the
JoinHandle. The loop drains its current iteration's handler before
observing the buffered signal and exiting cleanly — no mid-protocol
state, no orphaned RPC ids on the CLI side.

`Drop for Session` calls `notify_one()` and unregisters from the
router. The handle is left in `event_loop` for tokio to reap when
the loop's spawned task exits naturally; we intentionally don't
await it because Drop is sync. Spawned child tasks inside
`handle_notification` (permission/tool/elicitation callbacks)
intentionally outlive the parent loop — RFD 400's "spawn background
tasks to perform cancel-unsafe operations" pattern, documented in
the loop's comment.

Mutex conversion (folded in from finding #5):

- `Session::event_loop`: `tokio::sync::Mutex<Option<JoinHandle<()>>>` ->
  `parking_lot::Mutex<Option<JoinHandle<()>>>`. Lock is never held
  across `.await` and the conversion eliminates the `try_lock()`
  Drop footgun (silent no-op on contention with stop_event_loop).
  This is the last tokio::sync::Mutex on Session — the import is no
  longer needed.

Tests: two new regression tests in tests/session_test.rs

- `stop_event_loop_completes_in_flight_handler`: install a
  `SlowHandler` whose userInput.request callback sleeps 150ms. Send
  the request from the server side, then 20ms in (handler is mid-
  flight) call `stop_event_loop`. Verify the response with
  `answer: "completed"` lands on the wire BEFORE stop_event_loop
  returns — proves the handler ran to completion, not aborted
  mid-await.
- `drop_session_does_not_abort_handler`: same shape but via
  `drop(session)` instead of `stop_event_loop`. Tracks handler
  completion via an AtomicBool that the handler sets only after
  its sleep completes; verifies both that the response lands on
  the wire AND that the handler actually completed despite the
  Session being dropped.

Existing 213 tests continue to pass; total now 215.

Validation: cargo test --all-features, cargo doc -D warnings, cargo
+nightly fmt --check, cargo clippy -- -D warnings all clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes RFD-400 review finding #6 (per-method cancel-safety
documentation). Doc-only follow-up to commits A/B/C; no behavioral
change. RFD 400 § "Marking APIs as cancel-unsafe" calls for explicit
documentation; with the underlying behavior now correct after A/B/C,
documenting the contract makes it inspectable from rustdoc rather
than requiring callers to read the implementation.

Added `# Cancel safety` rustdoc sections to:

- `Session::send` — cancel-safe (writer-actor); message lands on
  the wire even if the await is cancelled.
- `Session::send_and_wait` — cancel-safe (WaiterGuard RAII);
  outer-cancellation distinct from internal-timeout, both clear the
  slot.
- `Session::abort` — cancel-safe (single RPC via writer-actor).
- `Client::call` — cancel-safe at the wire level; documents the
  caveat that the CLI may still process the request even if the
  caller doesn't see the response (idempotent vs non-idempotent
  methods).
- `Client::stop` — cancel-unsafe-but-recoverable; the body
  sequentially destroys sessions (each individually cancel-safe)
  before killing the child. The existing `tokio::time::timeout`
  example with `force_stop` fallback is the documented recovery
  path.
- `Client::force_stop` — synchronous, cannot be cancelled. Designed
  as the recovery path for `stop`'s timeout case.
- `Subscription::recv` — cancel-safe by virtue of
  `tokio::sync::broadcast` + `BroadcastStream`.

Validation: cargo doc -D warnings clean, cargo test (215 pass), cargo
+nightly fmt --check clean, cargo clippy clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Closes "Mode 1 hands-off loop" gap A from the SDK release plan-doc:
update-copilot-dependency.yml is workflow_dispatch only today, so
when a new @github/copilot publishes nobody automatically opens the
regen PR. Today's bot-driven bump (commit ecdc5ec, "Update
@github/copilot to 1.0.39 (#1167)") was triggered manually after the
plan-doc session noticed the gap.

Goal: the SDK auto-detects new CLI versions, opens a draft PR with
all per-language regen + formatting + lockfile updates, and a human
just reviews + merges. github-app's bundled-binary tracker picks the
new SDK up via crates.io dependabot in the symmetric direction.

Changes:

1. **`schedule` trigger.** Daily cron at 10:00 UTC, deliberately
   symmetric with github-app's check-bundled-binary-versions.yml so
   the SDK and consumer halves of the loop tick in lockstep on
   release days.

2. **`version` input is now optional.** Manual override still works
   for emergency back-patches or cherry-picking specific
   prereleases; an empty input on workflow_dispatch flows through
   the same auto-detect path the schedule uses.

3. **Two-job split: `detect` -> `update`.** The detect job is cheap
   (one `npm view @github/copilot version` + a jq read of
   nodejs/package.json + a strip of the `^`/`~` prefix). It sets
   two outputs:
     - `version`: the resolved target (input override, or latest
       from npm)
     - `should_continue`: 'true' if the resolved version differs
       from the current pin, 'false' otherwise
   The update job has `needs: detect` and
   `if: needs.detect.outputs.should_continue == 'true'`, so it
   skips entirely when there's nothing to do. This keeps the
   no-op daily cost effectively free.

4. **Validation moves into the resolve step.** Both manual and
   auto-detected versions go through the same semver regex check.
   The previous standalone "Validate version input" step is
   retired; the detect job's resolve step now owns format
   validation, version comparison, and skip logic.

5. **All `${{ inputs.version }}` references become
   `${{ needs.detect.outputs.version }}`.** Same value, just routed
   through the detect job's outputs so manual override and
   auto-detect both feed the update job uniformly.

Doesn't touch SDK source; cargo test / cargo publish --dry-run are
no-op confirmation post-merge. Companion gap on the github-app side
(auto-open sync PR after each new SDK release) lives on Sync session
PR #4140; this commit unblocks the SDK half.

Plan-doc capture: tracked under "Steady-state CLI bump automation
(Mode 1 hands-off loop)" — gap A closed; gap B (github-app
sync-PR auto-open) remains.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1164 · ● 1.7M

Comment thread rust/src/lib.rs Outdated
tclem and others added 2 commits April 29, 2026 18:02
…t-only

Bot review caught an inaccuracy in the rustdoc and README: the prior
wording claimed the underlying account.getQuota JSON-RPC endpoint was
exposed only by the Rust SDK. In fact only the top-level Client::get_quota
convenience wrapper is Rust-unique; the endpoint itself is generated and
exposed cross-SDK via each SDK's typed rpc() namespace.

Verified cross-SDK exposure:
- Node:   nodejs/src/generated/rpc.ts:2203 (account.getQuota in rpc() namespace)
- Python: python/copilot/generated/rpc.py:5647 (rpc().account.get_quota)
- Go:     go/rpc/generated_rpc.go:2252 (RPC.Account.GetQuota)
- .NET:   dotnet/src/Generated/Rpc.cs:3140 (Rpc().Account().GetQuotaAsync)
- Rust:   rust/src/generated/rpc.rs:90 (rpc().account().get_quota)

Updated rustdoc on Client::get_quota in rust/src/lib.rs and the
"Rust-only API" bullet in rust/README.md to correctly state that the
wrapper is Rust-only while the underlying endpoint is available across
all SDKs via their typed rpc() namespaces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors the existing MessageOptions::new() / with_* builder pattern
on the two other public #[non_exhaustive] config types that consumers
construct most often. Closes the cross-crate ergonomics gap that
#[non_exhaustive] introduces: external callers can't use struct-
literal syntax (with or without ..Default::default()) on these types,
so every field assignment becomes its own mut-let statement. The
builder shape collapses that into a single chained expression.

Caught by github-app PR #4140 review on src-tauri/src/session/cli.rs:
the mut-let-and-assign pattern read as awkward to reviewers even
after trimming to the four fields that diverge from default. Same
ergonomic friction that prompted the make_tool() helper consumers
were writing for Tool. Builder ships zero-cost convenience without
giving up #[non_exhaustive]'s SemVer protection on growable config
struct.

ClientOptions builder (rust/src/lib.rs):

- new() -> Self                         (documented entry point)
- with_program(impl Into<CliProgram>)
- with_prefix_args<I: IntoIterator<...>>(args)
- with_cwd(impl Into<PathBuf>)
- with_env<I: IntoIterator<(K, V)>>
- with_env_remove<I: IntoIterator<S>>
- with_extra_args<I: IntoIterator<S>>
- with_transport(Transport)
- with_github_token(impl Into<String>)
- with_use_logged_in_user(bool)
- with_log_level(LogLevel)
- with_session_idle_timeout_seconds(u64)
- with_list_models_handler<H: ListModelsHandler + 'static>     (Arc-wrapped internally)
- with_session_fs(SessionFsConfig)
- with_trace_context_provider<P: TraceContextProvider + 'static>    (Arc-wrapped internally)
- with_telemetry(TelemetryConfig)

Tool builder (rust/src/types.rs):

- new(impl Into<String>) -> Self        (name + defaults)
- with_namespaced_name(impl Into<String>)
- with_description(impl Into<String>)
- with_instructions(impl Into<String>)
- with_parameters(serde_json::Value)
- with_overrides_built_in_tool(bool)
- with_skip_permission(bool)

Pure additive: no existing ClientOptions::default() / mut-let
patterns break, no public field visibility changes. Both builders
compose by wrapping ::default() and mutating-and-returning self,
matching MessageOptions's idiom.

Tests: client_options_builder_composes verifies all 10 chained
builder methods produce the expected internal state.
tool_builder_composes does the same for Tool's seven methods, plus
tool_with_parameters_handles_non_object_value covers the
HashMap-from-Value extraction edge case (json!(null) -> empty map
rather than panic).

CHANGELOG: new "Builder ergonomics" subsection under Documentation,
explaining the rationale and listing both builders.

Validation: 220 tests pass (was 215 + 5 new builder tests across
unit + doctest), cargo doc -D warnings clean, cargo fmt --check
clean, cargo clippy --all-features --all-targets -- -D warnings
clean.

Companion ask from github-app PR #4140 review (Sync session)
follows up: github-app's `cli.rs` and ~25 Tool::new() call sites
will switch to the builder form once this lands and the next
sync round-trip pulls it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

tclem and others added 2 commits April 29, 2026 18:27
Mirrors the just-shipped ClientOptions/Tool builder pattern (commit
2d725a5) on the two structs that consumers reach for most when wiring
up a session. Both types are #[non_exhaustive], so external callers
have been writing the same mut-let-and-assign block at every
construction site:

    let mut config = ResumeSessionConfig::new(session_id);
    config.client_name = Some("...".into());
    config.streaming = Some(true);
    config.tools = Some(tools);
    // ...10+ more lines

The new chainable form collapses that to:

    ResumeSessionConfig::new(session_id)
        .with_client_name("...")
        .with_streaming(true)
        .with_tools(tools)
        // ...

Both types have ~30 public fields each. Coverage:

SessionConfig: with_session_id, with_model, with_client_name,
with_reasoning_effort, with_streaming, with_tools,
with_available_tools, with_excluded_tools, with_mcp_servers,
with_disabled_mcp_servers, with_env_value_mode,
with_enable_config_discovery, with_request_user_input,
with_request_permission, with_request_exit_plan_mode,
with_request_auto_mode_switch, with_request_elicitation,
with_skill_directories, with_disabled_skills, with_agent,
with_custom_agents, with_config_dir, with_working_directory,
with_github_token, with_include_sub_agent_streaming_events,
with_system_message, with_skip_compaction_history.

ResumeSessionConfig: same shape minus model/reasoning_effort/
available_tools/disabled_skills/disabled_mcp_servers (which the
resume RPC doesn't accept), plus with_disable_resume.

Existing closure-installing chains (with_handler, with_hooks,
with_transform, with_commands, with_session_fs_provider,
approve_all_permissions, deny_all_permissions,
approve_permissions_if) and the manual mut-let pattern continue to
work unchanged. The hooks bool is intentionally not exposed via a
new with_* setter; with_hooks(handler) auto-enables the flag, and
power users wanting to override can still set the .hooks field
directly.

Adds two integration-style tests covering composition across all
field categories (scalars, bool flags, Vec setters, struct setters,
HashMap setters) for both types. CHANGELOG entry extends the
existing "Builder ergonomics" subsection.

This is the third commit in the pre-1.0 builder-pattern series
(MessageOptions in the original branch, ClientOptions/Tool in
2d725a5, SessionConfig/ResumeSessionConfig here). Pure additive,
zero migration cost. Driven by github-app PR #4140 reviewer
feedback on src-tauri/src/session/cli.rs and core.rs construction
sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The copilot-sdk repo is public; github-app is private. Public-repo
content must not name or depend on github-app, even in comments or
test fixtures.

- .github/workflows/update-copilot-dependency.yml: drop the
  "symmetric with github-app's scheduled" framing on the daily cron;
  rewrite the comment to describe what the schedule does in
  SDK-local terms only.
- rust/CHANGELOG.md: rephrase the Tool builder rationale to talk
  about "real-world consumer code" rather than github-app's tool
  catalog specifically.
- rust/src/lib.rs: change the `source_name` test fixture from
  "github-app" to "my-app" — it was just an example string for
  OTEL source-name plumbing, but the explicit consumer naming is
  unnecessary and confusing in a public repo.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

tclem and others added 2 commits April 29, 2026 18:53
After landing per-field builders on the four highest-traffic
consumer-facing config structs (MessageOptions, ClientOptions, Tool,
SessionConfig/ResumeSessionConfig), this commit closes the long tail.

Rust ecosystem research (tokio, reqwest, tonic, axum, AWS SDK) confirms
two design points:

1. The dominant pattern for #[non_exhaustive] config builders is the
   single `with_<field>(impl Into<T>) -> Self` setter that always
   Some-wraps. AWS SDK's dual `field` + `set_field(Option<T>)` shape
   is justified by codegen, not by ergonomics — and the
   `impl Into<Option<T>>` magic that some older crates use has fallen
   out of fashion (it conflicts with `impl Into<T>` chaining and
   produces poor type-inference errors).

2. Direct field assignment on a `pub` field is idiomatic Rust for
   forwarding `Option<T>` values from upstream code. The
   http::request::Parts / hyper::Body::Builder pattern. We already
   support it; we just hadn't documented it.

This commit therefore:

- Adds full builders to the four bare consumer-facing structs:
  CustomAgentConfig (name + prompt required, six with_* setters),
  InfiniteSessionConfig (three with_* setters), ProviderConfig
  (base_url required, six with_* setters), SystemMessageConfig
  (three with_* setters).
- Expands TraceContext with a symmetric new() + with_traceparent pair
  alongside the existing from_traceparent shorthand. The shorthand
  is now expressed in terms of the chain (`new().with_traceparent(x)`)
  for consistency.
- Documents the direct-field-assignment escape hatch on SessionConfig
  and ResumeSessionConfig, with a doc example showing how to mix the
  fluent chain (for compile-time-known values) with direct mutation
  (for Option<T> pass-throughs).

Per-field `with_<x>_opt(Option<T>)` setters were considered and
deliberately rejected: doubling the method count for ~30-field
configs would hurt rustdoc discoverability and conflict with the
single-setter precedent we just shipped. The mut-let escape hatch
covers the same use case without polluting the API surface.

Adds 5 unit tests (one per new builder + a TraceContext suite) and
extends the existing CHANGELOG "Builder ergonomics" subsection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This reverts commit a88c3eb.

The scheduled trigger was added to close a hands-off-loop gap that
was framed in terms of a downstream consumer's needs. On reflection,
the SDK should not be the only language in this repo with scheduled
auto-detect; that's a cross-language platform decision, not a single-
SDK one. Reverting brings update-copilot-dependency.yml back to
manual-only (workflow_dispatch with a required version input) — the
same shape as every other language's CLI update workflow in this
repo.

When the SDK collectively decides to add scheduled CLI tracking,
that should land as a unified change across all languages, not as a
Rust-only special case driven by a downstream consumer's automation
desires. Manual dispatch remains the supported path until then.

Conflict resolution: the e93ce8e ("Scrub github-app references")
commit landed after a88c3eb and rewrote the cron block's comment.
The revert drops the entire schedule: block (including the rewritten
comment), restoring the workflow to its pre-a88c3eb manual-only
shape exactly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Final missing piece in the consumer-facing #[non_exhaustive] config
struct audit. `TelemetryConfig` had public optional fields (otlp_endpoint,
file_path, exporter_type, source_name, capture_content) and an
is_empty() helper, but no `new()` or `with_*` setters — leaving callers
to write `TelemetryConfig { otlp_endpoint: Some(...), ..Default::default() }`
which doesn't compile from outside the crate due to #[non_exhaustive].

Adds `TelemetryConfig::new()` (delegates to Default) and `with_*`
setters for all five fields, matching the existing pattern.
The is_empty() helper stays unchanged.

After this commit, every consumer-facing #[non_exhaustive] struct
in the SDK has a fluent builder: ClientOptions, TelemetryConfig,
Tool, CommandDefinition, CustomAgentConfig, InfiniteSessionConfig,
ProviderConfig, SessionConfig, ResumeSessionConfig,
SystemMessageConfig, MessageOptions, SessionFsConfig, TraceContext.

One unit test (telemetry_config_builder_composes) covering all five
setters + is_empty behavior on a fresh new(). Extends the existing
CHANGELOG "Builder ergonomics" round-out bullet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@tclem tclem marked this pull request as ready for review April 30, 2026 14:43
tclem and others added 2 commits April 30, 2026 09:34
The bundled-CLI build path (gated by the embedded-cli feature and the
COPILOT_CLI_VERSION env var) previously shelled out to `curl` to fetch
SHA256SUMS.txt and the platform tarball from the copilot-cli releases.
Two problems with that:

1. Implicit system dependency. `expect("curl is required ...")` panics
   when curl isn't on PATH — true on minimal Windows MSVC environments,
   some Docker images, and the CI matrices crates.io publishers tend
   to validate against. crates.io build environments themselves don't
   guarantee curl.

2. No retry on transient errors. Network blips during CI bundling
   surfaced as build failures rather than self-healing waits.

Switch to ureq 2 with default-features=false + ["tls"] (rustls +
webpki-roots, pure-Rust TLS, no OpenSSL or native-tls). Adds about
1.7MB and ~12 transitive deps to the build graph but only when the
embedded-cli feature is on; the default cargo build is unaffected.

Retry policy:
- Up to 3 retries (4 total attempts) with exponential backoff:
  1s, 2s, 4s.
- 5xx responses, connect timeouts, read timeouts, and other
  transport-layer errors are treated as transient and retried.
- 4xx responses fail fast (the URL is wrong; retrying won't help).

Connect/read timeouts (30s/120s respectively) prevent indefinite
hangs on stalled connections. The cache-hit path still bypasses
the network entirely; only fresh downloads pay the HTTP cost.

End-to-end smoke test against
https://github.com/github/copilot-cli/releases/download/v1.0.39 :
SHA256SUMS download succeeded, tarball download succeeded
(134MB extracted, 44MB compressed), SHA-256 verified, embedded
build succeeded.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The default `cargo test` job in rust-sdk-tests.yml runs on all three
supported platforms but does not set `COPILOT_CLI_VERSION` — which
means `build.rs` short-circuits immediately and the embedded-cli
bundle path (download + SHA-256 verify + extract + zstd compress +
embed) is never exercised in CI. After the recent switch from
shelling out to `curl` over to `ureq` with retry logic in
`build.rs`, that gap matters: the new HTTP client + cross-platform
TLS code path needs cross-platform CI coverage before it ships.

Adds a parallel `bundle` job to the same workflow:

- 3-OS matrix: ubuntu-latest, macos-latest, windows-latest.
- Reads the pinned CLI version from `nodejs/package.json` so the
  bundle test always tracks the same version the rest of the SDK
  ships against — no separate version source to drift.
- Caches the downloaded archive in `./rust/.bundled-cli-cache` keyed
  on OS + CLI version, so steady-state CI doesn't refetch ~130 MB
  on every run. Cache miss (e.g. first run after a CLI bump)
  exercises the full download path end-to-end, which is the
  regression surface this job is meant to catch.
- Runs `cargo build --features embedded-cli` to drive `build.rs`
  through the full pipeline. Doesn't run `cargo test` — the test
  job already covers runtime behavior; this job is specifically
  validating the bundle build.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

This PR adds a comprehensive new Rust SDK — overall well structured and strongly aligned with the existing Node.js, Python, Go, and .NET SDKs. The core client/session lifecycle, transport options, permission handling, tool registration, hooks, session-fs, streaming, and codegen pipeline all follow the same patterns as the other SDKs. Scenario coverage is thorough.

✅ Consistent across all SDKs

Feature Node Python Go .NET Rust
createSession / resumeSession
send / sendAndWait
setModel / set_model / SetModel
disconnect / destroy / abort
getMessages
log
ui (confirm / select / input / elicitation)
Permission handler callback
Hooks, transforms, session-fs
ping, getStatus, getAuthStatus
listSessions, deleteSession, etc.

⚠️ Rust introduces new high-level helpers not yet in other SDKs

The Rust SDK adds several convenient session-level wrappers over protocol operations that the other SDKs currently only expose through their raw generated RPC layer. These are all valid thin wrappers — flagging them as potential follow-up additions for Node.js, Python, Go, and .NET:

Method Rust Node Python Go .NET
Session::get_model()
Session::set_mode() / get_mode()
Session::get_name() / set_name()
Session::set_approve_all_permissions()
Session::list_workspace_files() / read_workspace_file() / create_workspace_file()
Session::read_plan() / update_plan() / delete_plan()
Session::start_fleet()
Session::send_telemetry()
Client::get_quota()
Client::send_telemetry()

These are not blockers for this PR — given the "technical preview" framing this is expected — but they represent opportunities for follow-up parity work in the other SDK implementations.

Minor design note

resume_session in Rust silently defaults to DenyAllHandler when no handler is provided, while Node.js, Go, and Python return an error if onPermissionRequest is omitted. Both are safe defaults but it's worth noting the behavioral difference in case callers expect parity here.


Overall: the Rust SDK is a solid addition with strong cross-SDK alignment on the core API surface. The gaps noted above are additive (Rust does more, not less) and are reasonable deferred work for the other languages.

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.7M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1164 · ● 2.7M

Comment thread rust/src/session.rs
}

/// Get the current model.
pub async fn get_model(&self) -> Result<Option<String>, Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: get_model() is a new high-level method in Rust that doesn't exist in any other SDK. Every SDK already exposes setModel/set_model/SetModel, but none expose the read side as a convenience wrapper — callers must go through the raw RPC layer today. This is a useful addition that would be worth backfilling in Node.js (getModel(): Promise<string | undefined>), Python (get_model()), Go (GetModel(ctx) (string, error)), and .NET (GetModelAsync()).

Comment thread rust/src/session.rs
}

/// List files in the session workspace.
pub async fn list_workspace_files(&self) -> Result<Vec<String>, Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: list_workspace_files(), read_workspace_file(), and create_workspace_file() (lines 481–507), plus read_plan() / update_plan() / delete_plan() (lines 509–530) and start_fleet() (line 591) are all new high-level session helpers not present in any other SDK. The other SDKs have the matching generated RPC stubs (workspaces.listFiles, workspaces.readFile, workspaces.createFile, session.plan.*, session.fleet.start) but no first-class Session methods wrapping them. These convenience wrappers seem broadly useful across all SDKs.

Comment thread rust/src/lib.rs
/// .NET `client.Rpc().Account().GetQuotaAsync()`), and in Rust at
/// `client.rpc().account().get_quota()`. This wrapper is a thin shortcut
/// for that same call.
pub async fn get_quota(&self) -> Result<generated::api_types::AccountGetQuotaResult, Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: Client::get_quota() and Client::send_telemetry() / Session::send_telemetry() (lines 1529, 553 in session.rs) are new convenience wrappers not present in the Node.js, Python, Go, or .NET client APIs. get_quota in particular is noted in the doc comment as "Go client.Rpc().Account().GetQuota()" which confirms the other SDKs require going through the raw RPC layer today. Fine to have these in Rust now, but good candidates for cross-SDK parity follow-ups.

Comment thread rust/src/session.rs
}

/// Get the current session mode.
pub async fn get_mode(&self) -> Result<String, Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: set_mode() and get_mode() are also new high-level wrappers introduced here (alongside get_name() / set_name() below at lines 429–443). These have no counterpart in the Node.js, Python, Go, or .NET SDKs. All four are protocol-level capabilities (session.mode.set, session.mode.get, session.name.get, session.name.set) that the other SDKs only expose through their generated RPC layer. Worth considering for parity.

Comment thread rust/src/session.rs
}

/// Enable or disable session-wide auto-approval for tool permission requests.
pub async fn set_approve_all_permissions(&self, enabled: bool) -> Result<(), Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note (design difference): set_approve_all_permissions() provides a runtime escape hatch to flip the setApproveAll flag on a live session. The other SDKs take a callback-centric approach — they accept a onPermissionRequest handler at createSession/resumeSession time instead. Both approaches are valid, but the runtime mutability here is a meaningful ergonomic difference. If this proves useful, the other SDKs could adopt a similar session-level method alongside their callback patterns.

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.

5 participants