Skip to content

Commit 80d09ff

Browse files
jmoseleyCopilotSteveSandersonMS
authored
Expose session feature flag overrides across SDKs (github#2451)
* Expose Rust session feature flag overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9b0b5b94-f94a-4df4-8e57-6d7ed6fe04c1 * Expose session feature flags across SDKs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document Python feature flag options Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com> Copilot-Session: 9b0b5b94-f94a-4df4-8e57-6d7ed6fe04c1
1 parent 7591560 commit 80d09ff

19 files changed

Lines changed: 371 additions & 0 deletions

File tree

‎dotnet/src/Client.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
12851285
Providers: config.Providers,
12861286
Models: config.Models,
12871287
ToolFilterPrecedence: toolFilter.ToolFilterPrecedence,
1288+
FeatureFlags: config.FeatureFlags,
12881289
ExpAssignments: config.ExpAssignments,
12891290
EnableManagedSettings: config.EnableManagedSettings,
12901291
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
@@ -1526,6 +1527,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
15261527
Providers: config.Providers,
15271528
Models: config.Models,
15281529
ToolFilterPrecedence: toolFilter.ToolFilterPrecedence,
1530+
FeatureFlags: config.FeatureFlags,
15291531
ExpAssignments: config.ExpAssignments,
15301532
EnableManagedSettings: config.EnableManagedSettings,
15311533
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
@@ -3021,6 +3023,7 @@ internal record CreateSessionRequest(
30213023
IList<NamedProviderConfig>? Providers = null,
30223024
IList<ProviderModelConfig>? Models = null,
30233025
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
3026+
[property: JsonPropertyName("featureFlags")] IDictionary<string, bool>? FeatureFlags = null,
30243027
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
30253028
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
30263029
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
@@ -3139,6 +3142,7 @@ internal record ResumeSessionRequest(
31393142
IList<NamedProviderConfig>? Providers = null,
31403143
IList<ProviderModelConfig>? Models = null,
31413144
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
3145+
[property: JsonPropertyName("featureFlags")] IDictionary<string, bool>? FeatureFlags = null,
31423146
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
31433147
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
31443148
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,

‎dotnet/src/Types.cs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3225,6 +3225,9 @@ protected SessionConfigBase(SessionConfigBase? other)
32253225
GitHubToken = other.GitHubToken;
32263226
GitHubTokenProvider = other.GitHubTokenProvider;
32273227
RemoteSession = other.RemoteSession;
3228+
FeatureFlags = other.FeatureFlags is not null
3229+
? new Dictionary<string, bool>(other.FeatureFlags)
3230+
: null;
32283231
ExpAssignments = other.ExpAssignments;
32293232
EnableManagedSettings = other.EnableManagedSettings;
32303233
ManagedSettings = other.ManagedSettings;
@@ -3725,6 +3728,12 @@ protected SessionConfigBase(SessionConfigBase? other)
37253728
[EditorBrowsable(EditorBrowsableState.Never)]
37263729
public CopilotExpAssignmentResponse? ExpAssignments { get; set; }
37273730

3731+
/// <summary>
3732+
/// Feature-flag values resolved by the host for this session.
3733+
/// Re-supply them when resuming after a runtime restart.
3734+
/// </summary>
3735+
public IDictionary<string, bool>? FeatureFlags { get; set; }
3736+
37283737
/// <summary>
37293738
/// Opt-in: when <c>true</c>, the runtime self-fetches enterprise managed
37303739
/// settings (bypass-permissions policy) at session bootstrap using the

‎dotnet/test/Unit/SerializationTests.cs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,46 @@ public void ResumeSessionConfigClone_PreservesExpAssignments()
630630
Assert.Equal("exp-resume", clone.ExpAssignments!.Configs[0].Id);
631631
}
632632

633+
[Fact]
634+
public void SessionRequests_CanSerializeFeatureFlags_WithSdkOptions()
635+
{
636+
var options = GetSerializerOptions();
637+
var flags = new Dictionary<string, bool>
638+
{
639+
["ENABLED_TEST_FLAG"] = true,
640+
["DISABLED_TEST_FLAG"] = false,
641+
};
642+
643+
foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" })
644+
{
645+
var requestType = GetNestedType(typeof(CopilotClient), requestName);
646+
var request = CreateInternalRequest(
647+
requestType,
648+
("SessionId", "session-id"),
649+
("FeatureFlags", flags));
650+
using var document = JsonDocument.Parse(
651+
JsonSerializer.Serialize(request, requestType, options));
652+
var serializedFlags = document.RootElement.GetProperty("featureFlags");
653+
Assert.True(serializedFlags.GetProperty("ENABLED_TEST_FLAG").GetBoolean());
654+
Assert.False(serializedFlags.GetProperty("DISABLED_TEST_FLAG").GetBoolean());
655+
}
656+
}
657+
658+
[Fact]
659+
public void SessionConfigClone_CopiesFeatureFlags()
660+
{
661+
var config = new SessionConfig
662+
{
663+
FeatureFlags = new Dictionary<string, bool> { ["TEST_FLAG"] = true },
664+
};
665+
666+
var clone = config.Clone();
667+
clone.FeatureFlags!["TEST_FLAG"] = false;
668+
669+
Assert.True(config.FeatureFlags["TEST_FLAG"]);
670+
Assert.False(clone.FeatureFlags["TEST_FLAG"]);
671+
}
672+
633673
[Fact]
634674
public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions()
635675
{

‎go/client.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
906906
req.ExtensionSDKPath = config.ExtensionSDKPath
907907
req.ExtensionInfo = config.ExtensionInfo
908908
req.ExpAssignments = config.ExpAssignments
909+
if config.FeatureFlags != nil {
910+
req.FeatureFlags = &config.FeatureFlags
911+
}
909912
req.EnableManagedSettings = config.EnableManagedSettings
910913
req.ManagedSettings = config.ManagedSettings
911914

@@ -1316,6 +1319,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
13161319
req.ExtensionSDKPath = config.ExtensionSDKPath
13171320
req.ExtensionInfo = config.ExtensionInfo
13181321
req.ExpAssignments = config.ExpAssignments
1322+
if config.FeatureFlags != nil {
1323+
req.FeatureFlags = &config.FeatureFlags
1324+
}
13191325
req.EnableManagedSettings = config.EnableManagedSettings
13201326
req.ManagedSettings = config.ManagedSettings
13211327
if config.OnPermissionRequest != nil {

‎go/client_test.go‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4012,6 +4012,62 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) {
40124012
})
40134013
}
40144014

4015+
func TestSessionRequests_FeatureFlags(t *testing.T) {
4016+
featureFlags := map[string]bool{
4017+
"ENABLED_TEST_FLAG": true,
4018+
"DISABLED_TEST_FLAG": false,
4019+
}
4020+
4021+
for _, tc := range []struct {
4022+
name string
4023+
req any
4024+
}{
4025+
{"create", createSessionRequest{FeatureFlags: &featureFlags}},
4026+
{"resume", resumeSessionRequest{SessionID: "s1", FeatureFlags: &featureFlags}},
4027+
} {
4028+
t.Run(tc.name, func(t *testing.T) {
4029+
data, err := json.Marshal(tc.req)
4030+
if err != nil {
4031+
t.Fatalf("Failed to marshal: %v", err)
4032+
}
4033+
var payload map[string]any
4034+
if err := json.Unmarshal(data, &payload); err != nil {
4035+
t.Fatalf("Failed to unmarshal: %v", err)
4036+
}
4037+
got := payload["featureFlags"].(map[string]any)
4038+
if got["ENABLED_TEST_FLAG"] != true || got["DISABLED_TEST_FLAG"] != false {
4039+
t.Errorf("Unexpected featureFlags: %v", got)
4040+
}
4041+
})
4042+
}
4043+
4044+
emptyFlags := map[string]bool{}
4045+
for _, tc := range []struct {
4046+
name string
4047+
req any
4048+
}{
4049+
{"create empty", createSessionRequest{FeatureFlags: &emptyFlags}},
4050+
{"resume empty", resumeSessionRequest{SessionID: "s1", FeatureFlags: &emptyFlags}},
4051+
{"create unset", createSessionRequest{}},
4052+
{"resume unset", resumeSessionRequest{SessionID: "s1"}},
4053+
} {
4054+
t.Run(tc.name, func(t *testing.T) {
4055+
data, err := json.Marshal(tc.req)
4056+
if err != nil {
4057+
t.Fatalf("Failed to marshal: %v", err)
4058+
}
4059+
var payload map[string]any
4060+
if err := json.Unmarshal(data, &payload); err != nil {
4061+
t.Fatalf("Failed to unmarshal: %v", err)
4062+
}
4063+
_, present := payload["featureFlags"]
4064+
if strings.Contains(tc.name, "empty") != present {
4065+
t.Errorf("featureFlags presence = %v for %s", present, tc.name)
4066+
}
4067+
})
4068+
}
4069+
}
4070+
40154071
func TestIsTerminal(t *testing.T) {
40164072
t.Run("IsTerminal is serialized in tool definition", func(t *testing.T) {
40174073
tool := Tool{

‎go/types.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,9 @@ type SessionConfig struct {
15741574
// intended for trusted out-of-process integrators, and is not intended for
15751575
// general external use.
15761576
ExpAssignments *CopilotExpAssignmentResponse
1577+
// FeatureFlags contains feature-flag values resolved by the host for this session.
1578+
// Re-supply them when resuming after a runtime restart.
1579+
FeatureFlags map[string]bool
15771580
// EnableManagedSettings, when set to true, opts the runtime into
15781581
// self-fetching enterprise managed settings (bypass-permissions policy) at
15791582
// session bootstrap using the session's GitHubToken. Requires GitHubToken to
@@ -2105,6 +2108,9 @@ type ResumeSessionConfig struct {
21052108
// intended for trusted out-of-process integrators, and is not intended for
21062109
// general external use.
21072110
ExpAssignments *CopilotExpAssignmentResponse
2111+
// FeatureFlags contains host-resolved feature-flag values to apply on resume.
2112+
// See SessionConfig.FeatureFlags.
2113+
FeatureFlags map[string]bool
21082114
// EnableManagedSettings injects the same opt-in flag on resume. See
21092115
// SessionConfig.EnableManagedSettings. Re-supply on resume so the runtime
21102116
// re-applies the managed-settings self-fetch after a CLI process restart.
@@ -2578,6 +2584,7 @@ type createSessionRequest struct {
25782584
ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"`
25792585
CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"`
25802586
ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"`
2587+
FeatureFlags *map[string]bool `json:"featureFlags,omitempty"`
25812588
EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"`
25822589
ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"`
25832590
Traceparent string `json:"traceparent,omitempty"`
@@ -2678,6 +2685,7 @@ type resumeSessionRequest struct {
26782685
ExtensionInfo *ExtensionInfo `json:"extensionInfo,omitempty"`
26792686
CanvasProvider *CanvasProviderIdentity `json:"canvasProvider,omitempty"`
26802687
ExpAssignments *CopilotExpAssignmentResponse `json:"expAssignments,omitempty"`
2688+
FeatureFlags *map[string]bool `json:"featureFlags,omitempty"`
26812689
EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"`
26822690
ManagedSettings *ManagedSettings `json:"managedSettings,omitempty"`
26832691
Traceparent string `json:"traceparent,omitempty"`

‎java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
201201
request.setGitHubToken(config.getGitHubToken());
202202
request.setRemoteSession(config.getRemoteSession());
203203
request.setCloud(config.getCloud());
204+
request.setFeatureFlags(config.getFeatureFlags());
204205
request.setExpAssignments(config.getExpAssignments());
205206
config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings);
206207
request.setManagedSettings(config.getManagedSettings());
@@ -340,6 +341,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
340341
}
341342
request.setGitHubToken(config.getGitHubToken());
342343
request.setRemoteSession(config.getRemoteSession());
344+
request.setFeatureFlags(config.getFeatureFlags());
343345
request.setExpAssignments(config.getExpAssignments());
344346
config.getEnableManagedSettings().ifPresent(request::setEnableManagedSettings);
345347
request.setManagedSettings(config.getManagedSettings());

‎java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ public final class CreateSessionRequest {
239239
@JsonProperty("expAssignments")
240240
private CopilotExpAssignmentResponse expAssignments;
241241

242+
@JsonProperty("featureFlags")
243+
private Map<String, Boolean> featureFlags;
244+
242245
@JsonProperty("enableManagedSettings")
243246
@JsonInclude(JsonInclude.Include.NON_NULL)
244247
private Boolean enableManagedSettings;
@@ -1126,6 +1129,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) {
11261129
this.expAssignments = expAssignments;
11271130
}
11281131

1132+
/** Gets host-resolved feature flags. @return the feature flags */
1133+
public Map<String, Boolean> getFeatureFlags() {
1134+
return featureFlags;
1135+
}
1136+
1137+
/** Sets host-resolved feature flags. @param featureFlags the feature flags */
1138+
public void setFeatureFlags(Map<String, Boolean> featureFlags) {
1139+
this.featureFlags = featureFlags;
1140+
}
1141+
11291142
/**
11301143
* Gets the self-fetch managed settings flag. @return the flag, or {@code null}
11311144
* if not set

‎java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public class ResumeSessionConfig {
112112
private String remoteSession;
113113
private CopilotExpAssignmentResponse expAssignments;
114114
private Boolean enableManagedSettings;
115+
private Map<String, Boolean> featureFlags;
115116
private ManagedSettings managedSettings;
116117

117118
/**
@@ -1995,6 +1996,23 @@ public ResumeSessionConfig setExpAssignments(CopilotExpAssignmentResponse expAss
19951996
return this;
19961997
}
19971998

1999+
/** Gets host-resolved feature-flag values. @return the feature flags */
2000+
public Map<String, Boolean> getFeatureFlags() {
2001+
return featureFlags;
2002+
}
2003+
2004+
/**
2005+
* Sets feature-flag values resolved by the host to apply on resume.
2006+
*
2007+
* @param featureFlags
2008+
* the feature flags
2009+
* @return this config for method chaining
2010+
*/
2011+
public ResumeSessionConfig setFeatureFlags(Map<String, Boolean> featureFlags) {
2012+
this.featureFlags = featureFlags;
2013+
return this;
2014+
}
2015+
19982016
/**
19992017
* Gets whether the runtime self-fetches enterprise managed settings at session
20002018
* bootstrap on resume.
@@ -2129,6 +2147,7 @@ public ResumeSessionConfig clone() {
21292147
copy.gitHubToken = this.gitHubToken;
21302148
copy.gitHubTokenProvider = this.gitHubTokenProvider;
21312149
copy.remoteSession = this.remoteSession;
2150+
copy.featureFlags = this.featureFlags != null ? new java.util.HashMap<>(this.featureFlags) : null;
21322151
copy.expAssignments = this.expAssignments;
21332152
copy.enableManagedSettings = this.enableManagedSettings;
21342153
copy.managedSettings = this.managedSettings;

‎java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ public final class ResumeSessionRequest {
241241
@JsonProperty("expAssignments")
242242
private CopilotExpAssignmentResponse expAssignments;
243243

244+
@JsonProperty("featureFlags")
245+
private Map<String, Boolean> featureFlags;
246+
244247
@JsonProperty("enableManagedSettings")
245248
@JsonInclude(JsonInclude.Include.NON_NULL)
246249
private Boolean enableManagedSettings;
@@ -1141,6 +1144,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) {
11411144
this.expAssignments = expAssignments;
11421145
}
11431146

1147+
/** Gets host-resolved feature flags. @return the feature flags */
1148+
public Map<String, Boolean> getFeatureFlags() {
1149+
return featureFlags;
1150+
}
1151+
1152+
/** Sets host-resolved feature flags. @param featureFlags the feature flags */
1153+
public void setFeatureFlags(Map<String, Boolean> featureFlags) {
1154+
this.featureFlags = featureFlags;
1155+
}
1156+
11441157
/**
11451158
* Gets the self-fetch managed settings flag. @return the flag, or {@code null}
11461159
* if not set

0 commit comments

Comments
 (0)