feat: ship agent-first mission control v0.2.57
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

This commit is contained in:
AzuTear
2026-07-31 22:39:47 +02:00
parent 3bc7622977
commit f5552218bc
535 changed files with 95242 additions and 8791 deletions
+127 -23
View File
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.IdentityModel.Tokens;
using ModelContextProtocol.AspNetCore;
using Nexus.Api.Data;
@@ -12,6 +15,7 @@ using Nexus.Api.Repositories;
using Nexus.Api.Routing;
using Nexus.Api.Services;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.RateLimiting;
@@ -53,7 +57,12 @@ public static class ServiceCollectionExtensions
};
});
services.AddAuthorization();
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
@@ -77,7 +86,7 @@ public static class ServiceCollectionExtensions
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.Headers.ContentType = "application/json";
context.HttpContext.Response.Headers.ContentType = "application/problem+json";
var retryAfterSeconds = 60;
@@ -93,13 +102,19 @@ public static class ServiceCollectionExtensions
context.HttpContext.Response.Headers["X-RateLimit-Reset"] =
DateTimeOffset.UtcNow.AddSeconds(retryAfterSeconds).ToUnixTimeSeconds().ToString();
var body = new
var body = new ProblemDetails
{
error = "rate_limit_exceeded",
message = $"Too many attempts. Try again in {retryAfterSeconds} second(s).",
remaining = 0,
retryAfterSeconds
Type = "https://httpstatuses.com/429",
Title = "Rate limit exceeded",
Status = StatusCodes.Status429TooManyRequests,
Detail = $"Too many attempts. Try again in {retryAfterSeconds} second(s)."
};
body.Extensions["code"] = "rate_limit_exceeded";
body.Extensions["remaining"] = 0;
body.Extensions["retryAfterSeconds"] = retryAfterSeconds;
body.Extensions["traceId"] =
System.Diagnostics.Activity.Current?.Id
?? context.HttpContext.TraceIdentifier;
await context.HttpContext.Response.WriteAsJsonAsync(body, ct);
};
@@ -131,13 +146,45 @@ public static class ServiceCollectionExtensions
/// <summary>
/// Configures forwarded headers for reverse proxy scenarios.
/// </summary>
public static IServiceCollection AddNexusForwardedHeaders(this IServiceCollection services)
public static IServiceCollection AddNexusForwardedHeaders(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
var forwardLimit = configuration.GetValue<int?>("ForwardedHeaders:ForwardLimit") ?? 1;
if (forwardLimit is < 1 or > 5)
throw new InvalidOperationException("ForwardedHeaders:ForwardLimit must be between 1 and 5.");
options.ForwardLimit = forwardLimit;
foreach (var configuredProxy in configuration
.GetSection("ForwardedHeaders:KnownProxies")
.Get<string[]>() ?? [])
{
if (string.IsNullOrWhiteSpace(configuredProxy))
continue;
if (!IPAddress.TryParse(configuredProxy, out var proxy))
throw new InvalidOperationException(
$"ForwardedHeaders:KnownProxies contains invalid IP address '{configuredProxy}'.");
options.KnownProxies.Add(proxy);
}
foreach (var configuredNetwork in configuration
.GetSection("ForwardedHeaders:KnownNetworks")
.Get<string[]>() ?? [])
{
if (string.IsNullOrWhiteSpace(configuredNetwork))
continue;
if (!System.Net.IPNetwork.TryParse(configuredNetwork, out var network))
throw new InvalidOperationException(
$"ForwardedHeaders:KnownNetworks contains invalid CIDR '{configuredNetwork}'.");
options.KnownIPNetworks.Add(network);
}
});
return services;
@@ -174,34 +221,58 @@ public static class ServiceCollectionExtensions
/// </summary>
public static IServiceCollection AddNexusHttpClients(this IServiceCollection services, IConfiguration configuration)
{
services.AddHttpClient<IAgentRuntime, OpenClawRuntime>(client =>
var runtimeReadClient = services.AddHttpClient<IAgentRuntime, OpenClawRuntime>(client =>
{
client.BaseAddress = new(configuration["Integrations:OpenClaw:BaseUrl"]
?? "http://127.0.0.1:18789");
client.Timeout = TimeSpan.FromSeconds(120);
client.Timeout = Timeout.InfiniteTimeSpan;
});
AddOpenClawReadResilience(runtimeReadClient);
services.AddHttpClient("gateway", client =>
var gatewayReadClient = services.AddHttpClient("gateway", client =>
{
client.BaseAddress = new(configuration["Integrations:OpenClaw:BaseUrl"]
?? "http://127.0.0.1:18789");
client.Timeout = TimeSpan.FromSeconds(120);
client.Timeout = Timeout.InfiniteTimeSpan;
});
AddOpenClawReadResilience(gatewayReadClient);
services.AddHttpClient<IOpenClawGatewayClient, OpenClawGatewayClient>(client =>
var historyReadClient = services.AddHttpClient<IOpenClawGatewayClient, OpenClawGatewayClient>(client =>
{
client.BaseAddress = new(configuration["Integrations:OpenClaw:BaseUrl"]
?? "http://127.0.0.1:18789");
client.Timeout = TimeSpan.FromSeconds(120);
client.Timeout = Timeout.InfiniteTimeSpan;
});
AddOpenClawReadResilience(historyReadClient);
return services;
}
private static void AddOpenClawReadResilience(IHttpClientBuilder client)
{
client.AddStandardResilienceHandler(options =>
{
options.RateLimiter.DefaultRateLimiterOptions.PermitLimit = 4;
options.RateLimiter.DefaultRateLimiterOptions.QueueLimit = 0;
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
options.Retry.MaxRetryAttempts = 2;
options.Retry.Delay = TimeSpan.FromMilliseconds(250);
options.Retry.UseJitter = true;
options.Retry.DisableForUnsafeHttpMethods();
options.CircuitBreaker.FailureRatio = 0.5;
options.CircuitBreaker.MinimumThroughput = 4;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
});
}
/// <summary>
/// Registers application domain services (transient, scoped, singleton).
/// </summary>
public static IServiceCollection AddNexusApplicationServices(this IServiceCollection services)
public static IServiceCollection AddNexusApplicationServices(
this IServiceCollection services,
bool includeHostedServices = true)
{
services.AddMcpServer()
.WithHttpTransport(options => options.Stateless = true)
@@ -209,6 +280,8 @@ public static class ServiceCollectionExtensions
services.AddOptions<StaleTaskRecoveryOptions>()
.BindConfiguration(StaleTaskRecoveryOptions.SectionName);
services.AddOptions<AgentProvisioningOptions>()
.BindConfiguration(AgentProvisioningOptions.SectionName);
services.AddHttpContextAccessor();
services.AddSingleton<LoginAttemptTracker>();
services.AddTransient<ModelRoutingService>();
@@ -219,21 +292,50 @@ public static class ServiceCollectionExtensions
services.AddScoped<ITaskService, TaskService>();
services.AddScoped<IOperationsService, OperationsService>();
services.AddScoped<ITeamService, TeamService>();
services.AddSingleton<IAgentConfigService, AgentConfigService>();
services.AddSingleton<IMemoryService, MemoryService>();
services.AddSingleton<IIncidentService, IncidentService>();
services.AddSingleton<IDocService, DocService>();
services.AddScoped<IMemoryService, MemoryService>();
services.AddScoped<IIncidentService, IncidentService>();
services.AddScoped<IDocService, DocService>();
services.AddSingleton<ILiveUpdateService, LiveUpdateService>();
services.AddSingleton<DomainEventStreamService>();
services.AddSingleton<IDomainEventStreamService>(serviceProvider =>
serviceProvider.GetRequiredService<DomainEventStreamService>());
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<ICalendarService, CalendarService>();
services.AddScoped<IOpenClawControlService, OpenClawControlService>();
services.AddScoped<IOpenClawAgentConfigurationService, OpenClawAgentConfigurationService>();
services.AddScoped<IOpenClawSetupService, OpenClawSetupService>();
services.AddSingleton<IOpenClawWizardService, OpenClawWizardService>();
services.AddSingleton<IOpenClawManagementState, OpenClawManagementState>();
services.AddSingleton<IOpenClawWriteGate, OpenClawWriteGate>();
services.AddSingleton<IOpenClawEventProjectionService, OpenClawEventProjectionService>();
services.AddSingleton<IOpenClawRunGateway, OpenClawRunGateway>();
services.AddScoped<IOpenClawRunService, OpenClawRunService>();
services.AddScoped<IOpenClawChatService, OpenClawChatService>();
services.AddScoped<IAgentProposalService, AgentProposalService>();
services.AddSingleton<AgentProvisioningSignal>();
services.AddScoped<IStaleTaskRecoveryService, StaleTaskRecoveryService>();
services.AddHostedService<StaleTaskRecoveryBackgroundService>();
// ── Gateway WebSocket Connector ──
services.AddOptions<GatewayConnectorOptions>()
.BindConfiguration(GatewayConnectorOptions.SectionName);
services.AddSingleton<IOpenClawDeviceIdentityStore, OpenClawDeviceIdentityStore>();
services.AddSingleton<
IOpenClawOperationAuditStore,
PostgresOpenClawOperationAuditStore>();
services.AddSingleton<IGatewayConnector, GatewayConnector>();
services.AddHostedService(sp => (GatewayConnector)sp.GetRequiredService<IGatewayConnector>());
if (includeHostedServices)
{
services.AddHostedService<OpenClawManagementStateInitializer>();
services.AddHostedService(serviceProvider =>
serviceProvider.GetRequiredService<DomainEventStreamService>());
services.AddHostedService<AgentProvisioningWorker>();
services.AddHostedService<StaleTaskRecoveryBackgroundService>();
services.AddHostedService(serviceProvider =>
(GatewayConnector)serviceProvider.GetRequiredService<IGatewayConnector>());
services.AddHostedService<OpenClawEventSubscriptionCoordinator>();
services.AddHostedService<OpenClawRunEventReconciler>();
}
// ── Backend Bridge (Agent-Command-Service) ──
services.AddScoped<ITaskBridgeService, TaskBridgeService>();
@@ -250,6 +352,8 @@ public static class ServiceCollectionExtensions
services.AddScoped<IProjectRepository, ProjectRepository>();
services.AddScoped<ITaskRepository, TaskRepository>();
services.AddScoped<IActivityRepository, ActivityRepository>();
services.AddScoped<IOpenClawRunRepository, OpenClawRunRepository>();
services.AddScoped<IOpenClawConnectionProfileRepository, OpenClawConnectionProfileRepository>();
return services;
}