From f0a09d4e59f36d9f28a6b31a8e3d9185b68d6937 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 31 Jul 2026 11:26:14 +1000 Subject: [PATCH 1/5] feat: add support for additional directories in session configuration - Introduced `additionalDirectories` field in various session-related configurations across SDKs (Java, Go, .NET, Node.js, Python, Rust). - Updated session creation and resume methods to handle `additionalDirectories`. - Enhanced tests to verify that additional directories are correctly forwarded during session creation and resumption. - Updated documentation to reflect the new configuration options. --- docs/troubleshooting/compatibility.md | 1 + dotnet/src/Client.cs | 12 ++-- dotnet/src/Types.cs | 7 ++ .../test/Unit/ClientSessionLifetimeTests.cs | 31 +++++++++ dotnet/test/Unit/CloneTests.cs | 8 +++ go/client.go | 2 + go/client_test.go | 54 +++++++++++++++ go/types.go | 8 +++ .../github/copilot/SessionRequestBuilder.java | 2 + .../copilot/rpc/CreateSessionRequest.java | 13 ++++ .../copilot/rpc/ResumeSessionConfig.java | 25 +++++++ .../copilot/rpc/ResumeSessionRequest.java | 13 ++++ .../com/github/copilot/rpc/SessionConfig.java | 25 +++++++ .../com/github/copilot/ConfigCloneTest.java | 6 ++ .../copilot/SessionRequestBuilderTest.java | 16 +++++ nodejs/src/client.ts | 2 + nodejs/src/types.ts | 7 ++ nodejs/test/client.test.ts | 36 ++++++++++ python/copilot/client.py | 6 ++ python/test_client.py | 40 +++++++++++ rust/src/types.rs | 66 +++++++++++++++++++ rust/src/wire.rs | 4 ++ 22 files changed, 380 insertions(+), 4 deletions(-) diff --git a/docs/troubleshooting/compatibility.md b/docs/troubleshooting/compatibility.md index c68d59cc7b..795c0f5fd4 100644 --- a/docs/troubleshooting/compatibility.md +++ b/docs/troubleshooting/compatibility.md @@ -84,6 +84,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Config directory | `configDir` config | Override default config location | | Client name | `clientName` config | Identify app in User-Agent | | Working directory | `workingDirectory` config | Set session cwd | +| Additional directories | `additionalDirectories` config | Grant session access beyond the working directory; re-supply on resume | | **Experimental** | | | | Agent management | `session.rpc.agent.*` | List, select, deselect, get current agent | | Fleet mode | `session.rpc.fleet.start()` | Parallel sub-agent execution; see [Fleet mode](../features/fleet-mode.md) | diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index c8d83dfee2..32f9d49e55 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1196,7 +1196,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, - EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null); + EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, + AdditionalDirectories: config.AdditionalDirectories); var rpcTimestamp = Stopwatch.GetTimestamp(); @@ -1410,7 +1411,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes ToolFilterPrecedence: toolFilter.ToolFilterPrecedence, ExpAssignments: config.ExpAssignments, EnableManagedSettings: config.EnableManagedSettings, - EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null); + EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null, + AdditionalDirectories: config.AdditionalDirectories); var rpcTimestamp = Stopwatch.GetTimestamp(); var response = await InvokeRpcAsync( @@ -2762,7 +2764,8 @@ internal record CreateSessionRequest( OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, - bool? EnableGitHubTelemetryForwarding = null); + bool? EnableGitHubTelemetryForwarding = null, + IList? AdditionalDirectories = null); #pragma warning restore GHCP001 internal record ToolDefinition( @@ -2868,7 +2871,8 @@ internal record ResumeSessionRequest( OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null, [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, - bool? EnableGitHubTelemetryForwarding = null); + bool? EnableGitHubTelemetryForwarding = null, + IList? AdditionalDirectories = null); #pragma warning restore GHCP001 internal record ResumeSessionResponse( diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 565204d39d..256bf4c29f 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -3051,6 +3051,7 @@ protected SessionConfigBase(SessionConfigBase? other) SystemMessage = other.SystemMessage; Tools = other.Tools is not null ? [.. other.Tools] : null; WorkingDirectory = other.WorkingDirectory; + AdditionalDirectories = other.AdditionalDirectories is not null ? [.. other.AdditionalDirectories] : null; } /// Client name to identify the application using the SDK. @@ -3308,6 +3309,12 @@ protected SessionConfigBase(SessionConfigBase? other) /// Working directory for the session. public string? WorkingDirectory { get; set; } + /// + /// Additional directories the agent may access beyond . + /// Relative paths resolve against the session working directory. Re-supply them when resuming. + /// + public IList? AdditionalDirectories { get; set; } + /// /// Enable streaming of assistant message and reasoning chunks. /// When true, assistant.message_delta and assistant.reasoning_delta events diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index e1143db17c..181582d8c1 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -255,6 +255,37 @@ public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unse Assert.False(agent.TryGetProperty("reasoningEffort", out _)); } + [Fact] + public async Task SessionRequests_Serialize_AdditionalDirectories() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + AdditionalDirectories = ["/repo/shared", "/repo/generated"], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var createRequest = Assert.Single(server.Requests, request => request.Method == "session.create"); + Assert.Equal( + new string?[] { "/repo/shared", "/repo/generated" }, + createRequest.Params.GetProperty("additionalDirectories").EnumerateArray().Select(value => value.GetString())); + + server.ClearRequests(); + + await using var resumed = await client.ResumeSessionAsync("resume-with-additional-directories", new ResumeSessionConfig + { + AdditionalDirectories = ["/repo/resumed"], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var resumeRequest = Assert.Single(server.Requests, request => request.Method == "session.resume"); + Assert.Equal( + new string?[] { "/repo/resumed" }, + resumeRequest.Params.GetProperty("additionalDirectories").EnumerateArray().Select(value => value.GetString())); + } + [Fact] public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured() { diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index ec509ab169..56f2802ee6 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -75,6 +75,7 @@ public void SessionConfig_Clone_CopiesAllProperties() ExcludedTools = ["tool3"], ExcludedBuiltInAgents = ["explore", "task"], WorkingDirectory = "/workspace", + AdditionalDirectories = ["/shared", "/generated"], Streaming = true, EnableCitations = true, EnableSessionTelemetry = false, @@ -119,6 +120,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.ExcludedTools, clone.ExcludedTools); Assert.Equal(original.ExcludedBuiltInAgents, clone.ExcludedBuiltInAgents); Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory); + Assert.Equal(original.AdditionalDirectories, clone.AdditionalDirectories); Assert.Equal(original.Streaming, clone.Streaming); Assert.Equal(original.EnableCitations, clone.EnableCitations); Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry); @@ -154,6 +156,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent() ExcludedBuiltInAgents = ["explore"], McpServers = new Dictionary { ["s1"] = new McpStdioServerConfig { Command = "echo" } }, CustomAgents = [new CustomAgentConfig { Name = "a1" }], + AdditionalDirectories = ["/shared"], SkillDirectories = ["/skills"], InstructionDirectories = ["/instructions"], DisabledSkills = ["skill1"], @@ -167,6 +170,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent() clone.ExcludedBuiltInAgents!.Add("task"); clone.McpServers!["s2"] = new McpStdioServerConfig { Command = "echo" }; clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" }); + clone.AdditionalDirectories!.Add("/generated"); clone.SkillDirectories!.Add("/more"); clone.InstructionDirectories!.Add("/more-instructions"); clone.DisabledSkills!.Add("skill99"); @@ -177,6 +181,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent() Assert.Single(original.ExcludedBuiltInAgents!); Assert.Single(original.McpServers!); Assert.Single(original.CustomAgents!); + Assert.Single(original.AdditionalDirectories!); Assert.Single(original.SkillDirectories!); Assert.Single(original.InstructionDirectories!); Assert.Single(original.DisabledSkills!); @@ -203,6 +208,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent() ExcludedBuiltInAgents = ["explore"], McpServers = new Dictionary { ["s1"] = new McpStdioServerConfig { Command = "echo" } }, CustomAgents = [new CustomAgentConfig { Name = "a1" }], + AdditionalDirectories = ["/shared"], SkillDirectories = ["/skills"], InstructionDirectories = ["/instructions"], DisabledSkills = ["skill1"], @@ -216,6 +222,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent() clone.ExcludedBuiltInAgents!.Add("task"); clone.McpServers!["s2"] = new McpStdioServerConfig { Command = "echo" }; clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" }); + clone.AdditionalDirectories!.Add("/generated"); clone.SkillDirectories!.Add("/more"); clone.InstructionDirectories!.Add("/more-instructions"); clone.DisabledSkills!.Add("skill99"); @@ -226,6 +233,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent() Assert.Single(original.ExcludedBuiltInAgents!); Assert.Single(original.McpServers!); Assert.Single(original.CustomAgents!); + Assert.Single(original.AdditionalDirectories!); Assert.Single(original.SkillDirectories!); Assert.Single(original.InstructionDirectories!); Assert.Single(original.DisabledSkills!); diff --git a/go/client.go b/go/client.go index 0d07e21f54..be09ffe10c 100644 --- a/go/client.go +++ b/go/client.go @@ -802,6 +802,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ManageScheduleEnabled = config.ManageScheduleEnabled req.ModelCapabilities = config.ModelCapabilities req.WorkingDirectory = config.WorkingDirectory + req.AdditionalDirectories = config.AdditionalDirectories req.MCPServers = config.MCPServers req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.EnvValueMode = "direct" @@ -1157,6 +1158,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.Hooks = Bool(true) } req.WorkingDirectory = config.WorkingDirectory + req.AdditionalDirectories = config.AdditionalDirectories req.ConfigDir = config.ConfigDirectory req.EnableConfigDiscovery = config.EnableConfigDiscovery req.SkipEmbeddingRetrieval = config.SkipEmbeddingRetrieval diff --git a/go/client_test.go b/go/client_test.go index 3e4675d0b3..374f87b46d 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -251,6 +251,60 @@ func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { assertCapiEnableWebSocketResponses(t, <-resumeParams) } +func TestClient_ForwardsAdditionalDirectoriesToSessionRequests(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + createParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + AdditionalDirectories: []string{"/repo/shared", "/repo/generated"}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + assertAdditionalDirectories(t, <-createParams, []string{"/repo/shared", "/repo/generated"}) + + resumeParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + return []byte(`{"sessionId":"resumed-additional-directories","workspacePath":"/workspace"}`), nil + }) + + _, err = client.ResumeSessionWithOptions( + t.Context(), + "resumed-additional-directories", + &ResumeSessionConfig{AdditionalDirectories: []string{"/repo/resumed"}}, + ) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + assertAdditionalDirectories(t, <-resumeParams, []string{"/repo/resumed"}) +} + +func assertAdditionalDirectories(t *testing.T, params json.RawMessage, want []string) { + t.Helper() + var payload struct { + AdditionalDirectories []string `json:"additionalDirectories"` + } + if err := json.Unmarshal(params, &payload); err != nil { + t.Fatalf("failed to decode request params: %v", err) + } + if !reflect.DeepEqual(payload.AdditionalDirectories, want) { + t.Fatalf("additionalDirectories = %v, want %v", payload.AdditionalDirectories, want) + } +} + func TestClient_ForwardsCanvasProviderToSessionRequests(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) diff --git a/go/types.go b/go/types.go index d1fc34ecd2..da6ad8f2e9 100644 --- a/go/types.go +++ b/go/types.go @@ -1229,6 +1229,9 @@ type SessionConfig struct { // WorkingDirectory is the working directory for the session. // Tool operations will be relative to this directory. WorkingDirectory string + // AdditionalDirectories are directories the agent may access beyond WorkingDirectory. + // Relative paths are resolved against WorkingDirectory. Re-supply them when resuming. + AdditionalDirectories []string // Streaming enables streaming of assistant message and reasoning chunks. // When non-nil and true, assistant.message_delta and assistant.reasoning_delta // events with deltaContent are sent as the response is generated. @@ -1715,6 +1718,9 @@ type ResumeSessionConfig struct { // WorkingDirectory is the working directory for the session. // Tool operations will be relative to this directory. WorkingDirectory string + // AdditionalDirectories are directories the agent may access beyond WorkingDirectory. + // Relative paths are resolved against WorkingDirectory. Re-supply them when resuming. + AdditionalDirectories []string // ConfigDirectory overrides the default configuration directory location. ConfigDirectory string // EnableConfigDiscovery enables runtime discovery of supported configuration. @@ -2289,6 +2295,7 @@ type createSessionRequest struct { RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` WorkingDirectory string `json:"workingDirectory,omitempty"` + AdditionalDirectories []string `json:"additionalDirectories,omitempty"` Streaming *bool `json:"streaming,omitempty"` IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` @@ -2379,6 +2386,7 @@ type resumeSessionRequest struct { RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` Hooks *bool `json:"hooks,omitempty"` WorkingDirectory string `json:"workingDirectory,omitempty"` + AdditionalDirectories []string `json:"additionalDirectories,omitempty"` ConfigDir string `json:"configDir,omitempty"` EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` SkipEmbeddingRetrieval *bool `json:"skipEmbeddingRetrieval,omitempty"` diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index 57d9a46a21..16c814f2e3 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -131,6 +131,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setHooks(true); } request.setWorkingDirectory(config.getWorkingDirectory()); + request.setAdditionalDirectories(config.getAdditionalDirectories()); if (config.isStreaming()) { request.setStreaming(true); } @@ -252,6 +253,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setHooks(true); } request.setWorkingDirectory(config.getWorkingDirectory()); + request.setAdditionalDirectories(config.getAdditionalDirectories()); request.setConfigDirectory(config.getConfigDirectory()); config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); config.getSkipEmbeddingRetrieval().ifPresent(request::setSkipEmbeddingRetrieval); diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 9a9f4f152f..eeb3b6669d 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -95,6 +95,9 @@ public final class CreateSessionRequest { @JsonProperty("workingDirectory") private String workingDirectory; + @JsonProperty("additionalDirectories") + private List additionalDirectories; + @JsonProperty("streaming") private Boolean streaming; @@ -492,6 +495,16 @@ public void setWorkingDirectory(String workingDirectory) { this.workingDirectory = workingDirectory; } + /** Gets additional directories. @return the additional directories */ + public List getAdditionalDirectories() { + return additionalDirectories; + } + + /** Sets additional directories. @param additionalDirectories the additional directories */ + public void setAdditionalDirectories(List additionalDirectories) { + this.additionalDirectories = additionalDirectories; + } + /** Gets streaming flag. @return the flag */ public Boolean getStreaming() { return streaming; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index b2adea7654..f8bbbee04c 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -67,6 +67,7 @@ public class ResumeSessionConfig { private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; + private List additionalDirectories; private String configDirectory; private Boolean enableConfigDiscovery; private Boolean skipEmbeddingRetrieval; @@ -811,6 +812,27 @@ public ResumeSessionConfig setWorkingDirectory(String workingDirectory) { return this; } + /** + * Gets the directories the agent may access beyond the working directory. + * + * @return the additional directory paths + */ + public List getAdditionalDirectories() { + return additionalDirectories; + } + + /** + * Sets directories the agent may access beyond the working directory. + * + * @param additionalDirectories + * the additional directory paths + * @return this config for method chaining + */ + public ResumeSessionConfig setAdditionalDirectories(List additionalDirectories) { + this.additionalDirectories = additionalDirectories; + return this; + } + /** * Gets the configuration directory path. * @@ -1832,6 +1854,9 @@ public ResumeSessionConfig clone() { copy.onUserInputRequest = this.onUserInputRequest; copy.hooks = this.hooks; copy.workingDirectory = this.workingDirectory; + copy.additionalDirectories = this.additionalDirectories != null + ? new ArrayList<>(this.additionalDirectories) + : null; copy.configDirectory = this.configDirectory; copy.enableConfigDiscovery = this.enableConfigDiscovery; copy.skipEmbeddingRetrieval = this.skipEmbeddingRetrieval; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index a81ed49dde..4b162fdf77 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -97,6 +97,9 @@ public final class ResumeSessionRequest { @JsonProperty("workingDirectory") private String workingDirectory; + @JsonProperty("additionalDirectories") + private List additionalDirectories; + @JsonProperty("configDir") private String configDirectory; @@ -497,6 +500,16 @@ public void setWorkingDirectory(String workingDirectory) { this.workingDirectory = workingDirectory; } + /** Gets additional directories. @return the additional directories */ + public List getAdditionalDirectories() { + return additionalDirectories; + } + + /** Sets additional directories. @param additionalDirectories the additional directories */ + public void setAdditionalDirectories(List additionalDirectories) { + this.additionalDirectories = additionalDirectories; + } + /** Gets config directory. @return the config directory */ public String getConfigDirectory() { return configDirectory; diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index a9af14506f..e4cc7da21d 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -67,6 +67,7 @@ public class SessionConfig { private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; + private List additionalDirectories; private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; @@ -861,6 +862,27 @@ public SessionConfig setWorkingDirectory(String workingDirectory) { return this; } + /** + * Gets the directories the agent may access beyond the working directory. + * + * @return the additional directory paths + */ + public List getAdditionalDirectories() { + return additionalDirectories; + } + + /** + * Sets directories the agent may access beyond the working directory. + * + * @param additionalDirectories + * the additional directory paths + * @return this config instance for method chaining + */ + public SessionConfig setAdditionalDirectories(List additionalDirectories) { + this.additionalDirectories = additionalDirectories; + return this; + } + /** * Returns whether streaming is enabled. * @@ -1967,6 +1989,9 @@ public SessionConfig clone() { copy.onUserInputRequest = this.onUserInputRequest; copy.hooks = this.hooks; copy.workingDirectory = this.workingDirectory; + copy.additionalDirectories = this.additionalDirectories != null + ? new ArrayList<>(this.additionalDirectories) + : null; copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; diff --git a/java/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/src/test/java/com/github/copilot/ConfigCloneTest.java index 6986ef7f0e..a8e7fb2e0b 100644 --- a/java/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -440,12 +440,15 @@ void resumeSessionConfigAllSetters() { void sessionConfigNewFieldsCloned() { SessionConfig original = new SessionConfig(); original.setGitHubToken("ghp_per_session_token"); + original.setAdditionalDirectories(new java.util.ArrayList<>(List.of("/repo/shared"))); DefaultAgentConfig defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); original.setDefaultAgent(defaultAgent); SessionConfig cloned = original.clone(); assertEquals("ghp_per_session_token", cloned.getGitHubToken()); + assertEquals(List.of("/repo/shared"), cloned.getAdditionalDirectories()); + assertNotSame(original.getAdditionalDirectories(), cloned.getAdditionalDirectories()); assertSame(defaultAgent, cloned.getDefaultAgent()); } @@ -453,12 +456,15 @@ void sessionConfigNewFieldsCloned() { void resumeSessionConfigNewFieldsCloned() { ResumeSessionConfig original = new ResumeSessionConfig(); original.setGitHubToken("ghp_per_session_token"); + original.setAdditionalDirectories(new java.util.ArrayList<>(List.of("/repo/resumed"))); DefaultAgentConfig defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); original.setDefaultAgent(defaultAgent); ResumeSessionConfig cloned = original.clone(); assertEquals("ghp_per_session_token", cloned.getGitHubToken()); + assertEquals(List.of("/repo/resumed"), cloned.getAdditionalDirectories()); + assertNotSame(original.getAdditionalDirectories(), cloned.getAdditionalDirectories()); assertSame(defaultAgent, cloned.getDefaultAgent()); } diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 652be026b1..dcaa6605ed 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -95,6 +95,14 @@ void testBuildCreateRequestSetsClientName() { assertEquals("my-app", request.getClientName()); } + @Test + void testBuildCreateRequestSetsAdditionalDirectories() { + var config = new SessionConfig() + .setAdditionalDirectories(List.of("/repo/shared", "/repo/generated")); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertEquals(List.of("/repo/shared", "/repo/generated"), request.getAdditionalDirectories()); + } + @Test void testBuildCreateRequestSetsReasoningSummary() { var config = new SessionConfig().setReasoningSummary("concise"); @@ -289,6 +297,14 @@ void testBuildResumeRequestSetsClientName() { assertEquals("my-app", request.getClientName()); } + @Test + void testBuildResumeRequestSetsAdditionalDirectories() { + var config = new ResumeSessionConfig().setAdditionalDirectories(List.of("/repo/resumed")); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-additional-directories", + config); + assertEquals(List.of("/repo/resumed"), request.getAdditionalDirectories()); + } + @Test void testBuildCreateRequestPropagatesGranularMultitenancyFields() { var config = new SessionConfig().setSkipEmbeddingRetrieval(true) diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 6d99ce49e3..bc6c52be38 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1562,6 +1562,7 @@ export class CopilotClient { requestAutoModeSwitch: !!config.onAutoModeSwitchRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, + additionalDirectories: config.additionalDirectories, streaming: config.streaming, includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true, ...(this.onGitHubTelemetry != null @@ -1794,6 +1795,7 @@ export class CopilotClient { requestAutoModeSwitch: !!config.onAutoModeSwitchRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, + additionalDirectories: config.additionalDirectories, configDir: config.configDirectory, enableConfigDiscovery: config.enableConfigDiscovery, skipEmbeddingRetrieval: config.skipEmbeddingRetrieval, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 3da5e3bc4c..143c518ab5 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2347,6 +2347,13 @@ export interface SessionConfigBase { */ workingDirectory?: string; + /** + * Additional directories the agent may access beyond the working directory. + * Relative paths are resolved against the session's working directory. + * Re-supply these directories when resuming a session. + */ + additionalDirectories?: string[]; + /** * Enable streaming of assistant message and reasoning chunks. * When true, ephemeral assistant.message_delta and assistant.reasoning_delta diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 77149bc4b9..085721bef5 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -184,6 +184,42 @@ describe("CopilotClient", () => { ); }); + it("forwards additional directories when creating and resuming sessions", 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" || method === "session.resume") { + return { sessionId: params.sessionId, workspacePath: "/workspace" }; + } + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + sessionId: "create-with-additional-directories", + additionalDirectories: ["/repo/shared", "/repo/generated"], + onPermissionRequest: approveAll, + }); + await client.resumeSession("resume-with-additional-directories", { + additionalDirectories: ["/repo/resumed"], + onPermissionRequest: approveAll, + }); + + expect(spy).toHaveBeenCalledWith( + "session.create", + expect.objectContaining({ + additionalDirectories: ["/repo/shared", "/repo/generated"], + }) + ); + expect(spy).toHaveBeenCalledWith( + "session.resume", + expect.objectContaining({ additionalDirectories: ["/repo/resumed"] }) + ); + }); + it("registers MCP OAuth interest after cloud create only when an auth handler is configured", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 6f29e96596..a7c3d5dbba 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2010,6 +2010,7 @@ async def create_session( on_user_input_request: UserInputHandler | None = None, hooks: SessionHooks | None = None, working_directory: str | None = None, + additional_directories: list[str] | None = None, provider: ProviderConfig | None = None, capi: CapiSessionOptions | None = None, providers: list[NamedProviderConfig] | None = None, @@ -2344,6 +2345,8 @@ async def create_session( # Add working directory if provided if working_directory: payload["workingDirectory"] = working_directory + if additional_directories: + payload["additionalDirectories"] = additional_directories # Add streaming option if provided if streaming is not None: @@ -2678,6 +2681,7 @@ async def resume_session( on_user_input_request: UserInputHandler | None = None, hooks: SessionHooks | None = None, working_directory: str | None = None, + additional_directories: list[str] | None = None, provider: ProviderConfig | None = None, capi: CapiSessionOptions | None = None, providers: list[NamedProviderConfig] | None = None, @@ -3036,6 +3040,8 @@ async def resume_session( if working_directory: payload["workingDirectory"] = working_directory + if additional_directories: + payload["additionalDirectories"] = additional_directories if config_directory: payload["configDir"] = config_directory if enable_config_discovery is not None: diff --git a/python/test_client.py b/python/test_client.py index a37d8dce2b..5a9d00f03b 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -162,6 +162,46 @@ async def test_resume_session_allows_none_permission_handler(self): class TestCreateSessionConfig: + @pytest.mark.asyncio + async def test_additional_directories_forwarded_on_create_and_resume(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + + async def mock_request(method, params, **kwargs): + captured.append((method, params)) + if method == "session.create": + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + if method == "session.resume": + return {"sessionId": params["sessionId"], "workspacePath": None} + return {} + + client._client.request = mock_request + await client.create_session( + session_id="create-with-additional-directories", + additional_directories=["/repo/shared", "/repo/generated"], + ) + await client.resume_session( + "resume-with-additional-directories", + additional_directories=["/repo/resumed"], + ) + + create_payload = next( + params for method, params in captured if method == "session.create" + ) + resume_payload = next( + params for method, params in captured if method == "session.resume" + ) + assert create_payload["additionalDirectories"] == ["/repo/shared", "/repo/generated"] + assert resume_payload["additionalDirectories"] == ["/repo/resumed"] + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_mcp_auth_handler_registers_interest_in_create_session(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/types.rs b/rust/src/types.rs index 11a92ad514..0270de3655 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1911,6 +1911,10 @@ pub struct SessionConfig { /// Working directory for the session. Tool operations resolve /// relative paths against this directory. pub working_directory: Option, + /// Additional directories the agent may access beyond the working directory. + /// Relative paths resolve against the session working directory. Re-supply + /// them when resuming a session. + pub additional_directories: Option>, /// Per-session GitHub token. Distinct from /// [`ClientOptions::github_token`](crate::ClientOptions::github_token), /// which authenticates the CLI process itself; this token determines @@ -2074,6 +2078,7 @@ impl std::fmt::Debug for SessionConfig { .field("memory", &self.memory) .field("config_directory", &self.config_directory) .field("working_directory", &self.working_directory) + .field("additional_directories", &self.additional_directories) .field( "github_token", &self.github_token.as_ref().map(|_| ""), @@ -2188,6 +2193,7 @@ impl Default for SessionConfig { memory: None, config_directory: None, working_directory: None, + additional_directories: None, github_token: None, remote_session: None, cloud: None, @@ -2346,6 +2352,7 @@ impl SessionConfig { memory: self.memory, config_dir: self.config_directory, working_directory: self.working_directory, + additional_directories: self.additional_directories, github_token: self.github_token, remote_session: self.remote_session, cloud: self.cloud, @@ -2868,6 +2875,16 @@ impl SessionConfig { self } + /// Set directories the agent may access beyond the working directory. + pub fn with_additional_directories(mut self, paths: I) -> Self + where + I: IntoIterator, + P: Into, + { + self.additional_directories = Some(paths.into_iter().map(Into::into).collect()); + self + } + /// Set the per-session GitHub token. Distinct from /// [`ClientOptions::github_token`](crate::ClientOptions::github_token); /// this token determines the GitHub identity used for content exclusion, @@ -3102,6 +3119,9 @@ pub struct ResumeSessionConfig { pub config_directory: Option, /// Per-session working directory on resume. pub working_directory: Option, + /// Additional directories the agent may access on resume. Relative paths + /// resolve against the session working directory. + pub additional_directories: Option>, /// Per-session GitHub token on resume. See /// [`SessionConfig::github_token`]. pub github_token: Option, @@ -3245,6 +3265,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("memory", &self.memory) .field("config_directory", &self.config_directory) .field("working_directory", &self.working_directory) + .field("additional_directories", &self.additional_directories) .field( "github_token", &self.github_token.as_ref().map(|_| ""), @@ -3402,6 +3423,7 @@ impl ResumeSessionConfig { memory: self.memory, config_dir: self.config_directory, working_directory: self.working_directory, + additional_directories: self.additional_directories, github_token: self.github_token, remote_session: self.remote_session, include_sub_agent_streaming_events: self.include_sub_agent_streaming_events, @@ -3493,6 +3515,7 @@ impl ResumeSessionConfig { memory: None, config_directory: None, working_directory: None, + additional_directories: None, github_token: None, remote_session: None, include_sub_agent_streaming_events: None, @@ -3991,6 +4014,16 @@ impl ResumeSessionConfig { self } + /// Set directories the agent may access beyond the working directory on resume. + pub fn with_additional_directories(mut self, paths: I) -> Self + where + I: IntoIterator, + P: Into, + { + self.additional_directories = Some(paths.into_iter().map(Into::into).collect()); + self + } + /// Set the per-session GitHub token on resume. See /// [`SessionConfig::github_token`] for distinction from the /// client-level token. @@ -6220,6 +6253,7 @@ mod tests { .with_agent("researcher") .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) .with_github_token("ghp_test") .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) .with_enable_session_telemetry(false) @@ -6257,6 +6291,10 @@ mod tests { assert_eq!(cfg.agent.as_deref(), Some("researcher")); assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!( cfg.capi, @@ -6291,6 +6329,7 @@ mod tests { .with_agent("researcher") .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) + .with_additional_directories([PathBuf::from("/tmp/shared")]) .with_github_token("ghp_test") .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) .with_enable_session_telemetry(false) @@ -6328,6 +6367,10 @@ mod tests { assert_eq!(cfg.agent.as_deref(), Some("researcher")); assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); + assert_eq!( + cfg.additional_directories.as_deref(), + Some(&[PathBuf::from("/tmp/shared")][..]) + ); assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!( cfg.capi, @@ -6362,6 +6405,29 @@ mod tests { assert!(json.get("continuePendingWork").is_none()); } + #[test] + fn session_configs_serialize_additional_directories() { + let create = SessionConfig::default().with_additional_directories([ + PathBuf::from("/tmp/shared"), + PathBuf::from("/tmp/generated"), + ]); + let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!( + create_json["additionalDirectories"], + serde_json::json!(["/tmp/shared", "/tmp/generated"]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_additional_directories([PathBuf::from("/tmp/resumed")]); + let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!( + resume_json["additionalDirectories"], + serde_json::json!(["/tmp/resumed"]) + ); + } + /// The Rust field is `suppress_resume_event`, but the wire field stays /// `disableResume` to preserve compatibility with the runtime and other /// SDKs. diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 2eabbe848d..c2bbbb334e 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -156,6 +156,8 @@ pub(crate) struct SessionCreateWire { pub config_dir: Option, #[serde(skip_serializing_if = "Option::is_none")] pub working_directory: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_directories: Option>, #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] pub github_token: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -292,6 +294,8 @@ pub(crate) struct SessionResumeWire { pub config_dir: Option, #[serde(skip_serializing_if = "Option::is_none")] pub working_directory: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_directories: Option>, #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")] pub github_token: Option, #[serde(skip_serializing_if = "Option::is_none")] From 796c4a41752138241384b8a5eadea5d464316deb Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 31 Jul 2026 12:17:31 +1000 Subject: [PATCH 2/5] fix: address SDK CI checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/Unit/ClientSessionLifetimeTests.cs | 13 +++++++------ .../github/copilot/rpc/CreateSessionRequest.java | 7 ++++++- .../github/copilot/rpc/ResumeSessionRequest.java | 7 ++++++- .../github/copilot/SessionRequestBuilderTest.java | 6 ++---- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index 181582d8c1..a30e6d3030 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -268,9 +268,10 @@ public async Task SessionRequests_Serialize_AdditionalDirectories() }); var createRequest = Assert.Single(server.Requests, request => request.Method == "session.create"); - Assert.Equal( - new string?[] { "/repo/shared", "/repo/generated" }, - createRequest.Params.GetProperty("additionalDirectories").EnumerateArray().Select(value => value.GetString())); + Assert.Collection( + createRequest.Params.GetProperty("additionalDirectories").EnumerateArray(), + value => Assert.Equal("/repo/shared", value.GetString()), + value => Assert.Equal("/repo/generated", value.GetString())); server.ClearRequests(); @@ -281,9 +282,9 @@ public async Task SessionRequests_Serialize_AdditionalDirectories() }); var resumeRequest = Assert.Single(server.Requests, request => request.Method == "session.resume"); - Assert.Equal( - new string?[] { "/repo/resumed" }, - resumeRequest.Params.GetProperty("additionalDirectories").EnumerateArray().Select(value => value.GetString())); + Assert.Collection( + resumeRequest.Params.GetProperty("additionalDirectories").EnumerateArray(), + value => Assert.Equal("/repo/resumed", value.GetString())); } [Fact] diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index eeb3b6669d..ddbbe8f2eb 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -500,7 +500,12 @@ public List getAdditionalDirectories() { return additionalDirectories; } - /** Sets additional directories. @param additionalDirectories the additional directories */ + /** + * Sets additional directories. + * + * @param additionalDirectories + * the additional directories + */ public void setAdditionalDirectories(List additionalDirectories) { this.additionalDirectories = additionalDirectories; } diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 4b162fdf77..7f9e6e865b 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -505,7 +505,12 @@ public List getAdditionalDirectories() { return additionalDirectories; } - /** Sets additional directories. @param additionalDirectories the additional directories */ + /** + * Sets additional directories. + * + * @param additionalDirectories + * the additional directories + */ public void setAdditionalDirectories(List additionalDirectories) { this.additionalDirectories = additionalDirectories; } diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index dcaa6605ed..5d135ba351 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -97,8 +97,7 @@ void testBuildCreateRequestSetsClientName() { @Test void testBuildCreateRequestSetsAdditionalDirectories() { - var config = new SessionConfig() - .setAdditionalDirectories(List.of("/repo/shared", "/repo/generated")); + var config = new SessionConfig().setAdditionalDirectories(List.of("/repo/shared", "/repo/generated")); CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); assertEquals(List.of("/repo/shared", "/repo/generated"), request.getAdditionalDirectories()); } @@ -300,8 +299,7 @@ void testBuildResumeRequestSetsClientName() { @Test void testBuildResumeRequestSetsAdditionalDirectories() { var config = new ResumeSessionConfig().setAdditionalDirectories(List.of("/repo/resumed")); - ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-additional-directories", - config); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-additional-directories", config); assertEquals(List.of("/repo/resumed"), request.getAdditionalDirectories()); } From 89d1287b617c1e870bb537949cdbec570777d065 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 31 Jul 2026 12:53:22 +1000 Subject: [PATCH 3/5] ci: rerun SDK checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From ce24aa322cbafce5ede91e50d759bb6e927179f3 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 31 Jul 2026 16:10:53 +1000 Subject: [PATCH 4/5] fix(rust): isolate E2E cache state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/tests/e2e/support.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 6ad609f58e..4d9de5536b 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -330,6 +330,9 @@ impl E2eContext { .as_os_str() .to_owned(), ), + ]); + env.extend(isolated_cache_environment(self.home_dir.path())); + env.extend([ ("COPILOT_MCP_APPS".into(), "true".into()), ("MCP_APPS".into(), "true".into()), ("GH_TOKEN".into(), DEFAULT_TEST_TOKEN.into()), @@ -783,6 +786,20 @@ fn canonical_temp_path(path: &Path) -> PathBuf { std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } +fn isolated_cache_environment(path: &Path) -> [(OsString, OsString); 2] { + let home_dir = canonical_temp_path(path); + let cache_dir = home_dir.join(".cache"); + // COPILOT_HOME does not redirect platform cache paths, so isolate the cache + // to prevent concurrent CLI processes from sharing mutable startup state. + [ + ( + "COPILOT_CACHE_HOME".into(), + cache_dir.join("copilot").into_os_string(), + ), + ("XDG_CACHE_HOME".into(), cache_dir.into_os_string()), + ] +} + struct CapiProxy { child: Option, proxy_url: String, @@ -1003,3 +1020,25 @@ fn node_program() -> &'static str { fn npx_program() -> &'static str { if cfg!(windows) { "npx.cmd" } else { "npx" } } + +#[test] +fn e2e_context_isolates_copilot_cache() { + let home_dir = tempfile::tempdir().expect("create test home"); + let home_dir = canonical_temp_path(home_dir.path()); + let cache_dir = home_dir.join(".cache"); + let expected = [ + ("COPILOT_CACHE_HOME", cache_dir.join("copilot")), + ("XDG_CACHE_HOME", cache_dir), + ]; + + let environment = isolated_cache_environment(&home_dir); + + for (key, value) in expected { + assert!( + environment.iter().any(|(actual_key, actual_value)| { + actual_key == key && actual_value == value.as_os_str() + }), + "{key} should use the isolated test home" + ); + } +} From 318d50afe7d94c2d67cbf05250175db07f7bbe07 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 3 Aug 2026 10:39:29 +1000 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index d548c344ee..2fbc4e7187 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -2773,7 +2773,7 @@ internal record CreateSessionRequest( [property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null, [property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null, bool? EnableGitHubTelemetryForwarding = null, - [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null; + [property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null, IList? AdditionalDirectories = null); #pragma warning restore GHCP001