Skip to content

Commit 37f5ad2

Browse files
stephentoubCopilot
andcommitted
Add PermissionRequestResultKind type for .NET and Go SDKs
Replace magic strings for permission request result kinds with strongly-typed values. .NET uses a readonly struct following the ChatRole pattern from Microsoft.Extensions.AI. Go uses a typed string constant block following the existing ConnectionState pattern. Both remain extensible for custom values while making the well-known kinds (approved, denied-by-rules, denied-interactively-by-user, denied-no-approval-rule-and-could-not-request-from-user) discoverable via IntelliSense/autocomplete. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b9f746a commit 37f5ad2

14 files changed

Lines changed: 329 additions & 26 deletions

dotnet/src/Client.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,7 +1314,7 @@ public async Task<PermissionRequestResponse> OnPermissionRequest(string sessionI
13141314
{
13151315
return new PermissionRequestResponse(new PermissionRequestResult
13161316
{
1317-
Kind = "denied-no-approval-rule-and-could-not-request-from-user"
1317+
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
13181318
});
13191319
}
13201320

@@ -1328,7 +1328,7 @@ public async Task<PermissionRequestResponse> OnPermissionRequest(string sessionI
13281328
// If permission handler fails, deny the permission
13291329
return new PermissionRequestResponse(new PermissionRequestResult
13301330
{
1331-
Kind = "denied-no-approval-rule-and-could-not-request-from-user"
1331+
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
13321332
});
13331333
}
13341334
}

dotnet/src/PermissionHandlers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ public static class PermissionHandler
99
{
1010
/// <summary>A <see cref="PermissionRequestHandler"/> that approves all permission requests.</summary>
1111
public static PermissionRequestHandler ApproveAll { get; } =
12-
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = "approved" });
12+
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
1313
}

dotnet/src/Session.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ internal async Task<PermissionRequestResult> HandlePermissionRequestAsync(JsonEl
317317
{
318318
return new PermissionRequestResult
319319
{
320-
Kind = "denied-no-approval-rule-and-could-not-request-from-user"
320+
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
321321
};
322322
}
323323

dotnet/src/Types.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------------------------------------------*/
44

5+
using System.ComponentModel;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
58
using System.Text.Json;
69
using System.Text.Json.Serialization;
710
using Microsoft.Extensions.AI;
@@ -162,10 +165,71 @@ public class PermissionRequest
162165
public Dictionary<string, object>? ExtensionData { get; set; }
163166
}
164167

168+
/// <summary>Describes the kind of a permission request result.</summary>
169+
[JsonConverter(typeof(PermissionRequestResultKind.Converter))]
170+
[DebuggerDisplay("{Value,nq}")]
171+
public readonly struct PermissionRequestResultKind : IEquatable<PermissionRequestResultKind>
172+
{
173+
/// <summary>Gets the kind indicating the permission was approved.</summary>
174+
public static PermissionRequestResultKind Approved { get; } = new("approved");
175+
176+
/// <summary>Gets the kind indicating the permission was denied by rules.</summary>
177+
public static PermissionRequestResultKind DeniedByRules { get; } = new("denied-by-rules");
178+
179+
/// <summary>Gets the kind indicating the permission was denied because no approval rule was found and the user could not be prompted.</summary>
180+
public static PermissionRequestResultKind DeniedCouldNotRequestFromUser { get; } = new("denied-no-approval-rule-and-could-not-request-from-user");
181+
182+
/// <summary>Gets the kind indicating the permission was denied interactively by the user.</summary>
183+
public static PermissionRequestResultKind DeniedInteractivelyByUser { get; } = new("denied-interactively-by-user");
184+
185+
/// <summary>Gets the underlying string value of this <see cref="PermissionRequestResultKind"/>.</summary>
186+
public string Value { get; }
187+
188+
/// <summary>Initializes a new instance of the <see cref="PermissionRequestResultKind"/> struct.</summary>
189+
/// <param name="value">The string value for this kind.</param>
190+
[JsonConstructor]
191+
public PermissionRequestResultKind(string value)
192+
{
193+
ArgumentNullException.ThrowIfNull(value);
194+
Value = value;
195+
}
196+
197+
/// <inheritdoc/>
198+
public static bool operator ==(PermissionRequestResultKind left, PermissionRequestResultKind right) => left.Equals(right);
199+
200+
/// <inheritdoc/>
201+
public static bool operator !=(PermissionRequestResultKind left, PermissionRequestResultKind right) => !left.Equals(right);
202+
203+
/// <inheritdoc/>
204+
public override bool Equals([NotNullWhen(true)] object? obj) => obj is PermissionRequestResultKind other && Equals(other);
205+
206+
/// <inheritdoc/>
207+
public bool Equals(PermissionRequestResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
208+
209+
/// <inheritdoc/>
210+
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
211+
212+
/// <inheritdoc/>
213+
public override string ToString() => Value;
214+
215+
/// <summary>Provides a <see cref="JsonConverter{PermissionRequestResultKind}"/> for serializing <see cref="PermissionRequestResultKind"/> instances.</summary>
216+
[EditorBrowsable(EditorBrowsableState.Never)]
217+
public sealed class Converter : JsonConverter<PermissionRequestResultKind>
218+
{
219+
/// <inheritdoc/>
220+
public override PermissionRequestResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
221+
new(reader.GetString()!);
222+
223+
/// <inheritdoc/>
224+
public override void Write(Utf8JsonWriter writer, PermissionRequestResultKind value, JsonSerializerOptions options) =>
225+
writer.WriteStringValue(value.Value);
226+
}
227+
}
228+
165229
public class PermissionRequestResult
166230
{
167231
[JsonPropertyName("kind")]
168-
public string Kind { get; set; } = string.Empty;
232+
public PermissionRequestResultKind Kind { get; set; }
169233

170234
[JsonPropertyName("rules")]
171235
public List<object>? Rules { get; set; }
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.Text.Json;
6+
using Xunit;
7+
8+
namespace GitHub.Copilot.SDK.Test;
9+
10+
public class PermissionRequestResultKindTests
11+
{
12+
private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web)
13+
{
14+
TypeInfoResolver = TestJsonContext.Default,
15+
};
16+
17+
[Fact]
18+
public void WellKnownKinds_HaveExpectedValues()
19+
{
20+
Assert.Equal("approved", PermissionRequestResultKind.Approved.Value);
21+
Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.Value);
22+
Assert.Equal("denied-no-approval-rule-and-could-not-request-from-user", PermissionRequestResultKind.DeniedCouldNotRequestFromUser.Value);
23+
Assert.Equal("denied-interactively-by-user", PermissionRequestResultKind.DeniedInteractivelyByUser.Value);
24+
}
25+
26+
[Fact]
27+
public void Equals_SameValue_ReturnsTrue()
28+
{
29+
var a = new PermissionRequestResultKind("approved");
30+
Assert.True(a == PermissionRequestResultKind.Approved);
31+
Assert.True(a.Equals(PermissionRequestResultKind.Approved));
32+
Assert.True(a.Equals((object)PermissionRequestResultKind.Approved));
33+
}
34+
35+
[Fact]
36+
public void Equals_DifferentValue_ReturnsFalse()
37+
{
38+
Assert.True(PermissionRequestResultKind.Approved != PermissionRequestResultKind.DeniedByRules);
39+
Assert.False(PermissionRequestResultKind.Approved.Equals(PermissionRequestResultKind.DeniedByRules));
40+
}
41+
42+
[Fact]
43+
public void Equals_IsCaseInsensitive()
44+
{
45+
var upper = new PermissionRequestResultKind("APPROVED");
46+
Assert.Equal(PermissionRequestResultKind.Approved, upper);
47+
}
48+
49+
[Fact]
50+
public void GetHashCode_IsCaseInsensitive()
51+
{
52+
var upper = new PermissionRequestResultKind("APPROVED");
53+
Assert.Equal(PermissionRequestResultKind.Approved.GetHashCode(), upper.GetHashCode());
54+
}
55+
56+
[Fact]
57+
public void ToString_ReturnsValue()
58+
{
59+
Assert.Equal("approved", PermissionRequestResultKind.Approved.ToString());
60+
Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.ToString());
61+
}
62+
63+
[Fact]
64+
public void CustomValue_IsPreserved()
65+
{
66+
var custom = new PermissionRequestResultKind("custom-kind");
67+
Assert.Equal("custom-kind", custom.Value);
68+
Assert.Equal("custom-kind", custom.ToString());
69+
}
70+
71+
[Fact]
72+
public void Constructor_NullValue_Throws()
73+
{
74+
Assert.Throws<ArgumentNullException>(() => new PermissionRequestResultKind(null!));
75+
}
76+
77+
[Fact]
78+
public void Equals_NonPermissionRequestResultKindObject_ReturnsFalse()
79+
{
80+
Assert.False(PermissionRequestResultKind.Approved.Equals("approved"));
81+
}
82+
83+
[Fact]
84+
public void JsonSerialize_WritesStringValue()
85+
{
86+
var result = new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved };
87+
var json = JsonSerializer.Serialize(result, s_jsonOptions);
88+
Assert.Contains("\"kind\":\"approved\"", json);
89+
}
90+
91+
[Fact]
92+
public void JsonDeserialize_ReadsStringValue()
93+
{
94+
var json = """{"kind":"denied-by-rules"}""";
95+
var result = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
96+
Assert.Equal(PermissionRequestResultKind.DeniedByRules, result.Kind);
97+
}
98+
99+
[Fact]
100+
public void JsonRoundTrip_PreservesAllKinds()
101+
{
102+
var kinds = new[]
103+
{
104+
PermissionRequestResultKind.Approved,
105+
PermissionRequestResultKind.DeniedByRules,
106+
PermissionRequestResultKind.DeniedCouldNotRequestFromUser,
107+
PermissionRequestResultKind.DeniedInteractivelyByUser,
108+
};
109+
110+
foreach (var kind in kinds)
111+
{
112+
var result = new PermissionRequestResult { Kind = kind };
113+
var json = JsonSerializer.Serialize(result, s_jsonOptions);
114+
var deserialized = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
115+
Assert.Equal(kind, deserialized.Kind);
116+
}
117+
}
118+
119+
[Fact]
120+
public void JsonRoundTrip_CustomValue()
121+
{
122+
var result = new PermissionRequestResult { Kind = new PermissionRequestResultKind("custom") };
123+
var json = JsonSerializer.Serialize(result, s_jsonOptions);
124+
var deserialized = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
125+
Assert.Equal("custom", deserialized.Kind.Value);
126+
}
127+
}
128+
129+
[System.Text.Json.Serialization.JsonSerializable(typeof(PermissionRequestResult))]
130+
internal partial class TestJsonContext : System.Text.Json.Serialization.JsonSerializerContext;

dotnet/test/PermissionTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public async Task Should_Invoke_Permission_Handler_For_Write_Operations()
2121
{
2222
permissionRequests.Add(request);
2323
Assert.Equal(session!.SessionId, invocation.SessionId);
24-
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
24+
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
2525
}
2626
});
2727

@@ -50,7 +50,7 @@ public async Task Should_Deny_Permission_When_Handler_Returns_Denied()
5050
{
5151
return Task.FromResult(new PermissionRequestResult
5252
{
53-
Kind = "denied-interactively-by-user"
53+
Kind = PermissionRequestResultKind.DeniedInteractivelyByUser
5454
});
5555
}
5656
});
@@ -76,7 +76,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies()
7676
var session = await CreateSessionAsync(new SessionConfig
7777
{
7878
OnPermissionRequest = (_, _) =>
79-
Task.FromResult(new PermissionRequestResult { Kind = "denied-no-approval-rule-and-could-not-request-from-user" })
79+
Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser })
8080
});
8181
var permissionDenied = false;
8282

@@ -123,7 +123,7 @@ public async Task Should_Handle_Async_Permission_Handler()
123123
permissionRequestReceived = true;
124124
// Simulate async permission check
125125
await Task.Delay(10);
126-
return new PermissionRequestResult { Kind = "approved" };
126+
return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved };
127127
}
128128
});
129129

@@ -153,7 +153,7 @@ public async Task Should_Resume_Session_With_Permission_Handler()
153153
OnPermissionRequest = (request, invocation) =>
154154
{
155155
permissionRequestReceived = true;
156-
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
156+
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
157157
}
158158
});
159159

@@ -201,7 +201,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies_Aft
201201
var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig
202202
{
203203
OnPermissionRequest = (_, _) =>
204-
Task.FromResult(new PermissionRequestResult { Kind = "denied-no-approval-rule-and-could-not-request-from-user" })
204+
Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser })
205205
});
206206
var permissionDenied = false;
207207

@@ -235,7 +235,7 @@ public async Task Should_Receive_ToolCallId_In_Permission_Requests()
235235
{
236236
receivedToolCallId = true;
237237
}
238-
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
238+
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
239239
}
240240
});
241241

dotnet/test/ToolsTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ public async Task Invokes_Custom_Tool_With_Permission_Handler()
194194
OnPermissionRequest = (request, invocation) =>
195195
{
196196
permissionRequests.Add(request);
197-
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
197+
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
198198
},
199199
});
200200

@@ -229,7 +229,7 @@ public async Task Denies_Custom_Tool_When_Permission_Denied()
229229
Tools = [AIFunctionFactory.Create(EncryptStringDenied, "encrypt_string")],
230230
OnPermissionRequest = (request, invocation) =>
231231
{
232-
return Task.FromResult(new PermissionRequestResult { Kind = "denied-interactively-by-user" });
232+
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedInteractivelyByUser });
233233
},
234234
});
235235

go/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1342,7 +1342,7 @@ func (c *Client) handlePermissionRequest(req permissionRequestRequest) (*permiss
13421342
// Return denial on error
13431343
return &permissionRequestResponse{
13441344
Result: PermissionRequestResult{
1345-
Kind: "denied-no-approval-rule-and-could-not-request-from-user",
1345+
Kind: PermissionKindDeniedCouldNotRequestFromUser,
13461346
},
13471347
}, nil
13481348
}

go/internal/e2e/permissions_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestPermissions(t *testing.T) {
3131
t.Error("Expected non-empty session ID in invocation")
3232
}
3333

34-
return copilot.PermissionRequestResult{Kind: "approved"}, nil
34+
return copilot.PermissionRequestResult{Kind: copilot.PermissionKindApproved}, nil
3535
}
3636

3737
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
@@ -82,7 +82,7 @@ func TestPermissions(t *testing.T) {
8282
permissionRequests = append(permissionRequests, request)
8383
mu.Unlock()
8484

85-
return copilot.PermissionRequestResult{Kind: "approved"}, nil
85+
return copilot.PermissionRequestResult{Kind: copilot.PermissionKindApproved}, nil
8686
}
8787

8888
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
@@ -117,7 +117,7 @@ func TestPermissions(t *testing.T) {
117117
ctx.ConfigureForTest(t)
118118

119119
onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
120-
return copilot.PermissionRequestResult{Kind: "denied-interactively-by-user"}, nil
120+
return copilot.PermissionRequestResult{Kind: copilot.PermissionKindDeniedInteractivelyByUser}, nil
121121
}
122122

123123
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
@@ -162,7 +162,7 @@ func TestPermissions(t *testing.T) {
162162

163163
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
164164
OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
165-
return copilot.PermissionRequestResult{Kind: "denied-no-approval-rule-and-could-not-request-from-user"}, nil
165+
return copilot.PermissionRequestResult{Kind: copilot.PermissionKindDeniedCouldNotRequestFromUser}, nil
166166
},
167167
})
168168
if err != nil {
@@ -212,7 +212,7 @@ func TestPermissions(t *testing.T) {
212212

213213
session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{
214214
OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
215-
return copilot.PermissionRequestResult{Kind: "denied-no-approval-rule-and-could-not-request-from-user"}, nil
215+
return copilot.PermissionRequestResult{Kind: copilot.PermissionKindDeniedCouldNotRequestFromUser}, nil
216216
},
217217
})
218218
if err != nil {

0 commit comments

Comments
 (0)