Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -1196,7 +1196,8 @@ public async Task<CopilotSession> 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();

Expand Down Expand Up @@ -1410,7 +1411,8 @@ public async Task<CopilotSession> 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<ResumeSessionResponse>(
Expand Down Expand Up @@ -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<string>? AdditionalDirectories = null);
#pragma warning restore GHCP001

internal record ToolDefinition(
Expand Down Expand Up @@ -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<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 @@ -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;
}

/// <summary>Client name to identify the application using the SDK.</summary>
Expand Down Expand Up @@ -3308,6 +3309,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 @@ -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);
Expand Down Expand Up @@ -154,6 +156,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 @@ -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");
Expand All @@ -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!);
Expand All @@ -203,6 +208,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 @@ -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");
Expand All @@ -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!);
Expand Down
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
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 @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
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 @@ -492,6 +495,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 @@ -67,6 +67,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 @@ -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<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 @@ -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;
Expand Down
Loading
Loading