feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Nexus.Api.Controllers;
|
||||
using Nexus.Api.Data;
|
||||
using Nexus.Api.Models;
|
||||
using Nexus.Api.Repositories;
|
||||
using Nexus.Api.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Nexus.Api.Tests;
|
||||
|
||||
public sealed class OpenClawSetupServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void SetupController_IsOwnerOnly()
|
||||
{
|
||||
var authorize = Assert.Single(
|
||||
typeof(OpenClawSetupController)
|
||||
.GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true)
|
||||
.OfType<AuthorizeAttribute>());
|
||||
|
||||
Assert.Equal("owner", authorize.Roles);
|
||||
Assert.Empty(
|
||||
typeof(OpenClawSetupController)
|
||||
.GetMethods()
|
||||
.SelectMany(method =>
|
||||
method.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Discover_ReturnsOnlyConfiguredAndWellKnownCandidates_WithoutInvokingGateway()
|
||||
{
|
||||
var gateway = ConnectedGateway();
|
||||
var service = CreateService(gateway: gateway);
|
||||
|
||||
var result = await service.DiscoverAsync(new OpenClawDiscoveryRequest(IncludeMdns: true));
|
||||
|
||||
Assert.Equal("unsupported", result.MdnsState);
|
||||
Assert.Contains(result.Candidates, candidate =>
|
||||
candidate.Endpoint == "ws://openclaw-gateway:18789/"
|
||||
&& candidate.IsValid);
|
||||
Assert.Contains(result.Candidates, candidate =>
|
||||
candidate.Endpoint == "ws://127.0.0.1:18789/"
|
||||
&& candidate.IsCurrentConnectorEndpoint);
|
||||
Assert.DoesNotContain(result.Candidates, candidate =>
|
||||
candidate.Endpoint.Contains("0.0.0.0", StringComparison.Ordinal));
|
||||
Assert.Empty(gateway.Invocations);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ws://example.com:18789/", null)]
|
||||
[InlineData("wss://user:secret@example.com/", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
|
||||
[InlineData("wss://example.com/?token=secret", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
|
||||
[InlineData("wss://example.com/", null)]
|
||||
public async Task Probe_RejectsUnsafeExternalEndpoints(
|
||||
string endpoint,
|
||||
string? fingerprint)
|
||||
{
|
||||
var service = CreateService();
|
||||
|
||||
var result = await service.ProbeAsync(
|
||||
new ProbeOpenClawRequest(endpoint, fingerprint));
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.InvalidEndpoint, result.State);
|
||||
Assert.Null(result.Data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Probe_ValidExternalEndpointStaysBlockedWithoutOfficialClientIdentity()
|
||||
{
|
||||
var gateway = ConnectedGateway();
|
||||
var service = CreateService(gateway: gateway);
|
||||
|
||||
var result = await service.ProbeAsync(new ProbeOpenClawRequest(
|
||||
"wss://gateway.example.test/",
|
||||
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.ExperimentalBlocked, result.State);
|
||||
Assert.NotNull(result.Data);
|
||||
Assert.Empty(gateway.Invocations);
|
||||
Assert.Empty(gateway.ConfiguredEndpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Probe_BlocksUnofficialClientIdentity_EvenWhenLegacyConnectorIsConnected()
|
||||
{
|
||||
var service = CreateService(gateway: ConnectedGateway());
|
||||
|
||||
var result = await service.ProbeAsync(
|
||||
new ProbeOpenClawRequest("ws://127.0.0.1:18789/"));
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.ExperimentalBlocked, result.State);
|
||||
Assert.False(result.Data?.CanAttach);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Attach_UsesTransientBootstrapSecret_WithoutPersistingIt()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository();
|
||||
var gateway = ConnectedGateway();
|
||||
var service = CreateService(
|
||||
repository,
|
||||
gateway,
|
||||
externalIdentitySupported: true);
|
||||
var request = new AttachOpenClawRequest(
|
||||
"ws://127.0.0.1:18789/",
|
||||
"manual",
|
||||
BootstrapToken: "one-time-secret");
|
||||
|
||||
var result = await service.AttachAsync(request);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.Attached, result.State);
|
||||
Assert.NotNull(repository.Current);
|
||||
Assert.True(gateway.BootstrapTokenSupplied);
|
||||
Assert.DoesNotContain("one-time-secret", JsonSerializer.Serialize(repository.Current), StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("one-time-secret", JsonSerializer.Serialize(result), StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("one-time-secret", request.ToString(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Attach_PersistsOnlySecretFreeProfile_WhenSupportedReadOnlyConnectorIsReady()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository();
|
||||
var service = CreateService(
|
||||
repository,
|
||||
ConnectedGateway(),
|
||||
externalIdentitySupported: true);
|
||||
|
||||
var result = await service.AttachAsync(new AttachOpenClawRequest(
|
||||
"ws://127.0.0.1:18789/",
|
||||
"manual"));
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.Attached, result.State);
|
||||
Assert.NotNull(repository.Current);
|
||||
Assert.Equal(OpenClawAdoptionStates.Attached, repository.Current!.AdoptionState);
|
||||
Assert.False(repository.Current.ManagementEnabled);
|
||||
Assert.Equal(1, repository.Current.Revision);
|
||||
var serialized = JsonSerializer.Serialize(repository.Current);
|
||||
Assert.DoesNotContain("BootstrapToken", serialized, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Attach_RejectsLegacyAdminScopeBeforeAdoption()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository();
|
||||
var gateway = ConnectedGateway();
|
||||
gateway.GrantedScopes =
|
||||
new HashSet<string>(["operator.read", "operator.admin"], StringComparer.Ordinal);
|
||||
var service = CreateService(
|
||||
repository,
|
||||
gateway,
|
||||
externalIdentitySupported: true);
|
||||
|
||||
var result = await service.AttachAsync(new AttachOpenClawRequest(
|
||||
"ws://127.0.0.1:18789/",
|
||||
"configured"));
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.ExcessiveScope, result.State);
|
||||
Assert.Null(repository.Current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Verify_DoesNotAdvanceAttachedProfileWithAdminScope()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository
|
||||
{
|
||||
Current = Profile(OpenClawAdoptionStates.Attached, revision: 1)
|
||||
};
|
||||
var gateway = ConnectedGateway();
|
||||
gateway.GrantedScopes =
|
||||
new HashSet<string>(["operator.read", "operator.admin"], StringComparer.Ordinal);
|
||||
var service = CreateService(repository, gateway, externalIdentitySupported: true);
|
||||
|
||||
var result = await service.VerifyAsync(new VerifyOpenClawRequest(1));
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.ExcessiveScope, result.State);
|
||||
Assert.Equal(OpenClawAdoptionStates.Attached, repository.Current?.AdoptionState);
|
||||
Assert.Equal(1, repository.Current?.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Adopt_CollectsLiveInventory_AndDoesNotCopyResourcePayloads()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository
|
||||
{
|
||||
Current = Profile(
|
||||
adoptionState: OpenClawAdoptionStates.Verified,
|
||||
revision: 3)
|
||||
};
|
||||
var gateway = ConnectedGateway(
|
||||
"agents.list",
|
||||
"agents.files.list",
|
||||
"cron.list",
|
||||
"models.list",
|
||||
"channels.status",
|
||||
"nodes.list");
|
||||
gateway.Handler = (method, parameters) => method switch
|
||||
{
|
||||
"agents.list" => JsonNode.Parse(
|
||||
"""{"agents":[{"id":"iris"},{"id":"programmer"}]}"""),
|
||||
"agents.files.list" => JsonNode.Parse(
|
||||
"""{"files":[{"name":"AGENTS.md"},{"name":"SOUL.md"}]}"""),
|
||||
"cron.list" => JsonNode.Parse(
|
||||
"""{"jobs":[{},{},{},{},{},{},{}]}"""),
|
||||
"models.list" => JsonNode.Parse(
|
||||
"""{"models":[{},{},{}]}"""),
|
||||
"channels.status" => JsonNode.Parse(
|
||||
"""{"channels":{"telegram":{},"discord":{}}}"""),
|
||||
"nodes.list" => JsonNode.Parse(
|
||||
"""{"nodes":[{}]}"""),
|
||||
_ => null
|
||||
};
|
||||
var service = CreateService(
|
||||
repository,
|
||||
gateway,
|
||||
externalIdentitySupported: true);
|
||||
|
||||
var result = await service.AdoptAsync(new AdoptOpenClawRequest(3));
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.Equal(2, result.Data?.AgentCount);
|
||||
Assert.Equal(4, result.Data?.AgentFileCount);
|
||||
Assert.Equal(7, result.Data?.CronJobCount);
|
||||
Assert.Equal(3, result.Data?.ModelCount);
|
||||
Assert.Equal(2, result.Data?.ChannelCount);
|
||||
Assert.Equal(1, result.Data?.NodeCount);
|
||||
Assert.Equal(OpenClawAdoptionStates.Adopted, repository.Current?.AdoptionState);
|
||||
Assert.False(repository.Current?.ManagementEnabled);
|
||||
Assert.Equal(4, repository.Current?.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Management_RequiresExplicitAdminScopeUpgrade()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository
|
||||
{
|
||||
Current = Profile(
|
||||
adoptionState: OpenClawAdoptionStates.Adopted,
|
||||
revision: 4)
|
||||
};
|
||||
var gateway = ConnectedGateway();
|
||||
var service = CreateService(
|
||||
repository,
|
||||
gateway,
|
||||
externalIdentitySupported: true);
|
||||
|
||||
var blocked = await service.SetManagementAsync(
|
||||
new SetOpenClawManagementRequest(true, true, 4));
|
||||
|
||||
Assert.False(blocked.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.ScopeUpgradeRequired, blocked.State);
|
||||
Assert.False(repository.Current?.ManagementEnabled);
|
||||
Assert.Contains(
|
||||
gateway.RequestedScopeSets,
|
||||
scopes => scopes.SetEquals(["operator.read", "operator.admin"]));
|
||||
|
||||
gateway.GrantedScopes =
|
||||
new HashSet<string>(["operator.read", "operator.admin"], StringComparer.Ordinal);
|
||||
var enabled = await service.SetManagementAsync(
|
||||
new SetOpenClawManagementRequest(true, true, 4));
|
||||
|
||||
Assert.True(enabled.Ok);
|
||||
Assert.True(repository.Current?.ManagementEnabled);
|
||||
Assert.Equal(5, repository.Current?.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProfileMutation_RejectsStaleRevision()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository
|
||||
{
|
||||
Current = Profile(
|
||||
adoptionState: OpenClawAdoptionStates.Adopted,
|
||||
revision: 7)
|
||||
};
|
||||
var service = CreateService(
|
||||
repository,
|
||||
ConnectedGateway(),
|
||||
externalIdentitySupported: true);
|
||||
|
||||
var result = await service.SetManagementAsync(
|
||||
new SetOpenClawManagementRequest(false, true, 6));
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.ConcurrencyConflict, result.State);
|
||||
Assert.Equal(7, repository.Current?.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_RequiresExactEndpointAndDeviceConfirmation()
|
||||
{
|
||||
var repository = new FakeOpenClawConnectionProfileRepository
|
||||
{
|
||||
Current = Profile(
|
||||
adoptionState: OpenClawAdoptionStates.Adopted,
|
||||
revision: 2,
|
||||
deviceId: "device-bao")
|
||||
};
|
||||
var gateway = ConnectedGateway();
|
||||
var service = CreateService(repository, gateway);
|
||||
|
||||
var rejected = await service.DeleteAsync(
|
||||
new DeleteOpenClawConnectionRequest(
|
||||
"ws://localhost:18789/",
|
||||
"device-bao",
|
||||
2));
|
||||
Assert.False(rejected.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.InvalidRequest, rejected.State);
|
||||
Assert.NotNull(repository.Current);
|
||||
|
||||
var removed = await service.DeleteAsync(
|
||||
new DeleteOpenClawConnectionRequest(
|
||||
"ws://127.0.0.1:18789/",
|
||||
"device-bao",
|
||||
2));
|
||||
Assert.True(removed.Ok);
|
||||
Assert.Equal(OpenClawSetupStates.Removed, removed.State);
|
||||
Assert.Null(repository.Current);
|
||||
Assert.True(gateway.DisconnectRequested);
|
||||
}
|
||||
|
||||
private static OpenClawSetupService CreateService(
|
||||
FakeOpenClawConnectionProfileRepository? repository = null,
|
||||
SetupGatewayConnector? gateway = null,
|
||||
bool externalIdentitySupported = false)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Integrations:OpenClaw:BaseUrl"] = "http://127.0.0.1:18789",
|
||||
["Integrations:OpenClaw:RequiredVersion"] = "2026.7.1",
|
||||
["GatewayConnector:WebSocketPath"] = "/",
|
||||
["OpenClawSetup:ExternalClientIdentitySupported"] =
|
||||
externalIdentitySupported.ToString()
|
||||
})
|
||||
.Build();
|
||||
return new OpenClawSetupService(
|
||||
repository ?? new FakeOpenClawConnectionProfileRepository(),
|
||||
gateway ?? ConnectedGateway(),
|
||||
configuration);
|
||||
}
|
||||
|
||||
private static SetupGatewayConnector ConnectedGateway(params string[] additionalMethods)
|
||||
{
|
||||
var methods = new HashSet<string>(additionalMethods, StringComparer.Ordinal);
|
||||
return new SetupGatewayConnector
|
||||
{
|
||||
ConnectionState = GatewayConnectionState.Connected,
|
||||
GatewayVersion = "2026.7.1",
|
||||
RequiredVersion = "2026.7.1",
|
||||
ProtocolVersion = 4,
|
||||
DeviceId = "device-bao",
|
||||
DeviceTokenConfigured = true,
|
||||
ActiveEndpoint = "ws://127.0.0.1:18789/",
|
||||
GrantedScopes = new HashSet<string>(["operator.read"], StringComparer.Ordinal),
|
||||
AdvertisedMethods = methods
|
||||
};
|
||||
}
|
||||
|
||||
private static OpenClawConnectionProfile Profile(
|
||||
string adoptionState,
|
||||
int revision,
|
||||
string? deviceId = "device-bao")
|
||||
=> new()
|
||||
{
|
||||
Endpoint = "ws://127.0.0.1:18789/",
|
||||
DiscoverySource = "configured",
|
||||
RequiredVersion = "2026.7.1",
|
||||
AdoptionState = adoptionState,
|
||||
ManagementEnabled = false,
|
||||
CapabilityHash = new string('a', 64),
|
||||
DeviceId = deviceId,
|
||||
Revision = revision,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed class FakeOpenClawConnectionProfileRepository
|
||||
: IOpenClawConnectionProfileRepository
|
||||
{
|
||||
public OpenClawConnectionProfile? Current { get; set; }
|
||||
|
||||
public Task<OpenClawConnectionProfile?> GetPrimaryAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(Clone(Current));
|
||||
}
|
||||
|
||||
public Task<OpenClawConnectionProfile> SavePrimaryAsync(
|
||||
OpenClawConnectionProfile profile,
|
||||
int? expectedRevision,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (Current is null)
|
||||
{
|
||||
if (expectedRevision is not null and not 0)
|
||||
throw new OpenClawConnectionProfileConcurrencyException("Profile missing.");
|
||||
Current = Clone(profile)!;
|
||||
Current.Revision = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expectedRevision != Current.Revision)
|
||||
{
|
||||
throw new OpenClawConnectionProfileConcurrencyException(
|
||||
"Profile changed.",
|
||||
Current.Revision);
|
||||
}
|
||||
|
||||
Current = Clone(profile)!;
|
||||
Current.Revision = expectedRevision.Value + 1;
|
||||
}
|
||||
|
||||
Current.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
return Task.FromResult(Clone(Current)!);
|
||||
}
|
||||
|
||||
public Task<bool> DeletePrimaryAsync(
|
||||
int expectedRevision,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (Current is null)
|
||||
return Task.FromResult(false);
|
||||
if (Current.Revision != expectedRevision)
|
||||
{
|
||||
throw new OpenClawConnectionProfileConcurrencyException(
|
||||
"Profile changed.",
|
||||
Current.Revision);
|
||||
}
|
||||
|
||||
Current = null;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static OpenClawConnectionProfile? Clone(OpenClawConnectionProfile? profile)
|
||||
=> profile is null
|
||||
? null
|
||||
: new OpenClawConnectionProfile
|
||||
{
|
||||
ProfileId = profile.ProfileId,
|
||||
Endpoint = profile.Endpoint,
|
||||
DiscoverySource = profile.DiscoverySource,
|
||||
RequiredVersion = profile.RequiredVersion,
|
||||
TlsCertificateFingerprint = profile.TlsCertificateFingerprint,
|
||||
AdoptionState = profile.AdoptionState,
|
||||
ManagementEnabled = profile.ManagementEnabled,
|
||||
CapabilityHash = profile.CapabilityHash,
|
||||
DeviceId = profile.DeviceId,
|
||||
Revision = profile.Revision,
|
||||
CreatedAt = profile.CreatedAt,
|
||||
UpdatedAt = profile.UpdatedAt,
|
||||
LastProbedAt = profile.LastProbedAt,
|
||||
LastVerifiedAt = profile.LastVerifiedAt,
|
||||
AdoptedAt = profile.AdoptedAt
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed class SetupGatewayConnector : IGatewayConnector
|
||||
{
|
||||
public GatewayConnectionState ConnectionState { get; set; }
|
||||
public string? GatewayVersion { get; set; }
|
||||
public string? RequiredVersion { get; set; }
|
||||
public DateTimeOffset? LastConnectedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public int ReconnectAttempts { get; set; }
|
||||
public string? StatusMessage { get; set; }
|
||||
public string? DeviceId { get; set; }
|
||||
public bool DeviceTokenConfigured { get; set; }
|
||||
public bool PairingRequired { get; set; }
|
||||
public string? PairingRequestId { get; set; }
|
||||
public int? ProtocolVersion { get; set; }
|
||||
public IReadOnlySet<string> AdvertisedMethods { get; set; } =
|
||||
new HashSet<string>(StringComparer.Ordinal);
|
||||
public IReadOnlySet<string> AdvertisedEvents { get; set; } =
|
||||
new HashSet<string>(StringComparer.Ordinal);
|
||||
public IReadOnlySet<string> GrantedScopes { get; set; } =
|
||||
new HashSet<string>(StringComparer.Ordinal);
|
||||
public string? ActiveEndpoint { get; set; } = "ws://127.0.0.1:18789/";
|
||||
public string? ActiveTlsFingerprint { get; set; }
|
||||
public DateTimeOffset? LastEventAt { get; set; }
|
||||
public Func<string, JsonNode?, JsonNode?>? Handler { get; set; }
|
||||
public List<(string Method, JsonNode? Parameters)> Invocations { get; } = [];
|
||||
public List<string> ConfiguredEndpoints { get; } = [];
|
||||
public bool BootstrapTokenSupplied { get; private set; }
|
||||
public List<HashSet<string>> RequestedScopeSets { get; } = [];
|
||||
public bool DisconnectRequested { get; private set; }
|
||||
|
||||
public Task RequestOperatorScopesAsync(
|
||||
IReadOnlyCollection<string> scopes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
RequestedScopeSets.Add(scopes.ToHashSet(StringComparer.Ordinal));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ConfigureEndpointAsync(
|
||||
string endpoint,
|
||||
string? tlsFingerprint,
|
||||
string? bootstrapToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ActiveEndpoint = endpoint;
|
||||
ActiveTlsFingerprint = tlsFingerprint;
|
||||
ConfiguredEndpoints.Add(endpoint);
|
||||
BootstrapTokenSupplied |= !string.IsNullOrWhiteSpace(bootstrapToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DisconnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
DisconnectRequested = true;
|
||||
ConnectionState = GatewayConnectionState.Disconnected;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public bool Supports(string method) => AdvertisedMethods.Contains(method);
|
||||
|
||||
public Task<JsonNode?> InvokeAsync(
|
||||
string method,
|
||||
object? parameters = null,
|
||||
TimeSpan? timeout = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
OpenClawInvocationContext? invocationContext = null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var node = parameters switch
|
||||
{
|
||||
null => null,
|
||||
JsonNode jsonNode => jsonNode.DeepClone(),
|
||||
_ => JsonSerializer.SerializeToNode(parameters)
|
||||
};
|
||||
Invocations.Add((method, node));
|
||||
return Task.FromResult(Handler?.Invoke(method, node));
|
||||
}
|
||||
|
||||
public IReadOnlyList<GatewayEventEnvelope> GetRecentEvents(int limit = 100) => [];
|
||||
}
|
||||
Reference in New Issue
Block a user