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
94 changes: 94 additions & 0 deletions src/OpenClaw.Gateway/A2A/A2AEndpointExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#if OPENCLAW_ENABLE_MAF_EXPERIMENT
using A2A;
using A2A.AspNetCore;
using Microsoft.Extensions.Options;
using OpenClaw.Gateway.Bootstrap;
using OpenClaw.Gateway.Composition;
using OpenClaw.Gateway.Endpoints;
using OpenClaw.MicrosoftAgentFrameworkAdapter;
using OpenClaw.MicrosoftAgentFrameworkAdapter.A2A;

namespace OpenClaw.Gateway.A2A;

/// <summary>
/// Maps the A2A protocol endpoints (<c>message/send</c>, <c>message/stream</c>,
/// <c>.well-known/agent-card.json</c>) into the Gateway's HTTP pipeline,
/// protected by the same auth and rate limiting as all other OpenClaw endpoints.
/// </summary>
internal static class A2AEndpointExtensions
{
/// <summary>
/// Adds an authorization + rate-limiting middleware for requests targeting
/// the A2A path prefix, then maps the A2A JSON-RPC and agent-card endpoints.
/// </summary>
public static void MapOpenClawA2AEndpoints(
this WebApplication app,
GatewayStartupContext startup,
GatewayAppRuntime runtime)
{
var options = app.Services.GetRequiredService<IOptions<MafOptions>>().Value;
if (!options.EnableA2A)
return;

var pathPrefix = options.A2APathPrefix.TrimEnd('/');

// Resolve the A2A request handler (A2AServer) from DI
var requestHandler = app.Services.GetRequiredService<IA2ARequestHandler>();

// Build and register the agent card for service discovery
var cardFactory = app.Services.GetRequiredService<OpenClawAgentCardFactory>();
var agentUrl = $"http://{startup.Config.BindAddress}:{startup.Config.Port}{pathPrefix}";
var agentCard = cardFactory.Create(agentUrl);

// Map the A2A protocol endpoint (handles JSON-RPC POST for message/send, message/stream, etc.)
// and the .well-known/agent-card.json discovery endpoint using the official SDK extension
app.MapHttpA2A(requestHandler, agentCard, pathPrefix);
app.MapWellKnownAgentCard(agentCard, pathPrefix);
app.Logger.LogInformation(
"A2A protocol endpoints mapped at {PathPrefix} with agent '{AgentName}'.",
pathPrefix,
options.AgentName);
}

/// <summary>
/// Adds a middleware that enforces token-based authorization and rate limiting
/// on requests targeting the A2A path prefix.
/// </summary>
public static void UseOpenClawA2AAuth(
this WebApplication app,
GatewayStartupContext startup,
GatewayAppRuntime runtime)
{
var options = app.Services.GetRequiredService<IOptions<MafOptions>>().Value;
if (!options.EnableA2A)
return;

var pathPrefix = options.A2APathPrefix.TrimEnd('/');

app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.StartsWithSegments(pathPrefix, StringComparison.OrdinalIgnoreCase)
|| ctx.Request.Path.StartsWithSegments("/.well-known", StringComparison.OrdinalIgnoreCase))
{
if (!EndpointHelpers.IsAuthorizedRequest(ctx, startup.Config, startup.IsNonLoopbackBind))
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}

if (!runtime.Operations.ActorRateLimits.TryConsume(
"ip",
EndpointHelpers.GetRemoteIpKey(ctx),
"a2a_http",
out _))
{
ctx.Response.StatusCode = StatusCodes.Status429TooManyRequests;
return;
}
}

await next(ctx);
});
}
}
#endif
31 changes: 31 additions & 0 deletions src/OpenClaw.Gateway/A2A/A2AServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#if OPENCLAW_ENABLE_MAF_EXPERIMENT
using A2A;
using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenClaw.Gateway.Mcp;
using OpenClaw.MicrosoftAgentFrameworkAdapter.A2A;

namespace OpenClaw.Gateway.A2A;

internal static class A2AServiceExtensions
{
public static IServiceCollection AddOpenClawA2AServices(this IServiceCollection services)
{
services.TryAddSingleton<GatewayRuntimeHolder>();
services.AddSingleton<IOpenClawA2AExecutionBridge, OpenClawA2AExecutionBridge>();
services.AddSingleton<OpenClawA2AAgentHandler>();
services.AddSingleton<OpenClawAgentCardFactory>();
services.AddSingleton<ITaskStore, InMemoryTaskStore>();
services.AddSingleton<ChannelEventNotifier>();
services.AddSingleton<IA2ARequestHandler>(sp =>
{
var handler = sp.GetRequiredService<OpenClawA2AAgentHandler>();
var store = sp.GetRequiredService<ITaskStore>();
var notifier = sp.GetRequiredService<ChannelEventNotifier>();
var logger = sp.GetRequiredService<ILogger<A2AServer>>();
return new A2AServer(handler, store, notifier, logger);
});

return services;
}
}
#endif
87 changes: 87 additions & 0 deletions src/OpenClaw.Gateway/A2A/OpenClawA2AExecutionBridge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#if OPENCLAW_ENABLE_MAF_EXPERIMENT
using OpenClaw.Core.Middleware;
using OpenClaw.Core.Models;
using OpenClaw.Gateway.Mcp;
using OpenClaw.MicrosoftAgentFrameworkAdapter.A2A;

namespace OpenClaw.Gateway.A2A;

internal sealed class OpenClawA2AExecutionBridge : IOpenClawA2AExecutionBridge
{
private readonly GatewayRuntimeHolder _runtimeHolder;
private readonly ILogger<OpenClawA2AExecutionBridge> _logger;

public OpenClawA2AExecutionBridge(
GatewayRuntimeHolder runtimeHolder,
ILogger<OpenClawA2AExecutionBridge> logger)
{
_runtimeHolder = runtimeHolder;
_logger = logger;
}

public async Task ExecuteStreamingAsync(
OpenClawA2AExecutionRequest request,
Func<AgentStreamEvent, CancellationToken, ValueTask> onEvent,
CancellationToken cancellationToken)
{
var runtime = _runtimeHolder.Runtime;
var session = await runtime.SessionManager.GetOrCreateByIdAsync(
request.SessionId,
request.ChannelId,
request.SenderId,
cancellationToken);

await using var sessionLock = await runtime.SessionManager.AcquireSessionLockAsync(session.Id, cancellationToken);

var (handled, commandResponse) = await runtime.CommandProcessor.TryProcessCommandAsync(session, request.UserText, cancellationToken);
if (handled)
{
if (!string.IsNullOrWhiteSpace(commandResponse))
await onEvent(AgentStreamEvent.TextDelta(commandResponse), cancellationToken);

await onEvent(AgentStreamEvent.Complete(), cancellationToken);
await runtime.SessionManager.PersistAsync(session, cancellationToken, sessionLockHeld: true);
return;
}

var mwContext = new MessageContext
{
ChannelId = request.ChannelId,
SenderId = request.SenderId,
Text = request.UserText,
MessageId = request.MessageId,
SessionId = session.Id,
SessionInputTokens = session.TotalInputTokens,
SessionOutputTokens = session.TotalOutputTokens
};

if (!await runtime.MiddlewarePipeline.ExecuteAsync(mwContext, cancellationToken))
{
await onEvent(AgentStreamEvent.TextDelta(mwContext.ShortCircuitResponse ?? "Request blocked."), cancellationToken);
await onEvent(AgentStreamEvent.Complete(), cancellationToken);
await runtime.SessionManager.PersistAsync(session, cancellationToken, sessionLockHeld: true);
return;
}

try
{
await foreach (var evt in runtime.AgentRuntime.RunStreamingAsync(session, mwContext.Text, cancellationToken))
await onEvent(evt, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "A2A execution failed for session {SessionId}", session.Id);
await onEvent(AgentStreamEvent.ErrorOccurred("A2A request failed."), cancellationToken);
await onEvent(AgentStreamEvent.Complete(), cancellationToken);
}
finally
{
await runtime.SessionManager.PersistAsync(session, cancellationToken, sessionLockHeld: true);
}
}
}
#endif
8 changes: 8 additions & 0 deletions src/OpenClaw.Gateway/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using OpenClaw.Gateway.Profiles;
#if OPENCLAW_ENABLE_MAF_EXPERIMENT
using OpenClaw.MicrosoftAgentFrameworkAdapter;
using OpenClaw.Gateway.A2A;
#endif
#if OPENCLAW_ENABLE_OPENSANDBOX
using OpenClawNet.Sandbox.OpenSandbox;
Expand Down Expand Up @@ -34,6 +35,7 @@
builder.Services.ApplyOpenClawRuntimeProfile(startup);
#if OPENCLAW_ENABLE_MAF_EXPERIMENT
builder.Services.AddMicrosoftAgentFrameworkExperiment(builder.Configuration);
builder.Services.AddOpenClawA2AServices();
#endif
#if OPENCLAW_ENABLE_OPENSANDBOX
builder.Services.AddOpenSandboxIntegration(builder.Configuration);
Expand All @@ -44,10 +46,16 @@

app.InitializeMcpRuntime(runtime);
app.UseOpenClawMcpAuth(startup, runtime);
#if OPENCLAW_ENABLE_MAF_EXPERIMENT
app.UseOpenClawA2AAuth(startup, runtime);
#endif

app.UseOpenClawPipeline(startup, runtime);
app.MapOpenApi("/openapi/{documentName}.json");
app.MapOpenClawEndpoints(startup, runtime);
app.MapMcp("/mcp");
#if OPENCLAW_ENABLE_MAF_EXPERIMENT
app.MapOpenClawA2AEndpoints(startup, runtime);
#endif

app.Run($"http://{startup.Config.BindAddress}:{startup.Config.Port}");
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using OpenClaw.Core.Models;

namespace OpenClaw.MicrosoftAgentFrameworkAdapter.A2A;

public sealed record OpenClawA2AExecutionRequest
{
public required string SessionId { get; init; }
public required string ChannelId { get; init; }
public required string SenderId { get; init; }
public required string UserText { get; init; }
public string? MessageId { get; init; }
}

public interface IOpenClawA2AExecutionBridge
{
Task ExecuteStreamingAsync(
OpenClawA2AExecutionRequest request,
Func<AgentStreamEvent, CancellationToken, ValueTask> onEvent,
CancellationToken cancellationToken);
}
Loading
Loading