Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,7 @@ public async Task<CopilotSession> 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,
Expand Down Expand Up @@ -1491,6 +1492,7 @@ public async Task<CopilotSession> 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,
Expand Down Expand Up @@ -2930,6 +2932,7 @@ internal record CreateSessionRequest(
IList<NamedProviderConfig>? Providers = null,
IList<ProviderModelConfig>? Models = null,
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
[property: JsonPropertyName("featureFlags")] IDictionary<string, bool>? FeatureFlags = null,
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
Expand Down Expand Up @@ -3047,6 +3050,7 @@ internal record ResumeSessionRequest(
IList<NamedProviderConfig>? Providers = null,
IList<ProviderModelConfig>? Models = null,
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
[property: JsonPropertyName("featureFlags")] IDictionary<string, bool>? FeatureFlags = null,
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
Expand Down
9 changes: 9 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, bool>(other.FeatureFlags)
: null;
ExpAssignments = other.ExpAssignments;
EnableManagedSettings = other.EnableManagedSettings;
ManagedSettings = other.ManagedSettings;
Expand Down Expand Up @@ -3696,6 +3699,12 @@ protected SessionConfigBase(SessionConfigBase? other)
[EditorBrowsable(EditorBrowsableState.Never)]
public CopilotExpAssignmentResponse? ExpAssignments { get; set; }

/// <summary>
/// Feature-flag values resolved by the host for this session.
/// Re-supply them when resuming after a runtime restart.
/// </summary>
public IDictionary<string, bool>? FeatureFlags { get; set; }

/// <summary>
/// Opt-in: when <c>true</c>, the runtime self-fetches enterprise managed
/// settings (bypass-permissions policy) at session bootstrap using the
Expand Down
40 changes: 40 additions & 0 deletions dotnet/test/Unit/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, bool>
{
["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<string, bool> { ["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()
{
Expand Down
6 changes: 6 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
8 changes: 8 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ public final class CreateSessionRequest {
@JsonProperty("expAssignments")
private CopilotExpAssignmentResponse expAssignments;

@JsonProperty("featureFlags")
private Map<String, Boolean> featureFlags;

@JsonProperty("enableManagedSettings")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean enableManagedSettings;
Expand Down Expand Up @@ -1113,6 +1116,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) {
this.expAssignments = expAssignments;
}

/** Gets host-resolved feature flags. @return the feature flags */
public Map<String, Boolean> getFeatureFlags() {
return featureFlags;
}

/** Sets host-resolved feature flags. @param featureFlags the feature flags */
public void setFeatureFlags(Map<String, Boolean> featureFlags) {
this.featureFlags = featureFlags;
}

/**
* Gets the self-fetch managed settings flag. @return the flag, or {@code null}
* if not set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public class ResumeSessionConfig {
private String remoteSession;
private CopilotExpAssignmentResponse expAssignments;
private Boolean enableManagedSettings;
private Map<String, Boolean> featureFlags;
private ManagedSettings managedSettings;

/**
Expand Down Expand Up @@ -1969,6 +1970,23 @@ public ResumeSessionConfig setExpAssignments(CopilotExpAssignmentResponse expAss
return this;
}

/** Gets host-resolved feature-flag values. @return the feature flags */
public Map<String, Boolean> 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<String, Boolean> featureFlags) {
this.featureFlags = featureFlags;
return this;
}

/**
* Gets whether the runtime self-fetches enterprise managed settings at session
* bootstrap on resume.
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ public final class ResumeSessionRequest {
@JsonProperty("expAssignments")
private CopilotExpAssignmentResponse expAssignments;

@JsonProperty("featureFlags")
private Map<String, Boolean> featureFlags;

@JsonProperty("enableManagedSettings")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean enableManagedSettings;
Expand Down Expand Up @@ -1128,6 +1131,16 @@ public void setExpAssignments(CopilotExpAssignmentResponse expAssignments) {
this.expAssignments = expAssignments;
}

/** Gets host-resolved feature flags. @return the feature flags */
public Map<String, Boolean> getFeatureFlags() {
return featureFlags;
}

/** Sets host-resolved feature flags. @param featureFlags the feature flags */
public void setFeatureFlags(Map<String, Boolean> featureFlags) {
this.featureFlags = featureFlags;
}

/**
* Gets the self-fetch managed settings flag. @return the flag, or {@code null}
* if not set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public class SessionConfig {
private CloudSessionOptions cloud;
private CopilotExpAssignmentResponse expAssignments;
private Boolean enableManagedSettings;
private Map<String, Boolean> featureFlags;
private ManagedSettings managedSettings;

/**
Expand Down Expand Up @@ -2097,6 +2098,23 @@ public SessionConfig setExpAssignments(CopilotExpAssignmentResponse expAssignmen
return this;
}

/** Gets host-resolved feature-flag values. @return the feature flags */
public Map<String, Boolean> 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<String, Boolean> featureFlags) {
this.featureFlags = featureFlags;
return this;
}

/**
* Gets whether the runtime self-fetches enterprise managed settings at session
* bootstrap.
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading