Skip to content
Draft
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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@ All notable changes to the Copilot SDK are documented in this file.
This changelog is automatically generated by an AI agent when stable releases are published.
See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the full list.

## [Unreleased]

### Feature: host-injected managed settings permissions

Session create and resume accept a new optional `managedSettings` option that injects an enterprise permissions policy at session startup, alongside the existing `enableManagedSettings` self-fetch flag. The current contract is permissions-only: `disableBypassPermissionsMode` (the literal `"disable"`), plus `deny`, `ask`, and `allow` rule lists. The layer composes restrictively with any server- or device-level managed settings (deny/ask are unioned, every present allow list must admit a tool, and `disableBypassPermissionsMode` is deny-wins).

This layer is startup-only and is not persisted with the session, so it must be re-supplied on resume to remain in effect; omitting it on resume clears the previously injected layer. It can be combined with `enableManagedSettings`. Older runtimes may ignore the additive field, so hosts must not rely on injected policy until they ship a Copilot CLI runtime whose schema includes managed settings.

```ts
const session = await client.createSession({
managedSettings: {
permissions: {
disableBypassPermissionsMode: "disable",
deny: ["shell(rm*)"],
ask: ["write"],
},
},
});
```

```cs
var session = await client.CreateSessionAsync(new SessionConfig
{
ManagedSettings = new ManagedSettings
{
Permissions = new ManagedSettingsPermissions
{
DisableBypassPermissionsMode = "disable",
Deny = ["shell(rm*)"],
Ask = ["write"],
},
},
});
```

## [v1.0.7](https://github.com/github/copilot-sdk/releases/tag/v1.0.7) (2026-07-16)

### Feature: in-process (FFI) transport
Expand Down
6 changes: 5 additions & 1 deletion dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ private CopilotSession InitializeSession(
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(
config.OnPermissionRequest,
config.EnableManagedSettings is true);
config.EnableManagedSettings is true || config.ManagedSettings is not null);
session.RegisterMcpAuthHandler(config.OnMcpAuthRequest);
session.RegisterCommands(config.Commands);
session.RegisterElicitationHandler(config.OnElicitationRequest);
Expand Down Expand Up @@ -1203,6 +1203,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
ExpAssignments: config.ExpAssignments,
EnableManagedSettings: config.EnableManagedSettings,
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
ManagedSettings: config.ManagedSettings,
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
AdditionalDirectories: config.AdditionalDirectories);

Expand Down Expand Up @@ -1421,6 +1422,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
ExpAssignments: config.ExpAssignments,
EnableManagedSettings: config.EnableManagedSettings,
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
ManagedSettings: config.ManagedSettings,
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
AdditionalDirectories: config.AdditionalDirectories);

Expand Down Expand Up @@ -2776,6 +2778,7 @@ internal record CreateSessionRequest(
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
bool? EnableGitHubTelemetryForwarding = null,
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null,
IList<string>? AdditionalDirectories = null);
Expand Down Expand Up @@ -2886,6 +2889,7 @@ internal record ResumeSessionRequest(
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
bool? EnableGitHubTelemetryForwarding = null,
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null,
IList<string>? AdditionalDirectories = null);
Expand Down
65 changes: 65 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2990,6 +2990,59 @@ public sealed class GitHubMcpToolConfig
public bool? DisableFormDeferral { get; set; }
}

/// <summary>
/// Permission rules injected as a managed-settings layer at session bootstrap.
/// All fields are optional; omitted fields impose no constraint from this layer.
/// </summary>
/// <remarks>
/// This layer composes restrictively with any server- or device-level managed
/// settings: <see cref="Deny"/> and <see cref="Ask"/> rules are unioned across
/// layers, every present <see cref="Allow"/> list must admit a tool for it to be
/// allowed, and <see cref="DisableBypassPermissionsMode"/> is honored if any
/// layer sets it (deny-wins).
/// </remarks>
public sealed class ManagedSettingsPermissions
{
/// <summary>
/// When set to <c>"disable"</c>, bypass-permissions mode is turned off for the
/// session regardless of other layers. Serialized as
/// <c>disableBypassPermissionsMode</c>.
/// </summary>
[JsonPropertyName("disableBypassPermissionsMode")]
public string? DisableBypassPermissionsMode { get; set; }

/// <summary>Tool-permission patterns that are always denied.</summary>
[JsonPropertyName("deny")]
public IList<string>? Deny { get; set; }

/// <summary>Tool-permission patterns that require an explicit ask.</summary>
[JsonPropertyName("ask")]
public IList<string>? Ask { get; set; }

/// <summary>Tool-permission patterns that are allowed without prompting.</summary>
[JsonPropertyName("allow")]
public IList<string>? Allow { get; set; }
}

/// <summary>
/// Managed-settings layer injected at session startup. Currently carries only a
/// <see cref="Permissions"/> object.
/// </summary>
/// <remarks>
/// This layer is startup-only and is not persisted with the session. It must be
/// re-supplied on <see cref="CopilotClient.ResumeSessionAsync"/> to remain in
/// effect; omitting it on resume clears the previously injected layer. It can be
/// combined with <see cref="SessionConfigBase.EnableManagedSettings"/>. Older
/// runtimes may ignore this additive field, so hosts must not rely on injected
/// policy until they ship a compatible runtime.
/// </remarks>
public sealed class ManagedSettings
{
/// <summary>Permission rules for this managed-settings layer.</summary>
[JsonPropertyName("permissions")]
public ManagedSettingsPermissions? Permissions { get; set; }
}

/// <summary>
/// Shared configuration properties for creating or resuming a Copilot session.
/// Use <see cref="SessionConfig"/> when creating a new session, or
Expand Down Expand Up @@ -3081,6 +3134,7 @@ protected SessionConfigBase(SessionConfigBase? other)
RemoteSession = other.RemoteSession;
ExpAssignments = other.ExpAssignments;
EnableManagedSettings = other.EnableManagedSettings;
ManagedSettings = other.ManagedSettings;
#pragma warning disable GHCP001
Canvases = other.Canvases is not null ? [.. other.Canvases] : null;
RequestCanvasRenderer = other.RequestCanvasRenderer;
Expand Down Expand Up @@ -3539,6 +3593,17 @@ protected SessionConfigBase(SessionConfigBase? other)
/// </summary>
public bool? EnableManagedSettings { get; set; }

/// <summary>
/// Optional managed-settings layer injected at session bootstrap. Currently
/// carries a permissions object that composes restrictively with any
/// server- or device-level managed settings. This layer is startup-only and
/// is not persisted: it must be re-supplied on resume to remain in effect,
/// and omitting it on resume clears the previously injected layer. Can be
/// combined with <see cref="EnableManagedSettings"/>. Serialized on the wire
/// as <c>managedSettings</c>.
/// </summary>
public ManagedSettings? ManagedSettings { get; set; }

#pragma warning disable GHCP001
/// <summary>
/// Canvas declarations advertised by this connection. The runtime forwards
Expand Down
74 changes: 74 additions & 0 deletions dotnet/test/Unit/ClientSessionLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,80 @@ private static int GetPrivateDictionaryCount(CopilotClient client, string fieldN
return (int)count.GetValue(dictionary)!;
}

[Fact]
public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions()
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
await client.StartAsync();

await using var session = await client.CreateSessionAsync(new SessionConfig
{
ManagedSettings = new ManagedSettings
{
Permissions = new ManagedSettingsPermissions
{
DisableBypassPermissionsMode = "disable",
Deny = ["shell(rm*)"],
Ask = ["write"],
Allow = []
}
},
OnPermissionRequest = PermissionHandler.ApproveAll
});

var request = Assert.Single(server.Requests, request => request.Method == "session.create");
Assert.False(request.Params.TryGetProperty("enableManagedSettings", out _));
var managedField = typeof(CopilotSession).GetField("_managedSettingsEnabled", BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Managed settings field was not found.");
Assert.True((bool)managedField.GetValue(session)!);
var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions");
Assert.Equal("disable", permissions.GetProperty("disableBypassPermissionsMode").GetString());
Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString());
Assert.Equal("write", Assert.Single(permissions.GetProperty("ask").EnumerateArray()).GetString());
Assert.Empty(permissions.GetProperty("allow").EnumerateArray());
}

[Fact]
public async Task CreateSessionAsync_Omits_ManagedSettings_When_Unset()
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
await client.StartAsync();

await using var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll
});

var request = Assert.Single(server.Requests, request => request.Method == "session.create");
Assert.False(request.Params.TryGetProperty("managedSettings", out _));
}

[Fact]
public async Task ResumeSessionAsync_Serializes_ManagedSettings_Permissions()
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });

await using var session = await client.ResumeSessionAsync("session-managed", new ResumeSessionConfig
{
ManagedSettings = new ManagedSettings
{
Permissions = new ManagedSettingsPermissions
{
Deny = ["shell(rm*)"]
}
},
OnPermissionRequest = PermissionHandler.ApproveAll,
OnEvent = _ => { }
});

var request = Assert.Single(server.Requests, request => request.Method == "session.resume");
var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions");
Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString());
}

private static void DispatchEvent(CopilotSession session, SessionEvent evt)
{
var method = typeof(CopilotSession).GetMethod("DispatchEvent", BindingFlags.Instance | BindingFlags.NonPublic)
Expand Down
10 changes: 8 additions & 2 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,10 @@ func extractTransformCallbacks(config *SystemMessageConfig) (*SystemMessageConfi
return wireConfig, callbacks
}

func hasManagedSettings(enableManagedSettings *bool, managedSettings *ManagedSettings) bool {
return (enableManagedSettings != nil && *enableManagedSettings) || managedSettings != nil
}

func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) {
if config == nil {
config = &SessionConfig{}
Expand Down Expand Up @@ -830,6 +834,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.ExtensionInfo = config.ExtensionInfo
req.ExpAssignments = config.ExpAssignments
req.EnableManagedSettings = config.EnableManagedSettings
req.ManagedSettings = config.ManagedSettings

if len(config.Commands) > 0 {
cmds := make([]wireCommand, 0, len(config.Commands))
Expand Down Expand Up @@ -913,7 +918,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
sessionID,
c.client,
"",
config.EnableManagedSettings != nil && *config.EnableManagedSettings,
hasManagedSettings(config.EnableManagedSettings, config.ManagedSettings),
)

s.registerTools(config.Tools)
Expand Down Expand Up @@ -1207,6 +1212,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.ExtensionInfo = config.ExtensionInfo
req.ExpAssignments = config.ExpAssignments
req.EnableManagedSettings = config.EnableManagedSettings
req.ManagedSettings = config.ManagedSettings
if config.OnPermissionRequest != nil {
req.RequestPermission = Bool(true)
}
Expand Down Expand Up @@ -1242,7 +1248,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
sessionID,
c.client,
"",
config.EnableManagedSettings != nil && *config.EnableManagedSettings,
hasManagedSettings(config.EnableManagedSettings, config.ManagedSettings),
)

session.registerTools(config.Tools)
Expand Down
Loading
Loading