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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,10 @@ public async Task<CopilotSession> 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)
{
Expand Down
9 changes: 9 additions & 0 deletions dotnet/test/Unit/ClientSessionLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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<string, object?>
{
["success"] = true
},
"session.eventLog.registerInterest" => new Dictionary<string, object?>
{
["id"] = "interest-1"
Expand Down
8 changes: 8 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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) {
Expand Down
16 changes: 14 additions & 2 deletions java/src/main/java/com/github/copilot/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -835,10 +836,21 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
sessions.put(returnedId, session);
}

return updateSessionOptionsForMode(session, config.getSkipCustomInstructions().orElse(null),
CompletableFuture<Void> 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,
Expand Down
3 changes: 3 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Comment thread
DonJayamanne marked this conversation as resolved.
if (config.onMcpAuthRequest) {
await this.connection!.sendRequest("session.eventLog.registerInterest", {
sessionId,
Expand Down
7 changes: 7 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
});

Expand All @@ -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",
});
Comment thread
DonJayamanne marked this conversation as resolved.
});

it("registers MCP OAuth interest after cloud create only when an auth handler is configured", async () => {
Expand Down
25 changes: 25 additions & 0 deletions nodejs/test/e2e/permissions.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];

Expand Down
3 changes: 3 additions & 0 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
ModelBillingTokenPrices,
ModelBillingTokenPricesLongContext, # noqa: F401
OpenCanvasInstance,
PermissionPathsAddParams,
RemoteSessionMode,
ServerRpc,
_ConnectResult,
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions python/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down
11 changes: 11 additions & 0 deletions rust/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
68 changes: 68 additions & 0 deletions rust/tests/session_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading