feat(a2a): per-caller SDK servers behind a key filter, cards and push receiver

This commit is contained in:
EugeneTes
2026-10-07 13:10:19 +00:00
parent f925eaaafd
commit 9c51c97c93
11 changed files with 538 additions and 1 deletions

View File

@@ -0,0 +1,56 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volcanic.AgenticCore.A2A.Server.Callers;
using Volcanic.AgenticCore.A2A.Server.Hosting;
namespace Volcanic.AgenticCore.A2A.Server.Tests;
public sealed class CardTests : IDisposable
{
private readonly string _ws = Path.Combine(Path.GetTempPath(), "a2a-card", Guid.NewGuid().ToString("N"));
private A2ACaller Caller => new("x", "isolated", _ws, "p", "1");
private static A2ACardProvider Cards(string? publicUrl = null) =>
new(Options.Create(new A2AHostOptions { PublicUrl = publicUrl }), NullLogger<A2ACardProvider>.Instance);
public CardTests() => Directory.CreateDirectory(_ws);
[Fact]
public void Card_Public_IsGeneric_WithInterfacesAndApiKeyScheme()
{
var card = Cards("https://bot.example").Public(null);
Assert.Equal("Volcanic assistant", card.Name);
Assert.Contains(card.SupportedInterfaces, i => i.Url == "https://bot.example/a2a" && i.ProtocolBinding == "JSONRPC");
Assert.Contains(card.SupportedInterfaces, i => i.Url == "https://bot.example/a2a/v1" && i.ProtocolBinding == "HTTP+JSON");
Assert.True(card.Capabilities.ExtendedAgentCard);
Assert.Equal("X-API-Key", card.SecuritySchemes!["apiKey"].ApiKeySecurityScheme!.Name);
}
[Fact]
public void Card_Public_UsesRequestBaseUrlWithoutPublicUrl() =>
Assert.Equal("http://localhost:5000/a2a", Cards().Public("http://localhost:5000").SupportedInterfaces[0].Url);
[Fact]
public void Card_Extended_FromCardJson()
{
Directory.CreateDirectory(Path.Combine(_ws, ".volcanic", "a2a"));
File.WriteAllText(A2ACardProvider.CardPath(_ws), """
{ "name": "Eugene's assistant", "description": "Schedules meetings.",
"skills": [ { "id": "scheduling", "name": "Scheduling", "description": "Books slots", "tags": ["calendar"], "examples": ["book 30 min"] } ] }
""");
var card = Cards().Extended(Caller, null);
Assert.Equal("Eugene's assistant", card.Name);
Assert.Equal("scheduling", Assert.Single(card.Skills).Id);
}
[Fact]
public void Card_Extended_FallsBackToPersona()
{
File.WriteAllText(Path.Combine(_ws, "PERSONA.md"), "\n# I am Eugene's secretary\nmore");
var card = Cards().Extended(Caller, null);
Assert.Equal("Volcanic assistant", card.Name);
Assert.Equal("I am Eugene's secretary", card.Description);
Assert.Equal("chat", Assert.Single(card.Skills).Id);
}
public void Dispose() { try { Directory.Delete(_ws, true); } catch (IOException) { } }
}

View File

@@ -0,0 +1,33 @@
using System.Net;
using Microsoft.Extensions.Options;
using Volcanic.AgenticCore.A2A.Server.Push;
namespace Volcanic.AgenticCore.A2A.Server.Tests;
public class PushUrlGuardTests
{
private static PushUrlGuard Guard(bool allowPrivate = false, params string[] resolved) =>
new(Options.Create(new A2AHostOptions { AllowPrivatePushUrls = allowPrivate }),
(_, _) => Task.FromResult(resolved.Select(IPAddress.Parse).ToArray()));
[Theory]
[InlineData("http://hooks.example/x", "must be https")]
[InlineData("https://127.0.0.1/x", "private")]
[InlineData("https://10.1.2.3/x", "private")]
[InlineData("https://100.64.0.7/x", "private")]
[InlineData("not a url", "absolute")]
public async Task Push_PrivateOrPlainUrl_Rejected(string url, string expected) =>
Assert.Contains(expected, await Guard(false, "93.184.216.34").ProblemAsync(url, default));
[Fact]
public async Task Push_HostResolvingToPrivate_Rejected() =>
Assert.Contains("private", await Guard(false, "192.168.1.5").ProblemAsync("https://hooks.example/x", default));
[Fact]
public async Task Push_PublicHttps_Accepted() =>
Assert.Null(await Guard(false, "93.184.216.34").ProblemAsync("https://hooks.example/x", default));
[Fact]
public async Task Push_PrivateAllowed_Accepted() =>
Assert.Null(await Guard(true).ProblemAsync("http://127.0.0.1:5000/x", default));
}