223 lines
7.1 KiB
C#
223 lines
7.1 KiB
C#
using System.Text.Json.Nodes;
|
|
using Nexus.Api.Models;
|
|
|
|
namespace Nexus.Api.Services;
|
|
|
|
/// <summary>
|
|
/// Maintains a persistent WebSocket connection to the OpenClaw Gateway
|
|
/// for real-time event streaming and health monitoring.
|
|
/// </summary>
|
|
public interface IGatewayConnector
|
|
{
|
|
/// <summary>
|
|
/// Current WebSocket connection state.
|
|
/// </summary>
|
|
GatewayConnectionState ConnectionState { get; }
|
|
|
|
/// <summary>
|
|
/// Gateway version reported at last connection.
|
|
/// </summary>
|
|
string? GatewayVersion { get; }
|
|
|
|
/// <summary>
|
|
/// Required version from configuration, falling back to Nexus' verified
|
|
/// OpenClaw release pin when the setting is absent or blank.
|
|
/// </summary>
|
|
string? RequiredVersion { get; }
|
|
|
|
/// <summary>
|
|
/// Timestamp of the last successful connection attempt.
|
|
/// </summary>
|
|
DateTimeOffset? LastConnectedAt { get; }
|
|
|
|
/// <summary>
|
|
/// Number of consecutive failed connection attempts.
|
|
/// </summary>
|
|
int ReconnectAttempts { get; }
|
|
|
|
/// <summary>
|
|
/// Detailed status message (e.g. error or version info).
|
|
/// </summary>
|
|
string? StatusMessage { get; }
|
|
|
|
/// <summary>
|
|
/// Stable Nexus backend device id used for non-loopback Gateway pairing.
|
|
/// The private key and device token are never exposed through this contract.
|
|
/// </summary>
|
|
string? DeviceId { get; }
|
|
|
|
/// <summary>
|
|
/// Whether a paired backend device token is available in protected server-side storage.
|
|
/// </summary>
|
|
bool DeviceTokenConfigured { get; }
|
|
|
|
/// <summary>
|
|
/// Whether the Gateway is waiting for an operator to approve the current device request.
|
|
/// </summary>
|
|
bool PairingRequired { get; }
|
|
|
|
/// <summary>
|
|
/// Exact pending OpenClaw pairing request id, when supplied by the Gateway.
|
|
/// </summary>
|
|
string? PairingRequestId { get; }
|
|
|
|
/// <summary>
|
|
/// Negotiated Gateway protocol version from the latest hello-ok frame.
|
|
/// </summary>
|
|
int? ProtocolVersion { get; }
|
|
|
|
/// <summary>
|
|
/// RPC methods advertised by the connected Gateway.
|
|
/// </summary>
|
|
IReadOnlySet<string> AdvertisedMethods { get; }
|
|
|
|
/// <summary>
|
|
/// Event families advertised by the connected Gateway.
|
|
/// </summary>
|
|
IReadOnlySet<string> AdvertisedEvents { get; }
|
|
|
|
/// <summary>
|
|
/// Operator scopes granted by the Gateway during the handshake.
|
|
/// </summary>
|
|
IReadOnlySet<string> GrantedScopes { get; }
|
|
|
|
/// <summary>
|
|
/// Normalized endpoint and TLS pin currently used by the running connector.
|
|
/// These values never include credentials.
|
|
/// </summary>
|
|
string? ActiveEndpoint => null;
|
|
string? ActiveTlsFingerprint => null;
|
|
|
|
/// <summary>
|
|
/// Timestamp of the latest event frame received from the Gateway.
|
|
/// </summary>
|
|
DateTimeOffset? LastEventAt { get; }
|
|
|
|
/// <summary>
|
|
/// Returns whether the current Gateway explicitly advertises an RPC method.
|
|
/// </summary>
|
|
bool Supports(string method);
|
|
|
|
/// <summary>
|
|
/// Invokes a Gateway RPC over the authenticated protocol-v4 connection.
|
|
/// </summary>
|
|
Task<JsonNode?> InvokeAsync(
|
|
string method,
|
|
object? parameters = null,
|
|
TimeSpan? timeout = null,
|
|
CancellationToken cancellationToken = default,
|
|
OpenClawInvocationContext? invocationContext = null);
|
|
|
|
/// <summary>
|
|
/// Returns a bounded newest-first snapshot of recently received Gateway events.
|
|
/// </summary>
|
|
IReadOnlyList<GatewayEventEnvelope> GetRecentEvents(int limit = 100);
|
|
|
|
/// <summary>
|
|
/// Requests a new explicit operator-scope set for the next Gateway
|
|
/// handshake. The production connector closes the current socket so
|
|
/// OpenClaw can start its normal pairing or scope-upgrade flow.
|
|
/// Test connectors may keep the default no-op implementation.
|
|
/// </summary>
|
|
Task RequestOperatorScopesAsync(
|
|
IReadOnlyCollection<string> scopes,
|
|
CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
|
|
/// <summary>
|
|
/// Re-targets the single connector after Setup has validated the endpoint.
|
|
/// The optional bootstrap token remains process-memory-only until OpenClaw
|
|
/// issues a bound device token.
|
|
/// </summary>
|
|
Task ConfigureEndpointAsync(
|
|
string endpoint,
|
|
string? tlsFingerprint,
|
|
string? bootstrapToken,
|
|
CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
|
|
/// <summary>
|
|
/// Closes the active socket after a local detach. The background connector
|
|
/// may return to an unauthenticated discovery/pairing state, but the
|
|
/// adopted profile and protected device token are no longer active.
|
|
/// </summary>
|
|
Task DisconnectAsync(CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
public sealed record GatewayEventEnvelope(
|
|
string Event,
|
|
JsonNode? Payload,
|
|
long? Sequence,
|
|
long? StateVersion,
|
|
DateTimeOffset ReceivedAt);
|
|
|
|
/// <summary>
|
|
/// Correlation metadata for one logical Nexus-to-OpenClaw invocation.
|
|
/// Only fields defined by the OpenClaw wire schema are sent to the Gateway:
|
|
/// correlation is reflected in the request id, W3C traceparent is attached to
|
|
/// the request frame, and idempotencyKey is added to params only when
|
|
/// <see cref="IncludeIdempotencyParameter"/> is explicitly enabled for a
|
|
/// schema-confirmed method. Actor and the complete context remain in Nexus'
|
|
/// local audit boundary.
|
|
/// </summary>
|
|
public sealed record OpenClawInvocationContext(
|
|
string IdempotencyKey,
|
|
string CorrelationId,
|
|
string Actor,
|
|
string TraceParent,
|
|
bool IncludeIdempotencyParameter = false)
|
|
{
|
|
public static OpenClawInvocationContext Create(
|
|
string? actor = null,
|
|
string? idempotencyKey = null,
|
|
string? correlationId = null,
|
|
string? traceParent = null,
|
|
bool includeIdempotencyParameter = false)
|
|
=> OpenClawInvocationContextFactory.Create(
|
|
actor,
|
|
idempotencyKey,
|
|
correlationId,
|
|
traceParent,
|
|
includeIdempotencyParameter);
|
|
}
|
|
|
|
public sealed class OpenClawGatewayRpcException : Exception
|
|
{
|
|
public OpenClawGatewayRpcException(
|
|
string code,
|
|
string message,
|
|
JsonNode? details = null,
|
|
bool retryable = false,
|
|
int? retryAfterMs = null)
|
|
: base(message)
|
|
{
|
|
Code = code;
|
|
Details = details;
|
|
Retryable = retryable;
|
|
RetryAfterMs = retryAfterMs;
|
|
}
|
|
|
|
public string Code { get; }
|
|
public JsonNode? Details { get; }
|
|
public bool Retryable { get; }
|
|
public int? RetryAfterMs { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the current connection state of the GatewayConnector.
|
|
/// </summary>
|
|
public enum GatewayConnectionState
|
|
{
|
|
/// <summary>Not yet attempted or initializing.</summary>
|
|
Initializing,
|
|
/// <summary>Connected and healthy.</summary>
|
|
Connected,
|
|
/// <summary>Disconnected, waiting for reconnect.</summary>
|
|
Disconnected,
|
|
/// <summary>Recoverable error, retrying.</summary>
|
|
Reconnecting,
|
|
/// <summary>Failed after maximum retries.</summary>
|
|
Failed
|
|
}
|