Skip to content

Commit 137a592

Browse files
feat: add granular per-session flags for multitenancy hardening
Add 7 new optional fields to SessionConfigBase across all 6 SDK languages (Node.js, Go, Python, .NET, Rust, Java): - skipEmbeddingRetrieval: prevent cross-session info leakage via embedding cache - organizationCustomInstructions: inject org-level instructions into system prompt - enableOnDemandInstructionDiscovery: control instruction file discovery after file views - enableFileHooks: control loading of file-based hooks from .github/hooks/ - enableHostGitOperations: control git operations on host filesystem - enableSessionStore: control cross-session store for search/retrieval - enableSkills: control skill loading (builtin + discovered) All fields are optional and pass through to the runtime without SDK-side default coercion. Empty mode applies restrictive defaults for multitenancy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7780b50 commit 137a592

24 files changed

Lines changed: 1937 additions & 9 deletions

dotnet/src/Client.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,12 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
572572
if (_options.Mode == CopilotClientMode.Empty)
573573
{
574574
config.EnableSessionTelemetry ??= false;
575+
config.SkipEmbeddingRetrieval ??= true;
576+
config.EnableOnDemandInstructionDiscovery ??= false;
577+
config.EnableFileHooks ??= false;
578+
config.EnableHostGitOperations ??= false;
579+
config.EnableSessionStore ??= false;
580+
config.EnableSkills ??= false;
575581
}
576582
}
577583

@@ -829,6 +835,13 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
829835
config.Agent,
830836
config.ConfigDir,
831837
config.EnableConfigDiscovery,
838+
config.SkipEmbeddingRetrieval,
839+
config.OrganizationCustomInstructions,
840+
config.EnableOnDemandInstructionDiscovery,
841+
config.EnableFileHooks,
842+
config.EnableHostGitOperations,
843+
config.EnableSessionStore,
844+
config.EnableSkills,
832845
config.SkillDirectories,
833846
config.DisabledSkills,
834847
config.InfiniteSessions,
@@ -995,6 +1008,13 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
9951008
config.WorkingDirectory,
9961009
config.ConfigDir,
9971010
config.EnableConfigDiscovery,
1011+
config.SkipEmbeddingRetrieval,
1012+
config.OrganizationCustomInstructions,
1013+
config.EnableOnDemandInstructionDiscovery,
1014+
config.EnableFileHooks,
1015+
config.EnableHostGitOperations,
1016+
config.EnableSessionStore,
1017+
config.EnableSkills,
9981018
config.SuppressResumeEvent is true ? true : null,
9991019
config.Streaming is true ? true : null,
10001020
config.IncludeSubAgentStreamingEvents,
@@ -2110,6 +2130,13 @@ internal record CreateSessionRequest(
21102130
string? Agent,
21112131
string? ConfigDir,
21122132
bool? EnableConfigDiscovery,
2133+
bool? SkipEmbeddingRetrieval,
2134+
string? OrganizationCustomInstructions,
2135+
bool? EnableOnDemandInstructionDiscovery,
2136+
bool? EnableFileHooks,
2137+
bool? EnableHostGitOperations,
2138+
bool? EnableSessionStore,
2139+
bool? EnableSkills,
21132140
IList<string>? SkillDirectories,
21142141
IList<string>? DisabledSkills,
21152142
InfiniteSessionConfig? InfiniteSessions,
@@ -2174,6 +2201,13 @@ internal record ResumeSessionRequest(
21742201
string? WorkingDirectory,
21752202
string? ConfigDir,
21762203
bool? EnableConfigDiscovery,
2204+
bool? SkipEmbeddingRetrieval,
2205+
string? OrganizationCustomInstructions,
2206+
bool? EnableOnDemandInstructionDiscovery,
2207+
bool? EnableFileHooks,
2208+
bool? EnableHostGitOperations,
2209+
bool? EnableSessionStore,
2210+
bool? EnableSkills,
21772211
bool? SuppressResumeEvent,
21782212
bool? Streaming,
21792213
bool? IncludeSubAgentStreamingEvents,

dotnet/src/Types.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2345,6 +2345,13 @@ protected SessionConfigBase(SessionConfigBase? other)
23452345
Agent = other.Agent;
23462346
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
23472347
EnableConfigDiscovery = other.EnableConfigDiscovery;
2348+
SkipEmbeddingRetrieval = other.SkipEmbeddingRetrieval;
2349+
OrganizationCustomInstructions = other.OrganizationCustomInstructions;
2350+
EnableOnDemandInstructionDiscovery = other.EnableOnDemandInstructionDiscovery;
2351+
EnableFileHooks = other.EnableFileHooks;
2352+
EnableHostGitOperations = other.EnableHostGitOperations;
2353+
EnableSessionStore = other.EnableSessionStore;
2354+
EnableSkills = other.EnableSkills;
23482355
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
23492356
Hooks = other.Hooks;
23502357
InfiniteSessions = other.InfiniteSessions;
@@ -2422,6 +2429,56 @@ protected SessionConfigBase(SessionConfigBase? other)
24222429
/// </summary>
24232430
public bool? EnableConfigDiscovery { get; set; }
24242431

2432+
/// <summary>
2433+
/// When <see langword="true"/>, skips embedding-based retrieval for this session.
2434+
/// Use in multitenant deployments to prevent cross-session information leakage
2435+
/// through the shared embedding cache.
2436+
/// </summary>
2437+
public bool? SkipEmbeddingRetrieval { get; set; }
2438+
2439+
/// <summary>
2440+
/// Organization-level custom instructions to include in the system prompt.
2441+
/// Allows hosts to inject organization-specific guidance without relying on
2442+
/// filesystem-based instruction discovery.
2443+
/// </summary>
2444+
public string? OrganizationCustomInstructions { get; set; }
2445+
2446+
/// <summary>
2447+
/// When <see langword="true"/>, enables on-demand discovery of instruction files
2448+
/// (for example <c>AGENTS.md</c> and <c>.github/copilot-instructions.md</c>)
2449+
/// after successful file views.
2450+
/// </summary>
2451+
public bool? EnableOnDemandInstructionDiscovery { get; set; }
2452+
2453+
/// <summary>
2454+
/// When <see langword="true"/>, enables loading of file-based hooks from
2455+
/// <c>.github/hooks/</c>. This is separate from <see cref="Hooks"/>, which
2456+
/// controls SDK hook callback registration.
2457+
/// </summary>
2458+
public bool? EnableFileHooks { get; set; }
2459+
2460+
/// <summary>
2461+
/// When <see langword="true"/>, enables git operations on the host filesystem
2462+
/// such as branch detection, file status, and commit history. When
2463+
/// <see langword="false"/>, no git context is surfaced in the system prompt.
2464+
/// </summary>
2465+
public bool? EnableHostGitOperations { get; set; }
2466+
2467+
/// <summary>
2468+
/// When <see langword="true"/>, enables the cross-session store for search and
2469+
/// retrieval across sessions. When <see langword="false"/>, session content is
2470+
/// not written to or read from the shared session store.
2471+
/// </summary>
2472+
public bool? EnableSessionStore { get; set; }
2473+
2474+
/// <summary>
2475+
/// When <see langword="true"/>, enables skill loading, including built-in
2476+
/// skills and discovered skill directories. When <see langword="false"/>, no
2477+
/// skills are loaded regardless of <see cref="SkillDirectories"/> or
2478+
/// <see cref="EnableConfigDiscovery"/>.
2479+
/// </summary>
2480+
public bool? EnableSkills { get; set; }
2481+
24252482
/// <summary>
24262483
/// Custom tool declarations available to the language model during the session.
24272484
/// Declarations backed by an <see cref="AIFunction"/> are invoked automatically; declarations without one

dotnet/test/E2E/ClientOptionsE2ETests.cs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,77 @@ public async Task Should_Omit_EnableSessionTelemetry_When_Not_Set()
177177
await session.DisposeAsync();
178178
}
179179

180+
[Fact]
181+
public async Task Should_Forward_New_Session_Config_Fields_In_Create_Wire_Request()
182+
{
183+
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
184+
185+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
186+
{
187+
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
188+
UseLoggedInUser = false,
189+
});
190+
191+
await client.StartAsync();
192+
193+
var session = await client.CreateSessionAsync(new SessionConfig
194+
{
195+
SkipEmbeddingRetrieval = false,
196+
OrganizationCustomInstructions = "Follow org policy.",
197+
EnableOnDemandInstructionDiscovery = true,
198+
EnableFileHooks = true,
199+
EnableHostGitOperations = false,
200+
EnableSessionStore = true,
201+
EnableSkills = false,
202+
OnPermissionRequest = PermissionHandler.ApproveAll,
203+
});
204+
205+
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
206+
var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create");
207+
Assert.False(createRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
208+
Assert.Equal("Follow org policy.", createRequest.GetProperty("organizationCustomInstructions").GetString());
209+
Assert.True(createRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
210+
Assert.True(createRequest.GetProperty("enableFileHooks").GetBoolean());
211+
Assert.False(createRequest.GetProperty("enableHostGitOperations").GetBoolean());
212+
Assert.True(createRequest.GetProperty("enableSessionStore").GetBoolean());
213+
Assert.False(createRequest.GetProperty("enableSkills").GetBoolean());
214+
215+
await session.DisposeAsync();
216+
}
217+
218+
[Fact]
219+
public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request()
220+
{
221+
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
222+
223+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
224+
{
225+
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
226+
Mode = CopilotClientMode.Empty,
227+
UseLoggedInUser = false,
228+
});
229+
230+
await client.StartAsync();
231+
232+
var session = await client.CreateSessionAsync(new SessionConfig
233+
{
234+
OnPermissionRequest = PermissionHandler.ApproveAll,
235+
});
236+
237+
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
238+
var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create");
239+
Assert.False(createRequest.GetProperty("enableSessionTelemetry").GetBoolean());
240+
Assert.True(createRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
241+
Assert.False(createRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
242+
Assert.False(createRequest.GetProperty("enableFileHooks").GetBoolean());
243+
Assert.False(createRequest.GetProperty("enableHostGitOperations").GetBoolean());
244+
Assert.False(createRequest.GetProperty("enableSessionStore").GetBoolean());
245+
Assert.False(createRequest.GetProperty("enableSkills").GetBoolean());
246+
Assert.False(createRequest.TryGetProperty("organizationCustomInstructions", out _));
247+
248+
await session.DisposeAsync();
249+
}
250+
180251
[Fact]
181252
public async Task Should_Propagate_Activity_TraceContext_To_Session_Create_And_Send()
182253
{
@@ -293,6 +364,77 @@ public async Task Should_Propagate_Activity_TraceContext_To_Session_Resume()
293364
await session.DisposeAsync();
294365
}
295366

367+
[Fact]
368+
public async Task Should_Forward_New_Session_Config_Fields_In_Resume_Wire_Request()
369+
{
370+
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
371+
372+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
373+
{
374+
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
375+
UseLoggedInUser = false,
376+
});
377+
378+
await client.StartAsync();
379+
380+
var session = await client.ResumeSessionAsync("resume-session", new ResumeSessionConfig
381+
{
382+
SkipEmbeddingRetrieval = false,
383+
OrganizationCustomInstructions = "Resume org policy.",
384+
EnableOnDemandInstructionDiscovery = true,
385+
EnableFileHooks = true,
386+
EnableHostGitOperations = false,
387+
EnableSessionStore = true,
388+
EnableSkills = false,
389+
OnPermissionRequest = PermissionHandler.ApproveAll,
390+
});
391+
392+
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
393+
var resumeRequest = GetCapturedRequestParams(capture.RootElement, "session.resume");
394+
Assert.False(resumeRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
395+
Assert.Equal("Resume org policy.", resumeRequest.GetProperty("organizationCustomInstructions").GetString());
396+
Assert.True(resumeRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
397+
Assert.True(resumeRequest.GetProperty("enableFileHooks").GetBoolean());
398+
Assert.False(resumeRequest.GetProperty("enableHostGitOperations").GetBoolean());
399+
Assert.True(resumeRequest.GetProperty("enableSessionStore").GetBoolean());
400+
Assert.False(resumeRequest.GetProperty("enableSkills").GetBoolean());
401+
402+
await session.DisposeAsync();
403+
}
404+
405+
[Fact]
406+
public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request()
407+
{
408+
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
409+
410+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
411+
{
412+
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
413+
Mode = CopilotClientMode.Empty,
414+
UseLoggedInUser = false,
415+
});
416+
417+
await client.StartAsync();
418+
419+
var session = await client.ResumeSessionAsync("resume-empty-session", new ResumeSessionConfig
420+
{
421+
OnPermissionRequest = PermissionHandler.ApproveAll,
422+
});
423+
424+
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
425+
var resumeRequest = GetCapturedRequestParams(capture.RootElement, "session.resume");
426+
Assert.False(resumeRequest.GetProperty("enableSessionTelemetry").GetBoolean());
427+
Assert.True(resumeRequest.GetProperty("skipEmbeddingRetrieval").GetBoolean());
428+
Assert.False(resumeRequest.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
429+
Assert.False(resumeRequest.GetProperty("enableFileHooks").GetBoolean());
430+
Assert.False(resumeRequest.GetProperty("enableHostGitOperations").GetBoolean());
431+
Assert.False(resumeRequest.GetProperty("enableSessionStore").GetBoolean());
432+
Assert.False(resumeRequest.GetProperty("enableSkills").GetBoolean());
433+
Assert.False(resumeRequest.TryGetProperty("organizationCustomInstructions", out _));
434+
435+
await session.DisposeAsync();
436+
}
437+
296438
[Fact]
297439
public void Should_Accept_GitHubToken_Option()
298440
{

go/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,13 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
608608
if config.EnableConfigDiscovery {
609609
req.EnableConfigDiscovery = Bool(true)
610610
}
611+
req.SkipEmbeddingRetrieval = config.SkipEmbeddingRetrieval
612+
req.OrganizationCustomInstructions = config.OrganizationCustomInstructions
613+
req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery
614+
req.EnableFileHooks = config.EnableFileHooks
615+
req.EnableHostGitOperations = config.EnableHostGitOperations
616+
req.EnableSessionStore = config.EnableSessionStore
617+
req.EnableSkills = config.EnableSkills
611618
req.Tools = config.Tools
612619
systemMessage := c.systemMessageForMode(config.SystemMessage)
613620
wireSystemMessage, transformCallbacks := extractTransformCallbacks(systemMessage)
@@ -866,6 +873,13 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
866873
if config.EnableConfigDiscovery {
867874
req.EnableConfigDiscovery = Bool(true)
868875
}
876+
req.SkipEmbeddingRetrieval = config.SkipEmbeddingRetrieval
877+
req.OrganizationCustomInstructions = config.OrganizationCustomInstructions
878+
req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery
879+
req.EnableFileHooks = config.EnableFileHooks
880+
req.EnableHostGitOperations = config.EnableHostGitOperations
881+
req.EnableSessionStore = config.EnableSessionStore
882+
req.EnableSkills = config.EnableSkills
869883
if config.SuppressResumeEvent {
870884
req.DisableResume = Bool(true)
871885
}

go/mode_empty.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@ func (c *Client) applyConfigDefaultsForMode(config *SessionConfig) {
126126
f := false
127127
config.EnableSessionTelemetry = &f
128128
}
129+
if config.SkipEmbeddingRetrieval == nil {
130+
t := true
131+
config.SkipEmbeddingRetrieval = &t
132+
}
133+
if config.EnableOnDemandInstructionDiscovery == nil {
134+
f := false
135+
config.EnableOnDemandInstructionDiscovery = &f
136+
}
137+
if config.EnableFileHooks == nil {
138+
f := false
139+
config.EnableFileHooks = &f
140+
}
141+
if config.EnableHostGitOperations == nil {
142+
f := false
143+
config.EnableHostGitOperations = &f
144+
}
145+
if config.EnableSessionStore == nil {
146+
f := false
147+
config.EnableSessionStore = &f
148+
}
149+
if config.EnableSkills == nil {
150+
f := false
151+
config.EnableSkills = &f
152+
}
129153
}
130154

131155
func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
@@ -136,6 +160,30 @@ func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
136160
f := false
137161
config.EnableSessionTelemetry = &f
138162
}
163+
if config.SkipEmbeddingRetrieval == nil {
164+
t := true
165+
config.SkipEmbeddingRetrieval = &t
166+
}
167+
if config.EnableOnDemandInstructionDiscovery == nil {
168+
f := false
169+
config.EnableOnDemandInstructionDiscovery = &f
170+
}
171+
if config.EnableFileHooks == nil {
172+
f := false
173+
config.EnableFileHooks = &f
174+
}
175+
if config.EnableHostGitOperations == nil {
176+
f := false
177+
config.EnableHostGitOperations = &f
178+
}
179+
if config.EnableSessionStore == nil {
180+
f := false
181+
config.EnableSessionStore = &f
182+
}
183+
if config.EnableSkills == nil {
184+
f := false
185+
config.EnableSkills = &f
186+
}
139187
}
140188

141189
// updateSessionOptionsForMode applies the per-mode safe-defaults patch via

0 commit comments

Comments
 (0)