Skip to content

Commit a8726db

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 a493e0e commit a8726db

22 files changed

Lines changed: 486 additions & 26 deletions

dotnet/src/Client.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,7 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
916916
config.EnableSkills ??= false;
917917
config.Memory ??= new MemoryConfiguration { Enabled = false };
918918
config.McpOAuthTokenStorage ??= McpOAuthTokenStorageMode.InMemory;
919+
config.CustomAgentsLocalOnly ??= true;
919920
}
920921
}
921922

dotnet/test/E2E/ClientOptionsE2ETests.cs

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

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

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

204204
await session.DisposeAsync();
205205
}
@@ -226,13 +226,13 @@ public async Task Should_Forward_CustomAgentsLocalOnly_In_Resume_Wire_Request()
226226

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

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

237237
await resumeSession.DisposeAsync();
238238
}
@@ -507,6 +507,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request
507507
Assert.False(createRequest.GetProperty("enableHostGitOperations").GetBoolean());
508508
Assert.False(createRequest.GetProperty("enableSessionStore").GetBoolean());
509509
Assert.False(createRequest.GetProperty("enableSkills").GetBoolean());
510+
Assert.True(createRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
510511
Assert.False(createRequest.TryGetProperty("organizationCustomInstructions", out _));
511512

512513
await session.DisposeAsync();
@@ -781,6 +782,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request
781782
Assert.False(resumeRequest.GetProperty("enableHostGitOperations").GetBoolean());
782783
Assert.False(resumeRequest.GetProperty("enableSessionStore").GetBoolean());
783784
Assert.False(resumeRequest.GetProperty("enableSkills").GetBoolean());
785+
Assert.True(resumeRequest.GetProperty("customAgentsLocalOnly").GetBoolean());
784786
Assert.False(resumeRequest.TryGetProperty("organizationCustomInstructions", out _));
785787

786788
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
@@ -165,12 +165,12 @@ func TestClientOptionsE2E(t *testing.T) {
165165
EnableConfigDiscovery: copilot.Bool(true),
166166
EnableOnDemandInstructionDiscovery: copilot.Bool(true),
167167
IncludeSubAgentStreamingEvents: copilot.Bool(false),
168+
CustomAgentsLocalOnly: copilot.Bool(false),
168169
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
169170
})
170171
if err != nil {
171172
t.Fatalf("CreateSession failed: %v", err)
172173
}
173-
t.Cleanup(func() { session.Disconnect() })
174174

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

202303
t.Run("should forward advanced session creation options to the CLI", func(t *testing.T) {
@@ -763,7 +864,7 @@ function handleMessage(message) {
763864
writeResponse(message.id, { message: "pong", protocolVersion: 3, timestamp: Date.now() });
764865
return;
765866
}
766-
if (message.method === "session.create") {
867+
if (message.method === "session.create" || message.method === "session.resume") {
767868
const sessionId = (message.params && message.params.sessionId) || "fake-session";
768869
writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null });
769870
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
@@ -593,7 +593,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
593593
registeredIdHolder[0] = localSessionId;
594594
}
595595

596-
var request = SessionRequestBuilder.buildCreateRequest(config, localSessionId);
596+
var request = SessionRequestBuilder.buildCreateRequest(config, localSessionId, options.getMode());
597597
if (extracted.wireSystemMessage() != config.getSystemMessage()) {
598598
request.setSystemMessage(extracted.wireSystemMessage());
599599
}
@@ -754,7 +754,7 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
754754
if (extracted.transformCallbacks() != null) {
755755
session.registerTransformCallbacks(extracted.transformCallbacks());
756756
}
757-
var request = SessionRequestBuilder.buildResumeRequest(sessionId, config);
757+
var request = SessionRequestBuilder.buildResumeRequest(sessionId, config, options.getMode());
758758
if (extracted.wireSystemMessage() != config.getSystemMessage()) {
759759
request.setSystemMessage(extracted.wireSystemMessage());
760760
}

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());
@@ -217,6 +225,11 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) {
217225
* @return the built request object
218226
*/
219227
static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config) {
228+
return buildResumeRequest(sessionId, config, CopilotClientMode.COPILOT_CLI);
229+
}
230+
231+
static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config,
232+
CopilotClientMode mode) {
220233
var request = new ResumeSessionRequest();
221234
request.setSessionId(sessionId);
222235
// Always request permission callbacks to enable deny-by-default behavior
@@ -225,6 +238,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
225238
request.setEnvValueMode("direct");
226239

227240
if (config == null) {
241+
request.setCustomAgentsLocalOnly(resolveCustomAgentsLocalOnly(null, mode));
228242
return request;
229243
}
230244

@@ -276,6 +290,8 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
276290
request.setMcpServers(config.getMcpServers());
277291
request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
278292
request.setCustomAgents(config.getCustomAgents());
293+
request.setCustomAgentsLocalOnly(
294+
resolveCustomAgentsLocalOnly(config.getCustomAgentsLocalOnly().orElse(null), mode));
279295
request.setDefaultAgent(config.getDefaultAgent());
280296
request.setAgent(config.getAgent());
281297
request.setSkillDirectories(config.getSkillDirectories());
@@ -314,6 +330,13 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
314330
return request;
315331
}
316332

333+
private static Boolean resolveCustomAgentsLocalOnly(Boolean customAgentsLocalOnly, CopilotClientMode mode) {
334+
if (customAgentsLocalOnly != null) {
335+
return customAgentsLocalOnly;
336+
}
337+
return mode == CopilotClientMode.EMPTY ? true : null;
338+
}
339+
317340
/**
318341
* Configures a session with handlers from the given config.
319342
*

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

@@ -553,6 +556,19 @@ public void setCustomAgents(List<CustomAgentConfig> customAgents) {
553556
this.customAgents = customAgents;
554557
}
555558

559+
/** Gets whether custom agents are local only. @return the flag */
560+
public Boolean getCustomAgentsLocalOnly() {
561+
return customAgentsLocalOnly;
562+
}
563+
564+
/**
565+
* Sets whether custom agents are local only. @param customAgentsLocalOnly the
566+
* flag
567+
*/
568+
public void setCustomAgentsLocalOnly(Boolean customAgentsLocalOnly) {
569+
this.customAgentsLocalOnly = customAgentsLocalOnly;
570+
}
571+
556572
/** Gets the default agent config. @return the default agent config */
557573
public DefaultAgentConfig getDefaultAgent() {
558574
return defaultAgent;

0 commit comments

Comments
 (0)