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
40 changes: 40 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,13 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
if (_options.Mode == CopilotClientMode.Empty)
{
config.EnableSessionTelemetry ??= false;
config.SkipEmbeddingRetrieval ??= true;
config.EmbeddingCacheStorage ??= EmbeddingCacheStorageMode.InMemory;
config.EnableOnDemandInstructionDiscovery ??= false;
config.EnableFileHooks ??= false;
config.EnableHostGitOperations ??= false;
config.EnableSessionStore ??= false;
config.EnableSkills ??= false;
config.McpOAuthTokenStorage ??= McpOAuthTokenStorageMode.InMemory;
}
}
Expand Down Expand Up @@ -876,6 +883,14 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Agent,
config.ConfigDirectory,
config.EnableConfigDiscovery,
config.SkipEmbeddingRetrieval,
config.EmbeddingCacheStorage,
config.OrganizationCustomInstructions,
config.EnableOnDemandInstructionDiscovery,
config.EnableFileHooks,
config.EnableHostGitOperations,
config.EnableSessionStore,
config.EnableSkills,
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions,
Expand Down Expand Up @@ -1053,6 +1068,14 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.WorkingDirectory,
config.ConfigDirectory,
config.EnableConfigDiscovery,
config.SkipEmbeddingRetrieval,
config.EmbeddingCacheStorage,
config.OrganizationCustomInstructions,
config.EnableOnDemandInstructionDiscovery,
config.EnableFileHooks,
config.EnableHostGitOperations,
config.EnableSessionStore,
config.EnableSkills,
config.SuppressResumeEvent is true ? true : null,
config.Streaming is true ? true : null,
config.IncludeSubAgentStreamingEvents,
Expand Down Expand Up @@ -2179,6 +2202,14 @@ internal record CreateSessionRequest(
string? Agent,
[property: JsonPropertyName("configDir")] string? ConfigDirectory,
bool? EnableConfigDiscovery,
bool? SkipEmbeddingRetrieval,
EmbeddingCacheStorageMode? EmbeddingCacheStorage,
string? OrganizationCustomInstructions,
bool? EnableOnDemandInstructionDiscovery,
bool? EnableFileHooks,
bool? EnableHostGitOperations,
bool? EnableSessionStore,
bool? EnableSkills,
IList<string>? SkillDirectories,
IList<string>? DisabledSkills,
InfiniteSessionConfig? InfiniteSessions,
Expand Down Expand Up @@ -2247,6 +2278,14 @@ internal record ResumeSessionRequest(
string? WorkingDirectory,
[property: JsonPropertyName("configDir")] string? ConfigDirectory,
bool? EnableConfigDiscovery,
bool? SkipEmbeddingRetrieval,
EmbeddingCacheStorageMode? EmbeddingCacheStorage,
string? OrganizationCustomInstructions,
bool? EnableOnDemandInstructionDiscovery,
bool? EnableFileHooks,
bool? EnableHostGitOperations,
bool? EnableSessionStore,
bool? EnableSkills,
bool? SuppressResumeEvent,
bool? Streaming,
bool? IncludeSubAgentStreamingEvents,
Expand Down Expand Up @@ -2349,6 +2388,7 @@ internal record HooksInvokeResponse(
[JsonSerializable(typeof(GetSessionMetadataRequest))]
[JsonSerializable(typeof(GetSessionMetadataResponse))]
[JsonSerializable(typeof(McpOAuthTokenStorageMode))]
[JsonSerializable(typeof(EmbeddingCacheStorageMode))]
[JsonSerializable(typeof(ModelCapabilitiesOverride))]
[JsonSerializable(typeof(ProviderConfig))]
[JsonSerializable(typeof(ResumeSessionRequest))]
Expand Down
80 changes: 80 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,21 @@ public enum McpOAuthTokenStorageMode
InMemory
}

/// <summary>
/// Controls how the embedding cache is stored for a session.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<EmbeddingCacheStorageMode>))]
public enum EmbeddingCacheStorageMode
{
/// <summary>Embeddings are cached on disk, shared across sessions and restarts.</summary>
[JsonStringEnumMemberName("persistent")]
Persistent,

/// <summary>Embeddings are cached in memory only and discarded when the session ends.</summary>
[JsonStringEnumMemberName("in-memory")]
InMemory
}

/// <summary>
/// Abstract base class for MCP server configurations.
/// </summary>
Expand Down Expand Up @@ -2402,6 +2417,14 @@ protected SessionConfigBase(SessionConfigBase? other)
Agent = other.Agent;
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
EnableConfigDiscovery = other.EnableConfigDiscovery;
SkipEmbeddingRetrieval = other.SkipEmbeddingRetrieval;
EmbeddingCacheStorage = other.EmbeddingCacheStorage;
OrganizationCustomInstructions = other.OrganizationCustomInstructions;
EnableOnDemandInstructionDiscovery = other.EnableOnDemandInstructionDiscovery;
EnableFileHooks = other.EnableFileHooks;
EnableHostGitOperations = other.EnableHostGitOperations;
EnableSessionStore = other.EnableSessionStore;
EnableSkills = other.EnableSkills;
EnableMcpApps = other.EnableMcpApps;
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
Hooks = other.Hooks;
Expand Down Expand Up @@ -2492,6 +2515,63 @@ protected SessionConfigBase(SessionConfigBase? other)
/// </summary>
public bool? EnableConfigDiscovery { get; set; }

/// <summary>
/// When <see langword="true"/>, skips embedding-based retrieval for this session.
/// Use in multitenant deployments to prevent cross-session information leakage
/// through the shared embedding cache.
/// </summary>
public bool? SkipEmbeddingRetrieval { get; set; }

/// <summary>
/// Controls how the embedding cache is stored for this session.
/// <see cref="EmbeddingCacheStorageMode.Persistent"/>: Embeddings are cached on disk and shared across sessions/restarts.
/// <see cref="EmbeddingCacheStorageMode.InMemory"/>: Embeddings are cached in memory only and discarded when the session ends.
/// </summary>
public EmbeddingCacheStorageMode? EmbeddingCacheStorage { get; set; }

/// <summary>
/// Organization-level custom instructions to include in the system prompt.
/// Allows hosts to inject organization-specific guidance without relying on
/// filesystem-based instruction discovery.
/// </summary>
public string? OrganizationCustomInstructions { get; set; }

/// <summary>
/// When <see langword="true"/>, enables on-demand discovery of instruction files
/// (for example <c>AGENTS.md</c> and <c>.github/copilot-instructions.md</c>)
/// after successful file views.
/// </summary>
public bool? EnableOnDemandInstructionDiscovery { get; set; }

/// <summary>
/// When <see langword="true"/>, enables loading of file-based hooks from
/// <c>.github/hooks/</c>. This is separate from <see cref="Hooks"/>, which
/// controls SDK hook callback registration.
/// </summary>
public bool? EnableFileHooks { get; set; }

/// <summary>
/// When <see langword="true"/>, enables git operations on the host filesystem
/// such as branch detection, file status, and commit history. When
/// <see langword="false"/>, no git context is surfaced in the system prompt.
/// </summary>
public bool? EnableHostGitOperations { get; set; }

/// <summary>
/// When <see langword="true"/>, enables the cross-session store for search and
/// retrieval across sessions. When <see langword="false"/>, session content is
/// not written to or read from the shared session store.
/// </summary>
public bool? EnableSessionStore { get; set; }

/// <summary>
/// When <see langword="true"/>, enables skill loading, including built-in
/// skills and discovered skill directories. When <see langword="false"/>, no
/// skills are loaded regardless of <see cref="SkillDirectories"/> or
/// <see cref="EnableConfigDiscovery"/>.
/// </summary>
public bool? EnableSkills { get; set; }

/// <summary>
/// Custom tool declarations available to the language model during the session.
/// Declarations backed by an <see cref="AIFunction"/> are invoked automatically; declarations without one
Expand Down
152 changes: 152 additions & 0 deletions dotnet/test/E2E/ClientOptionsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,82 @@ public async Task Should_Omit_EnableSessionTelemetry_When_Not_Set()
await session.DisposeAsync();
}

[Fact]
public async Task Should_Forward_Granular_Multitenancy_Fields_In_Create_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();

await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
UseLoggedInUser = false,
});

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
{
SkipEmbeddingRetrieval = false,
OrganizationCustomInstructions = "Follow org policy.",
EnableOnDemandInstructionDiscovery = true,
EmbeddingCacheStorage = EmbeddingCacheStorageMode.Persistent,
EnableFileHooks = true,
EnableHostGitOperations = false,
EnableSessionStore = true,
EnableSkills = false,
OnPermissionRequest = PermissionHandler.ApproveAll,
});

using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create");
Assert.False(createRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
Assert.Equal("Follow org policy.", createRequest.GetProperty("organizationCustomInstructions").GetString());
Assert.True(createRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
Assert.Equal("persistent", createRequest.GetProperty("embeddingCacheStorage").GetString());
Assert.True(createRequest.GetProperty("enableFileHooks").GetBoolean());
Assert.False(createRequest.GetProperty("enableHostGitOperations").GetBoolean());
Assert.True(createRequest.GetProperty("enableSessionStore").GetBoolean());
Assert.False(createRequest.GetProperty("enableSkills").GetBoolean());

await session.DisposeAsync();
}

[Fact]
public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();

await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
Mode = CopilotClientMode.Empty,
BaseDirectory = Ctx.WorkDir,
UseLoggedInUser = false,
});

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated),
});
Comment thread
MackinnonBuck marked this conversation as resolved.

using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create");
Assert.False(createRequest.GetProperty("enableSessionTelemetry").GetBoolean());
Assert.True(createRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
Assert.False(createRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
Assert.Equal("in-memory", createRequest.GetProperty("embeddingCacheStorage").GetString());
Assert.False(createRequest.GetProperty("enableFileHooks").GetBoolean());
Assert.False(createRequest.GetProperty("enableHostGitOperations").GetBoolean());
Assert.False(createRequest.GetProperty("enableSessionStore").GetBoolean());
Assert.False(createRequest.GetProperty("enableSkills").GetBoolean());
Assert.False(createRequest.TryGetProperty("organizationCustomInstructions", out _));

await session.DisposeAsync();
}

[Fact]
public async Task Should_Propagate_Activity_TraceContext_To_Session_Create_And_Send()
{
Expand Down Expand Up @@ -293,6 +369,82 @@ public async Task Should_Propagate_Activity_TraceContext_To_Session_Resume()
await session.DisposeAsync();
}

[Fact]
public async Task Should_Forward_Granular_Multitenancy_Fields_In_Resume_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();

await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
UseLoggedInUser = false,
});

await client.StartAsync();

var session = await client.ResumeSessionAsync("resume-session", new ResumeSessionConfig
{
SkipEmbeddingRetrieval = false,
OrganizationCustomInstructions = "Resume org policy.",
EnableOnDemandInstructionDiscovery = true,
EmbeddingCacheStorage = EmbeddingCacheStorageMode.Persistent,
EnableFileHooks = true,
EnableHostGitOperations = false,
EnableSessionStore = true,
EnableSkills = false,
OnPermissionRequest = PermissionHandler.ApproveAll,
});

using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
var resumeRequest = GetCapturedRequestParams(capture.RootElement, "session.resume");
Assert.False(resumeRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
Assert.Equal("Resume org policy.", resumeRequest.GetProperty("organizationCustomInstructions").GetString());
Assert.True(resumeRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
Assert.Equal("persistent", resumeRequest.GetProperty("embeddingCacheStorage").GetString());
Assert.True(resumeRequest.GetProperty("enableFileHooks").GetBoolean());
Assert.False(resumeRequest.GetProperty("enableHostGitOperations").GetBoolean());
Assert.True(resumeRequest.GetProperty("enableSessionStore").GetBoolean());
Assert.False(resumeRequest.GetProperty("enableSkills").GetBoolean());

await session.DisposeAsync();
}

[Fact]
public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();

await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
Mode = CopilotClientMode.Empty,
BaseDirectory = Ctx.WorkDir,
UseLoggedInUser = false,
});

await client.StartAsync();

var session = await client.ResumeSessionAsync("resume-empty-session", new ResumeSessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated),
});
Comment thread
MackinnonBuck marked this conversation as resolved.

using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
var resumeRequest = GetCapturedRequestParams(capture.RootElement, "session.resume");
Assert.False(resumeRequest.GetProperty("enableSessionTelemetry").GetBoolean());
Assert.True(resumeRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
Assert.False(resumeRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
Assert.Equal("in-memory", resumeRequest.GetProperty("embeddingCacheStorage").GetString());
Assert.False(resumeRequest.GetProperty("enableFileHooks").GetBoolean());
Assert.False(resumeRequest.GetProperty("enableHostGitOperations").GetBoolean());
Assert.False(resumeRequest.GetProperty("enableSessionStore").GetBoolean());
Assert.False(resumeRequest.GetProperty("enableSkills").GetBoolean());
Assert.False(resumeRequest.TryGetProperty("organizationCustomInstructions", out _));

await session.DisposeAsync();
}

[Fact]
public void Should_Accept_GitHubToken_Option()
{
Expand Down
16 changes: 16 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,14 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
if config.EnableConfigDiscovery {
req.EnableConfigDiscovery = Bool(true)
}
req.SkipEmbeddingRetrieval = config.SkipEmbeddingRetrieval
req.EmbeddingCacheStorage = config.EmbeddingCacheStorage
req.OrganizationCustomInstructions = config.OrganizationCustomInstructions
req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery
req.EnableFileHooks = config.EnableFileHooks
req.EnableHostGitOperations = config.EnableHostGitOperations
req.EnableSessionStore = config.EnableSessionStore
req.EnableSkills = config.EnableSkills
req.Tools = config.Tools
systemMessage := c.systemMessageForMode(config.SystemMessage)
wireSystemMessage, transformCallbacks := extractTransformCallbacks(systemMessage)
Expand Down Expand Up @@ -952,6 +960,14 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
if config.EnableConfigDiscovery {
req.EnableConfigDiscovery = Bool(true)
}
req.SkipEmbeddingRetrieval = config.SkipEmbeddingRetrieval
req.EmbeddingCacheStorage = config.EmbeddingCacheStorage
req.OrganizationCustomInstructions = config.OrganizationCustomInstructions
req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery
req.EnableFileHooks = config.EnableFileHooks
req.EnableHostGitOperations = config.EnableHostGitOperations
req.EnableSessionStore = config.EnableSessionStore
req.EnableSkills = config.EnableSkills
if config.SuppressResumeEvent {
req.DisableResume = Bool(true)
}
Expand Down
Loading
Loading