Skip to content

Rust SDK API review fixes#1367

Open
SteveSandersonMS wants to merge 18 commits into
mainfrom
stevesandersonms/rust-sdk-api-review-fixes
Open

Rust SDK API review fixes#1367
SteveSandersonMS wants to merge 18 commits into
mainfrom
stevesandersonms/rust-sdk-api-review-fixes

Conversation

@SteveSandersonMS
Copy link
Copy Markdown
Contributor

@SteveSandersonMS SteveSandersonMS commented May 21, 2026

Rust SDK API review fixes

This PR brings the Rust SDK in line with the cross-SDK API review work already merged for C# (#1343), TypeScript (#1357), and Go (#1360), and applies the Rust-specific items from api_review_rust.md. It is a deliberate breaking-change pass, intentionally landed before we declare the SDK stable.

The end result is a tighter API surface: ~6,500 lines deleted against ~2,500 added (net −4,000 lines in the Rust crate alone), with stronger cross-SDK behavioural parity.


Breaking changes for consumers

Every change below requires a downstream code edit. They are grouped by the symbol you'd grep for.

Handler API (the biggest churn)

Removed Replacement
trait SessionHandler (with on_event, on_permission_request, on_user_input, on_elicitation, on_external_tool, on_exit_plan_mode, on_auto_mode_switch, on_session_event) Five optional single-method traits: PermissionHandler::handle, ElicitationHandler::handle, UserInputHandler::handle, ExitPlanModeHandler::handle, AutoModeSwitchHandler::handle, plus ToolHandler::call (already existed). Implement only the trait(s) you need.
enum HandlerEvent Typed inputs per trait method; no event enum.
enum HandlerResponse Typed return per trait method (PermissionResult, ElicitationResult, Option<UserInputResponse>, ExitPlanModeResult, AutoModeSwitchResponse, ToolResult).
struct NoopHandler None on the corresponding optional handler field (i.e. don't call the with_*_handler builder).
struct ToolHandlerRouter SessionConfig::with_tool_handlers(Vec<Arc<dyn ToolHandler>>). The SDK builds the name→handler map internally. Duplicate tool names produce Error::InvalidConfig.
SessionConfig::with_handler(...) Per-trait builders: with_permission_handler, with_elicitation_handler, with_user_input_handler, with_exit_plan_mode_handler, with_auto_mode_switch_handler, with_tool_handlers.
SessionHandler::on_session_event (catch-all observer) session.subscribe()Stream<Item = SessionEvent>. Already part of the API; the trait method was redundant.

ApproveAllHandler and DenyAllHandler still exist but now implement PermissionHandler (one trait, one method), not the old SessionHandler. Replace with_handler(Arc::new(ApproveAllHandler)) with with_permission_handler(Arc::new(ApproveAllHandler)).

Session and Client method renames

Old New Notes
Session::get_messages() Session::get_events() Return type was always Vec<SessionEvent>, never Messages.
Session::destroy() Session::disconnect() destroy() retained with #[deprecated] for a transition window.
ResumeSessionConfig::disable_resume_event field ResumeSessionConfig::suppress_resume_event Wire field name (disableResume) is unchanged.

Type renames

Old New
InputOptions<'a> UiInputOptions<'a>

The struct shape is identical; only the name changed. Update the import and the parameter type on SessionUi::input.

Type changes (field signatures)

Type / field Old New
McpStdioServerConfig::tools Vec<String> (with #[serde(default)]) Option<Vec<String>> (tri-state)
McpHttpServerConfig::tools Vec<String> (with #[serde(default)]) Option<Vec<String>> (tri-state)
ClientOptions::log_level default Some(LogLevel::Info) None (CLI default applies if not set)

For the MCP tools fields: None means "expose all"; Some(vec![]) means "expose none"; Some(vec!["a", "b"]) means "expose only these". vec!["*"] is no longer the way to say "all" — omit the field instead.

Wire-flag fields on SessionConfig / ResumeSessionConfig

The user-facing request_permission / request_elicitation / request_user_input / request_exit_plan_mode / request_auto_mode_switch / hooks fields are no longer caller-controllable knobs. They are derived from handler presence at Client::create_session / resume_session time:

  • Install a PermissionHandler (or a permission policy) → requestPermission: true on the wire.
  • Don't install one → requestPermission: false.
  • Same for the other four request_* flags.
  • Install a SessionHooks impl via with_hookshooks: true on the wire.

If your code was setting these fields directly to influence wire behaviour, remove those assignments and install (or don't install) the matching handler instead.

Permission policy ordering

with_permission_handler(...), approve_all_permissions(), deny_all_permissions(), and approve_permissions_if(...) are now order-independent: calling them in any sequence produces the same effective handler. If your code relied on the old "later call overwrites earlier" semantics (e.g. to "clear" a wrap), it will behave differently — the policy and the handler now combine deterministically.

What is NOT a breaking change

  • The on-the-wire JSON-RPC payload shape is unchanged. Servers and other SDKs interoperate identically.
  • The protocol version requirement is unchanged.
  • Generated types in rust/src/generated/** are unchanged.
  • MSRV is unchanged (rust 1.94.0).

Design overview

Per-event handler traits (the headline change)

The old SessionHandler mega-trait — one trait, eight default-impl methods, plus an on_event(HandlerEvent) -> HandlerResponse dispatcher — has been replaced with five focused, single-method, optional handler traits:

Trait Method Returns
PermissionHandler handle(session_id, request_id, data) PermissionResult
ElicitationHandler handle(session_id, request_id, request) ElicitationResult
UserInputHandler handle(session_id, question, choices, allow_freeform) Option<UserInputResponse>
ExitPlanModeHandler handle(session_id, data) ExitPlanModeResult
AutoModeSwitchHandler handle(session_id, error_code, retry_after_seconds) AutoModeSwitchResponse

Each is wired up via a dedicated builder on SessionConfig / ResumeSessionConfig:

SessionConfig::default()
    .with_permission_handler(Arc::new(MyPermHandler))
    .with_elicitation_handler(Arc::new(MyElicit))
    .with_tool_handlers(vec![Arc::new(MyTool) as Arc<dyn ToolHandler>])

The presence of a handler is the only signal. No wants_*_dispatch probes, no default no-op implementations that silently swallow events: if you didn't install a handler, the corresponding wire-flag is false and the SDK doesn't dispatch. This matches TS/C# semantics exactly — clients that don't install a permission handler are silently skipped when permission broadcasts arrive (allowing another connected client to handle them), instead of incorrectly auto-replying.

ApproveAllHandler / DenyAllHandler are now PermissionHandler impls (one trait each, ~5 lines), used in tests and as built-in convenience handlers.

Wire payload separated from user config

A new private rust/src/wire.rs module defines pub(crate) SessionCreateWire / SessionResumeWire structs — the exact JSON shape sent on the session.create / session.resume RPCs. SessionConfig::to_wire() and ResumeSessionConfig::to_wire() build them.

Benefits:

  • The user-facing SessionConfig no longer carries #[serde(skip)] on callback fields. Wire shape and user-API shape are decoupled.
  • All request_* wire flags are derived from handler presence at Client::create_session / resume_session time. They are no longer caller-controllable knobs.
  • Same for the hooks flag.

Permission policy ordering bug fixed

Pre-PR, the order of with_handler(...) and approve_all_permissions() mattered — with_handler could overwrite a previously-set policy and vice versa. Now both are stored as separate permission_handler and permission_policy fields, and Client::create_session composes them via crate::permission::resolve_handler(). Order-independent.

Observability: Session::subscribe(), not a callback method

Removed SessionHandler::on_session_event (which was the catch-all observation method). Consumers wanting to observe every event use session.subscribe() — already present pre-PR — which yields a tokio_stream::Stream over SessionEvent values. This is strictly more composable (works with select!, filter, take, timeout) and avoids the silent-discard footgun of an un-overridden default trait method.

Multi-client broadcast handling

The old default on_permission_request returned PermissionResult::Approved. In a multi-client deployment, every client connected to a session would auto-respond, causing duplicate / conflicting responses on the wire. The new design — optional handler fields, presence is the signal, missing handler → SDK doesn't respond — fixes this: another connected client gets to handle the request.


Cross-SDK companion fix (included in this PR)

The runtime side (copilot-agent-runtime) already supports presence-derived requestPermission: enablePermissionCallback: params.requestPermission ?? false. But every SDK was hardcoding requestPermission: true on session.create, defeating the runtime's gating.

Fixed in this PR for TS (nodejs/src/client.ts), C# (dotnet/src/Client.cs), Go (go/client.go), and Python (python/copilot/client.py) — each now derives the flag from handler presence, matching the Rust behaviour. Without this, no SDK can correctly participate in a multi-client session.


Items explicitly NOT in this PR

  • Hook timestamps as typed DateTime — kept as i64 (epoch ms) to avoid pulling in chrono / time.
  • SessionMetadata timestamps as typed DateTime — same reason.
  • MCP "local" alias removal"local" is documented in docs/features/mcp.md and standard in the wider MCP ecosystem.
  • SessionConfig / ResumeSessionConfig shared base — the two structs still share ~30 duplicated fields and builders. A macro_rules!-based extraction is feasible but invasive and doesn't change usability. Follow-up.
  • SessionFsProvider method renames (mkdirmake_directory, etc.) — Go-specific; in Rust the short names mirror std::fs::create_dir / read_dir etc.
  • AsyncDisposable analogue — Rust has Drop (sync) and explicit stop() / force_stop(). There is no AsyncDrop.

Testing

  • 134 lib tests + 96 session_test integration tests + 21 across jsonrpc_test / integration_test / protocol_version_test / api_types_test all pass locally.
  • E2E suite (tests/e2e/*) compiles cleanly; runs against the live CLI in CI.
  • All 22 Rust scenarios under test/scenarios/**/rust/ build cleanly.
  • cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check clean.
  • cargo clippy --all-targets --all-features -- -D warnings clean.
  • cargo doc --no-deps --all-features clean (no broken intra-doc links, no redundant explicit links).

Test migration summary

  • rust/tests/session_test.rs: +22 tests, −4 tests (net +18). The removed tests covered helpers, the now-deleted tool.call direct-RPC arm, and an old assertion-shape test replaced by stop_is_safe_to_call. Behaviour coverage of every removed test lives on, either in an updated test or in a sibling test exercising the broadcast path.
  • rust/tests/e2e/*: net 0 test count change. All migrated to the new API.
  • rust/src/** inline tests: +5, −1.

Known small coverage regression worth fixing before merge

  • stop_transitions_state_to_disconnected (which asserted client.state() == ConnectionState::Disconnected after stop()) was reduced to stop_is_safe_to_call (just stop().await.expect(...)). The state-machine assertion is no longer covered. Easy to restore.

Notes for reviewers

  • This PR contains a cross-SDK companion fix (one-line in TS/Go/Python, two-site in C#) that is necessary for the Rust changes to deliver their stated benefit. Without those fixes, no SDK would correctly participate in multi-client sessions even with the runtime's existing support.
  • No changes to rust/src/generated/** (those remain owned by codegen).
  • The wire shape on the JSON-RPC interface is unchanged by this PR. Only the Rust API surface and the wire-flag derivation changed.

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

SteveSandersonMS and others added 8 commits May 21, 2026 18:48
- Rename Session::get_messages() to get_events(); keep get_messages as a
  #[deprecated] alias for one release.
- Rename ResumeSessionConfig::disable_resume to suppress_resume_event.
  Wire field stays disableResume for runtime compatibility.
- Add #[deprecated] to Session::destroy(); point callers at disconnect().
- Update examples (chat, hooks, lifecycle_observer, tool_server) and all
  unit/integration test callsites.
- Add tests: serde round-trip for suppress_resume_event <-> disableResume,
  and an explicit #[allow(deprecated)] test exercising the get_messages
  alias.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
McpStdioServerConfig.tools and McpHttpServerConfig.tools change from
Vec<String> to Option<Vec<String>> with #[serde(default,
skip_serializing_if = Option::is_none)].

Semantics now match the runtime contract and the other SDKs:
  - None  (field omitted on wire) -> expose ALL tools
  - Some(vec![])                  -> expose NO tools
  - Some(non-empty)               -> explicit allowlist

Previously an empty Vec was treated as 'no tools' on the wire but the
runtime treats omission as 'all tools', so there was no way to opt back
into 'all' once the field was set.

Adds four tri-state serde tests (serialize + deserialize for each of
stdio and http).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When ClientOptions::log_level is None, the SDK no longer passes
--log-level to the spawned CLI process at all. The CLI's built-in
default takes over.

Previously the SDK silently overrode the CLI default with Info, which
made it impossible to opt back into the CLI default.

- Extract a log_level_args() helper alongside auth_args / remote_args.
- Update field rustdoc and remove 'Default.' annotation from LogLevel::Info.
- Add two unit tests: log_level_args_omitted_when_unset and
  log_level_args_emit_flag_when_set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renames the public UI-input options struct to match the Go SDK and to
make the connection to SessionUi::input explicit. Touches three files
(src/types.rs, src/session.rs, tests/e2e/elicitation.rs); zero behaviour
change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Folds previously-scattered transport configuration into the Transport enum:

  Transport::Tcp { port, connection_token }
  Transport::External { host, port, connection_token }

Removes:
  - ClientOptions::tcp_connection_token + with_tcp_connection_token
  - the runtime check that rejected (Stdio + token) -- now a type error
  - public ConnectionState enum and Client::state() accessor

Renames:
  - ClientOptions::copilot_home -> base_directory + with_base_directory
  - ClientOptions::remote -> enable_remote_sessions
    + with_enable_remote_sessions
  - the runtime env var passed to the child stays COPILOT_HOME
  - the spawn arg stays --remote

ConnectionState is demoted to pub(crate) and loses its serde derives;
nothing outside the crate reads it. The internal state-tracking field
on ClientInner is unchanged.

All call sites in tests, examples, and e2e support helpers updated to
the new Transport shape. Adds a unit test for empty-string
connection_token on External transport (the existing Tcp test was
adapted, the External one is new).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase H -- match TS/C# semantically for permission/elicitation/external-tool
broadcasts on shared CLI connections:

  1. Three new SessionHandler probe methods (default false):
       wants_permission_dispatch()
       wants_elicitation_dispatch()
       wants_external_tool_dispatch(tool_name)
     ApproveAllHandler and DenyAllHandler override to true for permission;
     NoopHandler keeps the safe default of false everywhere.
     PermissionOverrideHandler claims permission and forwards the others
     to its inner handler. ToolHandlerRouter advertises the specific tool
     names it knows.

  2. session.rs broadcast dispatcher gates on the probes BEFORE invoking
     the handler or sending an RPC response:
       - permission.requested: skip if data.resolvedByHook is true, skip
         if !wants_permission_dispatch()
       - elicitation.requested: skip if !wants_elicitation_dispatch()
       - external_tool.requested: skip if !wants_external_tool_dispatch(name)

  3. SessionConfig::default() / ResumeSessionConfig::new() no longer
     hardcode requestPermission=true / requestElicitation=true. Those
     wire flags are now derived from the handler probes at
     Client::create_session and Client::resume_session time, so the
     runtime never broadcasts permission/elicitation events to a client
     that wouldn't respond.

  4. Removed the public SessionConfig::with_request_permission /
     with_request_elicitation builders (and ResumeSessionConfig
     equivalents) -- the flags are derived now, not user-set.

Phase I -- approve_all_permissions ordering trap:

  The previous design wrapped the handler in-place inside the policy
  builder, which made the call order to with_handler significant. Now
  permission_policy lives in a separate (pub(crate)) field on
  SessionConfig/ResumeSessionConfig and the wrap is applied at
  Client::create_session / Client::resume_session time, after both fields
  are finalised. Call order is now irrelevant.

Tests:
  - request_elicitation_sent_in_create_params updated for the new
    derivation (ApproveAllHandler -> requestPermission=true,
    requestElicitation=false).
  - new noop_handler_sends_request_permission_false validates the
    H1a wire-flag derivation in both directions.
  - new external_tool_broadcast_for_unknown_tool_is_not_responded_to,
    permission_broadcast_with_resolved_by_hook_is_not_responded_to,
    permission_broadcast_with_no_claiming_handler_is_not_responded_to,
    elicitation_broadcast_with_no_claiming_handler_is_not_responded_to.
  - new session_config_*_is_order_independent tests for Phase I.
  - multi_client e2e tests' PermissionDecisionHandler /
    SelectiveToolHandler updated to advertise the probes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- rust/README.md: drop the 'call approve_all_permissions AFTER
  with_handler' warning -- the policy field makes both orderings
  equivalent now. Replace the elicitation snippet with the new
  wants_elicitation_dispatch probe.
- docs/features/remote-sessions.md: with_remote -> with_enable_remote_sessions.
- test/scenarios/**/rust: rename .destroy() -> .disconnect().
- test/scenarios/transport/tcp/rust: add connection_token field to
  Transport::External literal.

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

TS, C#, Go, and Python all currently hardcoded requestPermission: true
on session.create regardless of whether the caller supplied an
onPermissionRequest handler. (Resume / join paths already derived from
handler presence.)

The runtime supports the presence-derived shape: when no client opts
in via requestPermission=true, the session short-circuits permission
prompts with user-not-available instead of broadcasting. So the
hardcoded true forced the runtime to broadcast permission events to
clients that would never respond, wasting a roundtrip and creating a
discrepancy between create and resume.

Aligns all four SDKs to: requestPermission := onPermissionRequest != null
on both session.create and session.resume. (Rust will land the same
shape in its API-review-fixes commit on this branch.)

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

This comment has been minimized.

Replaces the monolithic SessionHandler trait with five focused
single-method optional handler traits in src/handler.rs:
PermissionHandler, ElicitationHandler, UserInputHandler,
ExitPlanModeHandler, AutoModeSwitchHandler. Each handler is an optional
field on SessionConfig / ResumeSessionConfig; presence is the signal,
matching TypeScript/C# semantics.

- Drops SessionHandler, HandlerEvent, HandlerResponse, NoopHandler,
  ToolHandlerRouter, and the wants_*_dispatch probe methods.
- ApproveAllHandler / DenyAllHandler now implement PermissionHandler.
- Tool dispatch lives in an internal HashMap<name, Arc<dyn ToolHandler>>
  built from SessionConfig::tool_handlers. ToolHandlerRouter is gone;
  callers pass handlers directly via with_tool_handlers(...).
- Wire flags on session.create / session.resume (requestPermission,
  requestElicitation, requestUserInput, requestExitPlanMode,
  requestAutoModeSwitch, hooks) are derived from handler presence at
  Client::create_session / resume_session time. No probes, no
  caller-visible knobs.
- New src/wire.rs: SessionCreateWire and SessionResumeWire structs
  (pub(crate)) carry the wire payload. SessionConfig::to_wire() and
  ResumeSessionConfig::to_wire() build them. SessionConfig and
  ResumeSessionConfig keep their serde derives (used only for tests),
  but Client::create_session and Client::resume_session now serialize
  the wire struct, not the user-facing config.
- permission::approve_all / deny_all / approve_if are now
  Arc<dyn PermissionHandler> producers. New private
  permission::resolve_handler(handler, policy) used by Client to apply
  a stored policy on top of (or in lieu of) a caller-supplied handler --
  the policy wins, making with_permission_handler / approve_all_permissions
  order-independent.
- Broadcast dispatcher in session.rs:
    - permission.requested: skip if data.resolvedByHook OR
      permission_handler is None; else dispatch via handle().
    - elicitation.requested: skip if elicitation_handler is None.
    - external_tool.requested: look up by tool_name in the registry;
      silently skip if absent.
- Direct RPC dispatcher in session.rs:
    - tool.call: REMOVED (legacy; runtime only emits external_tool.requested
      broadcasts now).
    - permission.request: kept as v2 back-compat; returns
      { kind: 'user-not-available' } when no permission handler.
    - userInput.request / exitPlanMode.request / autoModeSwitch.request:
      kept (this is how the runtime still emits these). Defaults when
      no handler match TS: { noResponse: true }, { approved: true },
      and { response: 'no' }.

Tests in rust/tests/ and e2e tests are migrated in a follow-up commit.

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

This comment has been minimized.

SteveSandersonMS and others added 4 commits May 21, 2026 22:30
…dler API

Mechanical migration of all consumers to the new optional handler-trait
API introduced in 455a17b:

- rust/tests/session_test.rs: ~30 SessionHandler impls split into the
  corresponding PermissionHandler / ElicitationHandler / UserInputHandler /
  ExitPlanModeHandler / AutoModeSwitchHandler / ToolHandler impls. Tests
  that called handler.on_event(...) directly now call handler.handle(...)
  on the appropriate trait. The legacy tool.call direct-RPC test was
  dropped; equivalent coverage is already provided by
  external_tool_requested_dispatches_to_handler_and_responds.
- rust/tests/e2e/*: per-trait migration; with_handler(...) -> the matching
  with_*_handler builder.
- rust/examples/*: same migration.
- test/scenarios/**/rust/src/main.rs: same migration.

Also fixes a bug in Client::create_session / Client::resume_session
where to_wire() was called after .take()-ing all handler fields, so the
derived wire flags (requestPermission, requestElicitation, hooks, etc.)
were always false. Computing the wire payload now happens before the
takes.

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

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

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

This comment has been minimized.

The request_user_input / request_permission / request_elicitation /
request_exit_plan_mode / request_auto_mode_switch fields on
SessionConfig and ResumeSessionConfig were unused: to_wire() derives
each flag from handler presence, matching the per-trait dispatch
semantics of the TypeScript and C# SDKs.

Leaving the public fields (and their with_request_* builders) in place
would have been a footgun: callers could set them and silently get no
effect. Removed:

- pub request_user_input / request_permission / request_elicitation /
  request_exit_plan_mode / request_auto_mode_switch fields from
  SessionConfig and ResumeSessionConfig.
- with_request_user_input / with_request_exit_plan_mode /
  with_request_auto_mode_switch builders on both configs.
- The corresponding entries in Default impls and Debug impls.
- Vestigial assertions in the in-file builder tests.

The two 'default flags' tests are replaced with new tests that build
the wire payload directly and assert every request_* flag (and hooks)
is false when no handler is installed. The existing
'request_elicitation_sent_in_create_params' and
'noop_handler_sends_request_permission_false' integration tests already
assert via the wire serialization, so they are unchanged.

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

This comment has been minimized.

Phase B made McpStdioServerConfig.tools an Option<Vec<String>>; the
scenario still set tools: vec![*], which under the new tri-state
contract would mean 'only allow the tool literally named *', not
'all tools'. Drop the field and rely on Default (None) to expose the
full MCP server tool list.

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

This comment has been minimized.

ElicitationHandler is imported in types.rs, so the bare label resolves
to the same item; the explicit path triggers rustdoc::redundant-explicit-links.

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

This comment has been minimized.

Earlier PowerShell editing of this file double-corrupted some em-dashes,
right-arrows, and a section sign (the bytes had been UTF-8 -> cp1252 ->
UTF-8 -> cp1252 -> UTF-8 round-tripped). Restored to plain UTF-8.

19 instances fixed (11 em-dashes, 7 right-arrows, 1 section sign).

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

This comment has been minimized.

@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review May 21, 2026 22:16
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner May 21, 2026 22:16
Copilot AI review requested due to automatic review settings May 21, 2026 22:16
The request_* wire flags are no longer caller-controllable; they are
derived from handler presence. Installing with_user_input_handler is
sufficient to set requestUserInput=true on the wire.

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

Cross-SDK Consistency Review ✅

This PR applies the requestPermission wire-flag fix consistently across all four active SDK implementations:

SDK Location Change
Node.js/TS nodejs/src/client.ts requestPermission: !!config.onPermissionRequest
Python python/copilot/client.py bool(on_permission_request) (both create_session and resume_session)
Go go/client.go Conditional RequestPermission = Bool(true) (both CreateSession and ResumeSessionWithOptions)
.NET dotnet/src/Client.cs config.OnPermissionRequest != null ? true : null (both CreateSessionAsync and ResumeSessionAsync)

All SDKs now derive requestPermission from handler presence rather than hardcoding true, which is the semantically correct behaviour for multi-client session participation. The fix is applied symmetrically to both create_session and resume_session paths in every SDK — no gaps found.

The Java SDK only has a README stub at this point (no client implementation), so it is not affected.

No cross-SDK consistency issues identified.

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

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

This PR refactors the Rust SDK’s public callback/handler surface to match the cross-SDK API review direction (focused per-event handler traits, wire payload structs separated from user config), and updates scenarios/tests/examples accordingly. It also applies a companion cross-SDK fix so requestPermission is derived from handler presence (enabling correct multi-client behavior).

Changes:

  • Rust: Replace the legacy SessionHandler mega-trait and ToolHandlerRouter pattern with focused handler traits and SessionConfig::with_*_handler / with_tool_handlers, plus a dedicated wire payload module.
  • Rust: Rename/adjust key APIs used by tests/scenarios (e.g., get_messagesget_events, destroydisconnect, transport connection token changes).
  • Cross-SDK: Derive requestPermission from permission handler presence in Node.js, Python, Go, and .NET clients.
Show a summary per file
File Description
test/scenarios/transport/tcp/rust/src/main.rs Updates TCP scenario for new transport token field and new session config/teardown APIs.
test/scenarios/transport/stdio/rust/src/main.rs Updates stdio scenario to new permission handler builder and disconnect().
test/scenarios/tools/tool-overrides/rust/src/main.rs Migrates tool override scenario to ToolHandler + with_tool_handlers API.
test/scenarios/tools/tool-filtering/rust/src/main.rs Migrates tool filtering scenario to new permission handler builder + disconnect().
test/scenarios/tools/skills/rust/src/main.rs Migrates skills scenario to new handler builders and disconnect().
test/scenarios/tools/no-tools/rust/src/main.rs Migrates “no tools” scenario to new permission handler builder + disconnect().
test/scenarios/tools/mcp-servers/rust/src/main.rs Updates MCP server scenario for new MCP tools tri-state semantics and new handler builder.
test/scenarios/tools/custom-agents/rust/src/main.rs Migrates custom agents scenario to tool handlers + new permission handler builder.
test/scenarios/sessions/streaming/rust/src/main.rs Reworks streaming scenario to use session.subscribe() for deltas and new handler model.
test/scenarios/sessions/session-resume/rust/src/main.rs Migrates session resume scenario to per-trait handlers and disconnect().
test/scenarios/sessions/infinite-sessions/rust/src/main.rs Migrates infinite sessions scenario to new handler builder + disconnect().
test/scenarios/sessions/concurrent-sessions/rust/src/main.rs Migrates concurrent sessions scenario to new handler builder + disconnect().
test/scenarios/prompts/system-message/rust/src/main.rs Migrates prompt scenario to new handler builder + disconnect().
test/scenarios/prompts/reasoning-effort/rust/src/main.rs Migrates prompt scenario to new handler builder + disconnect().
test/scenarios/prompts/attachments/rust/src/main.rs Migrates attachments scenario to new handler builder + disconnect().
test/scenarios/modes/default/rust/src/main.rs Migrates default mode scenario to new handler builder + disconnect().
test/scenarios/callbacks/user-input/rust/src/main.rs Migrates user input callback scenario to split handler traits.
test/scenarios/callbacks/permissions/rust/src/main.rs Migrates permission callback scenario to PermissionHandler.
test/scenarios/callbacks/hooks/rust/src/main.rs Migrates hooks scenario to new permission handler builder + disconnect().
rust/tests/e2e/tools.rs Updates tool E2E tests for tool handler registration + per-trait permission handlers.
rust/tests/e2e/tool_results.rs Updates tool result E2E tests for tool handler registration model.
rust/tests/e2e/telemetry.rs Updates telemetry E2E for tool handler registration model.
rust/tests/e2e/system_message_transform.rs Removes/empties the prior system message transform E2E coverage file content.
rust/tests/e2e/suspend.rs Removes/empties the prior suspend E2E coverage file content.
rust/tests/e2e/support.rs Updates E2E helpers for new APIs (get_events, new TCP token placement, new handler builder).
rust/tests/e2e/streaming_fidelity.rs Updates streaming fidelity E2E tests to new handler builder + get_events.
rust/tests/e2e/session.rs Broad migration of session E2E tests to new handler/tool registration APIs and event history rename.
rust/tests/e2e/session_lifecycle.rs Updates lifecycle E2E tests to get_events.
rust/tests/e2e/session_fs.rs Removes/empties the prior session FS E2E coverage file content.
rust/tests/e2e/rpc_mcp_and_skills.rs Updates RPC E2E for MCP tools tri-state and new APIs.
rust/tests/e2e/rpc_event_side_effects.rs Updates RPC side effects E2E to get_events.
rust/tests/e2e/permissions.rs Migrates permissions E2E tests to PermissionHandler trait and new builder.
rust/tests/e2e/per_session_auth.rs Updates per-session auth E2E tests to new permission handler builder.
rust/tests/e2e/pending_work_resume.rs Updates pending-work resume E2E tests for new transport token + handler model.
rust/tests/e2e/multi_client.rs Updates multi-client E2E tests for new handler/tool registration model and resume flag rename.
rust/tests/e2e/multi_client_commands_elicitation.rs Updates multi-client elicitation E2E tests to split handler traits and new transport token.
rust/tests/e2e/mode_handlers.rs Migrates mode handler E2E tests to ExitPlanModeHandler / AutoModeSwitchHandler.
rust/tests/e2e/mcp_and_agents.rs Removes/empties the prior MCP+agents E2E coverage file content.
rust/tests/e2e/hooks_extended.rs Migrates extended hooks E2E tests to new tool handler registration model.
rust/tests/e2e/event_fidelity.rs Updates event fidelity E2E tests to get_events.
rust/tests/e2e/error_resilience.rs Removes/empties the prior error resilience E2E coverage file content.
rust/tests/e2e/elicitation.rs Migrates elicitation E2E tests to ElicitationHandler / PermissionHandler split and renames.
rust/tests/e2e/compaction.rs Removes/empties the prior compaction E2E coverage file content.
rust/tests/e2e/commands.rs Removes/empties the prior commands E2E coverage file content.
rust/tests/e2e/client.rs Updates client E2E tests for new transport token placement and removal of connection state API.
rust/tests/e2e/client_options.rs Removes/empties the prior client options E2E coverage file content.
rust/tests/e2e/client_lifecycle.rs Updates lifecycle E2E tests for removal of connection state API.
rust/tests/e2e/ask_user.rs Migrates ask_user E2E tests to split handler traits.
rust/tests/e2e/abort.rs Migrates abort E2E tests to new tool handler registration model.
rust/src/wire.rs Introduces dedicated wire-format structs for session.create / session.resume payloads.
rust/src/tool.rs Updates tool framework docs and removes ToolHandlerRouter in favor of internal handler registry.
rust/src/subscription.rs Updates subscription documentation to reflect new handler model.
rust/src/permission.rs Refactors permission policy helpers to produce PermissionHandler and adds deterministic resolution.
rust/src/lib.rs Updates transport/auth/remote/log-level config surface and client startup token handling.
rust/src/handler.rs Replaces the mega-trait with focused optional handler traits; updates built-in handlers accordingly.
rust/README.md Updates Rust SDK documentation and examples to the new handler and tool registration model.
rust/examples/tool_server.rs Migrates tool server example to with_tool_handlers + permission handler builder.
rust/examples/session_fs.rs Updates session FS example to new permission handler builder.
rust/examples/lifecycle_observer.rs Updates lifecycle observer example to avoid removed connection state API and use pid().
rust/examples/hooks.rs Updates hooks example to new permission handler builder and disconnect().
rust/examples/chat.rs Updates chat example to split handlers and use subscribe() for streamed deltas.
python/copilot/client.py Derives requestPermission from whether a permission handler is provided.
nodejs/src/client.ts Derives requestPermission from onPermissionRequest presence.
go/client.go Sends RequestPermission only when a permission handler is configured.
dotnet/src/Client.cs Omits requestPermission unless a permission handler is configured.
docs/features/remote-sessions.md Updates Rust snippet to with_enable_remote_sessions(true).

Copilot's findings

Comments suppressed due to low confidence (2)

rust/tests/e2e/rpc_mcp_and_skills.rs:445

  • McpStdioServerConfig.tools is now tri-state where None means "expose all tools". Setting tools: Some(vec!["*"]) will be treated as a literal tool name filter and likely exposes no real tools; use tools: None/omit the field to expose all.
    rust/tests/e2e/compaction.rs:2
  • This integration test file is now empty. Consider deleting it entirely (so Cargo doesn’t build a no-op test crate) or adding a brief comment explaining why an empty placeholder file is intentionally kept.
  • Files reviewed: 71/71 changed files
  • Comments generated: 10

Comment on lines 77 to 83
let mut config = SessionConfig::default();
config.model = Some("claude-haiku-4.5".to_string());
config.request_user_input = Some(true);
let config = config
.with_handler(handler)
.with_permission_handler(handler.clone())
.with_user_input_handler(handler)
.with_hooks(Arc::new(AllowAllHooks));

Comment thread rust/README.md
Comment on lines +442 to +455
use async_trait::async_trait;
use github_copilot_sdk::handler::{ElicitationHandler, ElicitationResult};
use github_copilot_sdk::types::{ElicitationRequestData, RequestId, SessionId};

struct MyElicitation;

#[async_trait]
impl ElicitationHandler for MyElicitation {
async fn handle(
&self,
_sid: SessionId,
_rid: RequestId,
_data: ElicitationRequestData,
) -> ElicitationResult {
Comment on lines +146 to +157
let tool_handlers: Vec<Arc<dyn ToolHandler>> =
vec![Arc::new(GetWeatherTool), Arc::new(RollDiceTool)];
let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect();

let client = Client::start(ClientOptions::default()).await?;

let config = {
let mut cfg = SessionConfig::default();
let mut cfg = SessionConfig::default()
.with_permission_handler(Arc::new(ApproveAllHandler))
.with_tool_handlers(tool_handlers);
cfg.tools = Some(tools);
cfg.with_handler(handler)
cfg
Comment thread rust/tests/e2e/tools.rs
Comment on lines +50 to 60
let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(EncryptStringTool)];
let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect();
let __perm = Arc::new(ApproveAllHandler);
let session = client
.create_session(
SessionConfig::default()
.with_github_token(super::support::DEFAULT_TEST_TOKEN)
.with_handler(Arc::new(router))
.with_permission_handler(__perm)
.with_tool_handlers(tool_handlers)
.with_tools(tools),
)
Comment on lines +199 to 208
let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(tool)];
let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect();
let __perm = Arc::new(ApproveAllHandler);
client
.create_session(
SessionConfig::default()
.with_github_token(super::support::DEFAULT_TEST_TOKEN)
.with_handler(Arc::new(router))
.with_permission_handler(__perm)
.with_tool_handlers(tool_handlers)
.with_tools(tools),
Comment on lines +39 to 50
let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(EchoTelemetryTool {
name: tool_name.to_string(),
})],
Arc::new(ApproveAllHandler),
);
let tools = router.tools();
})];
let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect();
let __perm = Arc::new(ApproveAllHandler);
let session = client
.create_session(
SessionConfig::default()
.with_github_token(super::support::DEFAULT_TEST_TOKEN)
.with_handler(Arc::new(router))
.with_permission_handler(__perm)
.with_tool_handlers(tool_handlers)
.with_tools(tools),
Comment thread rust/tests/e2e/session.rs
Comment on lines +332 to 342
let tool_handlers: Vec<Arc<dyn ToolHandler>> = vec![Arc::new(SecretTool)];
let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect();
let __perm = Arc::new(ApproveAllHandler);
let session = client
.create_session(
SessionConfig::default()
.with_github_token(super::support::DEFAULT_TEST_TOKEN)
.with_handler(Arc::new(router))
.with_permission_handler(__perm)
.with_tool_handlers(tool_handlers)
.with_tools(tools)
.with_default_agent(DefaultAgentConfig {
Comment on lines +288 to 298
let echo_tool: Arc<dyn ToolHandler> = Arc::new(EchoValueTool);
let tools = vec![echo_tool.tool()];
let client = ctx.start_client().await;
let session = client
.create_session(
SessionConfig::default()
.with_github_token(super::support::DEFAULT_TEST_TOKEN)
.with_handler(Arc::new(router))
.with_permission_handler(Arc::new(ApproveAllHandler))
.with_tool_handlers(vec![echo_tool])
.with_tools(tools)
.with_hooks(Arc::new(RecordingHooks::pre_tool(tx))),
Comment on lines 56 to 66
let mut config = SessionConfig::default();
config.model = Some("claude-haiku-4.5".to_string());
config.tools = Some(tools);
config.default_agent = Some(DefaultAgentConfig {
excluded_tools: Some(vec!["analyze-codebase".to_string()]),
});
config.custom_agents = Some(vec![researcher]);
let config = config.with_handler(Arc::new(router));
let config = config
.with_permission_handler(Arc::new(ApproveAllHandler))
.with_tool_handlers(vec![analyze_codebase]);

Comment on lines 34 to 47
@@ -42,7 +41,9 @@ async fn main() -> Result<(), github_copilot_sdk::Error> {
let mut config = SessionConfig::default();
config.model = Some("claude-haiku-4.5".to_string());
config.tools = Some(tools);
let config = config.with_handler(Arc::new(router));
let config = config
.with_permission_handler(Arc::new(ApproveAllHandler))
.with_tool_handlers(vec![grep_tool]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants