Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 docs/guides/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func main() {
"./skills/documentation",
},
OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) {
return copilot.PermissionRequestResult{Kind: "approved"}, nil
return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil
},
})
if err != nil {
Expand Down Expand Up @@ -127,7 +127,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
"./skills/documentation",
},
OnPermissionRequest = (req, inv) =>
Task.FromResult(new PermissionRequestResult { Kind = "approved" }),
Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }),
});

// Copilot now has access to skills in those directories
Expand Down
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
77 changes: 76 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,82 @@ 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 => _value ?? string.Empty;

private readonly string? _value;

/// <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) => _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)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException("Expected string for PermissionRequestResultKind.");
}

var value = reader.GetString();
if (value is null)
{
throw new JsonException("PermissionRequestResultKind value cannot be null.");
}

return new PermissionRequestResultKind(value);
}

/// <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
140 changes: 140 additions & 0 deletions dotnet/test/PermissionRequestResultKindTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* 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_TreatedAsEmpty()
{
var kind = new PermissionRequestResultKind(null!);
Assert.Equal(string.Empty, kind.Value);
}

[Fact]
public void Default_HasEmptyStringValue()
{
var defaultKind = default(PermissionRequestResultKind);
Assert.Equal(string.Empty, defaultKind.Value);
Assert.Equal(string.Empty, defaultKind.ToString());
Assert.Equal(defaultKind.GetHashCode(), defaultKind.GetHashCode());
}

[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: PermissionRequestResultKindDeniedCouldNotRequestFromUser,
},
}, nil
}
Expand Down
Loading
Loading