Skip to content

Commit 6010405

Browse files
Multitenancy hardening: Client Mode (#1428)
1 parent 9017d85 commit 6010405

43 files changed

Lines changed: 5128 additions & 316 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dotnet/src/Client.cs

Lines changed: 249 additions & 8 deletions
Large diffs are not rendered by default.

dotnet/src/ToolSet.cs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.Text.RegularExpressions;
6+
7+
namespace GitHub.Copilot;
8+
9+
/// <summary>
10+
/// Builder for <see cref="SessionConfigBase.AvailableTools"/> /
11+
/// <see cref="SessionConfigBase.ExcludedTools"/> using source-qualified filter
12+
/// patterns (<c>builtin:*</c>, <c>mcp:&lt;name&gt;</c>, <c>custom:*</c>, etc.).
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// Tools are classified by the runtime at registration time (not from name
17+
/// parsing), so <c>AddBuiltIn("foo")</c> matches only tools the runtime
18+
/// registered as built-in, even if an MCP server or custom-agent extension
19+
/// happens to register a tool with the same wire name.
20+
/// </para>
21+
/// <para>
22+
/// <see cref="ToolSet"/> inherits from <c>List&lt;string&gt;</c>, so instances
23+
/// can be assigned directly to <see cref="SessionConfigBase.AvailableTools"/>
24+
/// or <see cref="SessionConfigBase.ExcludedTools"/>.
25+
/// </para>
26+
/// </remarks>
27+
/// <example>
28+
/// <code>
29+
/// var session = await client.CreateSessionAsync(new SessionConfig
30+
/// {
31+
/// AvailableTools = new ToolSet()
32+
/// .AddBuiltIn(BuiltInTools.Isolated)
33+
/// .AddMcp("*")
34+
/// .AddCustom("*"),
35+
/// });
36+
/// </code>
37+
/// </example>
38+
public sealed class ToolSet : List<string>
39+
{
40+
private static readonly Regex s_validToolName = new(@"^[a-zA-Z0-9_-]+$", RegexOptions.Compiled);
41+
42+
/// <summary>
43+
/// Adds one or more built-in tool patterns.
44+
/// </summary>
45+
/// <param name="name">A specific built-in tool name (e.g. <c>"bash"</c>) or
46+
/// <c>"*"</c> to match all built-in tools.</param>
47+
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
48+
public ToolSet AddBuiltIn(string name)
49+
{
50+
ValidateName("builtin", name);
51+
Add($"builtin:{name}");
52+
return this;
53+
}
54+
55+
/// <summary>
56+
/// Adds a list of built-in tool patterns
57+
/// (e.g. <see cref="BuiltInTools.Isolated"/>).
58+
/// </summary>
59+
/// <param name="names">Built-in tool names to add.</param>
60+
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
61+
public ToolSet AddBuiltIn(IEnumerable<string> names)
62+
{
63+
ArgumentNullException.ThrowIfNull(names);
64+
foreach (var name in names)
65+
{
66+
AddBuiltIn(name);
67+
}
68+
return this;
69+
}
70+
71+
/// <summary>
72+
/// Adds a custom tool pattern. Matches tools registered via the SDK's
73+
/// <see cref="SessionConfigBase.Tools"/> option or via custom agents.
74+
/// </summary>
75+
/// <param name="name">A specific custom tool name or <c>"*"</c> to match
76+
/// all custom tools.</param>
77+
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
78+
public ToolSet AddCustom(string name)
79+
{
80+
ValidateName("custom", name);
81+
Add($"custom:{name}");
82+
return this;
83+
}
84+
85+
/// <summary>
86+
/// Adds an MCP tool pattern. Matches tools advertised by any configured
87+
/// MCP server.
88+
/// </summary>
89+
/// <param name="toolName">The runtime's canonical wire name for the MCP
90+
/// tool (e.g. <c>"github-list_issues"</c>), or <c>"*"</c> to match all
91+
/// MCP tools from any server.</param>
92+
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
93+
public ToolSet AddMcp(string toolName)
94+
{
95+
ValidateName("mcp", toolName);
96+
Add($"mcp:{toolName}");
97+
return this;
98+
}
99+
100+
private static void ValidateName(string kind, string name)
101+
{
102+
if (string.IsNullOrEmpty(name))
103+
{
104+
throw new ArgumentException(
105+
$"Invalid {kind} tool name: must not be null or empty.",
106+
nameof(name));
107+
}
108+
if (name == "*")
109+
{
110+
return;
111+
}
112+
if (!s_validToolName.IsMatch(name))
113+
{
114+
throw new ArgumentException(
115+
$"Invalid {kind} tool name '{name}': tool names must match /^[a-zA-Z0-9_-]+$/ " +
116+
"or be the wildcard '*'.",
117+
nameof(name));
118+
}
119+
}
120+
}
121+
122+
/// <summary>
123+
/// Curated sets of built-in tool names for common scenarios. Each constant is
124+
/// meant to be passed to <see cref="ToolSet.AddBuiltIn(IEnumerable{string})"/>.
125+
/// </summary>
126+
public static class BuiltInTools
127+
{
128+
/// <summary>
129+
/// Built-in tools that operate only within the bounds of a single session
130+
/// — no host filesystem access outside the session, no cross-session
131+
/// state, no host environment access, no network. Safe to enable in
132+
/// <see cref="CopilotClientMode.Empty"/> scenarios (e.g. multi-tenant
133+
/// servers) without leaking host capabilities.
134+
/// </summary>
135+
/// <remarks>
136+
/// <para>
137+
/// <b>Contract:</b> tools in this set MUST NOT be extended (even behind
138+
/// options or args) to read or write state outside the session boundary.
139+
/// Adding cross-session or host-state behavior to one of these tools is a
140+
/// breaking change that requires removing it from this set.
141+
/// </para>
142+
/// </remarks>
143+
public static IReadOnlyList<string> Isolated { get; } =
144+
[
145+
"ask_user",
146+
"task_complete",
147+
"exit_plan_mode",
148+
"task",
149+
"read_agent",
150+
"write_agent",
151+
"list_agents",
152+
"send_inbox",
153+
"context_board",
154+
"skill",
155+
];
156+
}

dotnet/src/Types.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,48 @@ internal UriRuntimeConnection() { }
206206
public string? ConnectionToken { get; set; }
207207
}
208208

209+
/// <summary>
210+
/// Selects the defaulting strategy used by <see cref="CopilotClient"/>.
211+
/// </summary>
212+
public enum CopilotClientMode
213+
{
214+
/// <summary>
215+
/// Disables optional features by default. The app must explicitly opt into
216+
/// anything it needs. Required for any scenario where CLI-like ambient
217+
/// behavior is unsafe (e.g., multi-user servers).
218+
/// <para>
219+
/// When this mode is selected:
220+
/// </para>
221+
/// <list type="bullet">
222+
/// <item>The client constructor requires
223+
/// <see cref="CopilotClientOptions.BaseDirectory"/> or
224+
/// <see cref="CopilotClientOptions.SessionFs"/> to be set.</item>
225+
/// <item><see cref="SessionConfigBase.AvailableTools"/> must be supplied on
226+
/// every session — no tools are exposed by default.</item>
227+
/// <item><c>session.create</c> always sets
228+
/// <c>toolFilterPrecedence: "excluded"</c> so the allowlist and denylist
229+
/// compose naturally.</item>
230+
/// <item>The SDK injects safe defaults for ambient session features
231+
/// (telemetry, custom instructions, plugins, environment context, etc.).</item>
232+
/// <item><c>COPILOT_DISABLE_KEYTAR=1</c> is set on the spawned runtime so
233+
/// credentials are persisted to <c>COPILOT_HOME</c> rather than a
234+
/// process-wide system keychain.</item>
235+
/// </list>
236+
/// </summary>
237+
Empty,
238+
239+
/// <summary>
240+
/// Uses defaults equivalent to GitHub Copilot CLI. The default. Useful when
241+
/// building a coding agent that shares sessions with Copilot CLI.
242+
/// <para>
243+
/// <b>Do not use this mode for server-based multi-user applications</b> —
244+
/// the default coding agent has tools and capabilities that operate across
245+
/// sessions and can access the host OS environment.
246+
/// </para>
247+
/// </summary>
248+
CopilotCli,
249+
}
250+
209251
/// <summary>
210252
/// Configuration options for creating a <see cref="CopilotClient"/> instance.
211253
/// </summary>
@@ -237,8 +279,23 @@ private CopilotClientOptions(CopilotClientOptions? other)
237279
SessionFs = other.SessionFs;
238280
SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds;
239281
EnableRemoteSessions = other.EnableRemoteSessions;
282+
Mode = other.Mode;
240283
}
241284

285+
/// <summary>
286+
/// Selects the SDK defaulting strategy. See <see cref="CopilotClientMode"/>.
287+
/// </summary>
288+
/// <remarks>
289+
/// When set to <see cref="CopilotClientMode.Empty"/>, the SDK validates that
290+
/// the app has supplied the required configuration
291+
/// (<see cref="BaseDirectory"/> or <see cref="SessionFs"/>, plus
292+
/// <see cref="SessionConfigBase.AvailableTools"/> on each session) and
293+
/// translates session creation requests into runtime options that flip
294+
/// tool filter precedence to <c>excluded</c>-wins so exclusions are
295+
/// expressible.
296+
/// </remarks>
297+
public CopilotClientMode Mode { get; set; } = CopilotClientMode.CopilotCli;
298+
242299
/// <summary>
243300
/// How to connect to the runtime. When <c>null</c>, the default is
244301
/// <see cref="RuntimeConnection.ForStdio(string?, IList{string}?)"/> with the bundled runtime.
@@ -2306,6 +2363,10 @@ protected SessionConfigBase(SessionConfigBase? other)
23062363
OnUserInputRequest = other.OnUserInputRequest;
23072364
Provider = other.Provider;
23082365
EnableSessionTelemetry = other.EnableSessionTelemetry;
2366+
SkipCustomInstructions = other.SkipCustomInstructions;
2367+
CustomAgentsLocalOnly = other.CustomAgentsLocalOnly;
2368+
CoauthorEnabled = other.CoauthorEnabled;
2369+
ManageScheduleEnabled = other.ManageScheduleEnabled;
23092370
ReasoningEffort = other.ReasoningEffort;
23102371
CreateSessionFsProvider = other.CreateSessionFsProvider;
23112372
GitHubToken = other.GitHubToken;
@@ -2391,6 +2452,42 @@ protected SessionConfigBase(SessionConfigBase? other)
23912452
/// </summary>
23922453
public bool? EnableSessionTelemetry { get; set; }
23932454

2455+
/// <summary>
2456+
/// When <see langword="true"/>, suppresses loading of custom instruction files
2457+
/// (e.g. <c>.github/copilot-instructions.md</c>, <c>AGENTS.md</c>) from the working directory.
2458+
/// When <see langword="null"/>, the SDK chooses based on
2459+
/// <see cref="CopilotClientOptions.Mode"/>: <c>true</c> under
2460+
/// <see cref="CopilotClientMode.Empty"/> (instructions are not loaded
2461+
/// unless the app explicitly opts in), <c>null</c> otherwise.
2462+
/// </summary>
2463+
public bool? SkipCustomInstructions { get; set; }
2464+
2465+
/// <summary>
2466+
/// When <see langword="true"/>, custom-agent discovery is restricted to the
2467+
/// session's local working directory (no organisation-level discovery).
2468+
/// When <see langword="null"/>, the SDK chooses based on
2469+
/// <see cref="CopilotClientOptions.Mode"/>: <c>true</c> under
2470+
/// <see cref="CopilotClientMode.Empty"/>, <c>null</c> otherwise.
2471+
/// </summary>
2472+
public bool? CustomAgentsLocalOnly { get; set; }
2473+
2474+
/// <summary>
2475+
/// When <see langword="true"/>, allows the runtime to append a
2476+
/// <c>Co-authored-by</c> trailer when it commits on behalf of the user.
2477+
/// When <see langword="null"/>, the SDK chooses based on
2478+
/// <see cref="CopilotClientOptions.Mode"/>: <c>false</c> under
2479+
/// <see cref="CopilotClientMode.Empty"/>, <c>null</c> otherwise.
2480+
/// </summary>
2481+
public bool? CoauthorEnabled { get; set; }
2482+
2483+
/// <summary>
2484+
/// When <see langword="true"/>, enables the <c>manage_schedule</c> tool
2485+
/// (host scheduler integration). When <see langword="null"/>, the SDK
2486+
/// chooses based on <see cref="CopilotClientOptions.Mode"/>: <c>false</c>
2487+
/// under <see cref="CopilotClientMode.Empty"/>, <c>null</c> otherwise.
2488+
/// </summary>
2489+
public bool? ManageScheduleEnabled { get; set; }
2490+
23942491
/// <summary>Handler for permission requests from the server.</summary>
23952492
public Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? OnPermissionRequest { get; set; }
23962493

0 commit comments

Comments
 (0)