From 9ecade7e4f95e021d48025b7a2d2d46d51309af4 Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Thu, 14 May 2026 16:31:44 -0400 Subject: [PATCH] Add remote_session field to all SDK SessionConfig types Add per-session remote behavior control (Off/Export/On) to SessionConfig and ResumeSessionConfig across all SDK languages (Rust, Go, Node.js, Python, .NET). Each SDK wires the field into the JSON-RPC create/resume payloads using the existing generated RemoteSessionMode type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 4 ++++ dotnet/src/Types.cs | 18 ++++++++++++++++++ go/client.go | 2 ++ go/types.go | 10 ++++++++++ nodejs/src/client.ts | 2 ++ nodejs/src/index.ts | 1 + nodejs/src/types.ts | 11 +++++++++++ python/copilot/__init__.py | 2 ++ python/copilot/client.py | 11 +++++++++++ rust/src/types.rs | 33 +++++++++++++++++++++++++++++++++ rust/tests/session_test.rs | 11 +++++++++++ 11 files changed, 105 insertions(+) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 6f7acc747..b1e9dce0e 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -629,6 +629,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance Tracestate: tracestate, ModelCapabilities: config.ModelCapabilities, GitHubToken: config.GitHubToken, + RemoteSession: config.RemoteSession, InstructionDirectories: config.InstructionDirectories); var rpcTimestamp = Stopwatch.GetTimestamp(); @@ -786,6 +787,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes Tracestate: tracestate, ModelCapabilities: config.ModelCapabilities, GitHubToken: config.GitHubToken, + RemoteSession: config.RemoteSession, ContinuePendingWork: config.ContinuePendingWork, InstructionDirectories: config.InstructionDirectories); @@ -1981,6 +1983,7 @@ internal record CreateSessionRequest( string? Tracestate = null, ModelCapabilitiesOverride? ModelCapabilities = null, string? GitHubToken = null, + RemoteSessionMode? RemoteSession = null, IList? InstructionDirectories = null); internal record ToolDefinition( @@ -2041,6 +2044,7 @@ internal record ResumeSessionRequest( string? Tracestate = null, ModelCapabilitiesOverride? ModelCapabilities = null, string? GitHubToken = null, + RemoteSessionMode? RemoteSession = null, bool? ContinuePendingWork = null, IList? InstructionDirectories = null); diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index c1f4564f1..333e34978 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2028,6 +2028,7 @@ protected SessionConfig(SessionConfig? other) ReasoningEffort = other.ReasoningEffort; CreateSessionFsHandler = other.CreateSessionFsHandler; GitHubToken = other.GitHubToken; + RemoteSession = other.RemoteSession; SessionId = other.SessionId; SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null; @@ -2253,6 +2254,16 @@ protected SessionConfig(SessionConfig? other) /// public string? GitHubToken { get; set; } + /// + /// Per-session remote behavior control: + /// + /// "off" — local only, no remote export (default) + /// "export" — export session events to GitHub without enabling remote steering + /// "on" — export to GitHub AND enable remote steering + /// + /// + public RemoteSessionMode? RemoteSession { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -2319,6 +2330,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) ReasoningEffort = other.ReasoningEffort; CreateSessionFsHandler = other.CreateSessionFsHandler; GitHubToken = other.GitHubToken; + RemoteSession = other.RemoteSession; SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null; Streaming = other.Streaming; @@ -2555,6 +2567,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public string? GitHubToken { get; set; } + /// + /// Per-session remote behavior control. + /// See for details. + /// + public RemoteSessionMode? RemoteSession { get; set; } + /// /// Creates a shallow clone of this instance. /// diff --git a/go/client.go b/go/client.go index 5c99d8294..45d83d828 100644 --- a/go/client.go +++ b/go/client.go @@ -645,6 +645,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions req.GitHubToken = config.GitHubToken + req.RemoteSession = config.RemoteSession if len(config.Commands) > 0 { cmds := make([]wireCommand, 0, len(config.Commands)) @@ -848,6 +849,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions req.GitHubToken = config.GitHubToken + req.RemoteSession = config.RemoteSession req.RequestPermission = Bool(true) if len(config.Commands) > 0 { diff --git a/go/types.go b/go/types.go index 74ef3a2c3..566e54f0f 100644 --- a/go/types.go +++ b/go/types.go @@ -680,6 +680,11 @@ type SessionConfig struct { // When provided, the session authenticates as the token's owner instead of // using the global client-level auth. GitHubToken string `json:"-"` + // RemoteSession controls per-session remote behavior: + // - "off" — local only, no remote export (default) + // - "export" — export session events to GitHub without enabling remote steering + // - "on" — export to GitHub AND enable remote steering + RemoteSession rpc.RemoteSessionMode } type Tool struct { Name string `json:"name"` @@ -891,6 +896,9 @@ type ResumeSessionConfig struct { // When provided, the session authenticates as the token's owner instead of // using the global client-level auth. GitHubToken string `json:"-"` + // RemoteSession controls per-session remote behavior. + // See SessionConfig.RemoteSession for details. + RemoteSession rpc.RemoteSessionMode // DisableResume, when true, skips emitting the session.resume event. // Useful for reconnecting to a session without triggering resume-related side effects. DisableResume bool @@ -1142,6 +1150,7 @@ type createSessionRequest struct { Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` + RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } @@ -1196,6 +1205,7 @@ type resumeSessionRequest struct { Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` + RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 264e0a575..7a32080f5 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -835,6 +835,7 @@ export class CopilotClient { disabledSkills: config.disabledSkills, infiniteSessions: config.infiniteSessions, gitHubToken: config.gitHubToken, + remoteSession: config.remoteSession, }); const { workspacePath, capabilities } = response as { @@ -990,6 +991,7 @@ export class CopilotClient { disableResume: config.disableResume, continuePendingWork: config.continuePendingWork, gitHubToken: config.gitHubToken, + remoteSession: config.remoteSession, }); const { workspacePath, capabilities } = response as { diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 0c6b25ecd..ee231d79f 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -56,6 +56,7 @@ export type { PermissionRequest, PermissionRequestResult, ProviderConfig, + RemoteSessionMode, ResumeSessionConfig, SectionOverride, SectionOverrideAction, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 7b9348df0..c28b67a89 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -10,6 +10,8 @@ import type { SessionFsProvider } from "./sessionFsProvider.js"; import type { SessionEvent as GeneratedSessionEvent } from "./generated/session-events.js"; import type { CopilotSession } from "./session.js"; +import type { RemoteSessionMode } from "./generated/rpc.js"; +export type { RemoteSessionMode } from "./generated/rpc.js"; export type SessionEvent = GeneratedSessionEvent; export type { SessionFsProvider } from "./sessionFsProvider.js"; export { createSessionFsAdapter } from "./sessionFsProvider.js"; @@ -1477,6 +1479,14 @@ export interface SessionConfig { */ gitHubToken?: string; + /** + * Per-session remote behavior control: + * - `"off"` — local only, no remote export (default) + * - `"export"` — export session events to GitHub without enabling remote steering + * - `"on"` — export to GitHub AND enable remote steering + */ + remoteSession?: RemoteSessionMode; + /** * Optional event handler that is registered on the session before the * session.create RPC is issued. This guarantees that early events emitted @@ -1531,6 +1541,7 @@ export type ResumeSessionConfig = Pick< | "disabledSkills" | "infiniteSessions" | "gitHubToken" + | "remoteSession" | "onEvent" | "createSessionFsHandler" > & { diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 1963a2d41..377e480ff 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -11,6 +11,7 @@ ModelLimitsOverride, ModelSupportsOverride, ModelVisionLimitsOverride, + RemoteSessionMode, SubprocessConfig, ) from .session import ( @@ -67,6 +68,7 @@ "ModelSupportsOverride", "ModelVisionLimitsOverride", "ProviderConfig", + "RemoteSessionMode", "SessionCapabilities", "SessionFsConfig", "SessionFsFileInfo", diff --git a/python/copilot/client.py b/python/copilot/client.py index 848af4b92..4b265f6d5 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -38,6 +38,7 @@ from .generated.rpc import ( ClientSessionApiHandlers, ConnectRequest, + RemoteSessionMode, ServerRpc, _InternalServerRpc, register_client_session_api_handlers, @@ -1326,6 +1327,7 @@ async def create_session( on_auto_mode_switch: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, github_token: str | None = None, + remote_session: RemoteSessionMode | None = None, ) -> CopilotSession: """ Create a new conversation session with the Copilot CLI. @@ -1479,6 +1481,10 @@ async def create_session( if github_token is not None: payload["gitHubToken"] = github_token + # Add remote session mode if provided + if remote_session is not None: + payload["remoteSession"] = remote_session.value + # Add working directory if provided if working_directory: payload["workingDirectory"] = working_directory @@ -1686,6 +1692,7 @@ async def resume_session( on_auto_mode_switch: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, github_token: str | None = None, + remote_session: RemoteSessionMode | None = None, continue_pending_work: bool | None = None, ) -> CopilotSession: """ @@ -1857,6 +1864,10 @@ async def resume_session( if github_token is not None: payload["gitHubToken"] = github_token + # Add remote session mode if provided + if remote_session is not None: + payload["remoteSession"] = remote_session.value + if working_directory: payload["workingDirectory"] = working_directory if config_dir: diff --git a/rust/src/types.rs b/rust/src/types.rs index 546dc1acf..68850dbbf 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1081,6 +1081,13 @@ pub struct SessionConfig { /// quota checks for *this session*. #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] pub github_token: Option, + /// Per-session remote behavior control: + /// - `Off` — local only, no remote export (default) + /// - `Export` — export session events to GitHub without + /// enabling remote steering + /// - `On` — export to GitHub AND enable remote steering + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_session: Option, /// Forward sub-agent streaming events to this connection. When false, /// only non-streaming sub-agent events and `subagent.*` lifecycle events /// are delivered. Defaults to true on the CLI. @@ -1152,6 +1159,7 @@ impl std::fmt::Debug for SessionConfig { "github_token", &self.github_token.as_ref().map(|_| ""), ) + .field("remote_session", &self.remote_session) .field( "include_sub_agent_streaming_events", &self.include_sub_agent_streaming_events, @@ -1211,6 +1219,7 @@ impl Default for SessionConfig { config_dir: None, working_directory: None, github_token: None, + remote_session: None, include_sub_agent_streaming_events: None, commands: None, session_fs_provider: None, @@ -1528,6 +1537,15 @@ impl SessionConfig { self.include_sub_agent_streaming_events = Some(include); self } + + /// Set per-session remote behavior. + pub fn with_remote_session( + mut self, + mode: crate::generated::api_types::RemoteSessionMode, + ) -> Self { + self.remote_session = Some(mode); + self + } } /// Configuration for resuming an existing session via the `session.resume` RPC. @@ -1639,6 +1657,10 @@ pub struct ResumeSessionConfig { /// [`SessionConfig::github_token`]. #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] pub github_token: Option, + /// Per-session remote behavior control on resume. See + /// [`SessionConfig::remote_session`]. + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_session: Option, /// Forward sub-agent streaming events to this connection on resume. #[serde(skip_serializing_if = "Option::is_none")] pub include_sub_agent_streaming_events: Option, @@ -1712,6 +1734,7 @@ impl std::fmt::Debug for ResumeSessionConfig { "github_token", &self.github_token.as_ref().map(|_| ""), ) + .field("remote_session", &self.remote_session) .field( "include_sub_agent_streaming_events", &self.include_sub_agent_streaming_events, @@ -1770,6 +1793,7 @@ impl ResumeSessionConfig { config_dir: None, working_directory: None, github_token: None, + remote_session: None, include_sub_agent_streaming_events: None, commands: None, session_fs_provider: None, @@ -2054,6 +2078,15 @@ impl ResumeSessionConfig { self } + /// Set per-session remote behavior on resume. + pub fn with_remote_session( + mut self, + mode: crate::generated::api_types::RemoteSessionMode, + ) -> Self { + self.remote_session = Some(mode); + self + } + /// Force-fail resume if the session does not exist on disk, instead /// of silently starting a new session. pub fn with_disable_resume(mut self, disable: bool) -> Self { diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index c98c04d89..32196fdda 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -2597,6 +2597,8 @@ fn session_config_serializes_bucket_b_fields() { cfg.github_token = Some("ghs_secret".to_string()); cfg.include_sub_agent_streaming_events = Some(false); cfg.enable_session_telemetry = Some(false); + cfg.remote_session = + Some(github_copilot_sdk::generated::api_types::RemoteSessionMode::Export); cfg }; let json = serde_json::to_value(&cfg).unwrap(); @@ -2606,6 +2608,7 @@ fn session_config_serializes_bucket_b_fields() { assert_eq!(json["gitHubToken"], "ghs_secret"); assert_eq!(json["includeSubAgentStreamingEvents"], false); assert_eq!(json["enableSessionTelemetry"], false); + assert_eq!(json["remoteSession"], "export"); // Debug never leaks the token. let debug = format!("{cfg:?}"); @@ -2617,6 +2620,7 @@ fn session_config_serializes_bucket_b_fields() { assert!(empty.get("sessionId").is_none()); assert!(empty.get("gitHubToken").is_none()); assert!(empty.get("enableSessionTelemetry").is_none()); + assert!(empty.get("remoteSession").is_none()); } #[test] @@ -2631,6 +2635,7 @@ fn resume_session_config_serializes_bucket_b_fields() { cfg.github_token = Some("ghs_secret".to_string()); cfg.include_sub_agent_streaming_events = Some(true); cfg.enable_session_telemetry = Some(false); + cfg.remote_session = Some(github_copilot_sdk::generated::api_types::RemoteSessionMode::On); let json = serde_json::to_value(&cfg).unwrap(); assert_eq!(json["sessionId"], "sess-1"); assert_eq!(json["workingDirectory"], "/tmp/work"); @@ -2638,6 +2643,12 @@ fn resume_session_config_serializes_bucket_b_fields() { assert_eq!(json["gitHubToken"], "ghs_secret"); assert_eq!(json["includeSubAgentStreamingEvents"], true); assert_eq!(json["enableSessionTelemetry"], false); + assert_eq!(json["remoteSession"], "on"); + + // Unset remote_session is omitted on the wire. + let empty = ResumeSessionConfig::new(SessionId::from("sess-2")); + let empty_json = serde_json::to_value(&empty).unwrap(); + assert!(empty_json.get("remoteSession").is_none()); let debug = format!("{cfg:?}"); assert!(!debug.contains("ghs_secret"), "leaked token: {debug}");