Skip to content

Commit 57de986

Browse files
authored
Merge branch 'main' into edburns/1682-java-tool-ergonomics-review-draft-01
2 parents 35c7450 + ef2bb93 commit 57de986

99 files changed

Lines changed: 6220 additions & 1016 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/block-remove-before-merge.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ permissions:
1111
jobs:
1212
check-paths:
1313
name: "No remove-before-merge directories"
14+
if: github.event_name == 'pull_request'
1415
runs-on: ubuntu-latest
1516
steps:
1617
- name: Check for remove-before-merge paths in PR

dotnet/src/BearerTokenProvider.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace GitHub.Copilot;
8+
9+
/// <summary>
10+
/// Arguments passed to a bearer-token callback (the <c>BearerTokenProvider</c> property
11+
/// on <see cref="ProviderConfig"/> / <see cref="NamedProviderConfig"/>) when the
12+
/// runtime needs a fresh bearer token for a BYOK provider.
13+
/// </summary>
14+
/// <remarks>
15+
/// Part of the experimental managed-identity / bearer-token-provider surface and
16+
/// may change or be removed in future SDK or CLI releases.
17+
/// </remarks>
18+
[Experimental(Diagnostics.Experimental)]
19+
public sealed class ProviderTokenArgs
20+
{
21+
/// <summary>
22+
/// Name of the BYOK provider needing a token. For the singular, whole-session
23+
/// <see cref="ProviderConfig"/> this is the implicit provider name
24+
/// (<c>"default"</c>); for <see cref="NamedProviderConfig"/> entries it is
25+
/// <see cref="NamedProviderConfig.Name"/>.
26+
/// </summary>
27+
/// <remarks>
28+
/// The callback closes over its own token scope/audience; the runtime is
29+
/// provider-agnostic and forwards only the provider name.
30+
/// </remarks>
31+
public required string ProviderName { get; init; }
32+
33+
/// <summary>
34+
/// Id of the session that triggered this token request. A client-level
35+
/// shared callback registered for many sessions can use this to resolve the
36+
/// owning session and scope token acquisition or caching per session.
37+
/// </summary>
38+
public required string SessionId { get; init; }
39+
}

dotnet/src/Client.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ private CopilotSession InitializeSession(
652652
}
653653
ConfigureSessionFsHandlers(session, config.CreateSessionFsProvider);
654654
session.SetCanvasHandler(config.CanvasHandler);
655+
session.RegisterBearerTokenProviders(BuildBearerTokenCallbacks(config));
655656
RegisterSession(session);
656657
session.StartProcessingEvents();
657658
LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null,
@@ -664,6 +665,34 @@ private CopilotSession InitializeSession(
664665
return session;
665666
}
666667

668+
/// <summary>
669+
/// Implicit provider name for the singular, whole-session <see cref="ProviderConfig"/>.
670+
/// </summary>
671+
private const string DefaultBearerTokenProviderName = "default";
672+
673+
/// <summary>
674+
/// Collects the per-provider <c>BearerTokenProvider</c> callbacks keyed by
675+
/// provider name for session-side registration. The singular, whole-session
676+
/// <see cref="ProviderConfig"/> uses the implicit
677+
/// <see cref="DefaultBearerTokenProviderName"/>.
678+
/// </summary>
679+
private static Dictionary<string, Func<ProviderTokenArgs, Task<string>>> BuildBearerTokenCallbacks(SessionConfigBase config)
680+
{
681+
var callbacks = new Dictionary<string, Func<ProviderTokenArgs, Task<string>>>(StringComparer.Ordinal);
682+
if (config.Provider?.BearerTokenProvider is { } singular)
683+
{
684+
callbacks[DefaultBearerTokenProviderName] = singular;
685+
}
686+
if (config.Providers != null)
687+
{
688+
foreach (var provider in config.Providers.Where(provider => provider.BearerTokenProvider is not null))
689+
{
690+
callbacks[provider.Name] = provider.BearerTokenProvider!;
691+
}
692+
}
693+
return callbacks;
694+
}
695+
667696
/// <summary>
668697
/// Catches misuse of <see cref="SessionConfigBase.AvailableTools"/> /
669698
/// <see cref="SessionConfigBase.ExcludedTools"/> at the SDK boundary so

dotnet/src/Generated/Rpc.cs

Lines changed: 317 additions & 82 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Generated/SessionEvents.cs

Lines changed: 208 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Session.cs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public sealed partial class CopilotSession : IAsyncDisposable
5858
{
5959
private readonly Dictionary<string, AIFunction> _toolHandlers = [];
6060
private readonly Dictionary<string, Func<CommandContext, Task>> _commandHandlers = [];
61+
private readonly Dictionary<string, Func<ProviderTokenArgs, Task<string>>> _bearerTokenProviders = new(StringComparer.Ordinal);
6162
private readonly ILogger _logger;
6263
private readonly CopilotClient _parentClient;
6364

@@ -76,9 +77,7 @@ private sealed record EventSubscription(Type EventType, Action<SessionEvent> Han
7677
private Dictionary<string, Func<string, Task<string>>>? _transformCallbacks;
7778
private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1);
7879

79-
#pragma warning disable GHCP001
8080
private IReadOnlyList<OpenCanvasInstance> _openCanvases = Array.Empty<OpenCanvasInstance>();
81-
#pragma warning restore GHCP001
8281

8382
private int _isDisposed;
8483

@@ -126,7 +125,6 @@ public SessionCapabilities Capabilities
126125
private set;
127126
}
128127

129-
#pragma warning disable GHCP001
130128
/// <summary>
131129
/// Canvas instances currently known to be open for this session.
132130
/// </summary>
@@ -136,7 +134,6 @@ public SessionCapabilities Capabilities
136134
/// </remarks>
137135
[Experimental(Diagnostics.Experimental)]
138136
public IReadOnlyList<OpenCanvasInstance> OpenCanvases => _openCanvases;
139-
#pragma warning restore GHCP001
140137

141138
/// <summary>
142139
/// Gets the UI API for eliciting information from the user during this session.
@@ -873,6 +870,51 @@ internal void RegisterAutoModeSwitchHandler(Func<AutoModeSwitchRequest, AutoMode
873870
_autoModeSwitchHandler = handler;
874871
}
875872

873+
/// <summary>
874+
/// Registers per-provider <c>BearerTokenProvider</c> callbacks for BYOK
875+
/// providers configured with managed-identity / on-demand bearer-token auth.
876+
/// </summary>
877+
/// <remarks>
878+
/// The runtime never receives the callback itself; the SDK strips it from the
879+
/// provider config and instead sends <c>hasBearerTokenProvider: true</c>. When
880+
/// the runtime needs a token it issues a session-scoped
881+
/// <c>providerToken.getToken</c> request, which this handler routes to the
882+
/// matching per-provider callback.
883+
/// </remarks>
884+
/// <param name="providers">Map of provider name to callback, or null/empty to clear.</param>
885+
internal void RegisterBearerTokenProviders(IReadOnlyDictionary<string, Func<ProviderTokenArgs, Task<string>>>? providers)
886+
{
887+
_bearerTokenProviders.Clear();
888+
if (providers is null || providers.Count == 0)
889+
{
890+
ClientSessionApis.ProviderToken = null;
891+
return;
892+
}
893+
foreach (var (name, callback) in providers)
894+
{
895+
_bearerTokenProviders[name] = callback;
896+
}
897+
ClientSessionApis.ProviderToken = new BearerTokenProviderHandler(this);
898+
}
899+
900+
/// <summary>
901+
/// Routes runtime <c>providerToken.getToken</c> requests to the matching
902+
/// per-provider <c>BearerTokenProvider</c> callback registered on the session.
903+
/// </summary>
904+
private sealed class BearerTokenProviderHandler(CopilotSession session) : IProviderTokenHandler
905+
{
906+
public async Task<ProviderTokenAcquireResult> GetTokenAsync(ProviderTokenAcquireRequest request, CancellationToken cancellationToken = default)
907+
{
908+
if (!session._bearerTokenProviders.TryGetValue(request.ProviderName, out var callback))
909+
{
910+
throw new InvalidOperationException(
911+
$"No bearer-token provider registered for provider \"{request.ProviderName}\"");
912+
}
913+
var token = await callback(new ProviderTokenArgs { ProviderName = request.ProviderName, SessionId = request.SessionId }).ConfigureAwait(false);
914+
return new ProviderTokenAcquireResult { Token = token };
915+
}
916+
}
917+
876918
/// <summary>
877919
/// Sets the capabilities reported by the host for this session.
878920
/// </summary>
@@ -882,7 +924,6 @@ internal void SetCapabilities(SessionCapabilities? capabilities)
882924
Capabilities = capabilities ?? new SessionCapabilities();
883925
}
884926

885-
#pragma warning disable GHCP001
886927
internal void SetOpenCanvases(IList<OpenCanvasInstance>? canvases)
887928
{
888929
_openCanvases = canvases is { Count: > 0 }
@@ -911,22 +952,19 @@ private void UpdateOpenCanvasesFromEvent(SessionEvent sessionEvent)
911952
var data = canvasEvent.Data;
912953
if (string.IsNullOrEmpty(data.InstanceId)
913954
|| string.IsNullOrEmpty(data.CanvasId)
914-
|| string.IsNullOrEmpty(data.ExtensionId)
915-
|| string.IsNullOrEmpty(data.Availability.Value))
955+
|| string.IsNullOrEmpty(data.ExtensionId))
916956
{
917957
_logger.LogWarning("failed to deserialize session.canvas.opened payload");
918958
return;
919959
}
920960

921961
UpsertOpenCanvas(new OpenCanvasInstance
922962
{
923-
Availability = new CanvasInstanceAvailability(data.Availability.Value),
924963
CanvasId = data.CanvasId,
925964
ExtensionId = data.ExtensionId,
926965
ExtensionName = data.ExtensionName,
927966
Input = data.Input,
928967
InstanceId = data.InstanceId,
929-
Reopen = data.Reopen,
930968
Status = data.Status,
931969
Title = data.Title,
932970
Url = data.Url,
@@ -962,7 +1000,6 @@ private static JsonElement SerializeActionResult(object? value)
9621000
var element = CopilotClient.ToJsonElementForWire(value);
9631001
return element ?? NullJsonElement;
9641002
}
965-
#pragma warning restore GHCP001
9661003

9671004
private sealed class CanvasHandlerAdapter(ICanvasHandler handler) : Rpc.ICanvasHandler
9681005
{

dotnet/src/Types.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
using GitHub.Copilot.Rpc;
66
using Microsoft.Extensions.AI;
77
using Microsoft.Extensions.Logging;
8+
using System;
89
using System.ComponentModel;
910
using System.Diagnostics;
1011
using System.Diagnostics.CodeAnalysis;
1112
using System.Text.Json;
1213
using System.Text.Json.Nodes;
1314
using System.Text.Json.Serialization;
15+
using System.Threading.Tasks;
1416

1517
namespace GitHub.Copilot;
1618

@@ -2041,6 +2043,29 @@ public sealed class ProviderConfig
20412043
[JsonPropertyName("bearerToken")]
20422044
public string? BearerToken { get; set; }
20432045

2046+
/// <summary>
2047+
/// Wire-only flag, emitted automatically when <see cref="BearerTokenProvider"/> is set, that tells
2048+
/// the runtime to request a token over the session-scoped <c>providerToken.getToken</c> RPC
2049+
/// before each outbound request to this provider. Derived from <see cref="BearerTokenProvider"/>;
2050+
/// internal and never part of the public API.
2051+
/// </summary>
2052+
[JsonInclude]
2053+
[JsonPropertyName("hasBearerTokenProvider")]
2054+
internal bool? HasBearerTokenProvider => BearerTokenProvider is not null ? true : null;
2055+
2056+
/// <summary>
2057+
/// Per-request callback that resolves a bearer token on demand for this BYOK provider (for
2058+
/// example via Azure Managed Identity). The Copilot SDK takes no identity dependency: supply a
2059+
/// callback backed by your own identity library. Never serialized — setting it makes the SDK send
2060+
/// <c>hasBearerTokenProvider: true</c> on the wire and answer the runtime's
2061+
/// <c>providerToken.getToken</c> requests. When set alongside <see cref="ApiKey"/>/<see cref="BearerToken"/>, this callback takes precedence:
2062+
/// the runtime applies the token it returns as the Authorization: Bearer header for each request
2063+
/// and does not send the static credential.
2064+
/// </summary>
2065+
[JsonIgnore]
2066+
[Experimental(Diagnostics.Experimental)]
2067+
public Func<ProviderTokenArgs, Task<string>>? BearerTokenProvider { get; set; }
2068+
20442069
/// <summary>
20452070
/// Azure-specific configuration options.
20462071
/// </summary>
@@ -2173,6 +2198,29 @@ public sealed class NamedProviderConfig
21732198
[JsonPropertyName("bearerToken")]
21742199
public string? BearerToken { get; set; }
21752200

2201+
/// <summary>
2202+
/// Wire-only flag, emitted automatically when <see cref="BearerTokenProvider"/> is set, that tells
2203+
/// the runtime to request a token over the session-scoped <c>providerToken.getToken</c> RPC
2204+
/// before each outbound request to this provider. Derived from <see cref="BearerTokenProvider"/>;
2205+
/// internal and never part of the public API.
2206+
/// </summary>
2207+
[JsonInclude]
2208+
[JsonPropertyName("hasBearerTokenProvider")]
2209+
internal bool? HasBearerTokenProvider => BearerTokenProvider is not null ? true : null;
2210+
2211+
/// <summary>
2212+
/// Per-request callback that resolves a bearer token on demand for this BYOK provider (for
2213+
/// example via Azure Managed Identity). The Copilot SDK takes no identity dependency: supply a
2214+
/// callback backed by your own identity library. Never serialized — setting it makes the SDK send
2215+
/// <c>hasBearerTokenProvider: true</c> on the wire and answer the runtime's
2216+
/// <c>providerToken.getToken</c> requests. When set alongside <see cref="ApiKey"/>/<see cref="BearerToken"/>, this callback takes precedence:
2217+
/// the runtime applies the token it returns as the Authorization: Bearer header for each request
2218+
/// and does not send the static credential.
2219+
/// </summary>
2220+
[JsonIgnore]
2221+
[Experimental(Diagnostics.Experimental)]
2222+
public Func<ProviderTokenArgs, Task<string>>? BearerTokenProvider { get; set; }
2223+
21762224
/// <summary>
21772225
/// Azure-specific configuration options.
21782226
/// </summary>

0 commit comments

Comments
 (0)