Commit 19b4ad5
Add Rust SDK (technical preview) (#1164)
* Add Rust SDK
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>
* Polish public API for 0.1.0 release
- **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>
* Route generated SessionId/RequestId fields through hand-authored newtypes
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>
* Pass ToolInvocation to define_tool closures
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>
* Make ping message argument optional
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>
* Build Rust docs with all features in CI
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>
* Address PR #1164 review feedback
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>
* Regenerate Rust types for @github/copilot 1.0.39-0
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>
* Scope codegen-check workflow changes to Rust only
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>
* Point rust-publish-release workflow header to RELEASING.md
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>
* Update Rust scenario binaries for new define_tool signature
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>
* Rename Session::send_message -> send and align MessageOptions
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>
* Wrap subscribe() in EventSubscription / LifecycleSubscription newtypes
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>
* Apply nightly rustfmt to subscription module
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>
* Fix workspaces RPC method names (was singular `workspace.*`)
`Session::list_workspace_files`, `read_workspace_file`, and
`create_workspace_file` were calling the singular `session.workspace.*`
RPC methods. The CLI exposes these under the plural namespace
`session.workspaces.*` (see Node `session.rpc.workspaces`, Go
`session.RPC.Workspaces`, .NET `session.Rpc.Workspaces`, Python
`session.rpc.workspaces`, and the generated `SESSION_WORKSPACES_*`
constants).
Route the three calls through the typed constants in
`generated::api_types::rpc_methods` so the bug class is structurally
gone — a stale string literal can no longer drift from the schema.
Add three integration tests against the mock server in
`session_test.rs` asserting the wire method name and request shape for
each helper. This mirrors the
`session_rpc_methods_send_correct_method_names` table-driven test that
already covers the rest of the session RPCs.
Also fold the missing `create_workspace_file` into the workspace
helpers bullet in CHANGELOG.md.
The bug never shipped — the SDK is pre-publish — so no migration
notes. Heads-up filed to the github-app sister copy.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* rust: add typed RPC namespace, route helpers through it
Phase 4 § 4.6.A.5 — generate a fully-typed `client.rpc.*` /
`session.rpc.*` namespace from the Copilot CLI schema, mirroring the
Node/Python/Go/.NET SDKs. All hand-authored ergonomic helpers
(`list_workspace_files`, `read_plan`, `set_mode`, `list_models`,
`get_quota`, etc.) become one-line delegations over the namespace —
their public signatures are unchanged, but wire-method strings now
exist in exactly one place (`generated/rpc.rs`).
This makes the `session.workspace.*` → `session.workspaces.*` typo
bug class structurally impossible: helpers can't drift from the
schema because they no longer reference wire strings, and new RPCs
land in the namespace immediately as the schema regenerates.
- scripts/codegen/rust.ts: emit rust/src/generated/rpc.rs alongside
api_types.rs. Builds a namespace tree from `rpcMethod` paths,
resolves $ref/title/inline schemas for params + results, injects
sessionId for session methods, dispatches via rpc_methods constants.
- rust/src/generated/rpc.rs: new ~1370 LOC generated file.
- rust/src/lib.rs: Client::rpc() accessor; ping/list_models/get_quota
rewritten as delegations.
- rust/src/session.rs: Session::rpc() accessor; workspaces/plan/mode/
model/name/log/fleet/permissions helpers rewritten as delegations.
- rust/tests/session_test.rs: add 3 namespace-only tests
(session.rpc().agent().list, session.rpc().tasks().list,
client.rpc().models().list); update session.log mock response to
match typed LogResult shape (eventId required).
- rust/CHANGELOG.md, rust/README.md: document the new namespace.
cargo +nightly-2026-04-14 fmt --check, cargo clippy
--all-features --all-targets -- -D warnings, and cargo test
--all-features all green.
No breaking changes for SDK or github-app consumers — public helper
signatures are preserved by construction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename crate to `github-copilot-sdk`
Per formal naming decision, the published crate name on crates.io will
be `github-copilot-sdk` and the import path will be
`use github_copilot_sdk::...`.
Changes:
- `Cargo.toml`: `name = "github-copilot-sdk"`, `[lib] name = "github_copilot_sdk"`,
`documentation = "https://docs.rs/github-copilot-sdk"`. Repository and
homepage URLs continue to point at `github/copilot-sdk` (the repo
name is unchanged).
- All `use copilot::` / `copilot::` references migrated to
`use github_copilot_sdk::` / `github_copilot_sdk::` across `src/`,
`tests/`, `examples/`, README, CHANGELOG, RELEASING.md, and codegen
scripts.
- Embedded-CLI cache directory renamed from `~/.cache/copilot-sdk-*` to
`~/.cache/github-copilot-sdk-*` for naming consistency. Pre-release
caches will be orphaned; acceptable given the crate is unpublished.
- `release-plz.toml` package name updated.
Verified: `cargo check`, `cargo clippy -D warnings`, `cargo test`,
nightly `cargo fmt --check` all green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add typed wrappers for filter/MCP/permission shapes (Bucket A.1, A.3, A.4)
Replaces the remaining `serde_json::Value` blobs on the public surface
with typed structs/enums, mirroring the wire shapes documented in
Node/Go/Python/.NET. Helper signatures already changed from `Value` to
typed equivalents in the same call-sites; this lands the type
definitions and constructor-site updates.
A.3 — typed wrappers
* `SessionListFilter { cwd, git_root, repository, branch }` for
`Client::list_sessions`. Was `Option<serde_json::Value>`.
* `McpServerConfig` tagged enum (Stdio/Http/Sse) with
`McpStdioServerConfig` / `McpHttpServerConfig` payloads.
`SessionConfig::mcp_servers`, `ResumeSessionConfig::mcp_servers`, and
`CustomAgentConfig::mcp_servers` are now
`Option<HashMap<String, McpServerConfig>>`. Accepts `type: "local"`
alias on deserialize for back-compat with the legacy CLI shape.
* `PermissionRequestData` gains `kind: Option<PermissionRequestKind>`
and `tool_call_id: Option<String>` fields. The eight CLI permission
categories (shell/write/read/url/mcp/custom-tool/memory/hook) are
enumerated; unknown kinds fall through to `Unknown`. Existing
`extra: Value` flatten is preserved so `data.extra["command"]` etc.
keep working.
A.4 — PermissionResult variants
* Adds `UserNotAvailable` (encodes as `{kind: "user-not-available"}`)
and `NoResult` (encodes as `{kind: "no-result"}`). Both paths —
notification (`handlePendingPermissionRequest`) and direct RPC
(`permission.request`) — emit the correct kind string.
* `pending_permission_result_kind` updated to handle `NoResult`
explicitly; `direct_permission_payload` updated to bypass
ApproveOnce/Reject conversion for these new variants.
A.1 — disable_resume
* `ResumeSessionConfig::disable_resume: Option<bool>`. Mirrors Node's
`ResumeSessionConfig.disableResume` and Go's `*bool DisableResume`.
Forces resume to fail if the session is missing on disk rather than
silently starting a new one.
Tests
* `list_sessions_serializes_typed_filter` — confirms the typed filter
serializes camelCase, omits None fields, and dispatches via
`session.list`.
* `mcp_server_config_roundtrips_through_tagged_enum` — confirms
`type: "stdio"` serialize, `type: "local"` alias on deserialize, and
HashMap roundtrip.
* `permission_request_data_extracts_typed_kind` — confirms typed kind
extraction including the kebab-case `custom-tool` rename and the
forward-compatible `Unknown` fallthrough.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document infinite_sessions parity + Client::stop deferral (Bucket A.2/A.6)
Notes-only CHANGELOG entries closing out the remaining Bucket A items:
* A.2 (infinite_sessions) — already wired on both SessionConfig and
ResumeSessionConfig as `Option<InfiniteSessionConfig>` with
default-omit-on-the-wire semantics. Matches Node/Go (CLI-applied
defaults when the field is absent). No code change; document the
parity decision so it doesn't get re-flagged.
* A.6 (Client::stop error aggregation) — defer behind a Client-level
session registry. Real aggregation requires iterating Session handles
to disconnect-then-kill, but the Rust Client only tracks per-session
channel senders today (router::SessionSenders), not Session instances.
A cosmetic Result<(), Vec<Error>> change with no behavior shift would
be pure pass-through wrapping. Tracked for Bucket B alongside the
registry refactor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Aggregate Client::stop errors across active sessions (Bucket B / A.6)
Rewrite `Client::stop` to cooperatively shut down every still-registered
session before terminating the CLI child:
1. Snapshot active session IDs from the router (no lock held across
the destroy RPCs).
2. For each, send `session.destroy` and unregister.
3. Take + terminate the child.
4. Collect any errors from steps 2 and 3 into a new `StopErrors`
aggregate and return `Result<(), StopErrors>`.
`StopErrors` is a public newtype around `Vec<Error>` implementing
`std::error::Error`; `errors()` and `into_errors()` expose the
underlying errors. Mirrors Node's `Error[]` shutdown return shape and
closes Bucket A.6 (deferred from the prior batch since true aggregation
required iterating active sessions, not just changing the signature).
Implementation detail: rather than introducing a parallel
`Weak<Session>` registry on the Client, this leans on the router's
existing session-ID HashMap. The router already tracks every session
the client has registered, so a `session_ids()` snapshot helper is the
only new state. No new Arc<Session> ceremony, no Drop-impl back-pointer,
no public API change to Session.
This is a breaking change to `Client::stop`'s return type. README's
quickstart example switches from `client.stop().await?` to
`client.stop().await.ok();` (best-effort shutdown) — callers that
care about per-session destroy errors can match on `StopErrors` and
inspect `errors()`.
Tests:
- `client_stop_sends_session_destroy_for_each_active_session` — two
registered sessions, both destroys observed on the wire, returns Ok.
- `client_stop_aggregates_session_destroy_errors` — destroy returns
a JSON-RPC error; `StopErrors` carries it and Display includes the
underlying message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.1 SessionConfig fields
Mirrors the six knobs the Node SDK exposes on `SessionConfig` /
`ResumeSessionConfig` that the Rust SDK was missing:
- session_id: Option<SessionId> on SessionConfig (custom session ID;
remains required on ResumeSessionConfig as the existing field).
- working_directory: Option<PathBuf> (per-session cwd override,
independent of ClientOptions::cwd).
- config_dir: Option<PathBuf> (override default config dir for this
session).
- model_capabilities: Option<ModelCapabilitiesOverride> (per-property
capability overrides, deep-merged at session create / resume time;
the type was already used by SetModelOptions).
- github_token: Option<String> (per-session GitHub token, distinct
from ClientOptions::github_token which authenticates the CLI process
itself). Redacted from Debug output.
- include_sub_agent_streaming_events: Option<bool>.
Wire format: snake_case round-trips to camelCase via
#[serde(rename_all = "camelCase")] except gitHubToken, which uses an
explicit #[serde(rename = "gitHubToken")] to match Node's mixed-case
spelling.
Adds two regression tests in tests/session_test.rs verifying serde
output for the wire field names + Debug-redaction of the token.
CHANGELOG: documents the six new fields under "Configuration parity".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.2 ClientOptions fields (log_level + idle timeout)
Mirrors two of the five Node CopilotClientOptions knobs the Rust SDK
was missing:
- log_level: Option<LogLevel> with a new typed enum (None / Error /
Warning / Info / Debug / All), serialized lowercase to match the
CLI's --log-level argument values. Replaces the previously hardcoded
--log-level info in spawn_stdio / spawn_tcp; when unset the SDK still
passes "info" for parity with prior behavior.
- session_idle_timeout_seconds: Option<u64>. When Some(n) with n > 0,
the SDK passes --session-idle-timeout <n>. None or Some(0) leaves
sessions running indefinitely (CLI default). Implementation lives
in a new Client::session_idle_timeout_args helper, mirroring
auth_args.
Adds four lib unit tests covering the helper plus LogLevel serde
round-trip.
Two of the five Node knobs are intentionally not ported and are
documented in the CHANGELOG as N/A:
- isChildProcess: requires a parent-stdio transport variant the Rust
SDK does not yet support; tracked as a future addition rather than
a Bucket B item.
- autoStart: does not apply to the Rust SDK's API shape — Client::start
is a single explicit constructor rather than the deferred-init
pattern Node uses.
The remaining onListModels (BYOK callback) is tracked separately;
its callback type design is non-trivial and warrants its own commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Bucket B.2 on_list_models BYOK callback override
Mirrors Node's `CopilotClientOptions.onListModels`: when set,
`Client::list_models` returns the handler's result without making
a `models.list` RPC. This is the BYOK escape hatch for environments
where the model catalog is provisioned separately from the CLI
(e.g. external inference servers selected via Transport::External).
API additions:
- `ListModelsHandler` async_trait with a single
`async fn list_models(&self) -> Result<Vec<Model>, Error>` method.
Mirrors the shape of `SessionHandler` / `SessionHooks` for
consistency.
- `ClientOptions::on_list_models: Option<Arc<dyn ListModelsHandler>>`.
- `ClientOptions` switches from `#[derive(Debug)]` to a manual Debug
impl that prints the handler as `<set>` / `None`. Same precedent
as `SessionConfig::handler` and the redacted `github_token` field.
Plumbing:
- `ClientInner` gains an `on_list_models` field carrying the handler.
- `Client::from_transport` takes the handler as a new parameter;
threaded through all 3 transport call sites in `Client::start`
(External / Tcp / Stdio). `Client::from_streams` (no
ClientOptions) passes None.
- `Client::list_models` consults the handler before falling back
to the RPC.
Tests:
- `client_options_debug_redacts_handler`: confirms manual Debug
prints `<set>` for the handler and continues redacting
`github_token`.
- `list_models_uses_on_list_models_handler_when_set`: end-to-end
override path with a counting handler.
Bucket B.2 is now complete: log_level, session_idle_timeout_seconds,
and on_list_models all ported. is_child_process and auto_start
remain documented as N/A (transport variant Rust doesn't have / API
shape mismatch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add MessageOptions.request_headers (Phase 4 § 4.5)
Mirrors Node's `MessageOptions.requestHeaders` and Go's
`MessageOptions.RequestHeaders`: custom per-turn HTTP headers
forwarded to the CLI on the `session.send` request, used by the
CLI to inject headers into outbound model API calls for that turn.
API additions:
- `MessageOptions::request_headers: Option<HashMap<String, String>>`
(already `#[non_exhaustive]` with builder pattern, so the new field
is fully additive for existing callers).
- `MessageOptions::with_request_headers(headers)` chainable builder.
Wire format:
- `session.send` request payload gains a `requestHeaders` object
when the field is `Some(map)` and the map is non-empty.
- Omitted entirely when `None` or empty — same `omitempty` semantics
as Node's optional field (per Node types.ts:1521-1524).
Tests:
- `send_serializes_request_headers` — multi-header case verifies
field name (camelCase `requestHeaders`) and value pass-through.
- `send_omits_request_headers_when_unset_or_empty` — covers both
the unset (None) and empty-map cases, ensuring neither sends
`"requestHeaders": {}` on the wire.
Bucket scope: this is the smallest of Phase 4's four 1.0-blocking
gaps. 4.1 (slash commands) and 4.2 (SessionFsProvider, ADR-worthy)
remain. 4.3 / 4.4 are pending team review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add slash command registration (Phase 4 § 4.1)
Introduces the consumer-facing surface for `/cmdname` slash commands,
matching Node's `SessionConfig.commands`, Python's, and Go's:
- `CommandHandler` async trait — single `on_command(ctx)` async method.
Mirrors the SDK's existing `SessionHandler` / `ToolHandler` pattern
(named trait, navigable in stack traces) over a boxed-closure shape.
- `CommandDefinition { name, description?, handler }` — `#[non_exhaustive]`
with `new` + `with_description` builders. Manual `Serialize` emits only
`{name, description?}` on the wire; handlers stay client-side. Manual
`Debug` prints the handler as `<set>` (same precedent as
`SessionConfig::handler`).
- `CommandContext { session_id, command, command_name, args }` —
`#[non_exhaustive]`, what the handler receives.
- `SessionConfig::commands` and `ResumeSessionConfig::commands` —
`Option<Vec<CommandDefinition>>` with `with_commands` builders. Field
is `skip_deserializing` since `CommandDefinition` carries an opaque
handler.
Wire-up:
- `Client::create_session` / `Client::resume_session` drain
`config.commands` into an `Arc<HashMap<String, Arc<dyn CommandHandler>>>`
via a new `build_command_handler_map` helper, threaded through
`spawn_event_loop` to `handle_notification`.
- New `SessionEventType::CommandExecute` arm dispatches incoming
`command.execute` notifications: looks up the handler by `commandName`,
invokes it on a spawned task, then acks via
`session.commands.handlePendingCommand` — no error on success,
`error: <handler message>` on `Err`, and
`error: "Unknown command: <name>"` when the name is unregistered
(matches Node's behavior verbatim).
Tests (4 new in `rust/tests/session_test.rs`, all green):
- `create_serializes_commands_strips_handler` — wire payload contains
only `{name, description?}`, no `handler` key.
- `command_execute_dispatches_to_registered_handler_and_acks_success`
— handler invoked with right `CommandContext`, ack has no error.
- `command_execute_unknown_command_acks_with_error` — unregistered
command name produces `Unknown command: <name>` ack.
- `command_execute_handler_error_propagates_to_ack` — handler `Err` is
surfaced verbatim in the ack's `error` field.
Schema-side types (`CommandExecuteData`,
`SessionRpcCommands::handle_pending_command`, `RPC_METHOD_*`) were
already in `rust/src/generated/` — no codegen changes needed.
Cross-repo impact (additive): github-app's `SessionConfig { ... }`
literal at `core.rs:710` will need `commands: None,` added at sync time,
same mechanical pattern as Bucket B.1 fields.
CHANGELOG entry added under "Configuration parity".
Gates:
- cargo +nightly-2026-04-14 fmt --check ✅
- cargo clippy --all-features --all-targets -- -D warnings ✅
- cargo test --all-features ✅ (84 lib + 5 + 3 + 68 + 17 = 177 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add ADR 0001: SessionFsProvider trait and plumbing (Phase 4 § 4.2)
Drafts the architecture decision record for the last 1.0 parity gap —
SessionFsProvider — before any implementation lands. Per the global
"ADRs for major decisions" convention and tclem's explicit guidance
on §4.2.
Establishes `rust/docs/adr/` as the home for Rust SDK ADRs (this is
the first one) with a short index in `rust/docs/adr/README.md`.
ADR scope:
- **Methodology** — explicit verify-before-drafting audit, citing the
consistent wins from A.2 / A.6 / 4.5 / 4.1. Captures the cross-SDK
source map (Node + Python + Go + generated Rust types) so reviewers
can verify the proposed shape against actual upstream code.
- **Trait shape** — async_trait with 10 methods mirroring Node's
provider, returning `Result<T, FsError>`. Sync alternative and
trait-erased boxed-closure alternative both rejected with rationale.
- **Method signatures** — Rust-idiomatic `Result<T, FsError>` with the
SDK adapting to the schema's `{ ..., error: Option<SessionFsError> }`
payload. `FsError::NotFound` → `ENOENT`, `FsError::Other` → `UNKNOWN`.
`From<io::Error>` provided so `tokio::fs`-backed handlers can `?`.
- **Concurrency model** — concurrent dispatch (each `sessionFs.*`
request on its own spawned task), `Send + Sync` providers must be
re-entrant. Per-session sequential dispatch rejected with rationale
(CLI parallelism assumption).
- **Plumbing** — direct `Arc<dyn SessionFsProvider>` registration on
SessionConfig, NOT a factory closure like Node/Python/Go. Sidesteps
the "lambdas as fn args" rule, idiomatic Rust, callers can carry
session-id refs themselves. Trade-off documented in "Differences
from other SDKs" call-out planned for the README. Future
factory-closure form can land additively if needed.
- **Inbound dispatch** — new arms in `handle_request` for all 10
`sessionFs.*` methods, dispatched through a per-session
`Arc<HashMap<SessionId, Arc<dyn SessionFsProvider>>>` map mirroring
the §4.1 `command_handlers` shape.
- **Naming + module layout** — `rust/src/session_fs.rs` for public
surface, `rust/src/session_fs_dispatch.rs` (`pub(crate)`) for the
request adapters. `SessionFsConventions { Posix, Windows }` is
hand-authored to avoid the generated enum's catch-all `Unknown`
variant on the consumer-input side.
- **Forward compat** — `#[non_exhaustive]` on `SessionFsConfig`,
`FsError`, `FileInfo`, `DirEntry`, `DirEntryKind`. Future trait
methods land with default impls returning `Err(FsError::Other(...))`
so existing impls continue to compile.
- **Tauri-app non-impact** — audited; `src-tauri/` does not register
a virtual filesystem provider today. `ClientOptions { ... }` literal
in `cli.rs` will need `session_fs: None,` added at sync time, same
mechanical pattern as Bucket B.2.
- **Implementation order** — 11-step ordered plan, from trait
authoring through CHANGELOG entry, so the actual implementation
commit can follow ADR approval without sequencing surprises.
This commit ships the ADR only — no implementation code, no public
API changes, no test changes. The ADR is `Status: Proposed` until
tclem and the Rust SDK working group sign off.
ASCII-only diagrams per repo style. No external markdown rendering
dependencies.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* rust: implement SessionFsProvider (Phase 4 § 4.2)
Adds a virtualizable filesystem provider that lets host applications
sandbox sessions, project files into in-memory or remote storage, and
apply permission policies before bytes move. Last 1.0-blocking parity
gap before public release.
Public API additions (in `crate::session_fs`, re-exported from
`crate::types`):
- `SessionFsProvider` async trait with 10 methods (`read_file`,
`write_file`, `append_file`, `exists`, `stat`, `mkdir`, `readdir`,
`readdir_with_types`, `rm`, `rename`). All methods have default impls
returning `Err(FsError::Other("not supported"))` so providers only
override what they need and forward-compatible schema additions land
without breaking implementors.
- `SessionFsConfig` (initial_cwd, session_state_path, conventions),
`SessionFsConventions` (Posix/Windows), `FsError` (NotFound/Other),
`FileInfo`, `DirEntry`, `DirEntryKind`. All `#[non_exhaustive]`.
- `ClientOptions::session_fs: Option<SessionFsConfig>` — when set,
`Client::start` calls `sessionFs.setProvider` after protocol-version
verification.
- `SessionConfig::with_session_fs_provider` /
`ResumeSessionConfig::with_session_fs_provider` builders for
registering an `Arc<dyn SessionFsProvider>` per session.
- `From<std::io::Error>` on `FsError` (NotFound→NotFound, anything
else→Other) so handlers backed by `std::fs` / `tokio::fs` can use `?`.
Wire dispatch:
- `pub(crate) crate::session_fs_dispatch` module bridges the trait to
the schema. `FsError::NotFound` maps to wire `ENOENT`; all other
errors map to `UNKNOWN` with the message preserved for diagnostics.
- The session event loop forwards inbound `sessionFs.*` requests to
the dispatch module; non-fs methods continue to the existing
`unknown method` error path.
- New `SessionError::SessionFsProviderRequired` and
`SessionError::InvalidSessionFsConfig` variants surface
configuration errors at `Client::start` / `create_session` time.
Divergence from Node/Python/Go:
- This SDK accepts `Arc<dyn SessionFsProvider>` directly, rather than a
factory closure that builds a provider per session. There is no
`Session` value to thread into a factory at config time, and the SDK
already prefers traits over boxed closures for handler-shaped APIs
(`SessionHandler`, `SessionHooks`, `ToolHandler`). See
`rust/docs/adr/0001-session-fs-provider.md` for the rejected-factory
rationale and a forward-compat escape hatch
(`with_session_fs_provider_factory`) that can be added additively
post-1.0 if a real factory use case emerges. ADR status flipped from
Proposed to Accepted in this commit.
Tests:
- 7 mock-server tests in `rust/tests/session_test.rs` covering
read_file dispatch, NotFound→ENOENT mapping, Other→UNKNOWN mapping,
write_file with mode, readdir_with_types, rm with force, and
validation rejecting empty `initial_cwd`.
- Inline unit tests in `session_fs.rs` for the io::Error→FsError
conversion.
Docs / examples:
- `rust/examples/session_fs.rs` — in-memory provider example.
- `rust/CHANGELOG.md` — entry under "Configuration parity".
- `rust/README.md` — new "Differences From Other SDKs" section
linking to the ADR.
Verified:
- `cargo +nightly-2026-04-14 fmt --check`
- `cargo clippy --all-features --all-targets -- -D warnings`
- `cargo test --all-features` (190 tests pass; 3 ignored require CLI)
Cross-repo impact (additive only):
- `src-tauri/src/session/cli.rs:337` `ClientOptions { ... }` literal —
adds `session_fs: None`.
- `src-tauri/src/session/core.rs:710` `SessionConfig { ... }` literal —
adds `session_fs_provider: None`.
- `src-tauri/src/session/core.rs:749` `ResumeSessionConfig { ... }`
literal — adds `session_fs_provider: None`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add W3C Trace Context propagation (Phase 4 § 4.3)
Implements the cross-SDK 1.0-blocking `traceparent` / `tracestate` plumbing
across the three RPC injection points and the inbound tool-invocation read
path. Hybrid shape combines Node's callback ergonomics with Go's per-turn
override:
- New `TraceContext` struct (`#[non_exhaustive]`) and `TraceContextProvider`
async trait in `crate::trace_context`, re-exported from `crate::types`.
- `ClientOptions::on_get_trace_context: Option<Arc<dyn TraceContextProvider>>`
supplies an ambient provider invoked on `session.create`, `session.resume`,
and `session.send`. Manual `Debug` impl prints `<set>` / `None` matching
the `on_list_models` precedent.
- `MessageOptions` gains `traceparent` / `tracestate: Option<String>` plus
three builders — `with_trace_context`, `with_traceparent`,
`with_tracestate`. Per-turn values override the callback (provider is not
invoked when MessageOptions already carries trace headers).
- `ToolInvocation` flipped to `#[non_exhaustive]` and exposes inbound
`traceparent` / `tracestate` populated from `external_tool.requested`
events, plus a `trace_context()` helper. Wire fields are omitted when
unset, matching the schema's `omitempty` semantics.
- New test-only constructor `Client::from_streams_with_trace_provider`
(gated on `cfg(test)` / `feature = "test-support"`) so integration tests
can exercise the callback path.
Tests: 7 new mock-server scenarios in `tests/session_test.rs` covering
provider invocation on create/resume/send, MessageOptions-overrides-callback,
MessageOptions-without-callback, ToolInvocation read path, and wire-omits-
when-unset. Full suite: 88 lib + 83 integration green; clippy and nightly
fmt clean.
No formal ADR — `ListModelsHandler` and `MessageOptions` precedents already
cover both shapes. CHANGELOG entry under "Configuration parity" documents
the cross-SDK rationale.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Implement Default on ToolInvocation for test ergonomics
Adds `#[derive(Default)]` to `ToolInvocation` (and to `SessionId`, which
the derive depends on). Pure additive: production code never constructs
`ToolInvocation` literals (it's a CLI-emitted read-only type), so the
"meaningless empty defaults" cost is genuinely zero in practice.
The win is downstream test scaffolding. With `..Default::default()`
available, test sites that build `ToolInvocation` literals to drive
handler tests can collapse boilerplate and absorb future
`#[non_exhaustive]` field additions automatically — no need to
re-touch every test scaffold every time the schema grows. Aligns with
the rust-coding-skill's "use `..Default::default()` in tests to reduce
boilerplate when adding fields" guidance, and mirrors `MessageOptions`
which already derives `Default`.
`SessionId` is a transparent newtype around `String`, so its `Default`
is `SessionId(String::new())` — already the natural identity for an
unset session ID.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add TelemetryConfig env-var passthrough on ClientOptions (Phase 4 § 4.4)
Closes the last 1.0-blocking gap in Phase 4 of the Rust SDK release prep.
Adds opt-in OpenTelemetry configuration mirroring Node/Python/Go.
## Surface
- `TelemetryConfig` (`#[non_exhaustive]`, `Debug + Clone + Default`) with
five `Option`-typed fields: `otlp_endpoint`, `file_path`,
`exporter_type`, `source_name`, `capture_content`. No `Serialize` /
`Deserialize` derives — this type is not wire-serialized; it's pure
spawn-time env-var injection.
- `OtelExporterType` enum (`#[non_exhaustive]`, `Serialize`/`Deserialize`
with `rename_all = "kebab-case"`): `OtlpHttp`, `File`. Typed rather than
`Option<String>` to match the `LogLevel` precedent (B.2) for finite,
enumerated CLI knobs.
- `ClientOptions::telemetry: Option<TelemetryConfig>`, defaulting to
`None`. Field added to the manual `Debug` impl.
## Wire behavior
When `ClientOptions::telemetry` is `Some(...)`, `Client::build_command`
sets `COPILOT_OTEL_ENABLED=true` plus, for each populated field:
- `otlp_endpoint` -> `OTEL_EXPORTER_OTLP_ENDPOINT`
- `file_path` -> `COPILOT_OTEL_FILE_EXPORTER_PATH`
- `exporter_type` -> `COPILOT_OTEL_EXPORTER_TYPE` (`"otlp-http"` / `"file"`)
- `source_name` -> `COPILOT_OTEL_SOURCE_NAME`
- `capture_content` -> `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`
(`"true"` / `"false"`)
Env-var names are byte-for-byte identical to the Node/Python/Go SDKs.
`capture_content` serializes as lowercase `"true"`/`"false"` matching
Node's `String(t.captureContent)` and Python's `str(...).lower()`.
## Env-var precedence
`build_command` env order: auth token -> telemetry -> `options.env`
(user override) -> `options.env_remove`. User-supplied env always wins
over both auth and telemetry, by ordering. Tests cover this.
## Design notes
- Pure env-var passthrough: zero new dependencies. The CLI itself owns
the OpenTelemetry exporter; the SDK is just a config conduit. All
three reference SDKs (Node, Python, Go) work this way — verified
before drafting (verify-before-drafting tally now at 8 wins).
- No ADR. Plan-doc explicitly endorsed skipping the ADR since this
isn't an architectural decision: the shape is fully determined by
cross-SDK precedent. ADR directory is reserved for non-obvious
choices; precedent verification belongs in CHANGELOG + commit body.
- `#[non_exhaustive]` on both `TelemetryConfig` and `OtelExporterType`
to absorb future CLI-side telemetry knobs (sampling, additional
exporters) without breaking changes.
- `PathBuf` (not `String`) for `file_path` for type safety;
`Command::env` accepts `AsRef<OsStr>` so no conversion needed.
## Tests
Five new tests in `lib.rs`:
- `build_command_sets_otel_env_when_telemetry_enabled` — full
TelemetryConfig set; asserts all six expected env vars propagate with
correct values.
- `build_command_omits_otel_env_when_telemetry_none` — default
`ClientOptions`; asserts none of the six env vars are present.
- `build_command_omits_unset_telemetry_fields` — only `otlp_endpoint`
set; asserts that single field plus the implicit enabled flag are
set, and the four others are absent.
- `build_command_lets_user_env_override_telemetry` — telemetry sets
`OTEL_EXPORTER_OTLP_ENDPOINT="http://from-config:..."`, user env
sets it to `"http://from-user-env:..."`; asserts user env wins.
- `telemetry_config_capture_content_serializes_as_lowercase_bool` —
asserts both `Some(true)` and `Some(false)` serialize as the bare
lowercase boolean strings.
All tests pass; `cargo fmt --check`, `cargo clippy -- -D warnings`,
`cargo test --all-features` all green.
## Cross-repo impact
Existing `ClientOptions { ... }` literal in github-app `cli.rs:337`
gains one new field: `telemetry: None`. No other consumer-side
changes. The Tauri app's existing telemetry pipeline is for the host
app and doesn't interact with the SDK config — this is purely about
forwarding telemetry env vars to the spawned CLI child.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document Rust-only API surface (Phase 4 § 4.7)
Doc-only follow-up to the Phase 4 parity work. Two pieces of content,
no API changes.
## Changes
1. **`Client::get_quota` rustdoc** — flags the method as Rust-only as
of 0.1.0. The underlying `account.getQuota` JSON-RPC endpoint is
exposed only by the Rust SDK in this release; Node, Python, Go, and
.NET don't surface it. Brief, factual, no backport-issue links.
2. **"Rust-only API" subsection in `rust/README.md`** — restructures
the existing "Differences From Other SDKs" section into two
subsections: "Shape divergence" (the existing `SessionFsProvider`
factory-vs-direct discussion) and a new "Rust-only API" listing the
surface that exists only on the Rust SDK as of 0.1.0:
- `Client::get_quota`
- First-class `Session` convenience methods (`set_mode`/`get_mode`,
`set_name`/`get_name`, `read_plan`/`update_plan`/`delete_plan`,
`start_fleet`, `list_workspace_files`/`read_workspace_file`/
`create_workspace_file`)
- Typed newtypes (`SessionId`, `RequestId`)
- Permission policy builders (`permission::approve_all`,
`permission::deny_all`, `permission::approve_if`)
- `Client::from_streams` (arbitrary `AsyncRead`/`AsyncWrite`)
- `enum Transport { Stdio, Tcp, External }`
- Split `prefix_args` / `extra_args`
Tone is "Rust gets to be Rust" — not apologetic, no backport
promises. Cross-SDK parity for these is explicitly framed as a
post-release conversation, not a release blocker.
## Bonus: drive-by intra-doc link fix
Fixed a pre-existing broken intra-doc link in `session_fs.rs` (`[`tokio::fs`]`
-> explicit URL link). Caught by `cargo doc --no-deps --all-features`,
which now builds clean. Doing it here rather than as a separate commit
since the testing-pass that comes next is going to require clean docs
output anyway, and the fix is one line.
`cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test --doc`,
and `cargo doc --no-deps --all-features` all green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Broaden skills discovery wording in copilot-instructions.md
Skills under .github/skills/ are auto-discovered by Copilot tooling
broadly (CLI, Copilot Coding Agent, etc.), not just Copilot CLI.
Per stephentoub's review on PR #1164.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix ConnectionState::Errored wire form to match Go ("error" not "errored")
Caught by stephentoub on PR #1164. ConnectionState's serde
`rename_all = "lowercase"` was producing "errored" for the Errored
variant, but Go's source-of-truth uses "error". Add explicit
`#[serde(rename = "error")]` on the variant so the wire form matches
Go's ConnectionState string. Variant name stays `Errored` to avoid
shadowing the std `Error` trait and the crate's own `Error` type.
Adds two unit tests locking in the wire format for all four variants.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename ConnectionState::Errored to ConnectionState::Error
Per follow-up on stephentoub's review of PR #1164: prefer renaming the
variant to drop the `#[serde(rename = "error")]` attribute. The variant
is unused outside types.rs (the Client transitions Disconnected /
Connecting / Connected today; Error is reserved for future use), so
renaming has no consumer impact and produces a cleaner enum surface.
`ConnectionState::Error` does not collide with anything in scope:
`types.rs` does not import `crate::Error` (it uses fully-qualified
`crate::Error` at use sites) and `std::error::Error` is unimported. The
variant lives inside a typed enum, so no shadowing concern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR #1164 cross-SDK consistency review
Three review-feedback fixes folded into a single commit:
- `session.ui()` sub-API: move `elicitation`, `confirm`, `select`,
`input` onto a new `SessionUi<'a>` view returned by `Session::ui()`.
Mirrors .NET `session.UI`, Python `session.ui`, Go `session.UI()`.
Wire-method names unchanged. Per stephentoub on
https://github.com/github/copilot-sdk/pull/1164#discussion_r3161986035
and the bot duplicate at #discussion_r3163241529.
- Typed `Client::get_status` and `Client::get_auth_status` returns:
introduce `GetStatusResponse { version, protocol_version }` and
`GetAuthStatusResponse { is_authenticated, auth_type, host, login,
status_message }`, both `#[non_exhaustive]`. Matches Node / Go /
Python typed shapes. Per the bot at
https://github.com/github/copilot-sdk/pull/1164#discussion_r3162629019.
- `Session::set_model` now returns `Result<(), Error>` instead of
`Result<Option<String>, Error>`. Mirrors Node / Python / Go / .NET
void-on-success semantics. Per the bot at
https://github.com/github/copilot-sdk/pull/1164#discussion_r3162816817.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Type MessageOptions::mode as DeliveryMode enum
Promote MessageOptions::mode from Option<String> to Option<DeliveryMode>,
where DeliveryMode is a #[non_exhaustive] enum with Enqueue (default) and
Immediate variants serializing to "enqueue" / "immediate". Mirrors Node
types.ts:1519 (mode?: "enqueue" | "immediate") and Go types.go:858
(MessageOptions.Mode is the message delivery mode (default: "enqueue")).
The prior rustdoc on MessageOptions::mode incorrectly described the field
as a permission mode. The field has always controlled message delivery
relative to in-flight session work; the doc lied. Strings other than
"enqueue" / "immediate" were silently no-ops at the CLI, so the typed enum
turns wrong-string-at-call-site from a runtime nothing-happens into a
compile error.
Precedent for typed enums on enumerated CLI knobs: B.2 LogLevel
(c4132c2), 4.4 OtelExporterType (aefb108), SessionFsConventions, and
DirEntryKind. The asymmetric request_headers (4.5) stayed
HashMap<String, String> because that value space is open; mode is finite
and CLI-controlled.
with_mode signature shrinks from impl Into<String> to DeliveryMode and
the wire injection in session.rs uses serde_json::to_value(m). Added a
roundtrip test in types.rs covering both variants.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Default permission-flow flags to Some(true)
SessionConfig::default() and ResumeSessionConfig::new() now set the four
permission-flow flags to Some(true):
- request_user_input
- request_permission
- request_exit_plan_mode
- request_elicitation
Mirrors Node's client.ts:749-751 which always sets requestPermission: true
and derives requestUserInput/requestElicitation from handler presence. In
Rust, SessionHandler is trait-based so a handler is always installed
(DenyAllHandler is the default) — opt-in defaults of None meant a Rust
caller could install a SessionHandler and forget to flip the flags,
silently breaking permission flow vs. the equivalent Node code.
The default DenyAllHandler refuses all permission requests so the wire
surface is safe out-of-the-box. Callers that want the wire surface fully
disabled set the flags explicitly to Some(false).
SessionConfig drops its derived Default in favor of a manual impl that
enumerates every field. ResumeSessionConfig::new() switches the four
field assignments from None to Some(true). Field rustdoc updated on each
of the four fields to call out the default. Two unit tests added in
types.rs covering both constructors.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Mark remaining public config types non_exhaustive
Adds #[non_exhaustive] to the 10 remaining public configuration types
that didn't already carry the attribute:
- SessionConfig
- ResumeSessionConfig
- ClientOptions
- ProviderConfig
- McpServerConfig
- Tool
- CustomAgentConfig
- InfiniteSessionConfig
- SystemMessageConfig
- ConnectionState
HookEvent, HookOutput, MessageOptions, TelemetryConfig, SessionFsConfig,
FsError, FileInfo, DirEntry, ToolInvocation, Error, Transport, and the
new DeliveryMode were already marked. Closing the asymmetry now means
adding fields to any of these post-1.0 is non-breaking on consumers that
construct via Default::default() plus field assignment or the with_*
builders.
Tradeoff: external crates can no longer use struct-literal syntax for
these types -- not even with ..Default::default(), which only works
inside the defining crate. Tests, examples, and the tool_parameters
doctest are migrated to the let-mut + field-assignment pattern. Callers
porting from 0.1.0-* will see the same compile error and apply the same
mechanical transform.
CHANGELOG entry under Configuration parity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix InputOptions doc-link to SessionUi::input
The UI-grouping commit f4aa8d9 moved input/select/confirm/elicitation off
Session into SessionUi, but the rustdoc on InputOptions still referenced
the old crate::session::Session::input path. Cargo doc with
-D rustdoc::broken_intra_doc_links rejected the link.
Repoints the link to crate::session::SessionUi::input.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Drop cross-SDK comparisons from Rust source comments
Per-symbol "Mirrors Node's Foo / Go's Bar / Python's Baz" rustdoc is
unscalable and drifts as the other SDKs evolve. The Rust SDK seeks parity
with Node/Python/Go/.NET; that fact is now stated once at the top of
rust/README.md, and intentional divergences live in the README's
"Differences From Other SDKs" section.
- Strip "Mirrors X" / "Unlike Y" / cross-SDK file:line citations from
rustdoc across lib.rs, session.rs, types.rs, session_fs.rs,
trace_context.rs. Replace with API-shape descriptions where the dropped
text carried real information (e.g. wire-string variants).
- README: add a one-line parity statement up top that points to the
existing Differences section.
- Update .github/skills/rust-coding-skill/SKILL.md to forbid cross-SDK
references in code comments and rustdoc going forward, with explicit
guidance that intra-SDK self-references ("Mirrors `from_streams`") are
still fine.
cargo doc -D warnings clean. cargo clippy clean. cargo fmt clean.
18 doctests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Move SessionFs ADR out of public crate
ADR 0001 (SessionFsProvider trait and plumbing) captured the design
rationale for choosing direct Arc<dyn SessionFsProvider> registration
over the factory-closure pattern that Node/Python/Go use. That
rationale is internal release-engineering history, not consumer-facing
API documentation; the README's "Differences From Other SDKs" section
already explains what consumers need to know about the divergence.
The full ADR is being relocated to the private SDK release-plan
documentation in github/github-app PR #3166 (docs/copilot/2026-04-14-sdk-release/).
- Delete rust/docs/ entirely (only contained ADR 0001 + index README).
- Strip ADR links from rustdoc, README, CHANGELOG, and the
session_fs example. The README's existing inline rationale (factory
pattern doesn't cleanly express in Rust at the session-config call
site, no `Session` value to thread in, SDK already prefers traits
over closures for handler-shaped APIs) is sufficient on its own.
cargo doc -D warnings clean. cargo clippy clean. cargo fmt clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix SessionUi::elicitation wire field name
The hand-authored `SessionUi::elicitation` convenience layer was
sending the JSON Schema payload as `"schema"` on the wire, but the
`session.ui.elicitation` request shape expects `"requestedSchema"`.
This is the field name in:
- this crate's own generated UIElicitationRequest type
(rust/src/generated/api_types.rs:1721, serde renames to
`requestedSchema` via #[serde(rename_all = "camelCase")])
- the generated typed RPC wrapper SessionRpcUi::elicitation
(rust/src/generated/rpc.rs:1245-1257), which is correct
- and the same wire field used by every other SDK we ship
So every elicitation call from the SessionUi convenience layer was
effectively dead — the CLI saw a missing required `requestedSchema`
field. The `confirm` / `select` / `input` helpers all delegate to
`elicitation`, so they were dead too.
The mock-server test for elicitation round-tripped through the same
misnamed field on both ends, so the bug slipped past unit tests
(`assert_eq!(request["params"]["schema"], schema)` matched the buggy
implementation). The fix is a one-line rename in session.rs plus a
test update that now asserts on `requestedSchema` and explicitly
rejects a stray `schema` key, so we can't regress the same way.
207 tests pass. doc / clippy / fmt clean.
Caught by the gap-analysis re-run before 0.1.0 cut. Wire-shape
divergence between hand-authored and generated layers — same class
of bug as the workspace RPC fix earlier in this stack.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add typed on_auto_mode_switch handler for rate-limit recovery
Wires the CLI's `autoModeSwitch.request` JSON-RPC callback (added by
copilot-agent-runtime PR #7024 — github/copilot-agent-runtime#7024) into
the SDK's typed handler surface. When an eligible rate limit is hit,
the runtime asks the SDK whether to switch the session to auto model;
this commit gives consumers a typed entry point for that prompt
matching the shape of the existing exit-plan-mode / user-input /
elicitation handshakes.
Background: the github-app's vendored copilot-sdk crate has carried a
private version of this handler since Apr 27 (github-app commit
02ec73588). The plumbing was never upstreamed because no other SDK had
a typed handler for it, and the wire-protocol gap on the runtime side
hadn't yet closed. With #7024 merged, the wire path is well-defined
(`autoModeSwitch.request` → `{ response: "yes" | "yes_always" | "no" }`)
and the typed handler can ship.
Cross-SDK divergence: typed handler exists only in the Rust SDK as of
0.1.0. Node, Python, Go, and .NET observe the request as a raw
JSON-RPC callback today; parity ports for those SDKs are post-release
follow-up work and are noted in the README's "Rust-only API" section
and in the changelog.
Wire shape and types:
- `handler::AutoModeSwitchResponse` — typed enum (`Yes`, `YesAlways`,
`No`) with `#[serde(rename_all = "snake_case")]`. Wire values are
byte-identical to the runtime's schema. `#[non_exhaustive]` so future
variants are additive. Ships as a typed enum rather than the
`{ response: String }` shape used in the github-app vendored copy —
consistent with the recently-landed `DeliveryMode` enum and the
general convention that finite, enumerated wire values get typed at
the API surface (LogLevel, OtelExporterType, ConnectionState,
PermissionRequestKind, etc.).
- `handler::HandlerEvent::AutoModeSwitch { session_id, error_code,
retry_after_seconds }` — new event variant. `error_code` is the
rate-limit kind (e.g. `user_weekly_rate_limited`). `retry_after_seconds`
is the RFC 9110 `Retry-After` `delta-seconds` value when the runtime
knows it; consumers can surface a humanized reset time alongside the
prompt.
- `handler::HandlerResponse::AutoModeSwitch(AutoModeSwitchResponse)` —
new response variant.
- `SessionHandler::on_auto_mode_switch` — new trait method with safe
default (`No`). Default `on_event` dispatches to it.
- `types::SessionConfig::request_auto_mode_switch: Option<bool>` and
`types::ResumeSessionConfig::request_auto_mode_switch: Option<bool>`
— new opt-in flags, both default to `Some(true)` via
`SessionConfig::default()` and `ResumeSessionConfig::new()` to match
the convention already established by the other four `request_*`
flags. Without the flag the runtime doesn't dispatch the callback.
- `session.rs` `handle_request` — new arm for `autoModeSwitch.request`
alongside the existing `permission.request` / `userInput.request` /
`exitPlanMode.request` cases. Extracts `errorCode` /
`retryAfterSeconds` from params, dispatches via
`handler.on_event(HandlerEvent::AutoModeSwitch { ... })`, serializes
the typed response back as `{ "response": "yes" | "yes_always" |
"no" }`. Falls through to `No` if the handler returns an unexpected
response variant.
Tests:
- `auto_mode_switch_dispatches_to_handler_and_serializes_response` —
asserts the inbound `autoModeSwitch.request` reaches the typed
handler with the expected `error_code` / `retry_after_seconds`, and
that returning `AutoModeSwitchResponse::YesAlways` serializes as
`{ "response": "yes_always" }` on the wire.
- `auto_mode_switch_default_handler_replies_no` — asserts the trait's
default impl replies `{ "response": "no" }` when no override is
provided.
- Updated `session_config_default_enables_permission_flow_flags` and
`resume_session_config_new_enables_permission_flow_flags` to assert
the new flag is `Some(true)` by default.
Documentation:
- README: new bullet under "Rust-only API" section.
- CHANGELOG: new entry under `### Added > Handlers + helpers`
documenting the wire path, the typed enum, and the cross-SDK
divergence with a note that Node / Python / Go / .NET parity is
post-release work.
- `request_auto_mode_switch` field rustdoc cross-references
`SessionHandler::on_auto_mode_switch`.
Validation:
- 209 tests pass (was 207 — 2 new auto-mode-switch tests).
- `cargo doc -D warnings` clean.
- `cargo +nightly-2026-04-14 fmt --check` clean.
- `cargo clippy --all-features --all-targets -- -D warnings` clean.
Migration note for the github-app sync session: the typed
`AutoModeSwitchResponse` enum replaces github-app's vendored
`AutoModeSwitchResponse { response: String }`. github-app's WebSocket
relay continues to receive the user's choice as a `String`; map to the
typed enum at the boundary
(`match s.as_str() { "yes" => Yes, "yes_always" => YesAlways, _ => No }`).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix Client::list_sessions wire shape — wrap filter under params.filter
The hand-authored `Client::list_sessions` was serializing the optional
`SessionListFilter` directly onto the JSON-RPC `params` object,
flattening fields like `repository` / `branch` / `cwd` / `gitRoot` to
the top level. The `session.list` request shape that the runtime accepts
puts the filter under `params.filter` — and that's what every other SDK
sends:
- Node `nodejs/src/client.ts:1178-1180`: `sendRequest("session.list", { filter })`
- Go `go/types.go`: `listSessionsRequest { Filter *SessionListFilter }`
- Python `python/copilot/client.py:1907-1911`: `payload["filter"] = ...`
- .NET `dotnet/src/Client.cs`: `record ListSessionsRequest(SessionListFilter? Filter)`
Because the runtime silently ignores unknown top-level keys on
`session.list`, calling `list_sessions(Some(filter))` was functionally
equivalent to `list_sessions(None)` in 0.0.x — every filter field was
discarded by the runtime, returning an unfiltered session list. No
runtime error, no log, just silently broken. Functionally dead on the
wire, same class as the elicitation `requestedSchema` fix in `c58e2f2`.
The mock-server test `list_sessions_serializes_typed_filter` asserted on
the flat shape it observed (`request["params"]["repository"]`) rather
than the schema's wrapped shape, so the bug round-tripped through both
ends — the implementation produced the wrong shape, the test verified
the wrong shape. Same root cause as the elicitation test gap.
Fix:
- `Client::list_sessions` now wraps the filter: `Some(f) ->
serde_json::json!({ "filter": f })`, `None -> serde_json::json!({})`.
`None` omits the filter key entirely (matches Go's `omitempty`
behavior; Node's `{ filter: undefined }` also omits via JSON-stringify).
- Mock-server test now asserts on the wrapped path
(`params.filter.repository`, `params.filter.branch`) AND explicitly
asserts the flattened fallback is gone (`params.get("repository")`
must return `None`). Same regression-prevention pattern as the
elicitation fix at `session_test.rs:1248-1251`.
- CHANGELOG entry under `### Fixed` documenting the wire-shape 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 gap-analysis structural-correctness pass walking every
hand-authored `client.call("...")` site against the schema and the four
other SDKs. This is the second wire-shape bug found by that pass; the
first was the `SessionUi::elicitation` `schema` -> `requestedSchema`
fix in `c58e2f2`. The audit confirms `session.list` is the only other
new bug — three Rust-unique surfaces (`session.respondToUserInput`,
`session.sendTelemetry`, top-level `sendTelemetry` /
`server.sendTelemetry`) are uncheckable cross-SDK and queued as
post-0.1.0 runtime-acceptance verification.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Bump @github/copilot pin to ^1.0.39 + regen Rust types
Per the auto-mode-switch CLI hunt session's verification, the
`@github/copilot@1.0.39-0` pin we were on does NOT contain
copilot-agent-runtime PR #7024 (auto-mode-switch wire support).
The runtime PR merged after `1.0.39-0` was cut as a release-candidate
and shipped in `1.0.39` final, with `1.0.40-0` after.
This commit:
- Bumps `nodejs/package.json` from `^1.0.39-0` to `^1.0.39` (final).
`npm install` resolved to `1.0.39`. The package-lock.json is
regenerated accordingly.
- Regenerates Rust t…1 parent 58cf64d commit 19b4ad5
118 files changed
Lines changed: 33780 additions & 9 deletions
File tree
- .github
- skills/rust-coding-skill
- workflows
- nodejs/scripts
- rust
- examples
- src
- generated
- tests
- scripts/codegen
- test/scenarios
- callbacks
- hooks
- rust
- src
- permissions
- rust
- src
- user-input
- rust
- src
- modes/default
- rust
- src
- prompts
- attachments
- rust
- src
- reasoning-effort
- rust
- src
- system-message
- rust
- src
- sessions
- concurrent-sessions
- rust
- src
- infinite-sessions
- rust
- src
- session-resume
- rust
- src
- streaming
- rust
- src
- tools
- custom-agents
- rust
- src
- mcp-servers
- rust
- src
- no-tools
- rust
- src
- skills
- rust
- src
- tool-filtering
- rust
- src
- tool-overrides
- rust
- src
- transport
- stdio
- rust
- src
- tcp
- rust
- src
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
52 | | - | |
| 51 | + | |
| 52 | + | |
53 | 53 | | |
54 | 54 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
0 commit comments