Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
257 changes: 249 additions & 8 deletions dotnet/src/Client.cs

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions dotnet/src/ToolSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using System.Text.RegularExpressions;

namespace GitHub.Copilot;

/// <summary>
/// Builder for <see cref="SessionConfigBase.AvailableTools"/> /
/// <see cref="SessionConfigBase.ExcludedTools"/> using source-qualified filter
/// patterns (<c>builtin:*</c>, <c>mcp:&lt;name&gt;</c>, <c>custom:*</c>, etc.).
/// </summary>
/// <remarks>
/// <para>
/// Tools are classified by the runtime at registration time (not from name
/// parsing), so <c>AddBuiltIn("foo")</c> matches only tools the runtime
/// registered as built-in, even if an MCP server or custom-agent extension
/// happens to register a tool with the same wire name.
/// </para>
/// <para>
/// <see cref="ToolSet"/> inherits from <c>List&lt;string&gt;</c>, so instances
/// can be assigned directly to <see cref="SessionConfigBase.AvailableTools"/>
/// or <see cref="SessionConfigBase.ExcludedTools"/>.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// var session = await client.CreateSessionAsync(new SessionConfig
/// {
/// AvailableTools = new ToolSet()
/// .AddBuiltIn(BuiltInTools.Isolated)
/// .AddMcp("*")
/// .AddCustom("*"),
/// });
/// </code>
/// </example>
public sealed class ToolSet : List<string>
{
private static readonly Regex s_validToolName = new(@"^[a-zA-Z0-9_-]+$", RegexOptions.Compiled);

/// <summary>
/// Adds one or more built-in tool patterns.
/// </summary>
/// <param name="name">A specific built-in tool name (e.g. <c>"bash"</c>) or
/// <c>"*"</c> to match all built-in tools.</param>
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
public ToolSet AddBuiltIn(string name)
{
ValidateName("builtin", name);
Add($"builtin:{name}");
return this;
}

/// <summary>
/// Adds a list of built-in tool patterns
/// (e.g. <see cref="BuiltInTools.Isolated"/>).
/// </summary>
/// <param name="names">Built-in tool names to add.</param>
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
public ToolSet AddBuiltIn(IEnumerable<string> names)
{
ArgumentNullException.ThrowIfNull(names);
foreach (var name in names)
{
AddBuiltIn(name);
}
return this;
}

/// <summary>
/// Adds a custom tool pattern. Matches tools registered via the SDK's
/// <see cref="SessionConfigBase.Tools"/> option or via custom agents.
/// </summary>
/// <param name="name">A specific custom tool name or <c>"*"</c> to match
/// all custom tools.</param>
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
public ToolSet AddCustom(string name)
{
ValidateName("custom", name);
Add($"custom:{name}");
return this;
}

/// <summary>
/// Adds an MCP tool pattern. Matches tools advertised by any configured
/// MCP server.
/// </summary>
/// <param name="toolName">The runtime's canonical wire name for the MCP
/// tool (e.g. <c>"github-list_issues"</c>), or <c>"*"</c> to match all
/// MCP tools from any server.</param>
/// <returns>This <see cref="ToolSet"/> for chaining.</returns>
public ToolSet AddMcp(string toolName)
{
ValidateName("mcp", toolName);
Add($"mcp:{toolName}");
return this;
}

private static void ValidateName(string kind, string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(
$"Invalid {kind} tool name: must not be null or empty.",
nameof(name));
}
if (name == "*")
{
return;
}
if (!s_validToolName.IsMatch(name))
{
throw new ArgumentException(
$"Invalid {kind} tool name '{name}': tool names must match /^[a-zA-Z0-9_-]+$/ " +
"or be the wildcard '*'.",
nameof(name));
}
}
}

/// <summary>
/// Curated sets of built-in tool names for common scenarios. Each constant is
/// meant to be passed to <see cref="ToolSet.AddBuiltIn(IEnumerable{string})"/>.
/// </summary>
public static class BuiltInTools
{
/// <summary>
/// Built-in tools that operate only within the bounds of a single session
/// — no host filesystem access outside the session, no cross-session
/// state, no host environment access, no network. Safe to enable in
/// <see cref="CopilotClientMode.Empty"/> scenarios (e.g. multi-tenant
/// servers) without leaking host capabilities.
/// </summary>
/// <remarks>
/// <para>
/// <b>Contract:</b> tools in this set MUST NOT be extended (even behind
/// options or args) to read or write state outside the session boundary.
/// Adding cross-session or host-state behavior to one of these tools is a
/// breaking change that requires removing it from this set.
/// </para>
/// </remarks>
public static IReadOnlyList<string> Isolated { get; } =
[
"ask_user",
"task_complete",
"exit_plan_mode",
"task",
"read_agent",
"write_agent",
"list_agents",
"send_inbox",
"context_board",
"skill",
];
}
97 changes: 97 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,48 @@ internal UriRuntimeConnection() { }
public string? ConnectionToken { get; set; }
}

/// <summary>
/// Selects the defaulting strategy used by <see cref="CopilotClient"/>.
/// </summary>
public enum CopilotClientMode
{
/// <summary>
/// Disables optional features by default. The app must explicitly opt into
/// anything it needs. Required for any scenario where CLI-like ambient
/// behavior is unsafe (e.g., multi-user servers).
/// <para>
/// When this mode is selected:
/// </para>
/// <list type="bullet">
/// <item>The client constructor requires
/// <see cref="CopilotClientOptions.BaseDirectory"/> or
/// <see cref="CopilotClientOptions.SessionFs"/> to be set.</item>
/// <item><see cref="SessionConfigBase.AvailableTools"/> must be supplied on
/// every session — no tools are exposed by default.</item>
/// <item><c>session.create</c> always sets
/// <c>toolFilterPrecedence: "excluded"</c> so the allowlist and denylist
/// compose naturally.</item>
/// <item>The SDK injects safe defaults for ambient session features
/// (telemetry, custom instructions, plugins, environment context, etc.).</item>
/// <item><c>COPILOT_DISABLE_KEYTAR=1</c> is set on the spawned runtime so
/// credentials are persisted to <c>COPILOT_HOME</c> rather than a
/// process-wide system keychain.</item>
/// </list>
/// </summary>
Empty,

/// <summary>
/// Uses defaults equivalent to GitHub Copilot CLI. The default. Useful when
/// building a coding agent that shares sessions with Copilot CLI.
/// <para>
/// <b>Do not use this mode for server-based multi-user applications</b> —
/// the default coding agent has tools and capabilities that operate across
/// sessions and can access the host OS environment.
/// </para>
/// </summary>
CopilotCli,
}

/// <summary>
/// Configuration options for creating a <see cref="CopilotClient"/> instance.
/// </summary>
Expand Down Expand Up @@ -237,8 +279,23 @@ private CopilotClientOptions(CopilotClientOptions? other)
SessionFs = other.SessionFs;
SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds;
EnableRemoteSessions = other.EnableRemoteSessions;
Mode = other.Mode;
}

/// <summary>
/// Selects the SDK defaulting strategy. See <see cref="CopilotClientMode"/>.
/// </summary>
/// <remarks>
/// When set to <see cref="CopilotClientMode.Empty"/>, the SDK validates that
/// the app has supplied the required configuration
/// (<see cref="BaseDirectory"/> or <see cref="SessionFs"/>, plus
/// <see cref="SessionConfigBase.AvailableTools"/> on each session) and
/// translates session creation requests into runtime options that flip
/// tool filter precedence to <c>excluded</c>-wins so exclusions are
/// expressible.
/// </remarks>
public CopilotClientMode Mode { get; set; } = CopilotClientMode.CopilotCli;

/// <summary>
/// How to connect to the runtime. When <c>null</c>, the default is
/// <see cref="RuntimeConnection.ForStdio(string?, IList{string}?)"/> with the bundled runtime.
Expand Down Expand Up @@ -2306,6 +2363,10 @@ protected SessionConfigBase(SessionConfigBase? other)
OnUserInputRequest = other.OnUserInputRequest;
Provider = other.Provider;
EnableSessionTelemetry = other.EnableSessionTelemetry;
SkipCustomInstructions = other.SkipCustomInstructions;
CustomAgentsLocalOnly = other.CustomAgentsLocalOnly;
CoauthorEnabled = other.CoauthorEnabled;
ManageScheduleEnabled = other.ManageScheduleEnabled;
ReasoningEffort = other.ReasoningEffort;
CreateSessionFsProvider = other.CreateSessionFsProvider;
GitHubToken = other.GitHubToken;
Expand Down Expand Up @@ -2391,6 +2452,42 @@ protected SessionConfigBase(SessionConfigBase? other)
/// </summary>
public bool? EnableSessionTelemetry { get; set; }

/// <summary>
/// When <see langword="true"/>, suppresses loading of custom instruction files
/// (e.g. <c>.github/copilot-instructions.md</c>, <c>AGENTS.md</c>) from the working directory.
/// When <see langword="null"/>, the SDK chooses based on
/// <see cref="CopilotClientOptions.Mode"/>: <c>true</c> under
/// <see cref="CopilotClientMode.Empty"/> (instructions are not loaded
/// unless the app explicitly opts in), <c>null</c> otherwise.
/// </summary>
public bool? SkipCustomInstructions { get; set; }

/// <summary>
/// When <see langword="true"/>, custom-agent discovery is restricted to the
/// session's local working directory (no organisation-level discovery).
/// When <see langword="null"/>, the SDK chooses based on
/// <see cref="CopilotClientOptions.Mode"/>: <c>true</c> under
/// <see cref="CopilotClientMode.Empty"/>, <c>null</c> otherwise.
/// </summary>
public bool? CustomAgentsLocalOnly { get; set; }

/// <summary>
/// When <see langword="true"/>, allows the runtime to append a
/// <c>Co-authored-by</c> trailer when it commits on behalf of the user.
/// When <see langword="null"/>, the SDK chooses based on
/// <see cref="CopilotClientOptions.Mode"/>: <c>false</c> under
/// <see cref="CopilotClientMode.Empty"/>, <c>null</c> otherwise.
/// </summary>
public bool? CoauthorEnabled { get; set; }

/// <summary>
/// When <see langword="true"/>, enables the <c>manage_schedule</c> tool
/// (host scheduler integration). When <see langword="null"/>, the SDK
/// chooses based on <see cref="CopilotClientOptions.Mode"/>: <c>false</c>
/// under <see cref="CopilotClientMode.Empty"/>, <c>null</c> otherwise.
/// </summary>
public bool? ManageScheduleEnabled { get; set; }

/// <summary>Handler for permission requests from the server.</summary>
public Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? OnPermissionRequest { get; set; }

Expand Down
Loading
Loading