Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1314,7 +1314,7 @@ public async Task<PermissionRequestResponse> OnPermissionRequest(string sessionI
{
return new PermissionRequestResponse(new PermissionRequestResult
{
Kind = "denied-no-approval-rule-and-could-not-request-from-user"
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
});
}

Expand All @@ -1328,7 +1328,7 @@ public async Task<PermissionRequestResponse> OnPermissionRequest(string sessionI
// If permission handler fails, deny the permission
return new PermissionRequestResponse(new PermissionRequestResult
{
Kind = "denied-no-approval-rule-and-could-not-request-from-user"
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion dotnet/src/PermissionHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ public static class PermissionHandler
{
/// <summary>A <see cref="PermissionRequestHandler"/> that approves all permission requests.</summary>
public static PermissionRequestHandler ApproveAll { get; } =
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = "approved" });
(_, _) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
}
2 changes: 1 addition & 1 deletion dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ internal async Task<PermissionRequestResult> HandlePermissionRequestAsync(JsonEl
{
return new PermissionRequestResult
{
Kind = "denied-no-approval-rule-and-could-not-request-from-user"
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
};
}

Expand Down
66 changes: 65 additions & 1 deletion dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
Expand Down Expand Up @@ -162,10 +165,71 @@ public class PermissionRequest
public Dictionary<string, object>? ExtensionData { get; set; }
}

/// <summary>Describes the kind of a permission request result.</summary>
[JsonConverter(typeof(PermissionRequestResultKind.Converter))]
[DebuggerDisplay("{Value,nq}")]
public readonly struct PermissionRequestResultKind : IEquatable<PermissionRequestResultKind>
{
/// <summary>Gets the kind indicating the permission was approved.</summary>
public static PermissionRequestResultKind Approved { get; } = new("approved");

/// <summary>Gets the kind indicating the permission was denied by rules.</summary>
public static PermissionRequestResultKind DeniedByRules { get; } = new("denied-by-rules");

/// <summary>Gets the kind indicating the permission was denied because no approval rule was found and the user could not be prompted.</summary>
public static PermissionRequestResultKind DeniedCouldNotRequestFromUser { get; } = new("denied-no-approval-rule-and-could-not-request-from-user");

/// <summary>Gets the kind indicating the permission was denied interactively by the user.</summary>
public static PermissionRequestResultKind DeniedInteractivelyByUser { get; } = new("denied-interactively-by-user");

/// <summary>Gets the underlying string value of this <see cref="PermissionRequestResultKind"/>.</summary>
public string Value { get; }

/// <summary>Initializes a new instance of the <see cref="PermissionRequestResultKind"/> struct.</summary>
/// <param name="value">The string value for this kind.</param>
[JsonConstructor]
public PermissionRequestResultKind(string value)
{
ArgumentNullException.ThrowIfNull(value);
Value = value;
}

/// <inheritdoc/>
public static bool operator ==(PermissionRequestResultKind left, PermissionRequestResultKind right) => left.Equals(right);

/// <inheritdoc/>
public static bool operator !=(PermissionRequestResultKind left, PermissionRequestResultKind right) => !left.Equals(right);

/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj) => obj is PermissionRequestResultKind other && Equals(other);

/// <inheritdoc/>
public bool Equals(PermissionRequestResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);

/// <inheritdoc/>
public override string ToString() => Value;

/// <summary>Provides a <see cref="JsonConverter{PermissionRequestResultKind}"/> for serializing <see cref="PermissionRequestResultKind"/> instances.</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Converter : JsonConverter<PermissionRequestResultKind>
{
/// <inheritdoc/>
public override PermissionRequestResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
new(reader.GetString()!);

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, PermissionRequestResultKind value, JsonSerializerOptions options) =>
writer.WriteStringValue(value.Value);
}
}

public class PermissionRequestResult
{
[JsonPropertyName("kind")]
public string Kind { get; set; } = string.Empty;
public PermissionRequestResultKind Kind { get; set; }

[JsonPropertyName("rules")]
public List<object>? Rules { get; set; }
Expand Down
130 changes: 130 additions & 0 deletions dotnet/test/PermissionRequestResultKindTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using System.Text.Json;
using Xunit;

namespace GitHub.Copilot.SDK.Test;

public class PermissionRequestResultKindTests
{
private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web)
{
TypeInfoResolver = TestJsonContext.Default,
};

[Fact]
public void WellKnownKinds_HaveExpectedValues()
{
Assert.Equal("approved", PermissionRequestResultKind.Approved.Value);
Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.Value);
Assert.Equal("denied-no-approval-rule-and-could-not-request-from-user", PermissionRequestResultKind.DeniedCouldNotRequestFromUser.Value);
Assert.Equal("denied-interactively-by-user", PermissionRequestResultKind.DeniedInteractivelyByUser.Value);
}

[Fact]
public void Equals_SameValue_ReturnsTrue()
{
var a = new PermissionRequestResultKind("approved");
Assert.True(a == PermissionRequestResultKind.Approved);
Assert.True(a.Equals(PermissionRequestResultKind.Approved));
Assert.True(a.Equals((object)PermissionRequestResultKind.Approved));
}

[Fact]
public void Equals_DifferentValue_ReturnsFalse()
{
Assert.True(PermissionRequestResultKind.Approved != PermissionRequestResultKind.DeniedByRules);
Assert.False(PermissionRequestResultKind.Approved.Equals(PermissionRequestResultKind.DeniedByRules));
}

[Fact]
public void Equals_IsCaseInsensitive()
{
var upper = new PermissionRequestResultKind("APPROVED");
Assert.Equal(PermissionRequestResultKind.Approved, upper);
}

[Fact]
public void GetHashCode_IsCaseInsensitive()
{
var upper = new PermissionRequestResultKind("APPROVED");
Assert.Equal(PermissionRequestResultKind.Approved.GetHashCode(), upper.GetHashCode());
}

[Fact]
public void ToString_ReturnsValue()
{
Assert.Equal("approved", PermissionRequestResultKind.Approved.ToString());
Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.ToString());
}

[Fact]
public void CustomValue_IsPreserved()
{
var custom = new PermissionRequestResultKind("custom-kind");
Assert.Equal("custom-kind", custom.Value);
Assert.Equal("custom-kind", custom.ToString());
}

[Fact]
public void Constructor_NullValue_Throws()
{
Assert.Throws<ArgumentNullException>(() => new PermissionRequestResultKind(null!));
}

[Fact]
public void Equals_NonPermissionRequestResultKindObject_ReturnsFalse()
{
Assert.False(PermissionRequestResultKind.Approved.Equals("approved"));
}

[Fact]
public void JsonSerialize_WritesStringValue()
{
var result = new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved };
var json = JsonSerializer.Serialize(result, s_jsonOptions);
Assert.Contains("\"kind\":\"approved\"", json);
}

[Fact]
public void JsonDeserialize_ReadsStringValue()
{
var json = """{"kind":"denied-by-rules"}""";
var result = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
Assert.Equal(PermissionRequestResultKind.DeniedByRules, result.Kind);
}

[Fact]
public void JsonRoundTrip_PreservesAllKinds()
{
var kinds = new[]
{
PermissionRequestResultKind.Approved,
PermissionRequestResultKind.DeniedByRules,
PermissionRequestResultKind.DeniedCouldNotRequestFromUser,
PermissionRequestResultKind.DeniedInteractivelyByUser,
};

foreach (var kind in kinds)
{
var result = new PermissionRequestResult { Kind = kind };
var json = JsonSerializer.Serialize(result, s_jsonOptions);
var deserialized = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
Assert.Equal(kind, deserialized.Kind);
}
}

[Fact]
public void JsonRoundTrip_CustomValue()
{
var result = new PermissionRequestResult { Kind = new PermissionRequestResultKind("custom") };
var json = JsonSerializer.Serialize(result, s_jsonOptions);
var deserialized = JsonSerializer.Deserialize<PermissionRequestResult>(json, s_jsonOptions)!;
Assert.Equal("custom", deserialized.Kind.Value);
}
}

[System.Text.Json.Serialization.JsonSerializable(typeof(PermissionRequestResult))]
internal partial class TestJsonContext : System.Text.Json.Serialization.JsonSerializerContext;
14 changes: 7 additions & 7 deletions dotnet/test/PermissionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task Should_Invoke_Permission_Handler_For_Write_Operations()
{
permissionRequests.Add(request);
Assert.Equal(session!.SessionId, invocation.SessionId);
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
}
});

Expand Down Expand Up @@ -50,7 +50,7 @@ public async Task Should_Deny_Permission_When_Handler_Returns_Denied()
{
return Task.FromResult(new PermissionRequestResult
{
Kind = "denied-interactively-by-user"
Kind = PermissionRequestResultKind.DeniedInteractivelyByUser
});
}
});
Expand All @@ -76,7 +76,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies()
var session = await CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = (_, _) =>
Task.FromResult(new PermissionRequestResult { Kind = "denied-no-approval-rule-and-could-not-request-from-user" })
Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser })
});
var permissionDenied = false;

Expand Down Expand Up @@ -123,7 +123,7 @@ public async Task Should_Handle_Async_Permission_Handler()
permissionRequestReceived = true;
// Simulate async permission check
await Task.Delay(10);
return new PermissionRequestResult { Kind = "approved" };
return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved };
}
});

Expand Down Expand Up @@ -153,7 +153,7 @@ public async Task Should_Resume_Session_With_Permission_Handler()
OnPermissionRequest = (request, invocation) =>
{
permissionRequestReceived = true;
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
}
});

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

Expand Down Expand Up @@ -235,7 +235,7 @@ public async Task Should_Receive_ToolCallId_In_Permission_Requests()
{
receivedToolCallId = true;
}
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
}
});

Expand Down
4 changes: 2 additions & 2 deletions dotnet/test/ToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public async Task Invokes_Custom_Tool_With_Permission_Handler()
OnPermissionRequest = (request, invocation) =>
{
permissionRequests.Add(request);
return Task.FromResult(new PermissionRequestResult { Kind = "approved" });
return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
},
});

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

Expand Down
2 changes: 1 addition & 1 deletion go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@ func (c *Client) handlePermissionRequest(req permissionRequestRequest) (*permiss
// Return denial on error
return &permissionRequestResponse{
Result: PermissionRequestResult{
Kind: "denied-no-approval-rule-and-could-not-request-from-user",
Kind: PermissionKindDeniedCouldNotRequestFromUser,
},
}, nil
}
Expand Down
10 changes: 5 additions & 5 deletions go/internal/e2e/permissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestPermissions(t *testing.T) {
t.Error("Expected non-empty session ID in invocation")
}

return copilot.PermissionRequestResult{Kind: "approved"}, nil
return copilot.PermissionRequestResult{Kind: copilot.PermissionKindApproved}, nil
}

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

return copilot.PermissionRequestResult{Kind: "approved"}, nil
return copilot.PermissionRequestResult{Kind: copilot.PermissionKindApproved}, nil
}

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

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

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

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

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