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
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ 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`. Host injection requires Copilot CLI `1.0.79-5` or later and does not require an SDK protocol version bump.

The generated session-event types also expose truthful injected-policy provenance: `session.managed_settings_resolved` can report `source` as `client` or `mixed`, with optional `clientManaged` metadata.

```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 = 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 @@ -1205,6 +1205,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 @@ -1425,6 +1426,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 @@ -2781,6 +2783,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 @@ -2895,6 +2898,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
76 changes: 76 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3044,6 +3044,70 @@ public sealed class GitHubMcpToolConfig
public bool? DisableFormDeferral { get; set; }
}

/// <summary>
/// Controls whether bypass-permissions mode is available in a managed session.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<DisableBypassPermissionsMode>))]
public enum DisableBypassPermissionsMode
{
/// <summary>Turn off bypass-permissions mode.</summary>
[JsonStringEnumMemberName("disable")]
Disable
}

/// <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 DisableBypassPermissionsMode? 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 @@ -3136,6 +3200,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 @@ -3601,6 +3666,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
89 changes: 89 additions & 0 deletions dotnet/test/Unit/ClientSessionLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using GitHub.Copilot.Rpc;
using Xunit;

namespace GitHub.Copilot.Test.Unit;
Expand Down Expand Up @@ -515,6 +516,94 @@ 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();
var permissionInvocation = new TaskCompletionSource<PermissionInvocation>(
TaskCreationOptions.RunContinuationsAsynchronously);

await using var session = await client.CreateSessionAsync(new SessionConfig
{
ManagedSettings = new ManagedSettings
{
Permissions = new ManagedSettingsPermissions
{
DisableBypassPermissionsMode = DisableBypassPermissionsMode.Disable,
Deny = ["shell(rm*)"],
Ask = ["write"],
Allow = []
}
},
OnPermissionRequest = (_, invocation) =>
{
permissionInvocation.TrySetResult(invocation);
return Task.FromResult(PermissionDecision.NoResult());
}
});

var request = Assert.Single(server.Requests, request => request.Method == "session.create");
Assert.False(request.Params.TryGetProperty("enableManagedSettings", out _));
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());

DispatchEvent(session, new PermissionRequestedEvent
{
Data = new PermissionRequestedData
{
PermissionRequest = new PermissionRequest { Kind = "read" },
RequestId = "managed-permission"
}
});
var invocation = await permissionInvocation.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.True(invocation.ManagedSettingsEnabled);
}

[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
61 changes: 61 additions & 0 deletions dotnet/test/Unit/SessionEventSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,65 @@ public void McpOauthRequiredData_Preserves_Static_Client_Secret()
Assert.NotNull(authEvent.Data.StaticClientConfig);
Assert.Equal("static-secret", authEvent.Data.StaticClientConfig.ClientSecret);
}

[Fact]
public void ManagedSettingsResolvedData_Preserves_Client_Provenance()
{
Assert.Equal("server", ManagedSettingsResolvedSource.Server.Value);
Assert.Equal("device", ManagedSettingsResolvedSource.Device.Value);
Assert.Equal("client", ManagedSettingsResolvedSource.Client.Value);
Assert.Equal("mixed", ManagedSettingsResolvedSource.Mixed.Value);
Assert.Equal("none", ManagedSettingsResolvedSource.None.Value);

const string clientJson = """
{
"id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-03-15T21:26:54.987Z",
"parentId": null,
"type": "session.managed_settings_resolved",
"data": {
"source": "client",
"serverManaged": false,
"deviceManaged": false,
"clientManaged": true,
"failClosed": false,
"bypassPermissionsDisabled": true,
"managedKeys": ["permissions"]
}
}
""";

var clientEvent = Assert.IsType<SessionManagedSettingsResolvedEvent>(
SessionEvent.FromJson(clientJson));
Assert.Equal(ManagedSettingsResolvedSource.Client, clientEvent.Data.Source);
Assert.True(clientEvent.Data.ClientManaged);
using (var document = JsonDocument.Parse(clientEvent.ToJson()))
{
Assert.True(document.RootElement.GetProperty("data").GetProperty("clientManaged").GetBoolean());
}

const string mixedJson = """
{
"id": "22222222-2222-2222-2222-222222222222",
"timestamp": "2026-03-15T21:26:54.987Z",
"parentId": null,
"type": "session.managed_settings_resolved",
"data": {
"source": "mixed",
"serverManaged": true,
"deviceManaged": true,
"failClosed": false,
"bypassPermissionsDisabled": true,
"managedKeys": ["permissions"]
}
}
""";

var mixedEvent = Assert.IsType<SessionManagedSettingsResolvedEvent>(
SessionEvent.FromJson(mixedJson));
Assert.Equal(ManagedSettingsResolvedSource.Mixed, mixedEvent.Data.Source);
Assert.Null(mixedEvent.Data.ClientManaged);
using var mixedDocument = JsonDocument.Parse(mixedEvent.ToJson());
Assert.False(mixedDocument.RootElement.GetProperty("data").TryGetProperty("clientManaged", out _));
}
}
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 @@ -833,6 +837,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 @@ -917,7 +922,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 @@ -1215,6 +1220,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 @@ -1250,7 +1256,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