From 8a8c5735d2ddcfc0eebe61afe834cd47bb5917c5 Mon Sep 17 00:00:00 2001 From: jmoseley Date: Mon, 31 Aug 2026 13:12:04 -0700 Subject: [PATCH 1/3] Expose Rust session feature flag overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b0b5b94-f94a-4df4-8e57-6d7ed6fe04c1 --- rust/src/types.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++ rust/src/wire.rs | 5 ++++ 2 files changed, 73 insertions(+) diff --git a/rust/src/types.rs b/rust/src/types.rs index 6e451eb452..bd020eae47 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -2141,6 +2141,12 @@ pub struct SessionConfig { /// each command appears as `/name` for the user to invoke and the /// associated [`CommandHandler`] is called when executed. pub commands: Option>, + /// Feature-flag values resolved by the host for this session. + /// + /// Re-supply these values through [`ResumeSessionConfig::feature_flags`] + /// when resuming after a CLI process restart. Set via + /// [`with_feature_flags`](Self::with_feature_flags). + pub feature_flags: Option>, /// ExP assignment ("flight") data injected by a trusted integrator, in /// the same JSON shape the Copilot CLI fetches from the experimentation /// service (`CopilotExpAssignmentResponse`). When supplied, the runtime @@ -2318,6 +2324,7 @@ impl std::fmt::Debug for SessionConfig { &self.include_sub_agent_streaming_events, ) .field("commands", &self.commands) + .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) .field("enable_experimental_mode", &self.enable_experimental_mode) @@ -2434,6 +2441,7 @@ impl Default for SessionConfig { cloud: None, include_sub_agent_streaming_events: None, commands: None, + feature_flags: None, exp_assignments: None, enable_managed_settings: None, managed_settings: None, @@ -2608,6 +2616,7 @@ impl SessionConfig { include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, enable_github_telemetry_forwarding: None, commands: wire_commands, + feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, is_experimental_mode: self.enable_experimental_mode, @@ -3241,6 +3250,12 @@ impl SessionConfig { self } + /// Set feature-flag values resolved by the host for this session. + pub fn with_feature_flags(mut self, feature_flags: HashMap) -> Self { + self.feature_flags = Some(feature_flags); + self + } + /// Inject ExP assignment ("flight") data for this session, in the same /// JSON shape the Copilot CLI fetches from the experimentation service /// (`CopilotExpAssignmentResponse`). The runtime feeds it into the same @@ -3464,6 +3479,10 @@ pub struct ResumeSessionConfig { /// [`SessionConfig::commands`] — commands are not persisted server-side, /// so the resume payload re-supplies the registration. pub commands: Option>, + /// Feature-flag values resolved by the host to apply on resume. + /// + /// See [`SessionConfig::feature_flags`]. + pub feature_flags: Option>, /// ExP assignment ("flight") data injected on resume. See /// [`SessionConfig::exp_assignments`]. Re-supply on resume so the runtime /// re-applies the assignments after a CLI process restart. Set via @@ -3627,6 +3646,7 @@ impl std::fmt::Debug for ResumeSessionConfig { &self.include_sub_agent_streaming_events, ) .field("commands", &self.commands) + .field("feature_flags", &self.feature_flags) .field("exp_assignments", &self.exp_assignments) .field("enable_managed_settings", &self.enable_managed_settings) .field("enable_experimental_mode", &self.enable_experimental_mode) @@ -3793,6 +3813,7 @@ impl ResumeSessionConfig { include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, enable_github_telemetry_forwarding: None, commands: wire_commands, + feature_flags: self.feature_flags, exp_assignments: self.exp_assignments, enable_managed_settings: self.enable_managed_settings, is_experimental_mode: self.enable_experimental_mode, @@ -3892,6 +3913,7 @@ impl ResumeSessionConfig { remote_session: None, include_sub_agent_streaming_events: None, commands: None, + feature_flags: None, exp_assignments: None, enable_managed_settings: None, managed_settings: None, @@ -4511,6 +4533,12 @@ impl ResumeSessionConfig { self } + /// Re-supply feature-flag values resolved by the host on resume. + pub fn with_feature_flags(mut self, feature_flags: HashMap) -> Self { + self.feature_flags = Some(feature_flags); + self + } + /// Inject ExP assignment ("flight") data on resume. See /// [`SessionConfig::with_exp_assignments`]. Re-supply the assignments on /// resume so the runtime re-applies them after a CLI process restart. @@ -6407,6 +6435,46 @@ mod tests { assert!(empty_json.get("memory").is_none()); } + #[test] + fn feature_flags_serialize_on_create_and_resume() { + let feature_flags = HashMap::from([ + ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true), + ("DISABLED_TEST_FLAG".to_string(), false), + ]); + let expected = serde_json::json!({ + "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true, + "DISABLED_TEST_FLAG": false, + }); + + let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone()); + assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags)); + let (create_wire, _) = create_config + .into_wire(Some(SessionId::from("feature-flags-create"))) + .expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["featureFlags"], expected); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume")) + .with_feature_flags(feature_flags) + .into_wire() + .expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["featureFlags"], expected); + + let (unset_create_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("feature-flags-create-unset"))) + .expect("no duplicate handlers"); + let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap(); + assert!(unset_create_json.get("featureFlags").is_none()); + + let (unset_resume_wire, _) = + ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap(); + assert!(unset_resume_json.get("featureFlags").is_none()); + } + fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse { CopilotExpAssignmentResponse { features: vec!["copilot_exp_flag".to_string()], diff --git a/rust/src/wire.rs b/rust/src/wire.rs index f7de33839c..914f2447a1 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -13,6 +13,7 @@ //! configs hold trait-object handlers, the wire structs hold only the //! plain data the runtime needs. +use std::collections::HashMap; use std::path::PathBuf; use indexmap::IndexMap; @@ -189,6 +190,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub commands: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, @@ -347,6 +350,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub feature_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub exp_assignments: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_managed_settings: Option, From 5bfa73724d04c33fa66b6dcc4ed9613e34d60616 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 1 Sep 2026 14:40:23 +0000 Subject: [PATCH 2/3] Expose session feature flags across SDKs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 4 ++ dotnet/src/Types.cs | 9 +++ dotnet/test/Unit/SerializationTests.cs | 40 +++++++++++++ go/client.go | 6 ++ go/client_test.go | 56 +++++++++++++++++++ go/types.go | 8 +++ .../github/copilot/SessionRequestBuilder.java | 2 + .../copilot/rpc/CreateSessionRequest.java | 13 +++++ .../copilot/rpc/ResumeSessionConfig.java | 19 +++++++ .../copilot/rpc/ResumeSessionRequest.java | 13 +++++ .../com/github/copilot/rpc/SessionConfig.java | 19 +++++++ .../copilot/SessionRequestBuilderTest.java | 20 +++++++ nodejs/src/client.ts | 2 + nodejs/src/types.ts | 6 ++ nodejs/test/client.test.ts | 33 +++++++++++ python/copilot/client.py | 8 +++ python/test_client.py | 35 ++++++++++++ 17 files changed, 293 insertions(+) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f2da0a48f7..e0c8cc1c32 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1251,6 +1251,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, + FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, GitHubMcpToolConfig: config.GitHubMcpToolConfig, @@ -1491,6 +1492,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes Providers: config.Providers, Models: config.Models, ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, + FeatureFlags: config.FeatureFlags, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, GitHubMcpToolConfig: config.GitHubMcpToolConfig, @@ -2930,6 +2932,7 @@ internal record CreateSessionRequest( IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, + [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, @@ -3047,6 +3050,7 @@ internal record ResumeSessionRequest( IList? Providers = null, IList? Models = null, OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, + [property: JsonPropertyName("featureFlags")] IDictionary? FeatureFlags = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, [property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 6ed05e3064..38fe43baee 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -3209,6 +3209,9 @@ protected SessionConfigBase(SessionConfigBase? other) GitHubToken = other.GitHubToken; GitHubTokenProvider = other.GitHubTokenProvider; RemoteSession = other.RemoteSession; + FeatureFlags = other.FeatureFlags is not null + ? new Dictionary(other.FeatureFlags) + : null; ExpAssignments = other.ExpAssignments; EnableManagedSettings = other.EnableManagedSettings; ManagedSettings = other.ManagedSettings; @@ -3696,6 +3699,12 @@ protected SessionConfigBase(SessionConfigBase? other) [EditorBrowsable(EditorBrowsableState.Never)] public CopilotExpAssignmentResponse? ExpAssignments { get; set; } + /// + /// Feature-flag values resolved by the host for this session. + /// Re-supply them when resuming after a runtime restart. + /// + public IDictionary? FeatureFlags { get; set; } + /// /// Opt-in: when true, the runtime self-fetches enterprise managed /// settings (bypass-permissions policy) at session bootstrap using the diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 6edf168093..2414093797 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -630,6 +630,46 @@ public void ResumeSessionConfigClone_PreservesExpAssignments() Assert.Equal("exp-resume", clone.ExpAssignments!.Configs[0].Id); } + [Fact] + public void SessionRequests_CanSerializeFeatureFlags_WithSdkOptions() + { + var options = GetSerializerOptions(); + var flags = new Dictionary + { + ["ENABLED_TEST_FLAG"] = true, + ["DISABLED_TEST_FLAG"] = false, + }; + + foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" }) + { + var requestType = GetNestedType(typeof(CopilotClient), requestName); + var request = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("FeatureFlags", flags)); + using var document = JsonDocument.Parse( + JsonSerializer.Serialize(request, requestType, options)); + var serializedFlags = document.RootElement.GetProperty("featureFlags"); + Assert.True(serializedFlags.GetProperty("ENABLED_TEST_FLAG").GetBoolean()); + Assert.False(serializedFlags.GetProperty("DISABLED_TEST_FLAG").GetBoolean()); + } + } + + [Fact] + public void SessionConfigClone_CopiesFeatureFlags() + { + var config = new SessionConfig + { + FeatureFlags = new Dictionary { ["TEST_FLAG"] = true }, + }; + + var clone = config.Clone(); + clone.FeatureFlags!["TEST_FLAG"] = false; + + Assert.True(config.FeatureFlags["TEST_FLAG"]); + Assert.False(clone.FeatureFlags["TEST_FLAG"]); + } + [Fact] public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions() { diff --git a/go/client.go b/go/client.go index 4e44696a55..a82623bded 100644 --- a/go/client.go +++ b/go/client.go @@ -883,6 +883,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + if config.FeatureFlags != nil { + req.FeatureFlags = &config.FeatureFlags + } req.EnableManagedSettings = config.EnableManagedSettings req.ManagedSettings = config.ManagedSettings @@ -1289,6 +1292,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.ExtensionSDKPath = config.ExtensionSDKPath req.ExtensionInfo = config.ExtensionInfo req.ExpAssignments = config.ExpAssignments + if config.FeatureFlags != nil { + req.FeatureFlags = &config.FeatureFlags + } req.EnableManagedSettings = config.EnableManagedSettings req.ManagedSettings = config.ManagedSettings if config.OnPermissionRequest != nil { diff --git a/go/client_test.go b/go/client_test.go index c6ab0808cb..ca43721c47 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -3904,6 +3904,62 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) { }) } +func TestSessionRequests_FeatureFlags(t *testing.T) { + featureFlags := map[string]bool{ + "ENABLED_TEST_FLAG": true, + "DISABLED_TEST_FLAG": false, + } + + for _, tc := range []struct { + name string + req any + }{ + {"create", createSessionRequest{FeatureFlags: &featureFlags}}, + {"resume", resumeSessionRequest{SessionID: "s1", FeatureFlags: &featureFlags}}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + got := payload["featureFlags"].(map[string]any) + if got["ENABLED_TEST_FLAG"] != true || got["DISABLED_TEST_FLAG"] != false { + t.Errorf("Unexpected featureFlags: %v", got) + } + }) + } + + emptyFlags := map[string]bool{} + for _, tc := range []struct { + name string + req any + }{ + {"create empty", createSessionRequest{FeatureFlags: &emptyFlags}}, + {"resume empty", resumeSessionRequest{SessionID: "s1", FeatureFlags: &emptyFlags}}, + {"create unset", createSessionRequest{}}, + {"resume unset", resumeSessionRequest{SessionID: "s1"}}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := json.Marshal(tc.req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + _, present := payload["featureFlags"] + if strings.Contains(tc.name, "empty") != present { + t.Errorf("featureFlags presence = %v for %s", present, tc.name) + } + }) + } +} + func TestIsTerminal(t *testing.T) { t.Run("IsTerminal is serialized in tool definition", func(t *testing.T) { tool := Tool{ diff --git a/go/types.go b/go/types.go index 1d98e06158..bad605cb8b 100644 --- a/go/types.go +++ b/go/types.go @@ -1559,6 +1559,9 @@ type SessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments *CopilotExpAssignmentResponse + // FeatureFlags contains feature-flag values resolved by the host for this session. + // Re-supply them when resuming after a runtime restart. + FeatureFlags map[string]bool // EnableManagedSettings, when set to true, opts the runtime into // self-fetching enterprise managed settings (bypass-permissions policy) at // session bootstrap using the session's GitHubToken. Requires GitHubToken to @@ -2085,6 +2088,9 @@ type ResumeSessionConfig struct { // intended for trusted out-of-process integrators, and is not intended for // general external use. ExpAssignments *CopilotExpAssignmentResponse + // FeatureFlags contains host-resolved feature-flag values to apply on resume. + // See SessionConfig.FeatureFlags. + FeatureFlags map[string]bool // EnableManagedSettings injects the same opt-in flag on resume. See // SessionConfig.EnableManagedSettings. Re-supply on resume so the runtime // re-applies the managed-settings self-fetch after a CLI process restart. @@ -2557,6 +2563,7 @@ type createSessionRequest struct { ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` + FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` @@ -2656,6 +2663,7 @@ type resumeSessionRequest struct { ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"` CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"` ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"` + FeatureFlags *map[string]bool `json:"featureFlags,omitempty"` EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"` Traceparent string `json:"traceparent,omitempty"` diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index 4254c04ec4..ad2c879296 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -200,6 +200,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setGitHubToken(config.getGitHubToken()); request.setRemoteSession(config.getRemoteSession()); request.setCloud(config.getCloud()); + request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); request.setManagedSettings(config.getManagedSettings()); @@ -338,6 +339,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } request.setGitHubToken(config.getGitHubToken()); request.setRemoteSession(config.getRemoteSession()); + request.setFeatureFlags(config.getFeatureFlags()); request.setExpAssignments(config.getExpAssignments()); config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings); request.setManagedSettings(config.getManagedSettings()); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 403893987d..d40e2d43f9 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -236,6 +236,9 @@ public final class CreateSessionRequest { @JsonProperty("expAssignments") private CopilotExpAssignmentResponse expAssignments; + @JsonProperty("featureFlags") + private Map featureFlags; + @JsonProperty("enableManagedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; @@ -1113,6 +1116,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) { this.expAssignments = expAssignments; } + /** Gets host-resolved feature flags. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** Sets host-resolved feature flags. @param featureFlags the feature flags */ + public void setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + } + /** * Gets the self-fetch managed settings flag. @return the flag, or {@code null} * if not set diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index a55c3454e7..affdd446bd 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -111,6 +111,7 @@ public class ResumeSessionConfig { private String remoteSession; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Map featureFlags; private ManagedSettings managedSettings; /** @@ -1969,6 +1970,23 @@ public ResumeSessionConfig setExpAssignments(CopilotExpAssignmentResponse expAss return this; } + /** Gets host-resolved feature-flag values. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** + * Sets feature-flag values resolved by the host to apply on resume. + * + * @param featureFlags + * the feature flags + * @return this config for method chaining + */ + public ResumeSessionConfig setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + return this; + } + /** * Gets whether the runtime self-fetches enterprise managed settings at session * bootstrap on resume. @@ -2102,6 +2120,7 @@ public ResumeSessionConfig clone() { copy.gitHubToken = this.gitHubToken; copy.gitHubTokenProvider = this.gitHubTokenProvider; copy.remoteSession = this.remoteSession; + copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; copy.managedSettings = this.managedSettings; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 9b8e897fda..d8243b18c0 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -238,6 +238,9 @@ public final class ResumeSessionRequest { @JsonProperty("expAssignments") private CopilotExpAssignmentResponse expAssignments; + @JsonProperty("featureFlags") + private Map featureFlags; + @JsonProperty("enableManagedSettings") @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean enableManagedSettings; @@ -1128,6 +1131,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) { this.expAssignments = expAssignments; } + /** Gets host-resolved feature flags. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** Sets host-resolved feature flags. @param featureFlags the feature flags */ + public void setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + } + /** * Gets the self-fetch managed settings flag. @return the flag, or {@code null} * if not set diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index 9f6ddb5efa..caa02a0627 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -112,6 +112,7 @@ public class SessionConfig { private CloudSessionOptions cloud; private CopilotExpAssignmentResponse expAssignments; private Boolean enableManagedSettings; + private Map featureFlags; private ManagedSettings managedSettings; /** @@ -2097,6 +2098,23 @@ public SessionConfig setExpAssignments(CopilotExpAssignmentResponse expAssignmen return this; } + /** Gets host-resolved feature-flag values. @return the feature flags */ + public Map getFeatureFlags() { + return featureFlags; + } + + /** + * Sets feature-flag values resolved by the host for this session. + * + * @param featureFlags + * the feature flags + * @return this config instance for method chaining + */ + public SessionConfig setFeatureFlags(Map featureFlags) { + this.featureFlags = featureFlags; + return this; + } + /** * Gets whether the runtime self-fetches enterprise managed settings at session * bootstrap. @@ -2243,6 +2261,7 @@ public SessionConfig clone() { copy.gitHubTokenProvider = this.gitHubTokenProvider; copy.remoteSession = this.remoteSession; copy.cloud = this.cloud; + copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null; copy.expAssignments = this.expAssignments; copy.enableManagedSettings = this.enableManagedSettings; copy.managedSettings = this.managedSettings; diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 9d76d18ee2..737f679db0 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -1055,6 +1055,26 @@ void testBuildRequestsOmitExpAssignmentsWhenUnset() throws Exception { assertFalse(resumeJson.contains("\"expAssignments\""), "expAssignments should be omitted when null"); } + @Test + void testBuildRequestsPropagateAndSerializeFeatureFlags() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var flags = Map.of("ENABLED_TEST_FLAG", true, "DISABLED_TEST_FLAG", false); + + var createConfig = new SessionConfig().setFeatureFlags(flags); + CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(createConfig, "session-1"); + assertEquals(flags, createRequest.getFeatureFlags()); + var createJson = mapper.readTree(mapper.writeValueAsString(createRequest)); + assertTrue(createJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean()); + assertFalse(createJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean()); + + var resumeConfig = new ResumeSessionConfig().setFeatureFlags(flags); + ResumeSessionRequest resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", resumeConfig); + assertEquals(flags, resumeRequest.getFeatureFlags()); + var resumeJson = mapper.readTree(mapper.writeValueAsString(resumeRequest)); + assertTrue(resumeJson.path("featureFlags").path("ENABLED_TEST_FLAG").asBoolean()); + assertFalse(resumeJson.path("featureFlags").path("DISABLED_TEST_FLAG").asBoolean()); + } + @Test void testClonePreservesAndForwardsExpAssignments() throws Exception { var mapper = JsonRpcClient.getObjectMapper(); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 9b853aa597..a8f88b1e94 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1720,6 +1720,7 @@ export class CopilotClient { gitHubTokenProviderRegistrationId, remoteSession: config.remoteSession, cloud: config.cloud, + featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, managedSettings: config.managedSettings, @@ -1990,6 +1991,7 @@ export class CopilotClient { gitHubTokenProviderRegistrationId, remoteSession: config.remoteSession, openCanvases: config.openCanvases, + featureFlags: config.featureFlags, expAssignments: config.expAssignments, enableManagedSettings: config.enableManagedSettings, managedSettings: config.managedSettings, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 616e15a467..0a1c07ec53 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2873,6 +2873,12 @@ export interface SessionConfigBase { */ createSessionFsProvider?: (session: CopilotSession) => SessionFsProvider; + /** + * Feature-flag values resolved by the host for this session. + * Re-supply them when resuming after a runtime restart. + */ + featureFlags?: Record; + /** * ExP assignment ("flight") data injected by a trusted integrator, in the * same JSON shape the Copilot CLI fetches from the experimentation service diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 3ffda2fa71..28832e0788 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1276,6 +1276,39 @@ describe("CopilotClient", () => { expect(resumePayload.expAssignments).toBeUndefined(); }); + it("forwards featureFlags in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + const featureFlags = { ENABLED_TEST_FLAG: true, DISABLED_TEST_FLAG: false }; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + featureFlags, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + featureFlags, + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.featureFlags).toEqual(featureFlags); + expect(resumePayload.featureFlags).toEqual(featureFlags); + }); + it("forwards capi options in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 271fad626c..37d31d360c 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2271,6 +2271,7 @@ async def create_session( extension_info: ExtensionInfo | None = None, canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, + feature_flags: dict[str, bool] | None = None, exp_assignments: CopilotExpAssignmentResponse | None = None, enable_managed_settings: bool | None = None, github_mcp_tool_config: GitHubMcpToolConfig | None = None, @@ -2584,6 +2585,9 @@ async def create_session( if cloud is not None: payload["cloud"] = _cloud_session_options_to_dict(cloud) + if feature_flags is not None: + payload["featureFlags"] = feature_flags + # Add ExP assignment data if provided (trusted integrator) if exp_assignments is not None: payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments) @@ -3033,6 +3037,7 @@ async def resume_session( canvas_provider: CanvasProviderIdentity | None = None, canvas_handler: CanvasHandler | None = None, open_canvases: list[OpenCanvasInstance] | None = None, + feature_flags: dict[str, bool] | None = None, exp_assignments: CopilotExpAssignmentResponse | None = None, enable_managed_settings: bool | None = None, github_mcp_tool_config: GitHubMcpToolConfig | None = None, @@ -3368,6 +3373,9 @@ async def resume_session( if remote_session is not None: payload["remoteSession"] = remote_session.value + if feature_flags is not None: + payload["featureFlags"] = feature_flags + # Add ExP assignment data if provided (trusted integrator) if exp_assignments is not None: payload["expAssignments"] = _exp_assignment_response_to_dict(exp_assignments) diff --git a/python/test_client.py b/python/test_client.py index a33f0ecd60..483a0e2b91 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -1336,6 +1336,41 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_feature_flags(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + feature_flags = {"ENABLED_TEST_FLAG": True, "DISABLED_TEST_FLAG": False} + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + feature_flags=feature_flags, + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + feature_flags=feature_flags, + ) + + assert captured["session.create"]["featureFlags"] == feature_flags + assert captured["session.resume"]["featureFlags"] == feature_flags + finally: + await client.force_stop() + class TestURLParsing: def test_parse_port_only_url(self): From c3a3724745f82581e3e534869e1f42926d098795 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 1 Sep 2026 14:42:35 +0000 Subject: [PATCH 3/3] Document Python feature flag options Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/copilot/client.py b/python/copilot/client.py index 37d31d360c..0015b99bf1 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2411,6 +2411,9 @@ async def create_session( on its own and has no effect unless MCP Apps are enabled for the session (see ``enable_mcp_apps``). Omitted from the wire payload entirely when None. + feature_flags: Feature-flag values resolved by the host for this + session. Re-supply them when resuming after a runtime restart. + Sent on the wire as ``featureFlags``. exp_assignments: ExP assignment ("flight") data injected by a trusted integrator, in the same JSON shape the Copilot CLI fetches from the experimentation service @@ -3179,6 +3182,8 @@ async def resume_session( tool calls or permission prompts that were still pending when the session was last suspended. When False (the default), the runtime treats pending work as interrupted on resume. + feature_flags: Feature-flag values resolved by the host to apply + on resume. Sent on the wire as ``featureFlags``. exp_assignments: ExP assignment ("flight") data injected by a trusted integrator, in the same JSON shape the Copilot CLI fetches from the experimentation service