Skip to content

Commit 3676a65

Browse files
joshspicerCopilot
andcommitted
feat(sdk): add optional managedSettings.permissions to session create/resume
Add an optional per-session `managedSettings` field (permissions-only contract) across all six language SDKs, alongside the existing `enableManagedSettings` boolean. Hosts can inject enterprise permission policy at session startup via: managedSettings.permissions = { disableBypassPermissionsMode?: "disable", deny?: string[], ask?: string[], allow?: string[], } Semantics: startup-only (not persisted), must be re-supplied on resume, composes restrictively with runtime-managed settings, and older runtimes fail closed. Wired through hand-written wire types at both create and resume in Node, Python, Go, .NET, Rust, and Java, plus tests, docs, and a CHANGELOG entry. Generated RPC mirror types regenerated from the runtime schema (TS/Python/Go/Rust; C# unaffected as it does not mirror SessionOpenOptions). No SDK protocol bump. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b88e8c8 commit 3676a65

30 files changed

Lines changed: 1305 additions & 4 deletions

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,41 @@ 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`. Older runtimes that do not recognize the field reject session creation (fail-closed) rather than silently ignoring it, so it requires a Copilot CLI runtime whose schema includes managed settings.
15+
16+
```ts
17+
const session = await client.createSession({
18+
managedSettings: {
19+
permissions: {
20+
disableBypassPermissionsMode: "disable",
21+
deny: ["shell(rm*)"],
22+
ask: ["write"],
23+
},
24+
},
25+
});
26+
```
27+
28+
```cs
29+
var session = await client.CreateSessionAsync(new SessionConfig
30+
{
31+
ManagedSettings = new ManagedSettings
32+
{
33+
Permissions = new ManagedSettingsPermissions
34+
{
35+
DisableBypassPermissionsMode = "disable",
36+
Deny = ["shell(rm*)"],
37+
Ask = ["write"],
38+
},
39+
},
40+
});
41+
```
42+
843
## [v1.0.7](https://github.com/github/copilot-sdk/releases/tag/v1.0.7) (2026-07-16)
944

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

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
12011201
ExpAssignments: config.ExpAssignments,
12021202
EnableManagedSettings: config.EnableManagedSettings,
12031203
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
1204+
ManagedSettings: config.ManagedSettings,
12041205
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null);
12051206

12061207
var rpcTimestamp = Stopwatch.GetTimestamp();
@@ -1417,6 +1418,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
14171418
ExpAssignments: config.ExpAssignments,
14181419
EnableManagedSettings: config.EnableManagedSettings,
14191420
GitHubMcpToolConfig: config.GitHubMcpToolConfig,
1421+
ManagedSettings: config.ManagedSettings,
14201422
EnableGitHubTelemetryForwarding: _options.OnGitHubTelemetry != null ? true : null);
14211423

14221424
var rpcTimestamp = Stopwatch.GetTimestamp();
@@ -2770,6 +2772,7 @@ internal record CreateSessionRequest(
27702772
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
27712773
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
27722774
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
2775+
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
27732776
bool? EnableGitHubTelemetryForwarding = null,
27742777
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null);
27752778
#pragma warning restore GHCP001
@@ -2878,6 +2881,7 @@ internal record ResumeSessionRequest(
28782881
OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence = null,
28792882
[property: JsonPropertyName("expAssignments")] CopilotExpAssignmentResponse? ExpAssignments = null,
28802883
[property: JsonPropertyName("enableManagedSettings")] bool? EnableManagedSettings = null,
2884+
[property: JsonPropertyName("managedSettings")] ManagedSettings? ManagedSettings = null,
28812885
bool? EnableGitHubTelemetryForwarding = null,
28822886
[property: JsonPropertyName("githubMcpToolConfig")] GitHubMcpToolConfig? GitHubMcpToolConfig = null);
28832887
#pragma warning restore GHCP001

dotnet/src/Types.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2990,6 +2990,59 @@ public sealed class GitHubMcpToolConfig
29902990
public bool? DisableFormDeferral { get; set; }
29912991
}
29922992

2993+
/// <summary>
2994+
/// Permission rules injected as a managed-settings layer at session bootstrap.
2995+
/// All fields are optional; omitted fields impose no constraint from this layer.
2996+
/// </summary>
2997+
/// <remarks>
2998+
/// This layer composes restrictively with any server- or device-level managed
2999+
/// settings: <see cref="Deny"/> and <see cref="Ask"/> rules are unioned across
3000+
/// layers, every present <see cref="Allow"/> list must admit a tool for it to be
3001+
/// allowed, and <see cref="DisableBypassPermissionsMode"/> is honored if any
3002+
/// layer sets it (deny-wins).
3003+
/// </remarks>
3004+
public sealed class ManagedSettingsPermissions
3005+
{
3006+
/// <summary>
3007+
/// When set to <c>"disable"</c>, bypass-permissions mode is turned off for the
3008+
/// session regardless of other layers. Serialized as
3009+
/// <c>disableBypassPermissionsMode</c>.
3010+
/// </summary>
3011+
[JsonPropertyName("disableBypassPermissionsMode")]
3012+
public string? DisableBypassPermissionsMode { get; set; }
3013+
3014+
/// <summary>Tool-permission patterns that are always denied.</summary>
3015+
[JsonPropertyName("deny")]
3016+
public IList<string>? Deny { get; set; }
3017+
3018+
/// <summary>Tool-permission patterns that require an explicit ask.</summary>
3019+
[JsonPropertyName("ask")]
3020+
public IList<string>? Ask { get; set; }
3021+
3022+
/// <summary>Tool-permission patterns that are allowed without prompting.</summary>
3023+
[JsonPropertyName("allow")]
3024+
public IList<string>? Allow { get; set; }
3025+
}
3026+
3027+
/// <summary>
3028+
/// Managed-settings layer injected at session startup. Currently carries only a
3029+
/// <see cref="Permissions"/> object.
3030+
/// </summary>
3031+
/// <remarks>
3032+
/// This layer is startup-only and is not persisted with the session. It must be
3033+
/// re-supplied on <see cref="CopilotClient.ResumeSessionAsync"/> to remain in
3034+
/// effect; omitting it on resume clears the previously injected layer. It can be
3035+
/// combined with <see cref="SessionConfigBase.EnableManagedSettings"/>. Older
3036+
/// runtimes that do not recognize the <c>managedSettings</c> field reject session
3037+
/// creation (fail-closed).
3038+
/// </remarks>
3039+
public sealed class ManagedSettings
3040+
{
3041+
/// <summary>Permission rules for this managed-settings layer.</summary>
3042+
[JsonPropertyName("permissions")]
3043+
public ManagedSettingsPermissions? Permissions { get; set; }
3044+
}
3045+
29933046
/// <summary>
29943047
/// Shared configuration properties for creating or resuming a Copilot session.
29953048
/// Use <see cref="SessionConfig"/> when creating a new session, or
@@ -3080,6 +3133,7 @@ protected SessionConfigBase(SessionConfigBase? other)
30803133
RemoteSession = other.RemoteSession;
30813134
ExpAssignments = other.ExpAssignments;
30823135
EnableManagedSettings = other.EnableManagedSettings;
3136+
ManagedSettings = other.ManagedSettings;
30833137
#pragma warning disable GHCP001
30843138
Canvases = other.Canvases is not null ? [.. other.Canvases] : null;
30853139
RequestCanvasRenderer = other.RequestCanvasRenderer;
@@ -3522,6 +3576,17 @@ protected SessionConfigBase(SessionConfigBase? other)
35223576
/// </summary>
35233577
public bool? EnableManagedSettings { get; set; }
35243578

3579+
/// <summary>
3580+
/// Optional managed-settings layer injected at session bootstrap. Currently
3581+
/// carries a permissions object that composes restrictively with any
3582+
/// server- or device-level managed settings. This layer is startup-only and
3583+
/// is not persisted: it must be re-supplied on resume to remain in effect,
3584+
/// and omitting it on resume clears the previously injected layer. Can be
3585+
/// combined with <see cref="EnableManagedSettings"/>. Serialized on the wire
3586+
/// as <c>managedSettings</c>.
3587+
/// </summary>
3588+
public ManagedSettings? ManagedSettings { get; set; }
3589+
35253590
#pragma warning disable GHCP001
35263591
/// <summary>
35273592
/// Canvas declarations advertised by this connection. The runtime forwards

dotnet/test/Unit/ClientSessionLifetimeTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,78 @@ private static int GetPrivateDictionaryCount(CopilotClient client, string fieldN
450450
return (int)count.GetValue(dictionary)!;
451451
}
452452

453+
[Fact]
454+
public async Task CreateSessionAsync_Serializes_ManagedSettings_Permissions()
455+
{
456+
await using var server = await FakeCopilotServer.StartAsync();
457+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
458+
await client.StartAsync();
459+
460+
await using var session = await client.CreateSessionAsync(new SessionConfig
461+
{
462+
EnableManagedSettings = true,
463+
ManagedSettings = new ManagedSettings
464+
{
465+
Permissions = new ManagedSettingsPermissions
466+
{
467+
DisableBypassPermissionsMode = "disable",
468+
Deny = ["shell(rm*)"],
469+
Ask = ["write"],
470+
Allow = []
471+
}
472+
},
473+
OnPermissionRequest = PermissionHandler.ApproveAll
474+
});
475+
476+
var request = Assert.Single(server.Requests, request => request.Method == "session.create");
477+
Assert.True(request.Params.GetProperty("enableManagedSettings").GetBoolean());
478+
var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions");
479+
Assert.Equal("disable", permissions.GetProperty("disableBypassPermissionsMode").GetString());
480+
Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString());
481+
Assert.Equal("write", Assert.Single(permissions.GetProperty("ask").EnumerateArray()).GetString());
482+
Assert.Empty(permissions.GetProperty("allow").EnumerateArray());
483+
}
484+
485+
[Fact]
486+
public async Task CreateSessionAsync_Omits_ManagedSettings_When_Unset()
487+
{
488+
await using var server = await FakeCopilotServer.StartAsync();
489+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
490+
await client.StartAsync();
491+
492+
await using var session = await client.CreateSessionAsync(new SessionConfig
493+
{
494+
OnPermissionRequest = PermissionHandler.ApproveAll
495+
});
496+
497+
var request = Assert.Single(server.Requests, request => request.Method == "session.create");
498+
Assert.False(request.Params.TryGetProperty("managedSettings", out _));
499+
}
500+
501+
[Fact]
502+
public async Task ResumeSessionAsync_Serializes_ManagedSettings_Permissions()
503+
{
504+
await using var server = await FakeCopilotServer.StartAsync();
505+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
506+
507+
await using var session = await client.ResumeSessionAsync("session-managed", new ResumeSessionConfig
508+
{
509+
ManagedSettings = new ManagedSettings
510+
{
511+
Permissions = new ManagedSettingsPermissions
512+
{
513+
Deny = ["shell(rm*)"]
514+
}
515+
},
516+
OnPermissionRequest = PermissionHandler.ApproveAll,
517+
OnEvent = _ => { }
518+
});
519+
520+
var request = Assert.Single(server.Requests, request => request.Method == "session.resume");
521+
var permissions = request.Params.GetProperty("managedSettings").GetProperty("permissions");
522+
Assert.Equal("shell(rm*)", Assert.Single(permissions.GetProperty("deny").EnumerateArray()).GetString());
523+
}
524+
453525
private static void DispatchEvent(CopilotSession session, SessionEvent evt)
454526
{
455527
var method = typeof(CopilotSession).GetMethod("DispatchEvent", BindingFlags.Instance | BindingFlags.NonPublic)

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
828828
req.ExtensionInfo = config.ExtensionInfo
829829
req.ExpAssignments = config.ExpAssignments
830830
req.EnableManagedSettings = config.EnableManagedSettings
831+
req.ManagedSettings = config.ManagedSettings
831832

832833
if len(config.Commands) > 0 {
833834
cmds := make([]wireCommand, 0, len(config.Commands))
@@ -1203,6 +1204,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
12031204
req.ExtensionInfo = config.ExtensionInfo
12041205
req.ExpAssignments = config.ExpAssignments
12051206
req.EnableManagedSettings = config.EnableManagedSettings
1207+
req.ManagedSettings = config.ManagedSettings
12061208
if config.OnPermissionRequest != nil {
12071209
req.RequestPermission = Bool(true)
12081210
}

go/client_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3367,3 +3367,100 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) {
33673367
}
33683368
})
33693369
}
3370+
3371+
func TestSessionRequests_ManagedSettings(t *testing.T) {
3372+
settings := &ManagedSettings{
3373+
Permissions: &ManagedSettingsPermissions{
3374+
DisableBypassPermissionsMode: String("disable"),
3375+
Deny: []string{"Shell(git push)"},
3376+
Ask: []string{"Domain(publish.example)"},
3377+
Allow: []string{"Read(**)"},
3378+
},
3379+
}
3380+
3381+
expectedPermissions := map[string]any{
3382+
"disableBypassPermissionsMode": "disable",
3383+
"deny": []any{"Shell(git push)"},
3384+
"ask": []any{"Domain(publish.example)"},
3385+
"allow": []any{"Read(**)"},
3386+
}
3387+
3388+
t.Run("includes managedSettings on create when set", func(t *testing.T) {
3389+
req := createSessionRequest{EnableManagedSettings: Bool(true), ManagedSettings: settings}
3390+
data, err := json.Marshal(req)
3391+
if err != nil {
3392+
t.Fatalf("Failed to marshal: %v", err)
3393+
}
3394+
var m map[string]any
3395+
if err := json.Unmarshal(data, &m); err != nil {
3396+
t.Fatalf("Failed to unmarshal: %v", err)
3397+
}
3398+
if m["enableManagedSettings"] != true {
3399+
t.Errorf("Expected enableManagedSettings true, got %v", m["enableManagedSettings"])
3400+
}
3401+
ms, ok := m["managedSettings"].(map[string]any)
3402+
if !ok {
3403+
t.Fatalf("Expected managedSettings object, got %v", m["managedSettings"])
3404+
}
3405+
perms, ok := ms["permissions"].(map[string]any)
3406+
if !ok {
3407+
t.Fatalf("Expected permissions object, got %v", ms["permissions"])
3408+
}
3409+
if !reflect.DeepEqual(perms, expectedPermissions) {
3410+
t.Errorf("permissions mismatch:\n got: %#v\nwant: %#v", perms, expectedPermissions)
3411+
}
3412+
})
3413+
3414+
t.Run("includes managedSettings on resume when set", func(t *testing.T) {
3415+
req := resumeSessionRequest{SessionID: "s1", ManagedSettings: settings}
3416+
data, err := json.Marshal(req)
3417+
if err != nil {
3418+
t.Fatalf("Failed to marshal: %v", err)
3419+
}
3420+
var m map[string]any
3421+
if err := json.Unmarshal(data, &m); err != nil {
3422+
t.Fatalf("Failed to unmarshal: %v", err)
3423+
}
3424+
if _, ok := m["managedSettings"].(map[string]any); !ok {
3425+
t.Fatalf("Expected managedSettings object, got %v", m["managedSettings"])
3426+
}
3427+
})
3428+
3429+
t.Run("omits managedSettings when nil", func(t *testing.T) {
3430+
req := createSessionRequest{}
3431+
data, _ := json.Marshal(req)
3432+
var m map[string]any
3433+
json.Unmarshal(data, &m)
3434+
if _, ok := m["managedSettings"]; ok {
3435+
t.Error("Expected managedSettings to be omitted when nil")
3436+
}
3437+
})
3438+
3439+
t.Run("omits empty permission arrays (omitempty idiom)", func(t *testing.T) {
3440+
// Go's `omitempty` drops both nil and empty slices; an empty rule list
3441+
// is semantically equivalent to no rules for that key.
3442+
req := createSessionRequest{ManagedSettings: &ManagedSettings{
3443+
Permissions: &ManagedSettingsPermissions{
3444+
DisableBypassPermissionsMode: String("disable"),
3445+
Deny: []string{},
3446+
Ask: []string{},
3447+
Allow: []string{},
3448+
},
3449+
}}
3450+
data, err := json.Marshal(req)
3451+
if err != nil {
3452+
t.Fatalf("Failed to marshal: %v", err)
3453+
}
3454+
var m map[string]any
3455+
json.Unmarshal(data, &m)
3456+
perms := m["managedSettings"].(map[string]any)["permissions"].(map[string]any)
3457+
if perms["disableBypassPermissionsMode"] != "disable" {
3458+
t.Errorf("Expected disableBypassPermissionsMode preserved, got %v", perms["disableBypassPermissionsMode"])
3459+
}
3460+
for _, key := range []string{"deny", "ask", "allow"} {
3461+
if _, ok := perms[key]; ok {
3462+
t.Errorf("Expected %s to be omitted when empty, got %v", key, perms[key])
3463+
}
3464+
}
3465+
})
3466+
}

0 commit comments

Comments
 (0)