Skip to content

Commit 528bd97

Browse files
stephentoubCopilot
andauthored
Add PermissionRequestResultKind type for .NET and Go SDKs (#631)
* 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> * Address PR feedback and fix CI issues Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Mock session.resume response in client test to fix fork PR CI The 'forwards clientName in session.resume request' test calls through to the real CLI, which requires COPILOT_HMAC_KEY for authentication. This secret is unavailable to fork PRs, causing CI failures. Mock the sendRequest return value since the test only verifies parameter forwarding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2fc65b4 commit 528bd97

File tree

21 files changed

+360
-34
lines changed

21 files changed

+360
-34
lines changed

docs/guides/skills.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func main() {
9292
"./skills/documentation",
9393
},
9494
OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
95-
return copilot.PermissionRequestResult{Kind: "approved"}, nil
95+
return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
9696
},
9797
})
9898
if err != nil {
@@ -127,7 +127,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
127127
"./skills/documentation",
128128
},
129129
OnPermissionRequest = (req, inv) =>
130-
Task.FromResult(new PermissionRequestResult { Kind = "approved" }),
130+
Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
131131
});
132132

133133
// Copilot now has access to skills in those directories

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: 76 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,82 @@ 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 => _value ?? string.Empty;
187+
188+
private readonly string? _value;
189+
190+
/// <summary>Initializes a new instance of the <see cref="PermissionRequestResultKind"/> struct.</summary>
191+
/// <param name="value">The string value for this kind.</param>
192+
[JsonConstructor]
193+
public PermissionRequestResultKind(string value) => _value = value;
194+
195+
/// <inheritdoc/>
196+
public static bool operator ==(PermissionRequestResultKind left, PermissionRequestResultKind right) => left.Equals(right);
197+
198+
/// <inheritdoc/>
199+
public static bool operator !=(PermissionRequestResultKind left, PermissionRequestResultKind right) => !left.Equals(right);
200+
201+
/// <inheritdoc/>
202+
public override bool Equals([NotNullWhen(true)] object? obj) => obj is PermissionRequestResultKind other && Equals(other);
203+
204+
/// <inheritdoc/>
205+
public bool Equals(PermissionRequestResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
206+
207+
/// <inheritdoc/>
208+
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
209+
210+
/// <inheritdoc/>
211+
public override string ToString() => Value;
212+
213+
/// <summary>Provides a <see cref="JsonConverter{PermissionRequestResultKind}"/> for serializing <see cref="PermissionRequestResultKind"/> instances.</summary>
214+
[EditorBrowsable(EditorBrowsableState.Never)]
215+
public sealed class Converter : JsonConverter<PermissionRequestResultKind>
216+
{
217+
/// <inheritdoc/>
218+
public override PermissionRequestResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
219+
{
220+
if (reader.TokenType != JsonTokenType.String)
221+
{
222+
throw new JsonException("Expected string for PermissionRequestResultKind.");
223+
}
224+
225+
var value = reader.GetString();
226+
if (value is null)
227+
{
228+
throw new JsonException("PermissionRequestResultKind value cannot be null.");
229+
}
230+
231+
return new PermissionRequestResultKind(value);
232+
}
233+
234+
/// <inheritdoc/>
235+
public override void Write(Utf8JsonWriter writer, PermissionRequestResultKind value, JsonSerializerOptions options) =>
236+
writer.WriteStringValue(value.Value);
237+
}
238+
}
239+
165240
public class PermissionRequestResult
166241
{
167242
[JsonPropertyName("kind")]
168-
public string Kind { get; set; } = string.Empty;
243+
public PermissionRequestResultKind Kind { get; set; }
169244

170245
[JsonPropertyName("rules")]
171246
public List<object>? Rules { get; set; }
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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_TreatedAsEmpty()
73+
{
74+
var kind = new PermissionRequestResultKind(null!);
75+
Assert.Equal(string.Empty, kind.Value);
76+
}
77+
78+
[Fact]
79+
public void Default_HasEmptyStringValue()
80+
{
81+
var defaultKind = default(PermissionRequestResultKind);
82+
Assert.Equal(string.Empty, defaultKind.Value);
83+
Assert.Equal(string.Empty, defaultKind.ToString());
84+
Assert.Equal(defaultKind.GetHashCode(), defaultKind.GetHashCode());
85+
}
86+
87+
[Fact]
88+
public void Equals_NonPermissionRequestResultKindObject_ReturnsFalse()
89+
{
90+
Assert.False(PermissionRequestResultKind.Approved.Equals("approved"));
91+
}
92+
93+
[Fact]
94+
public void JsonSerialize_WritesStringValue()
95+
{
96+
var result = new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved };
97+
var json = JsonSerializer.Serialize(result, s_jsonOptions);
98+
Assert.Contains("\"kind\":\"approved\"", json);
99+
}
100+
101+
[Fact]
102+
public void JsonDeserialize_ReadsStringValue()
103+
{
104+
var json = """{"kind":"denied-by-rules"}""";
105+
var result = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
106+
Assert.Equal(PermissionRequestResultKind.DeniedByRules, result.Kind);
107+
}
108+
109+
[Fact]
110+
public void JsonRoundTrip_PreservesAllKinds()
111+
{
112+
var kinds = new[]
113+
{
114+
PermissionRequestResultKind.Approved,
115+
PermissionRequestResultKind.DeniedByRules,
116+
PermissionRequestResultKind.DeniedCouldNotRequestFromUser,
117+
PermissionRequestResultKind.DeniedInteractivelyByUser,
118+
};
119+
120+
foreach (var kind in kinds)
121+
{
122+
var result = new PermissionRequestResult { Kind = kind };
123+
var json = JsonSerializer.Serialize(result, s_jsonOptions);
124+
var deserialized = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
125+
Assert.Equal(kind, deserialized.Kind);
126+
}
127+
}
128+
129+
[Fact]
130+
public void JsonRoundTrip_CustomValue()
131+
{
132+
var result = new PermissionRequestResult { Kind = new PermissionRequestResultKind("custom") };
133+
var json = JsonSerializer.Serialize(result, s_jsonOptions);
134+
var deserialized = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
135+
Assert.Equal("custom", deserialized.Kind.Value);
136+
}
137+
}
138+
139+
[System.Text.Json.Serialization.JsonSerializable(typeof(PermissionRequestResult))]
140+
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
@@ -223,7 +223,7 @@ public async Task Invokes_Custom_Tool_With_Permission_Handler()
223223
OnPermissionRequest = (request, invocation) =>
224224
{
225225
permissionRequests.Add(request);
226-
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
226+
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
227227
},
228228
});
229229

@@ -258,7 +258,7 @@ public async Task Denies_Custom_Tool_When_Permission_Denied()
258258
Tools = [AIFunctionFactory.Create(EncryptStringDenied, "encrypt_string")],
259259
OnPermissionRequest = (request, invocation) =>
260260
{
261-
return Task.FromResult(new PermissionRequestResult { Kind = "denied-interactively-by-user" });
261+
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedInteractivelyByUser });
262262
},
263263
});
264264

go/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,7 @@ func (c *Client) handlePermissionRequest(req permissionRequestRequest) (*permiss
13761376
// Return denial on error
13771377
return &permissionRequestResponse{
13781378
Result: PermissionRequestResult{
1379-
Kind: "denied-no-approval-rule-and-could-not-request-from-user",
1379+
Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser,
13801380
},
13811381
}, nil
13821382
}

0 commit comments

Comments
 (0)