Files
nexus/backend/Services/IGatewayConnector.cs
AzuTear f5552218bc
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s
feat: ship agent-first mission control v0.2.57
2026-07-31 22:39:47 +02:00

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
}