Skip to content

Commit 8fad5ce

Browse files
committed
fix: propagate custom agent locality across SDKs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6c5ceca0-342a-4fe7-8f7f-55637324e2b2
1 parent 45ce6f9 commit 8fad5ce

22 files changed

Lines changed: 487 additions & 27 deletions

dotnet/src/Client.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,7 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
763763
config.EnableSkills ??= false;
764764
config.Memory ??= new MemoryConfiguration { Enabled = false };
765765
config.McpOAuthTokenStorage ??= McpOAuthTokenStorageMode.InMemory;
766+
config.CustomAgentsLocalOnly ??= true;
766767
}
767768
}
768769

dotnet/test/E2E/ClientOptionsE2ETests.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,13 @@ public async Task Should_Forward_CustomAgentsLocalOnly_In_Create_Wire_Request()
194194

195195
var session = await client.CreateSessionAsync(new SessionConfig
196196
{
197-
CustomAgentsLocalOnly = true,
197+
CustomAgentsLocalOnly = false,
198198
OnPermissionRequest = PermissionHandler.ApproveAll,
199199
});
200200

201201
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
202202
var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create");
203-
Assert.True(createRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
203+
Assert.False(createRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
204204

205205
await session.DisposeAsync();
206206
}
@@ -227,13 +227,13 @@ public async Task Should_Forward_CustomAgentsLocalOnly_In_Resume_Wire_Request()
227227

228228
var resumeSession = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig
229229
{
230-
CustomAgentsLocalOnly = true,
230+
CustomAgentsLocalOnly = false,
231231
OnPermissionRequest = PermissionHandler.ApproveAll,
232232
});
233233

234234
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
235235
var resumeRequest = GetCapturedRequestParams(capture.RootElement, "session.resume");
236-
Assert.True(resumeRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
236+
Assert.False(resumeRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
237237

238238
await resumeSession.DisposeAsync();
239239
}
@@ -309,6 +309,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request
309309
Assert.False(createRequest.GetProperty("enableHostGitOperations").GetBoolean());
310310
Assert.False(createRequest.GetProperty("enableSessionStore").GetBoolean());
311311
Assert.False(createRequest.GetProperty("enableSkills").GetBoolean());
312+
Assert.True(createRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
312313
Assert.False(createRequest.TryGetProperty("organizationCustomInstructions", out _));
313314

314315
await session.DisposeAsync();
@@ -501,6 +502,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request
501502
Assert.False(resumeRequest.GetProperty("enableHostGitOperations").GetBoolean());
502503
Assert.False(resumeRequest.GetProperty("enableSessionStore").GetBoolean());
503504
Assert.False(resumeRequest.GetProperty("enableSkills").GetBoolean());
505+
Assert.True(resumeRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
504506
Assert.False(resumeRequest.TryGetProperty("organizationCustomInstructions", out _));
505507

506508
await session.DisposeAsync();

go/internal/e2e/client_options_e2e_test.go

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,12 @@ func TestClientOptionsE2E(t *testing.T) {
164164
EnableConfigDiscovery: copilot.Bool(true),
165165
EnableOnDemandInstructionDiscovery: copilot.Bool(true),
166166
IncludeSubAgentStreamingEvents: copilot.Bool(false),
167+
CustomAgentsLocalOnly: copilot.Bool(false),
167168
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
168169
})
169170
if err != nil {
170171
t.Fatalf("CreateSession failed: %v", err)
171172
}
172-
t.Cleanup(func() { session.Disconnect() })
173173

174174
updated := readCapture(t, capturePath)
175175
var createReq *capturedRequest
@@ -196,6 +196,107 @@ func TestClientOptionsE2E(t *testing.T) {
196196
if v, ok := params["includeSubAgentStreamingEvents"].(bool); !ok || v != false {
197197
t.Errorf("Expected session.create.params.includeSubAgentStreamingEvents=false, got %v", params["includeSubAgentStreamingEvents"])
198198
}
199+
if v, ok := params["customAgentsLocalOnly"].(bool); !ok || v != false {
200+
t.Errorf("Expected session.create.params.customAgentsLocalOnly=false, got %v", params["customAgentsLocalOnly"])
201+
}
202+
203+
sessionID := session.SessionID
204+
if err := session.Disconnect(); err != nil {
205+
t.Fatalf("Disconnect failed: %v", err)
206+
}
207+
resumed, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{
208+
CustomAgentsLocalOnly: copilot.Bool(false),
209+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
210+
})
211+
if err != nil {
212+
t.Fatalf("ResumeSession failed: %v", err)
213+
}
214+
t.Cleanup(func() { _ = resumed.Disconnect() })
215+
216+
resumedCapture := readCapture(t, capturePath)
217+
for _, req := range resumedCapture.Requests {
218+
if req.Method != "session.resume" {
219+
continue
220+
}
221+
resumeParams, ok := req.Params.(map[string]any)
222+
if !ok {
223+
t.Fatalf("Expected session.resume params to be an object, got %T", req.Params)
224+
}
225+
if v, ok := resumeParams["customAgentsLocalOnly"].(bool); !ok || v != false {
226+
t.Errorf("Expected session.resume.params.customAgentsLocalOnly=false, got %v",
227+
resumeParams["customAgentsLocalOnly"])
228+
}
229+
return
230+
}
231+
t.Fatalf("session.resume request was not captured. Captured requests: %+v", resumedCapture.Requests)
232+
})
233+
234+
t.Run("should send empty-mode custom agent locality defaults in initial requests", func(t *testing.T) {
235+
ctx := testharness.NewTestContext(t)
236+
cliPath := filepath.Join(ctx.WorkDir, "fake-cli-empty-"+randomHex(t)+".js")
237+
capturePath := filepath.Join(ctx.WorkDir, "fake-cli-empty-capture-"+randomHex(t)+".json")
238+
if err := os.WriteFile(cliPath, []byte(fakeStdioCliScript), 0644); err != nil {
239+
t.Fatalf("Failed to write fake CLI script: %v", err)
240+
}
241+
242+
client := ctx.NewClient(func(opts *copilot.ClientOptions) {
243+
opts.Connection = copilot.StdioConnection{
244+
Path: cliPath,
245+
Args: []string{"--capture-file", capturePath},
246+
}
247+
opts.Mode = copilot.ModeEmpty
248+
opts.BaseDirectory = ctx.WorkDir
249+
opts.UseLoggedInUser = copilot.Bool(false)
250+
})
251+
t.Cleanup(func() { client.ForceStop() })
252+
253+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
254+
AvailableTools: []string{"builtin:ask_user"},
255+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
256+
})
257+
if err != nil {
258+
t.Fatalf("CreateSession failed: %v", err)
259+
}
260+
sessionID := session.SessionID
261+
if err := session.Disconnect(); err != nil {
262+
t.Fatalf("Disconnect failed: %v", err)
263+
}
264+
265+
resumed, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{
266+
AvailableTools: []string{"builtin:ask_user"},
267+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
268+
})
269+
if err != nil {
270+
t.Fatalf("ResumeSession failed: %v", err)
271+
}
272+
t.Cleanup(func() { _ = resumed.Disconnect() })
273+
274+
capture := readCapture(t, capturePath)
275+
foundCreate := false
276+
foundResume := false
277+
for _, req := range capture.Requests {
278+
params, ok := req.Params.(map[string]any)
279+
if !ok {
280+
continue
281+
}
282+
switch req.Method {
283+
case "session.create":
284+
foundCreate = true
285+
if v, ok := params["customAgentsLocalOnly"].(bool); !ok || !v {
286+
t.Errorf("Expected session.create.params.customAgentsLocalOnly=true, got %v",
287+
params["customAgentsLocalOnly"])
288+
}
289+
case "session.resume":
290+
foundResume = true
291+
if v, ok := params["customAgentsLocalOnly"].(bool); !ok || !v {
292+
t.Errorf("Expected session.resume.params.customAgentsLocalOnly=true, got %v",
293+
params["customAgentsLocalOnly"])
294+
}
295+
}
296+
}
297+
if !foundCreate || !foundResume {
298+
t.Fatalf("Expected create and resume requests, got %+v", capture.Requests)
299+
}
199300
})
200301

201302
}
@@ -425,7 +526,7 @@ function handleMessage(message) {
425526
writeResponse(message.id, { message: "pong", protocolVersion: 3, timestamp: Date.now() });
426527
return;
427528
}
428-
if (message.method === "session.create") {
529+
if (message.method === "session.create" || message.method === "session.resume") {
429530
const sessionId = (message.params && message.params.sessionId) || "fake-session";
430531
writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null });
431532
return;

go/mode_empty.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ func (c *Client) applyConfigDefaultsForMode(config *SessionConfig) {
160160
if config.MCPOAuthTokenStorage == "" {
161161
config.MCPOAuthTokenStorage = "in-memory"
162162
}
163+
if config.CustomAgentsLocalOnly == nil {
164+
localOnly := true
165+
config.CustomAgentsLocalOnly = &localOnly
166+
}
163167
}
164168

165169
func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
@@ -204,6 +208,10 @@ func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
204208
if config.MCPOAuthTokenStorage == "" {
205209
config.MCPOAuthTokenStorage = "in-memory"
206210
}
211+
if config.CustomAgentsLocalOnly == nil {
212+
localOnly := true
213+
config.CustomAgentsLocalOnly = &localOnly
214+
}
207215
}
208216

209217
// updateSessionOptionsForMode applies the per-mode safe-defaults patch via

go/toolset_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ func TestApplyConfigDefaultsForMode_emptyDefaultsGranularFlags(t *testing.T) {
276276
if cfg.Memory == nil || cfg.Memory.Enabled != false {
277277
t.Errorf("expected Memory.Enabled=false in empty mode, got %v", cfg.Memory)
278278
}
279+
if cfg.CustomAgentsLocalOnly == nil || !*cfg.CustomAgentsLocalOnly {
280+
t.Errorf("expected CustomAgentsLocalOnly=true in empty mode, got %v", cfg.CustomAgentsLocalOnly)
281+
}
279282
}
280283

281284
func TestApplyConfigDefaultsForMode_emptyHonorsCallerGranularFlags(t *testing.T) {
@@ -291,6 +294,7 @@ func TestApplyConfigDefaultsForMode_emptyHonorsCallerGranularFlags(t *testing.T)
291294
EnableSessionStore: &trueVal,
292295
EnableSkills: &trueVal,
293296
Memory: &MemoryConfiguration{Enabled: true},
297+
CustomAgentsLocalOnly: &falseVal,
294298
}
295299
c.applyConfigDefaultsForMode(cfg)
296300
if *cfg.SkipEmbeddingRetrieval != false {
@@ -317,6 +321,9 @@ func TestApplyConfigDefaultsForMode_emptyHonorsCallerGranularFlags(t *testing.T)
317321
if cfg.Memory == nil || cfg.Memory.Enabled != true {
318322
t.Errorf("caller-supplied Memory must win")
319323
}
324+
if cfg.CustomAgentsLocalOnly == nil || *cfg.CustomAgentsLocalOnly {
325+
t.Errorf("caller-supplied CustomAgentsLocalOnly must win")
326+
}
320327
}
321328

322329
func TestApplyConfigDefaultsForMode_copilotCliLeavesGranularFlagsNil(t *testing.T) {
@@ -344,6 +351,25 @@ func TestApplyConfigDefaultsForMode_copilotCliLeavesGranularFlagsNil(t *testing.
344351
if cfg.Memory != nil {
345352
t.Errorf("non-empty mode must not default Memory")
346353
}
354+
if cfg.CustomAgentsLocalOnly != nil {
355+
t.Errorf("non-empty mode must not default CustomAgentsLocalOnly")
356+
}
357+
}
358+
359+
func TestApplyResumeDefaultsForMode_customAgentsLocalOnly(t *testing.T) {
360+
c := NewClient(&ClientOptions{Mode: ModeEmpty, BaseDirectory: t.TempDir()})
361+
362+
cfg := &ResumeSessionConfig{}
363+
c.applyResumeDefaultsForMode(cfg)
364+
if cfg.CustomAgentsLocalOnly == nil || !*cfg.CustomAgentsLocalOnly {
365+
t.Errorf("expected CustomAgentsLocalOnly=true in empty mode, got %v", cfg.CustomAgentsLocalOnly)
366+
}
367+
368+
cfg = &ResumeSessionConfig{CustomAgentsLocalOnly: Bool(false)}
369+
c.applyResumeDefaultsForMode(cfg)
370+
if cfg.CustomAgentsLocalOnly == nil || *cfg.CustomAgentsLocalOnly {
371+
t.Errorf("caller-supplied CustomAgentsLocalOnly must win")
372+
}
347373
}
348374

349375
func TestApplyConfigDefaultsForMode_emptyDefaultsMCPOAuthTokenStorage(t *testing.T) {

java/src/main/java/com/github/copilot/CopilotClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
584584
registeredIdHolder[0] = localSessionId;
585585
}
586586

587-
var request = SessionRequestBuilder.buildCreateRequest(config, localSessionId);
587+
var request = SessionRequestBuilder.buildCreateRequest(config, localSessionId, options.getMode());
588588
if (extracted.wireSystemMessage() != config.getSystemMessage()) {
589589
request.setSystemMessage(extracted.wireSystemMessage());
590590
}
@@ -745,7 +745,7 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
745745
if (extracted.transformCallbacks() != null) {
746746
session.registerTransformCallbacks(extracted.transformCallbacks());
747747
}
748-
var request = SessionRequestBuilder.buildResumeRequest(sessionId, config);
748+
var request = SessionRequestBuilder.buildResumeRequest(sessionId, config, options.getMode());
749749
if (extracted.wireSystemMessage() != config.getSystemMessage()) {
750750
request.setSystemMessage(extracted.wireSystemMessage());
751751
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.concurrent.CompletableFuture;
1111
import java.util.function.Function;
1212

13+
import com.github.copilot.rpc.CopilotClientMode;
1314
import com.github.copilot.rpc.CreateSessionRequest;
1415
import com.github.copilot.rpc.ProviderConfig;
1516
import com.github.copilot.rpc.NamedProviderConfig;
@@ -97,13 +98,18 @@ static ExtractedTransforms extractTransformCallbacks(SystemMessageConfig systemM
9798
* @return the built request object
9899
*/
99100
static CreateSessionRequest buildCreateRequest(SessionConfig config, String sessionId) {
101+
return buildCreateRequest(config, sessionId, CopilotClientMode.COPILOT_CLI);
102+
}
103+
104+
static CreateSessionRequest buildCreateRequest(SessionConfig config, String sessionId, CopilotClientMode mode) {
100105
var request = new CreateSessionRequest();
101106
// Always request permission callbacks to enable deny-by-default behavior
102107
request.setRequestPermission(true);
103108
// Always send envValueMode=direct for MCP servers
104109
request.setEnvValueMode("direct");
105110
request.setSessionId(sessionId);
106111
if (config == null) {
112+
request.setCustomAgentsLocalOnly(resolveCustomAgentsLocalOnly(null, mode));
107113
return request;
108114
}
109115

@@ -138,6 +144,8 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
138144
request.setMcpServers(config.getMcpServers());
139145
request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
140146
request.setCustomAgents(config.getCustomAgents());
147+
request.setCustomAgentsLocalOnly(
148+
resolveCustomAgentsLocalOnly(config.getCustomAgentsLocalOnly().orElse(null), mode));
141149
request.setDefaultAgent(config.getDefaultAgent());
142150
request.setAgent(config.getAgent());
143151
request.setInfiniteSessions(config.getInfiniteSessions());
@@ -215,6 +223,11 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) {
215223
* @return the built request object
216224
*/
217225
static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config) {
226+
return buildResumeRequest(sessionId, config, CopilotClientMode.COPILOT_CLI);
227+
}
228+
229+
static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config,
230+
CopilotClientMode mode) {
218231
var request = new ResumeSessionRequest();
219232
request.setSessionId(sessionId);
220233
// Always request permission callbacks to enable deny-by-default behavior
@@ -223,6 +236,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
223236
request.setEnvValueMode("direct");
224237

225238
if (config == null) {
239+
request.setCustomAgentsLocalOnly(resolveCustomAgentsLocalOnly(null, mode));
226240
return request;
227241
}
228242

@@ -274,6 +288,8 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
274288
request.setMcpServers(config.getMcpServers());
275289
request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
276290
request.setCustomAgents(config.getCustomAgents());
291+
request.setCustomAgentsLocalOnly(
292+
resolveCustomAgentsLocalOnly(config.getCustomAgentsLocalOnly().orElse(null), mode));
277293
request.setDefaultAgent(config.getDefaultAgent());
278294
request.setAgent(config.getAgent());
279295
request.setSkillDirectories(config.getSkillDirectories());
@@ -310,6 +326,13 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
310326
return request;
311327
}
312328

329+
private static Boolean resolveCustomAgentsLocalOnly(Boolean customAgentsLocalOnly, CopilotClientMode mode) {
330+
if (customAgentsLocalOnly != null) {
331+
return customAgentsLocalOnly;
332+
}
333+
return mode == CopilotClientMode.EMPTY ? true : null;
334+
}
335+
313336
/**
314337
* Configures a session with handlers from the given config.
315338
*

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ public final class CreateSessionRequest {
117117
@JsonProperty("customAgents")
118118
private List<CustomAgentConfig> customAgents;
119119

120+
@JsonProperty("customAgentsLocalOnly")
121+
private Boolean customAgentsLocalOnly;
122+
120123
@JsonProperty("defaultAgent")
121124
private DefaultAgentConfig defaultAgent;
122125

@@ -546,6 +549,19 @@ public void setCustomAgents(List<CustomAgentConfig> customAgents) {
546549
this.customAgents = customAgents;
547550
}
548551

552+
/** Gets whether custom agents are local only. @return the flag */
553+
public Boolean getCustomAgentsLocalOnly() {
554+
return customAgentsLocalOnly;
555+
}
556+
557+
/**
558+
* Sets whether custom agents are local only. @param customAgentsLocalOnly the
559+
* flag
560+
*/
561+
public void setCustomAgentsLocalOnly(Boolean customAgentsLocalOnly) {
562+
this.customAgentsLocalOnly = customAgentsLocalOnly;
563+
}
564+
549565
/** Gets the default agent config. @return the default agent config */
550566
public DefaultAgentConfig getDefaultAgent() {
551567
return defaultAgent;

0 commit comments

Comments
 (0)