Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/troubleshooting/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
12 changes: 8 additions & 4 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,7 +1203,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
ExpAssignments: config.ExpAssignments,
EnableManagedSettings: config.EnableManagedSettings,
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null);
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
AdditionalDirectories: config.AdditionalDirectories);

var rpcTimestamp = Stopwatch.GetTimestamp();

Expand Down Expand Up @@ -1420,7 +1421,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
ExpAssignments: config.ExpAssignments,
EnableManagedSettings: config.EnableManagedSettings,
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null);
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
AdditionalDirectories: config.AdditionalDirectories);

var rpcTimestamp = Stopwatch.GetTimestamp();
var response = await InvokeRpcAsync<ResumeSessionResponse>(
Expand Down Expand Up @@ -2775,7 +2777,8 @@ 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<string>? AdditionalDirectories = null);
#pragma warning restore GHCP001

internal record ToolDefinition(
Expand Down Expand Up @@ -2884,7 +2887,8 @@ internal record ResumeSessionRequest(
[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<string>? AdditionalDirectories = null);
#pragma warning restore GHCP001

internal record ResumeSessionResponse(
Expand Down
7 changes: 7 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3099,6 +3099,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;
}

/// <summary>Client name to identify the application using the SDK.</summary>
Expand Down Expand Up @@ -3372,6 +3373,12 @@ protected SessionConfigBase(SessionConfigBase? other)
/// <summary>Working directory for the session.</summary>
public string? WorkingDirectory { get; set; }

/// <summary>
/// Additional directories the agent may access beyond <see cref="WorkingDirectory"/>.
/// Relative paths resolve against the session working directory. Re-supply them when resuming.
/// </summary>
public IList<string>? AdditionalDirectories { get; set; }

/// <summary>
/// Enable streaming of assistant message and reasoning chunks.
/// When true, assistant.message_delta and assistant.reasoning_delta events
Expand Down
32 changes: 32 additions & 0 deletions dotnet/test/Unit/ClientSessionLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,38 @@ 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.Collection(
createRequest.Params.GetProperty("additionalDirectories").EnumerateArray(),
value => Assert.Equal("/repo/shared", value.GetString()),
value => Assert.Equal("/repo/generated", 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.Collection(
resumeRequest.Params.GetProperty("additionalDirectories").EnumerateArray(),
value => Assert.Equal("/repo/resumed", value.GetString()));
}

[Fact]
public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured()
{
Expand Down
8 changes: 8 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -120,6 +121,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);
Expand Down Expand Up @@ -156,6 +158,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
ExcludedBuiltInAgents = ["explore"],
McpServers = new Dictionary<string, McpServerConfig> { ["s1"] = new McpStdioServerConfig { Command = "echo" } },
CustomAgents = [new CustomAgentConfig { Name = "a1" }],
AdditionalDirectories = ["/shared"],
SkillDirectories = ["/skills"],
InstructionDirectories = ["/instructions"],
DisabledSkills = ["skill1"],
Expand All @@ -169,6 +172,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");
Expand All @@ -179,6 +183,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!);
Expand All @@ -205,6 +210,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
ExcludedBuiltInAgents = ["explore"],
McpServers = new Dictionary<string, McpServerConfig> { ["s1"] = new McpStdioServerConfig { Command = "echo" } },
CustomAgents = [new CustomAgentConfig { Name = "a1" }],
AdditionalDirectories = ["/shared"],
SkillDirectories = ["/skills"],
InstructionDirectories = ["/instructions"],
DisabledSkills = ["skill1"],
Expand All @@ -218,6 +224,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");
Expand All @@ -228,6 +235,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!);
Expand Down
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,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"
Expand Down Expand Up @@ -1165,6 +1166,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
Expand Down
54 changes: 54 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,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.
Expand Down Expand Up @@ -1740,6 +1743,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.
Expand Down Expand Up @@ -2319,6 +2325,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"`
Expand Down Expand Up @@ -2411,6 +2418,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"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,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);
}
Expand Down Expand Up @@ -272,6 +273,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public final class CreateSessionRequest {
@JsonProperty("workingDirectory")
private String workingDirectory;

@JsonProperty("additionalDirectories")
private List<String> additionalDirectories;

@JsonProperty("streaming")
private Boolean streaming;

Expand Down Expand Up @@ -502,6 +505,21 @@ public void setWorkingDirectory(String workingDirectory) {
this.workingDirectory = workingDirectory;
}

/** Gets additional directories. @return the additional directories */
public List<String> getAdditionalDirectories() {
return additionalDirectories;
}

/**
* Sets additional directories.
*
* @param additionalDirectories
* the additional directories
*/
public void setAdditionalDirectories(List<String> additionalDirectories) {
this.additionalDirectories = additionalDirectories;
}

/** Gets streaming flag. @return the flag */
public Boolean getStreaming() {
return streaming;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public class ResumeSessionConfig {
private UserInputHandler onUserInputRequest;
private SessionHooks hooks;
private String workingDirectory;
private List<String> additionalDirectories;
private String configDirectory;
private Boolean enableConfigDiscovery;
private Boolean skipEmbeddingRetrieval;
Expand Down Expand Up @@ -859,6 +860,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<String> 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<String> additionalDirectories) {
this.additionalDirectories = additionalDirectories;
return this;
}

/**
* Gets the configuration directory path.
*
Expand Down Expand Up @@ -1902,6 +1924,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;
Expand Down
Loading
Loading