Skip to content

Commit fa597e1

Browse files
authored
Add managed permission settings to session startup (#2139)
* Add managed permission settings to session startup Regenerate RPC and session-event mirrors from @github/copilot 1.0.79-5 and serialize permissions-only managed settings on create and resume across all six SDKs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c85d8afb-8b62-4636-9408-7a3ba6a82931 * Harden managed permission settings contracts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c85d8afb-8b62-4636-9408-7a3ba6a82931 * chore: retrigger CI after Actions outage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5d4d699-33e2-4a55-9d48-57d2e483dd3d --------- Copilot-Session: c85d8afb-8b62-4636-9408-7a3ba6a82931 Copilot-Session: d5d4d699-33e2-4a55-9d48-57d2e483dd3d Copilot-Session: 326a5a5a-aa12-4d85-8b86-c7e392cf1b23
1 parent cacf112 commit fa597e1

31 files changed

Lines changed: 1635 additions & 30 deletions

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,43 @@ All notable changes to the Copilot SDK are documented in this file.
55
This changelog is automatically generated by an AI agent when stable releases are published.
66
See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the full list.
77

8+
## [Unreleased]
9+
10+
### Feature: host-injected managed settings permissions
11+
12+
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).
13+
14+
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.
15+
16+
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.
17+
18+
```ts
19+
const session = await client.createSession({
20+
managedSettings: {
21+
permissions: {
22+
disableBypassPermissionsMode: "disable",
23+
deny: ["shell(rm*)"],
24+
ask: ["write"],
25+
},
26+
},
27+
});
28+
```
29+
30+
```cs
31+
var session = await client.CreateSessionAsync(new SessionConfig
32+
{
33+
ManagedSettings = new ManagedSettings
34+
{
35+
Permissions = new ManagedSettingsPermissions
36+
{
37+
DisableBypassPermissionsMode = DisableBypassPermissionsMode.Disable,
38+
Deny = ["shell(rm*)"],
39+
Ask = ["write"],
40+
},
41+
},
42+
});
43+
```
44+
845
## [v1.0.7](https://github.com/github/copilot-sdk/releases/tag/v1.0.7) (2026-07-16)
946

1047
### Feature: in-process (FFI) transport

dotnet/src/Client.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ private CopilotSession InitializeSession(
785785
session.RegisterTools(config.Tools ?? []);
786786
session.RegisterPermissionHandler(
787787
config.OnPermissionRequest,
788-
config.EnableManagedSettings is true);
788+
config.EnableManagedSettings is true || config.ManagedSettings is not null);
789789
session.RegisterMcpAuthHandler(config.OnMcpAuthRequest);
790790
session.RegisterCommands(config.Commands);
791791
session.RegisterElicitationHandler(config.OnElicitationRequest);
@@ -1205,6 +1205,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
12051205
ExpAssignments: config.ExpAssignments,
12061206
EnableManagedSettings: config.EnableManagedSettings,
12071207
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
1208+
ManagedSettings: config.ManagedSettings,
12081209
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
12091210
AdditionalDirectories: config.AdditionalDirectories);
12101211

@@ -1425,6 +1426,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
14251426
ExpAssignments: config.ExpAssignments,
14261427
EnableManagedSettings: config.EnableManagedSettings,
14271428
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
1429+
ManagedSettings: config.ManagedSettings,
14281430
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null,
14291431
AdditionalDirectories: config.AdditionalDirectories);
14301432

@@ -2781,6 +2783,7 @@ internal record CreateSessionRequest(
27812783
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
27822784
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
27832785
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
2786+
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
27842787
bool? EnableGitHubTelemetryForwarding = null,
27852788
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null,
27862789
IList<string>? AdditionalDirectories = null);
@@ -2895,6 +2898,7 @@ internal record ResumeSessionRequest(
28952898
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
28962899
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
28972900
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
2901+
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
28982902
bool? EnableGitHubTelemetryForwarding = null,
28992903
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null,
29002904
IList<string>? AdditionalDirectories = null);

dotnet/src/Types.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3044,6 +3044,70 @@ public sealed class GitHubMcpToolConfig
30443044
public bool? DisableFormDeferral { get; set; }
30453045
}
30463046

3047+
/// <summary>
3048+
/// Controls whether bypass-permissions mode is available in a managed session.
3049+
/// </summary>
3050+
[JsonConverter(typeof(JsonStringEnumConverter<DisableBypassPermissionsMode>))]
3051+
public enum DisableBypassPermissionsMode
3052+
{
3053+
/// <summary>Turn off bypass-permissions mode.</summary>
3054+
[JsonStringEnumMemberName("disable")]
3055+
Disable
3056+
}
3057+
3058+
/// <summary>
3059+
/// Permission rules injected as a managed-settings layer at session bootstrap.
3060+
/// All fields are optional; omitted fields impose no constraint from this layer.
3061+
/// </summary>
3062+
/// <remarks>
3063+
/// This layer composes restrictively with any server- or device-level managed
3064+
/// settings: <see cref="Deny"/> and <see cref="Ask"/> rules are unioned across
3065+
/// layers, every present <see cref="Allow"/> list must admit a tool for it to be
3066+
/// allowed, and <see cref="DisableBypassPermissionsMode"/> is honored if any
3067+
/// layer sets it (deny-wins).
3068+
/// </remarks>
3069+
public sealed class ManagedSettingsPermissions
3070+
{
3071+
/// <summary>
3072+
/// When set to <c>"disable"</c>, bypass-permissions mode is turned off for the
3073+
/// session regardless of other layers. Serialized as
3074+
/// <c>disableBypassPermissionsMode</c>.
3075+
/// </summary>
3076+
[JsonPropertyName("disableBypassPermissionsMode")]
3077+
public DisableBypassPermissionsMode? DisableBypassPermissionsMode { get; set; }
3078+
3079+
/// <summary>Tool-permission patterns that are always denied.</summary>
3080+
[JsonPropertyName("deny")]
3081+
public IList<string>? Deny { get; set; }
3082+
3083+
/// <summary>Tool-permission patterns that require an explicit ask.</summary>
3084+
[JsonPropertyName("ask")]
3085+
public IList<string>? Ask { get; set; }
3086+
3087+
/// <summary>Tool-permission patterns that are allowed without prompting.</summary>
3088+
[JsonPropertyName("allow")]
3089+
public IList<string>? Allow { get; set; }
3090+
}
3091+
3092+
/// <summary>
3093+
/// Managed-settings layer injected at session startup. Currently carries only a
3094+
/// <see cref="Permissions"/> object.
3095+
/// </summary>
3096+
/// <remarks>
3097+
/// This layer is startup-only and is not persisted with the session. It must be
3098+
/// re-supplied on <see cref="CopilotClient.ResumeSessionAsync"/> to remain in
3099+
/// effect; omitting it on resume clears the previously injected layer. It can be
3100+
/// combined with <see cref="SessionConfigBase.EnableManagedSettings"/>. Older
3101+
/// runtimes may ignore this additive field, so hosts must not rely on injected
3102+
/// policy until they ship a compatible runtime.
3103+
/// </remarks>
3104+
public sealed class ManagedSettings
3105+
{
3106+
/// <summary>Permission rules for this managed-settings layer.</summary>
3107+
[JsonPropertyName("permissions")]
3108+
public ManagedSettingsPermissions? Permissions { get; set; }
3109+
}
3110+
30473111
/// <summary>
30483112
/// Shared configuration properties for creating or resuming a Copilot session.
30493113
/// Use <see cref="SessionConfig"/> when creating a new session, or
@@ -3136,6 +3200,7 @@ protected SessionConfigBase(SessionConfigBase? other)
31363200
RemoteSession = other.RemoteSession;
31373201
ExpAssignments = other.ExpAssignments;
31383202
EnableManagedSettings = other.EnableManagedSettings;
3203+
ManagedSettings = other.ManagedSettings;
31393204
#pragma warning disable GHCP001
31403205
Canvases = other.Canvases is not null ? [.. other.Canvases] : null;
31413206
RequestCanvasRenderer = other.RequestCanvasRenderer;
@@ -3601,6 +3666,17 @@ protected SessionConfigBase(SessionConfigBase? other)
36013666
/// </summary>
36023667
public bool? EnableManagedSettings { get; set; }
36033668

3669+
/// <summary>
3670+
/// Optional managed-settings layer injected at session bootstrap. Currently
3671+
/// carries a permissions object that composes restrictively with any
3672+
/// server- or device-level managed settings. This layer is startup-only and
3673+
/// is not persisted: it must be re-supplied on resume to remain in effect,
3674+
/// and omitting it on resume clears the previously injected layer. Can be
3675+
/// combined with <see cref="EnableManagedSettings"/>. Serialized on the wire
3676+
/// as <c>managedSettings</c>.
3677+
/// </summary>
3678+
public ManagedSettings? ManagedSettings { get; set; }
3679+
36043680
#pragma warning disable GHCP001
36053681
/// <summary>
36063682
/// Canvas declarations advertised by this connection. The runtime forwards

dotnet/test/Unit/ClientSessionLifetimeTests.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Runtime.CompilerServices;
1111
using System.Text;
1212
using System.Text.Json;
13+
using GitHub.Copilot.Rpc;
1314
using Xunit;
1415

1516
namespace GitHub.Copilot.Test.Unit;
@@ -515,6 +516,94 @@ private static int GetPrivateDictionaryCount(CopilotClient client, string fieldN
515516
return (int)count.GetValue(dictionary)!;
516517
}
517518

519+
[Fact]
520+
public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions()
521+
{
522+
await using var server = await FakeCopilotServer.StartAsync();
523+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
524+
await client.StartAsync();
525+
var permissionInvocation = new TaskCompletionSource<PermissionInvocation>(
526+
TaskCreationOptions.RunContinuationsAsynchronously);
527+
528+
await using var session = await client.CreateSessionAsync(new SessionConfig
529+
{
530+
ManagedSettings = new ManagedSettings
531+
{
532+
Permissions = new ManagedSettingsPermissions
533+
{
534+
DisableBypassPermissionsMode = DisableBypassPermissionsMode.Disable,
535+
Deny = ["shell(rm*)"],
536+
Ask = ["write"],
537+
Allow = []
538+
}
539+
},
540+
OnPermissionRequest = (_, invocation) =>
541+
{
542+
permissionInvocation.TrySetResult(invocation);
543+
return Task.FromResult(PermissionDecision.NoResult());
544+
}
545+
});
546+
547+
var request = Assert.Single(server.Requests, request => request.Method == "session.create");
548+
Assert.False(request.Params.TryGetProperty("enableManagedSettings", out _));
549+
var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions");
550+
Assert.Equal("disable", permissions.GetProperty("disableBypassPermissionsMode").GetString());
551+
Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString());
552+
Assert.Equal("write", Assert.Single(permissions.GetProperty("ask").EnumerateArray()).GetString());
553+
Assert.Empty(permissions.GetProperty("allow").EnumerateArray());
554+
555+
DispatchEvent(session, new PermissionRequestedEvent
556+
{
557+
Data = new PermissionRequestedData
558+
{
559+
PermissionRequest = new PermissionRequest { Kind = "read" },
560+
RequestId = "managed-permission"
561+
}
562+
});
563+
var invocation = await permissionInvocation.Task.WaitAsync(TimeSpan.FromSeconds(5));
564+
Assert.True(invocation.ManagedSettingsEnabled);
565+
}
566+
567+
[Fact]
568+
public async Task CreateSessionAsync_Omits_ManagedSettings_When_Unset()
569+
{
570+
await using var server = await FakeCopilotServer.StartAsync();
571+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
572+
await client.StartAsync();
573+
574+
await using var session = await client.CreateSessionAsync(new SessionConfig
575+
{
576+
OnPermissionRequest = PermissionHandler.ApproveAll
577+
});
578+
579+
var request = Assert.Single(server.Requests, request => request.Method == "session.create");
580+
Assert.False(request.Params.TryGetProperty("managedSettings", out _));
581+
}
582+
583+
[Fact]
584+
public async Task ResumeSessionAsync_Serializes_ManagedSettings_Permissions()
585+
{
586+
await using var server = await FakeCopilotServer.StartAsync();
587+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
588+
589+
await using var session = await client.ResumeSessionAsync("session-managed", new ResumeSessionConfig
590+
{
591+
ManagedSettings = new ManagedSettings
592+
{
593+
Permissions = new ManagedSettingsPermissions
594+
{
595+
Deny = ["shell(rm*)"]
596+
}
597+
},
598+
OnPermissionRequest = PermissionHandler.ApproveAll,
599+
OnEvent = _ => { }
600+
});
601+
602+
var request = Assert.Single(server.Requests, request => request.Method == "session.resume");
603+
var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions");
604+
Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString());
605+
}
606+
518607
private static void DispatchEvent(CopilotSession session, SessionEvent evt)
519608
{
520609
var method = typeof(CopilotSession).GetMethod("DispatchEvent", BindingFlags.Instance | BindingFlags.NonPublic)

dotnet/test/Unit/SessionEventSerializationTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,65 @@ public void McpOauthRequiredData_Preserves_Static_Client_Secret()
368368
Assert.NotNull(authEvent.Data.StaticClientConfig);
369369
Assert.Equal("static-secret", authEvent.Data.StaticClientConfig.ClientSecret);
370370
}
371+
372+
[Fact]
373+
public void ManagedSettingsResolvedData_Preserves_Client_Provenance()
374+
{
375+
Assert.Equal("server", ManagedSettingsResolvedSource.Server.Value);
376+
Assert.Equal("device", ManagedSettingsResolvedSource.Device.Value);
377+
Assert.Equal("client", ManagedSettingsResolvedSource.Client.Value);
378+
Assert.Equal("mixed", ManagedSettingsResolvedSource.Mixed.Value);
379+
Assert.Equal("none", ManagedSettingsResolvedSource.None.Value);
380+
381+
const string clientJson = """
382+
{
383+
"id": "11111111-1111-1111-1111-111111111111",
384+
"timestamp": "2026-03-15T21:26:54.987Z",
385+
"parentId": null,
386+
"type": "session.managed_settings_resolved",
387+
"data": {
388+
"source": "client",
389+
"serverManaged": false,
390+
"deviceManaged": false,
391+
"clientManaged": true,
392+
"failClosed": false,
393+
"bypassPermissionsDisabled": true,
394+
"managedKeys": ["permissions"]
395+
}
396+
}
397+
""";
398+
399+
var clientEvent = Assert.IsType<SessionManagedSettingsResolvedEvent>(
400+
SessionEvent.FromJson(clientJson));
401+
Assert.Equal(ManagedSettingsResolvedSource.Client, clientEvent.Data.Source);
402+
Assert.True(clientEvent.Data.ClientManaged);
403+
using (var document = JsonDocument.Parse(clientEvent.ToJson()))
404+
{
405+
Assert.True(document.RootElement.GetProperty("data").GetProperty("clientManaged").GetBoolean());
406+
}
407+
408+
const string mixedJson = """
409+
{
410+
"id": "22222222-2222-2222-2222-222222222222",
411+
"timestamp": "2026-03-15T21:26:54.987Z",
412+
"parentId": null,
413+
"type": "session.managed_settings_resolved",
414+
"data": {
415+
"source": "mixed",
416+
"serverManaged": true,
417+
"deviceManaged": true,
418+
"failClosed": false,
419+
"bypassPermissionsDisabled": true,
420+
"managedKeys": ["permissions"]
421+
}
422+
}
423+
""";
424+
425+
var mixedEvent = Assert.IsType<SessionManagedSettingsResolvedEvent>(
426+
SessionEvent.FromJson(mixedJson));
427+
Assert.Equal(ManagedSettingsResolvedSource.Mixed, mixedEvent.Data.Source);
428+
Assert.Null(mixedEvent.Data.ClientManaged);
429+
using var mixedDocument = JsonDocument.Parse(mixedEvent.ToJson());
430+
Assert.False(mixedDocument.RootElement.GetProperty("data").TryGetProperty("clientManaged", out _));
431+
}
371432
}

go/client.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,10 @@ func extractTransformCallbacks(config *SystemMessageConfig) (*SystemMessageConfi
750750
return wireConfig, callbacks
751751
}
752752

753+
func hasManagedSettings(enableManagedSettings *bool, managedSettings *ManagedSettings) bool {
754+
return (enableManagedSettings != nil && *enableManagedSettings) || managedSettings != nil
755+
}
756+
753757
func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) {
754758
if config == nil {
755759
config = &SessionConfig{}
@@ -833,6 +837,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
833837
req.ExtensionInfo = config.ExtensionInfo
834838
req.ExpAssignments = config.ExpAssignments
835839
req.EnableManagedSettings = config.EnableManagedSettings
840+
req.ManagedSettings = config.ManagedSettings
836841

837842
if len(config.Commands) > 0 {
838843
cmds := make([]wireCommand, 0, len(config.Commands))
@@ -917,7 +922,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
917922
sessionID,
918923
c.client,
919924
"",
920-
config.EnableManagedSettings != nil && *config.EnableManagedSettings,
925+
hasManagedSettings(config.EnableManagedSettings, config.ManagedSettings),
921926
)
922927

923928
s.registerTools(config.Tools)
@@ -1215,6 +1220,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
12151220
req.ExtensionInfo = config.ExtensionInfo
12161221
req.ExpAssignments = config.ExpAssignments
12171222
req.EnableManagedSettings = config.EnableManagedSettings
1223+
req.ManagedSettings = config.ManagedSettings
12181224
if config.OnPermissionRequest != nil {
12191225
req.RequestPermission = Bool(true)
12201226
}
@@ -1250,7 +1256,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
12501256
sessionID,
12511257
c.client,
12521258
"",
1253-
config.EnableManagedSettings != nil && *config.EnableManagedSettings,
1259+
hasManagedSettings(config.EnableManagedSettings, config.ManagedSettings),
12541260
)
12551261

12561262
session.registerTools(config.Tools)

0 commit comments

Comments
 (0)