mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-08 12:48:48 +02:00
feat: surface MCP OAuth sidecar events
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -309,6 +309,23 @@ public sealed class UserInputRequestedEventDto : SidecarEventDto
|
|||||||
public bool? AllowFreeform { get; init; }
|
public bool? AllowFreeform { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class McpOauthStaticClientConfigDto
|
||||||
|
{
|
||||||
|
public string ClientId { get; init; } = string.Empty;
|
||||||
|
public bool? PublicClient { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class McpOauthRequiredEventDto : SidecarEventDto
|
||||||
|
{
|
||||||
|
public string SessionId { get; init; } = string.Empty;
|
||||||
|
public string OauthRequestId { get; init; } = string.Empty;
|
||||||
|
public string? AgentId { get; init; }
|
||||||
|
public string? AgentName { get; init; }
|
||||||
|
public string ServerName { get; init; } = string.Empty;
|
||||||
|
public string ServerUrl { get; init; } = string.Empty;
|
||||||
|
public McpOauthStaticClientConfigDto? StaticClientConfig { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class ExitPlanModeRequestedEventDto : SidecarEventDto
|
public sealed class ExitPlanModeRequestedEventDto : SidecarEventDto
|
||||||
{
|
{
|
||||||
public string SessionId { get; init; } = string.Empty;
|
public string SessionId { get; init; } = string.Empty;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Aryx.AgentHost.Contracts;
|
||||||
|
using GitHub.Copilot.SDK;
|
||||||
|
|
||||||
|
namespace Aryx.AgentHost.Services;
|
||||||
|
|
||||||
|
internal sealed class CopilotMcpOAuthCoordinator
|
||||||
|
{
|
||||||
|
public McpOauthRequiredEventDto BuildMcpOauthRequiredEvent(
|
||||||
|
RunTurnCommandDto command,
|
||||||
|
PatternAgentDefinitionDto agent,
|
||||||
|
McpOauthRequiredEvent request)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
ArgumentNullException.ThrowIfNull(agent);
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
McpOauthRequiredData requestData = request.Data
|
||||||
|
?? throw new InvalidOperationException("MCP OAuth request data is required.");
|
||||||
|
|
||||||
|
string oauthRequestId = NormalizeOptionalString(requestData.RequestId)
|
||||||
|
?? throw new InvalidOperationException("MCP OAuth request ID is required.");
|
||||||
|
string? normalizedAgentId = NormalizeOptionalString(agent.Id);
|
||||||
|
string? normalizedAgentName = NormalizeOptionalString(agent.Name) ?? normalizedAgentId;
|
||||||
|
|
||||||
|
return new McpOauthRequiredEventDto
|
||||||
|
{
|
||||||
|
Type = "mcp-oauth-required",
|
||||||
|
RequestId = command.RequestId,
|
||||||
|
SessionId = command.SessionId,
|
||||||
|
OauthRequestId = oauthRequestId,
|
||||||
|
AgentId = normalizedAgentId,
|
||||||
|
AgentName = normalizedAgentName,
|
||||||
|
ServerName = NormalizeOptionalString(requestData.ServerName) ?? string.Empty,
|
||||||
|
ServerUrl = NormalizeOptionalString(requestData.ServerUrl) ?? string.Empty,
|
||||||
|
StaticClientConfig = BuildStaticClientConfig(requestData.StaticClientConfig),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static McpOauthStaticClientConfigDto? BuildStaticClientConfig(
|
||||||
|
McpOauthRequiredDataStaticClientConfig? staticClientConfig)
|
||||||
|
{
|
||||||
|
if (staticClientConfig is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new McpOauthStaticClientConfigDto
|
||||||
|
{
|
||||||
|
ClientId = NormalizeOptionalString(staticClientConfig.ClientId) ?? string.Empty,
|
||||||
|
PublicClient = staticClientConfig.PublicClient,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptionalString(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ internal sealed class CopilotTurnExecutionState
|
|||||||
{
|
{
|
||||||
private readonly RunTurnCommandDto _command;
|
private readonly RunTurnCommandDto _command;
|
||||||
private readonly HashSet<string> _startedAgents = new(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> _startedAgents = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentQueue<McpOauthRequiredEventDto> _pendingMcpOauthRequests = new();
|
||||||
private readonly ConcurrentDictionary<string, AgentIdentity> _observedAgentsByMessageId = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, AgentIdentity> _observedAgentsByMessageId = new(StringComparer.Ordinal);
|
||||||
private readonly StreamingTranscriptBuffer _transcriptBuffer = new();
|
private readonly StreamingTranscriptBuffer _transcriptBuffer = new();
|
||||||
private int _fallbackMessageIndex;
|
private int _fallbackMessageIndex;
|
||||||
@@ -76,6 +77,9 @@ internal sealed class CopilotTurnExecutionState
|
|||||||
case AssistantReasoningDeltaEvent:
|
case AssistantReasoningDeltaEvent:
|
||||||
ActiveAgent = agent;
|
ActiveAgent = agent;
|
||||||
break;
|
break;
|
||||||
|
case McpOauthRequiredEvent:
|
||||||
|
ActiveAgent = agent;
|
||||||
|
break;
|
||||||
case ExitPlanModeRequestedEvent:
|
case ExitPlanModeRequestedEvent:
|
||||||
HasPendingExitPlanModeRequest = true;
|
HasPendingExitPlanModeRequest = true;
|
||||||
ActiveAgent = agent;
|
ActiveAgent = agent;
|
||||||
@@ -83,6 +87,23 @@ internal sealed class CopilotTurnExecutionState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void EnqueuePendingMcpOauthRequest(McpOauthRequiredEventDto request)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
_pendingMcpOauthRequests.Enqueue(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<McpOauthRequiredEventDto> DrainPendingMcpOauthRequests()
|
||||||
|
{
|
||||||
|
List<McpOauthRequiredEventDto> pending = [];
|
||||||
|
while (_pendingMcpOauthRequests.TryDequeue(out McpOauthRequiredEventDto? request))
|
||||||
|
{
|
||||||
|
pending.Add(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryResolveObservedAgentForMessage(string? messageId, out AgentIdentity agent)
|
public bool TryResolveObservedAgentForMessage(string? messageId, out AgentIdentity agent)
|
||||||
{
|
{
|
||||||
agent = default;
|
agent = default;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
private readonly PatternValidator _patternValidator;
|
private readonly PatternValidator _patternValidator;
|
||||||
private readonly CopilotApprovalCoordinator _approvalCoordinator = new();
|
private readonly CopilotApprovalCoordinator _approvalCoordinator = new();
|
||||||
private readonly CopilotUserInputCoordinator _userInputCoordinator = new();
|
private readonly CopilotUserInputCoordinator _userInputCoordinator = new();
|
||||||
|
private readonly CopilotMcpOAuthCoordinator _mcpOAuthCoordinator = new();
|
||||||
private readonly CopilotExitPlanModeCoordinator _exitPlanModeCoordinator = new();
|
private readonly CopilotExitPlanModeCoordinator _exitPlanModeCoordinator = new();
|
||||||
|
|
||||||
public CopilotWorkflowRunner(PatternValidator patternValidator)
|
public CopilotWorkflowRunner(PatternValidator patternValidator)
|
||||||
@@ -23,6 +24,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
Func<AgentActivityEventDto, Task> onActivity,
|
Func<AgentActivityEventDto, Task> onActivity,
|
||||||
Func<ApprovalRequestedEventDto, Task> onApproval,
|
Func<ApprovalRequestedEventDto, Task> onApproval,
|
||||||
Func<UserInputRequestedEventDto, Task> onUserInput,
|
Func<UserInputRequestedEventDto, Task> onUserInput,
|
||||||
|
Func<McpOauthRequiredEventDto, Task> onMcpOAuthRequired,
|
||||||
Func<ExitPlanModeRequestedEventDto, Task> onExitPlanMode,
|
Func<ExitPlanModeRequestedEventDto, Task> onExitPlanMode,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -58,6 +60,12 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
(agent, sessionEvent) =>
|
(agent, sessionEvent) =>
|
||||||
{
|
{
|
||||||
state.ObserveSessionEvent(agent, sessionEvent);
|
state.ObserveSessionEvent(agent, sessionEvent);
|
||||||
|
if (sessionEvent is McpOauthRequiredEvent mcpOauthRequired)
|
||||||
|
{
|
||||||
|
state.EnqueuePendingMcpOauthRequest(
|
||||||
|
_mcpOAuthCoordinator.BuildMcpOauthRequiredEvent(command, agent, mcpOauthRequired));
|
||||||
|
}
|
||||||
|
|
||||||
if (sessionEvent is ExitPlanModeRequestedEvent exitPlanModeRequested)
|
if (sessionEvent is ExitPlanModeRequestedEvent exitPlanModeRequested)
|
||||||
{
|
{
|
||||||
_exitPlanModeCoordinator.RecordExitPlanModeRequest(command, agent, exitPlanModeRequested);
|
_exitPlanModeCoordinator.RecordExitPlanModeRequest(command, agent, exitPlanModeRequested);
|
||||||
@@ -75,16 +83,19 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
{
|
{
|
||||||
bool shouldEndTurn = await HandleWorkflowEventAsync(command, evt, inputMessages, state, onDelta, onActivity)
|
bool shouldEndTurn = await HandleWorkflowEventAsync(command, evt, inputMessages, state, onDelta, onActivity)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
await EmitPendingMcpOauthRequestsAsync(state, onMcpOAuthRequired).ConfigureAwait(false);
|
||||||
if (shouldEndTurn)
|
if (shouldEndTurn)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await EmitPendingMcpOauthRequestsAsync(state, onMcpOAuthRequired).ConfigureAwait(false);
|
||||||
return state.FinalizeCompletedMessages();
|
return state.FinalizeCompletedMessages();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (runCancellation.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (runCancellation.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
await EmitPendingMcpOauthRequestsAsync(state, onMcpOAuthRequired).ConfigureAwait(false);
|
||||||
ExitPlanModeRequestedEventDto? exitPlanModeEvent =
|
ExitPlanModeRequestedEventDto? exitPlanModeEvent =
|
||||||
_exitPlanModeCoordinator.ConsumePendingRequest(command.RequestId);
|
_exitPlanModeCoordinator.ConsumePendingRequest(command.RequestId);
|
||||||
if (exitPlanModeEvent is null || !state.HasPendingExitPlanModeRequest)
|
if (exitPlanModeEvent is null || !state.HasPendingExitPlanModeRequest)
|
||||||
@@ -97,6 +108,16 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task EmitPendingMcpOauthRequestsAsync(
|
||||||
|
CopilotTurnExecutionState state,
|
||||||
|
Func<McpOauthRequiredEventDto, Task> onMcpOAuthRequired)
|
||||||
|
{
|
||||||
|
foreach (McpOauthRequiredEventDto request in state.DrainPendingMcpOauthRequests())
|
||||||
|
{
|
||||||
|
await onMcpOAuthRequired(request).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Task ResolveApprovalAsync(
|
public Task ResolveApprovalAsync(
|
||||||
ResolveApprovalCommandDto command,
|
ResolveApprovalCommandDto command,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public interface ITurnWorkflowRunner
|
|||||||
Func<AgentActivityEventDto, Task> onActivity,
|
Func<AgentActivityEventDto, Task> onActivity,
|
||||||
Func<ApprovalRequestedEventDto, Task> onApproval,
|
Func<ApprovalRequestedEventDto, Task> onApproval,
|
||||||
Func<UserInputRequestedEventDto, Task> onUserInput,
|
Func<UserInputRequestedEventDto, Task> onUserInput,
|
||||||
|
Func<McpOauthRequiredEventDto, Task> onMcpOAuthRequired,
|
||||||
Func<ExitPlanModeRequestedEventDto, Task> onExitPlanMode,
|
Func<ExitPlanModeRequestedEventDto, Task> onExitPlanMode,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ public sealed class SidecarProtocolHost
|
|||||||
activity => WriteAsync(context.Output, activity, turnCancellation.Token),
|
activity => WriteAsync(context.Output, activity, turnCancellation.Token),
|
||||||
approval => WriteAsync(context.Output, approval, turnCancellation.Token),
|
approval => WriteAsync(context.Output, approval, turnCancellation.Token),
|
||||||
userInput => WriteAsync(context.Output, userInput, turnCancellation.Token),
|
userInput => WriteAsync(context.Output, userInput, turnCancellation.Token),
|
||||||
|
mcpOauth => WriteAsync(context.Output, mcpOauth, turnCancellation.Token),
|
||||||
exitPlanMode => WriteAsync(context.Output, exitPlanMode, turnCancellation.Token),
|
exitPlanMode => WriteAsync(context.Output, exitPlanMode, turnCancellation.Token),
|
||||||
turnCancellation.Token)
|
turnCancellation.Token)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Aryx.AgentHost.Contracts;
|
||||||
|
using Aryx.AgentHost.Services;
|
||||||
|
using GitHub.Copilot.SDK;
|
||||||
|
|
||||||
|
namespace Aryx.AgentHost.Tests;
|
||||||
|
|
||||||
|
public sealed class CopilotMcpOAuthCoordinatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuildMcpOauthRequiredEvent_MapsSdkEventToProtocolEvent()
|
||||||
|
{
|
||||||
|
CopilotMcpOAuthCoordinator coordinator = new();
|
||||||
|
RunTurnCommandDto command = CreateCommand();
|
||||||
|
|
||||||
|
McpOauthRequiredEventDto oauthEvent = coordinator.BuildMcpOauthRequiredEvent(
|
||||||
|
command,
|
||||||
|
command.Pattern.Agents[0],
|
||||||
|
new McpOauthRequiredEvent
|
||||||
|
{
|
||||||
|
Data = new McpOauthRequiredData
|
||||||
|
{
|
||||||
|
RequestId = " oauth-request-1 ",
|
||||||
|
ServerName = " Example MCP ",
|
||||||
|
ServerUrl = " https://example.com/mcp ",
|
||||||
|
StaticClientConfig = new McpOauthRequiredDataStaticClientConfig
|
||||||
|
{
|
||||||
|
ClientId = " aryx-client ",
|
||||||
|
PublicClient = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("mcp-oauth-required", oauthEvent.Type);
|
||||||
|
Assert.Equal("turn-1", oauthEvent.RequestId);
|
||||||
|
Assert.Equal("session-1", oauthEvent.SessionId);
|
||||||
|
Assert.Equal("oauth-request-1", oauthEvent.OauthRequestId);
|
||||||
|
Assert.Equal("agent-1", oauthEvent.AgentId);
|
||||||
|
Assert.Equal("Primary", oauthEvent.AgentName);
|
||||||
|
Assert.Equal("Example MCP", oauthEvent.ServerName);
|
||||||
|
Assert.Equal("https://example.com/mcp", oauthEvent.ServerUrl);
|
||||||
|
Assert.NotNull(oauthEvent.StaticClientConfig);
|
||||||
|
Assert.Equal("aryx-client", oauthEvent.StaticClientConfig!.ClientId);
|
||||||
|
Assert.True(oauthEvent.StaticClientConfig.PublicClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RunTurnCommandDto CreateCommand()
|
||||||
|
{
|
||||||
|
return new RunTurnCommandDto
|
||||||
|
{
|
||||||
|
RequestId = "turn-1",
|
||||||
|
SessionId = "session-1",
|
||||||
|
Pattern = new PatternDefinitionDto
|
||||||
|
{
|
||||||
|
Id = "pattern-1",
|
||||||
|
Name = "MCP OAuth Pattern",
|
||||||
|
Mode = "single",
|
||||||
|
Availability = "available",
|
||||||
|
Agents =
|
||||||
|
[
|
||||||
|
new PatternAgentDefinitionDto
|
||||||
|
{
|
||||||
|
Id = "agent-1",
|
||||||
|
Name = "Primary",
|
||||||
|
Model = "gpt-5.4",
|
||||||
|
Instructions = "Help with the request.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Aryx.AgentHost.Contracts;
|
||||||
|
using Aryx.AgentHost.Services;
|
||||||
|
using GitHub.Copilot.SDK;
|
||||||
|
|
||||||
|
namespace Aryx.AgentHost.Tests;
|
||||||
|
|
||||||
|
public sealed class CopilotTurnExecutionStateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ObserveSessionEvent_McpOauthRequired_SetsActiveAgent()
|
||||||
|
{
|
||||||
|
RunTurnCommandDto command = CreateCommand();
|
||||||
|
CopilotTurnExecutionState state = new(command);
|
||||||
|
|
||||||
|
state.ObserveSessionEvent(
|
||||||
|
command.Pattern.Agents[0],
|
||||||
|
new McpOauthRequiredEvent
|
||||||
|
{
|
||||||
|
Data = new McpOauthRequiredData
|
||||||
|
{
|
||||||
|
RequestId = "oauth-request-1",
|
||||||
|
ServerName = "Example MCP",
|
||||||
|
ServerUrl = "https://example.com/mcp",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(state.ActiveAgent.HasValue);
|
||||||
|
Assert.Equal("agent-1", state.ActiveAgent.Value.AgentId);
|
||||||
|
Assert.Equal("Primary", state.ActiveAgent.Value.AgentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DrainPendingMcpOauthRequests_ReturnsQueuedRequestsAndClearsQueue()
|
||||||
|
{
|
||||||
|
RunTurnCommandDto command = CreateCommand();
|
||||||
|
CopilotTurnExecutionState state = new(command);
|
||||||
|
McpOauthRequiredEventDto request = new()
|
||||||
|
{
|
||||||
|
Type = "mcp-oauth-required",
|
||||||
|
RequestId = command.RequestId,
|
||||||
|
SessionId = command.SessionId,
|
||||||
|
OauthRequestId = "oauth-request-1",
|
||||||
|
ServerName = "Example MCP",
|
||||||
|
ServerUrl = "https://example.com/mcp",
|
||||||
|
};
|
||||||
|
|
||||||
|
state.EnqueuePendingMcpOauthRequest(request);
|
||||||
|
|
||||||
|
IReadOnlyList<McpOauthRequiredEventDto> firstDrain = state.DrainPendingMcpOauthRequests();
|
||||||
|
IReadOnlyList<McpOauthRequiredEventDto> secondDrain = state.DrainPendingMcpOauthRequests();
|
||||||
|
|
||||||
|
McpOauthRequiredEventDto drained = Assert.Single(firstDrain);
|
||||||
|
Assert.Equal("oauth-request-1", drained.OauthRequestId);
|
||||||
|
Assert.Empty(secondDrain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RunTurnCommandDto CreateCommand()
|
||||||
|
{
|
||||||
|
return new RunTurnCommandDto
|
||||||
|
{
|
||||||
|
RequestId = "turn-1",
|
||||||
|
SessionId = "session-1",
|
||||||
|
Pattern = new PatternDefinitionDto
|
||||||
|
{
|
||||||
|
Id = "pattern-1",
|
||||||
|
Name = "MCP OAuth Pattern",
|
||||||
|
Mode = "single",
|
||||||
|
Availability = "available",
|
||||||
|
Agents =
|
||||||
|
[
|
||||||
|
new PatternAgentDefinitionDto
|
||||||
|
{
|
||||||
|
Id = "agent-1",
|
||||||
|
Name = "Primary",
|
||||||
|
Model = "gpt-5.4",
|
||||||
|
Instructions = "Help with the request.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
{
|
{
|
||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) =>
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await onActivity(new AgentActivityEventDto
|
await onActivity(new AgentActivityEventDto
|
||||||
{
|
{
|
||||||
@@ -238,7 +238,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
string? capturedMode = null;
|
string? capturedMode = null;
|
||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) =>
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
{
|
{
|
||||||
capturedMode = command.Mode;
|
capturedMode = command.Mode;
|
||||||
return [];
|
return [];
|
||||||
@@ -274,7 +274,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
{
|
{
|
||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) =>
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await onApproval(new ApprovalRequestedEventDto
|
await onApproval(new ApprovalRequestedEventDto
|
||||||
{
|
{
|
||||||
@@ -352,7 +352,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
{
|
{
|
||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) =>
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await onUserInput(new UserInputRequestedEventDto
|
await onUserInput(new UserInputRequestedEventDto
|
||||||
{
|
{
|
||||||
@@ -420,12 +420,70 @@ public sealed class SidecarProtocolHostTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunTurnCommand_ReturnsMcpOauthRequiredEvents()
|
||||||
|
{
|
||||||
|
SidecarProtocolHost host = new(
|
||||||
|
new PatternValidator(),
|
||||||
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await onMcpOAuthRequired(new McpOauthRequiredEventDto
|
||||||
|
{
|
||||||
|
Type = "mcp-oauth-required",
|
||||||
|
RequestId = command.RequestId,
|
||||||
|
SessionId = command.SessionId,
|
||||||
|
OauthRequestId = "oauth-request-1",
|
||||||
|
AgentId = "agent-1",
|
||||||
|
AgentName = "Primary",
|
||||||
|
ServerName = "Example MCP",
|
||||||
|
ServerUrl = "https://example.com/mcp",
|
||||||
|
StaticClientConfig = new McpOauthStaticClientConfigDto
|
||||||
|
{
|
||||||
|
ClientId = "aryx-client",
|
||||||
|
PublicClient = true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
|
|
||||||
|
IReadOnlyList<JsonElement> events = await RunHostAsync(
|
||||||
|
CreateRunTurnCommand(requestId: "turn-mcp-oauth"),
|
||||||
|
host);
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
events,
|
||||||
|
oauthEvent =>
|
||||||
|
{
|
||||||
|
Assert.Equal("mcp-oauth-required", oauthEvent.GetProperty("type").GetString());
|
||||||
|
Assert.Equal("turn-mcp-oauth", oauthEvent.GetProperty("requestId").GetString());
|
||||||
|
Assert.Equal("session-1", oauthEvent.GetProperty("sessionId").GetString());
|
||||||
|
Assert.Equal("oauth-request-1", oauthEvent.GetProperty("oauthRequestId").GetString());
|
||||||
|
Assert.Equal("Primary", oauthEvent.GetProperty("agentName").GetString());
|
||||||
|
Assert.Equal("Example MCP", oauthEvent.GetProperty("serverName").GetString());
|
||||||
|
Assert.Equal("https://example.com/mcp", oauthEvent.GetProperty("serverUrl").GetString());
|
||||||
|
JsonElement staticClientConfig = oauthEvent.GetProperty("staticClientConfig");
|
||||||
|
Assert.Equal("aryx-client", staticClientConfig.GetProperty("clientId").GetString());
|
||||||
|
Assert.True(staticClientConfig.GetProperty("publicClient").GetBoolean());
|
||||||
|
},
|
||||||
|
completionEvent =>
|
||||||
|
{
|
||||||
|
Assert.Equal("turn-complete", completionEvent.GetProperty("type").GetString());
|
||||||
|
Assert.False(completionEvent.GetProperty("cancelled").GetBoolean());
|
||||||
|
},
|
||||||
|
commandCompleteEvent =>
|
||||||
|
{
|
||||||
|
Assert.Equal("command-complete", commandCompleteEvent.GetProperty("type").GetString());
|
||||||
|
Assert.Equal("turn-mcp-oauth", commandCompleteEvent.GetProperty("requestId").GetString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunTurnCommand_ReturnsExitPlanModeEvents()
|
public async Task RunTurnCommand_ReturnsExitPlanModeEvents()
|
||||||
{
|
{
|
||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) =>
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await onExitPlanMode(new ExitPlanModeRequestedEventDto
|
await onExitPlanMode(new ExitPlanModeRequestedEventDto
|
||||||
{
|
{
|
||||||
@@ -500,7 +558,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
{
|
{
|
||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) =>
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Delay(Timeout.Infinite, cancellationToken);
|
await Task.Delay(Timeout.Infinite, cancellationToken);
|
||||||
return [];
|
return [];
|
||||||
@@ -548,7 +606,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
{
|
{
|
||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => []));
|
new FakeWorkflowRunner(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => []));
|
||||||
|
|
||||||
await RunHostAsync(CreateRunTurnCommand(requestId: "turn-completed"), host);
|
await RunHostAsync(CreateRunTurnCommand(requestId: "turn-completed"), host);
|
||||||
|
|
||||||
@@ -571,7 +629,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(
|
new FakeWorkflowRunner(
|
||||||
handler: async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => [],
|
handler: async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => [],
|
||||||
resolveApprovalHandler: (command, cancellationToken) =>
|
resolveApprovalHandler: (command, cancellationToken) =>
|
||||||
{
|
{
|
||||||
captured = command;
|
captured = command;
|
||||||
@@ -602,7 +660,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
SidecarProtocolHost host = new(
|
SidecarProtocolHost host = new(
|
||||||
new PatternValidator(),
|
new PatternValidator(),
|
||||||
new FakeWorkflowRunner(
|
new FakeWorkflowRunner(
|
||||||
handler: async (command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken) => [],
|
handler: async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) => [],
|
||||||
resolveUserInputHandler: (command, cancellationToken) =>
|
resolveUserInputHandler: (command, cancellationToken) =>
|
||||||
{
|
{
|
||||||
captured = command;
|
captured = command;
|
||||||
@@ -892,6 +950,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
Func<AgentActivityEventDto, Task>,
|
Func<AgentActivityEventDto, Task>,
|
||||||
Func<ApprovalRequestedEventDto, Task>,
|
Func<ApprovalRequestedEventDto, Task>,
|
||||||
Func<UserInputRequestedEventDto, Task>,
|
Func<UserInputRequestedEventDto, Task>,
|
||||||
|
Func<McpOauthRequiredEventDto, Task>,
|
||||||
Func<ExitPlanModeRequestedEventDto, Task>,
|
Func<ExitPlanModeRequestedEventDto, Task>,
|
||||||
CancellationToken,
|
CancellationToken,
|
||||||
Task<IReadOnlyList<ChatMessageDto>>> _handler;
|
Task<IReadOnlyList<ChatMessageDto>>> _handler;
|
||||||
@@ -905,6 +964,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
Func<AgentActivityEventDto, Task>,
|
Func<AgentActivityEventDto, Task>,
|
||||||
Func<ApprovalRequestedEventDto, Task>,
|
Func<ApprovalRequestedEventDto, Task>,
|
||||||
Func<UserInputRequestedEventDto, Task>,
|
Func<UserInputRequestedEventDto, Task>,
|
||||||
|
Func<McpOauthRequiredEventDto, Task>,
|
||||||
Func<ExitPlanModeRequestedEventDto, Task>,
|
Func<ExitPlanModeRequestedEventDto, Task>,
|
||||||
CancellationToken,
|
CancellationToken,
|
||||||
Task<IReadOnlyList<ChatMessageDto>>> handler,
|
Task<IReadOnlyList<ChatMessageDto>>> handler,
|
||||||
@@ -922,10 +982,11 @@ public sealed class SidecarProtocolHostTests
|
|||||||
Func<AgentActivityEventDto, Task> onActivity,
|
Func<AgentActivityEventDto, Task> onActivity,
|
||||||
Func<ApprovalRequestedEventDto, Task> onApproval,
|
Func<ApprovalRequestedEventDto, Task> onApproval,
|
||||||
Func<UserInputRequestedEventDto, Task> onUserInput,
|
Func<UserInputRequestedEventDto, Task> onUserInput,
|
||||||
|
Func<McpOauthRequiredEventDto, Task> onMcpOAuthRequired,
|
||||||
Func<ExitPlanModeRequestedEventDto, Task> onExitPlanMode,
|
Func<ExitPlanModeRequestedEventDto, Task> onExitPlanMode,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _handler(command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode, cancellationToken);
|
return _handler(command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ResolveApprovalAsync(
|
public Task ResolveApprovalAsync(
|
||||||
|
|||||||
Reference in New Issue
Block a user