From 10885673a8c1caba279e45c43b7dac9c8faa37bf Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 4 Aug 2026 05:32:50 +1000 Subject: [PATCH 1/3] feat: add handling for additional directories in session permissions --- nodejs/src/client.ts | 3 +++ nodejs/test/client.test.ts | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 3e100edddd..2949f4c54b 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1862,6 +1862,9 @@ export class CopilotClient { session["_workspacePath"] = workspacePath; session.setCapabilities(capabilities); session.setOpenCanvases(openCanvases ?? []); + for (const path of config.additionalDirectories ?? []) { + await session.rpc.permissions.paths.add({ path }); + } if (config.onMcpAuthRequest) { await this.connection!.sendRequest("session.eventLog.registerInterest", { sessionId, diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index bbe6fbe666..d45d5348f3 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -281,6 +281,9 @@ describe("CopilotClient", () => { if (method === "session.create" || method === "session.resume") { return { sessionId: params.sessionId, workspacePath: "/workspace" }; } + if (method === "session.permissions.paths.add") { + return { success: true }; + } throw new Error(`Unexpected method: ${method}`); }); @@ -304,6 +307,10 @@ describe("CopilotClient", () => { "session.resume", expect.objectContaining({ additionalDirectories: ["/repo/resumed"] }) ); + expect(spy).toHaveBeenCalledWith("session.permissions.paths.add", { + sessionId: "resume-with-additional-directories", + path: "/repo/resumed", + }); }); it("registers MCP OAuth interest after cloud create only when an auth handler is configured", async () => { From 35fa9e8e4cbfe3535e2b3dc099f3424851c66440 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 4 Aug 2026 06:06:49 +1000 Subject: [PATCH 2/3] feat: add support for additional directories in session resume across all SDKs --- dotnet/src/Client.cs | 4 +++ .../test/Unit/ClientSessionLifetimeTests.cs | 9 +++++++ go/client.go | 8 ++++++ go/client_test.go | 15 +++++++++++ .../com/github/copilot/CopilotClient.java | 16 ++++++++++-- nodejs/test/e2e/permissions.e2e.test.ts | 25 +++++++++++++++++++ python/copilot/client.py | 3 +++ python/test_client.py | 9 +++++++ rust/src/session.rs | 11 ++++++++ 9 files changed, 98 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index b1199dac8a..91b69570cc 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1435,6 +1435,10 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes session.WorkspacePath = response.WorkspacePath; session.SetCapabilities(response.Capabilities); session.SetOpenCanvases(response.OpenCanvases); + foreach (var path in config.AdditionalDirectories ?? []) + { + await session.Rpc.Permissions.Paths.AddAsync(path, cancellationToken).ConfigureAwait(false); + } if (config.OnMcpAuthRequest is not null) { diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index a30e6d3030..362fd1bb87 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -285,6 +285,11 @@ public async Task SessionRequests_Serialize_AdditionalDirectories() Assert.Collection( resumeRequest.Params.GetProperty("additionalDirectories").EnumerateArray(), value => Assert.Equal("/repo/resumed", value.GetString())); + var addPathRequest = Assert.Single( + server.Requests, + request => request.Method == "session.permissions.paths.add"); + Assert.Equal("resume-with-additional-directories", addPathRequest.Params.GetProperty("sessionId").GetString()); + Assert.Equal("/repo/resumed", addPathRequest.Params.GetProperty("path").GetString()); } [Fact] @@ -688,6 +693,10 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel }, "session.create" => CreateSessionResult(request), "session.resume" => CreateSessionResult(request), + "session.permissions.paths.add" => new Dictionary + { + ["success"] = true + }, "session.eventLog.registerInterest" => new Dictionary { ["id"] = "interest-1" diff --git a/go/client.go b/go/client.go index f867b7db28..3351cd03bf 100644 --- a/go/client.go +++ b/go/client.go @@ -1333,6 +1333,14 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, session.workspacePath = response.WorkspacePath session.setCapabilities(response.Capabilities) session.setOpenCanvases(response.OpenCanvases) + for _, path := range config.AdditionalDirectories { + if _, err := session.RPC.Permissions.Paths().Add(ctx, &rpc.PermissionPathsAddParams{Path: path}); err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to add additional directory after resume: %w", err) + } + } if err := c.updateSessionOptionsForMode(ctx, session, optBackInFields{ SkipCustomInstructions: config.SkipCustomInstructions, diff --git a/go/client_test.go b/go/client_test.go index 14131bc4ce..e0364d3506 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -280,6 +280,11 @@ func TestClient_ForwardsAdditionalDirectoriesToSessionRequests(t *testing.T) { resumeParams <- append(json.RawMessage(nil), params...) return []byte(`{"sessionId":"resumed-additional-directories","workspacePath":"/workspace"}`), nil }) + addPathParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.permissions.paths.add", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + addPathParams <- append(json.RawMessage(nil), params...) + return []byte(`{"success":true}`), nil + }) _, err = client.ResumeSessionWithOptions( t.Context(), @@ -290,6 +295,16 @@ func TestClient_ForwardsAdditionalDirectoriesToSessionRequests(t *testing.T) { t.Fatalf("ResumeSessionWithOptions failed: %v", err) } assertAdditionalDirectories(t, <-resumeParams, []string{"/repo/resumed"}) + var addPathPayload struct { + SessionID string `json:"sessionId"` + Path string `json:"path"` + } + if err := json.Unmarshal(<-addPathParams, &addPathPayload); err != nil { + t.Fatalf("failed to decode permissions path add params: %v", err) + } + if addPathPayload.SessionID != "resumed-additional-directories" || addPathPayload.Path != "/repo/resumed" { + t.Fatalf("permissions path add params = %+v", addPathPayload) + } } func assertAdditionalDirectories(t *testing.T, params json.RawMessage, want []string) { diff --git a/java/src/main/java/com/github/copilot/CopilotClient.java b/java/src/main/java/com/github/copilot/CopilotClient.java index 44878b87ec..1903cccf13 100644 --- a/java/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/src/main/java/com/github/copilot/CopilotClient.java @@ -30,6 +30,7 @@ import com.github.copilot.generated.rpc.GitHubTelemetryNotification; import com.github.copilot.generated.rpc.ServerRpc; import com.github.copilot.generated.rpc.SessionEventLogRegisterInterestParams; +import com.github.copilot.generated.rpc.SessionPermissionsPathsAddParams; import com.github.copilot.rpc.DeleteSessionResponse; import com.github.copilot.rpc.GetAuthStatusResponse; import com.github.copilot.rpc.GetLastSessionIdResponse; @@ -835,10 +836,21 @@ public CompletableFuture resumeSession(String sessionId, ResumeS sessions.put(returnedId, session); } - return updateSessionOptionsForMode(session, config.getSkipCustomInstructions().orElse(null), + CompletableFuture additionalDirectories = CompletableFuture.completedFuture(null); + if (config.getAdditionalDirectories() != null) { + for (String path : config.getAdditionalDirectories()) { + additionalDirectories = additionalDirectories + .thenCompose(v -> session.getRpc().permissions.paths + .add(new SessionPermissionsPathsAddParams(session.getSessionId(), path)) + .thenApply(ignored -> null)); + } + } + + return additionalDirectories.thenCompose(v -> updateSessionOptionsForMode(session, + config.getSkipCustomInstructions().orElse(null), config.getCustomAgentsLocalOnly().orElse(null), config.getCoauthorEnabled().orElse(null), - config.getManageScheduleEnabled().orElse(null)).thenApply(v -> { + config.getManageScheduleEnabled().orElse(null))).thenApply(v -> { LoggingHelpers.logTiming(LOG, Level.FINE, "CopilotClient.resumeSession complete. Elapsed={Elapsed}, SessionId=" + sessionId, diff --git a/nodejs/test/e2e/permissions.e2e.test.ts b/nodejs/test/e2e/permissions.e2e.test.ts index e7c26a2930..72b21147bb 100644 --- a/nodejs/test/e2e/permissions.e2e.test.ts +++ b/nodejs/test/e2e/permissions.e2e.test.ts @@ -19,6 +19,31 @@ import { getFinalAssistantMessage, getNextEventOfType } from "./harness/sdkTestH describe("Permission callbacks", async () => { const { copilotClient: client, workDir } = await createSdkTestContext(); + it("should apply additional directories when resuming an active session", async () => { + const additionalDirectory = realpathSync(join(import.meta.dirname, "../../examples")); + const pathToCheck = join(additionalDirectory, "basic-example.ts"); + + const created = await client.createSession({ onPermissionRequest: approveAll }); + expect( + await created.rpc.permissions.paths.isPathWithinAllowedDirectories({ + path: pathToCheck, + }) + ).toEqual({ allowed: false }); + + const resumed = await client.resumeSession(created.sessionId, { + additionalDirectories: [additionalDirectory], + onPermissionRequest: approveAll, + }); + + expect( + await resumed.rpc.permissions.paths.isPathWithinAllowedDirectories({ + path: pathToCheck, + }) + ).toEqual({ allowed: true }); + + await resumed.disconnect(); + }); + it("should invoke permission handler for write operations", async () => { const permissionRequests: PermissionRequest[] = []; diff --git a/python/copilot/client.py b/python/copilot/client.py index 737619ef35..c7eb71e8da 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -72,6 +72,7 @@ ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, # noqa: F401 OpenCanvasInstance, + PermissionPathsAddParams, RemoteSessionMode, ServerRpc, _ConnectResult, @@ -3289,6 +3290,8 @@ async def resume_session( session._set_open_canvases( [OpenCanvasInstance.from_dict(inst) for inst in open_canvases_raw] ) + for path in additional_directories or []: + await session.rpc.permissions.paths.add(PermissionPathsAddParams(path)) if on_mcp_auth_request is not None: await self._client.request( "session.eventLog.registerInterest", diff --git a/python/test_client.py b/python/test_client.py index 0bba1ccd7f..5604c3dd12 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -180,6 +180,8 @@ async def mock_request(method, params, **kwargs): return result if method == "session.resume": return {"sessionId": params["sessionId"], "workspacePath": None} + if method == "session.permissions.paths.add": + return {"success": True} return {} client._client.request = mock_request @@ -200,6 +202,13 @@ async def mock_request(method, params, **kwargs): ) assert create_payload["additionalDirectories"] == ["/repo/shared", "/repo/generated"] assert resume_payload["additionalDirectories"] == ["/repo/resumed"] + assert ( + "session.permissions.paths.add", + { + "sessionId": "resume-with-additional-directories", + "path": "/repo/resumed", + }, + ) in captured finally: await client.force_stop() diff --git a/rust/src/session.rs b/rust/src/session.rs index d505541a50..d18fda025a 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1159,6 +1159,7 @@ impl Client { let opt_custom_agents_local_only = config.custom_agents_local_only; let opt_coauthor_enabled = config.coauthor_enabled; let opt_manage_schedule_enabled = config.manage_schedule_enabled; + let additional_directories = config.additional_directories.clone().unwrap_or_default(); let (mut wire, mut runtime) = config.into_wire()?; wire.enable_github_telemetry_forwarding = self.inner.on_github_telemetry.is_some().then_some(true); @@ -1276,6 +1277,16 @@ impl Client { if has_mcp_auth_handler { register_mcp_auth_interest(self, &session_id).await?; } + for path in additional_directories { + self.call( + rpc_methods::SESSION_PERMISSIONS_PATHS_ADD, + Some(serde_json::json!({ + "sessionId": session_id, + "path": path, + })), + ) + .await?; + } // Reload skills after resume (best-effort). let skills_reload_start = Instant::now(); From cebfcd888a88c9cf29dd2f48b9db43f0c88cf24f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 4 Aug 2026 06:56:07 +1000 Subject: [PATCH 3/3] feat: add test for resuming session with additional directories in permissions --- rust/tests/session_test.rs | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 41cae3e950..a1d01e8d97 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -494,6 +494,74 @@ async fn resume_session_registers_mcp_auth_interest_only_with_handler() { let _session = timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn resume_session_adds_additional_directories_to_permissions() { + use github_copilot_sdk::types::ResumeSessionConfig; + + let temp_dir = tempfile::tempdir().unwrap(); + let additional_directories = [ + temp_dir.path().join("first"), + temp_dir.path().join("second"), + ]; + let (client, mut server_read, mut server_write) = make_client(); + let resume_handle = tokio::spawn({ + let client = client.clone(); + let additional_directories = additional_directories.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::from( + "session-with-additional-directories", + )) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_additional_directories(additional_directories), + ) + .await + .unwrap() + } + }); + + let resume_req = timeout(TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap(); + assert_eq!(resume_req["method"], "session.resume"); + server_respond_create( + &mut server_write, + &resume_req, + "session-with-additional-directories", + ) + .await; + + for expected_path in additional_directories { + let add_req = timeout(TIMEOUT, read_framed(&mut server_read)) + .await + .unwrap(); + assert_eq!(add_req["method"], "session.permissions.paths.add"); + assert_eq!( + add_req["params"]["sessionId"], + "session-with-additional-directories" + ); + assert_eq!( + add_req["params"]["path"], + serde_json::to_value(expected_path).unwrap() + ); + let id = add_req["id"].as_u64().unwrap(); + write_framed( + &mut server_write, + &serde_json::to_vec(&serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "success": true }, + })) + .unwrap(), + ) + .await; + } + + respond_to_reload(&mut server_read, &mut server_write).await; + let _session = timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); +} + async fn server_respond_create( writer: &mut (impl AsyncWrite + Unpin), request: &Value,