CODEX_ADK/BACKEND/Codex.Api/Program.cs
Svrnty 229a0698a3 Initial commit: CODEX_ADK monorepo
Multi-agent AI laboratory with ASP.NET Core 8.0 backend and Flutter frontend.
Implements CQRS architecture, OpenAPI contract-first API design.

BACKEND: Agent management, conversations, executions with PostgreSQL + Ollama
FRONTEND: Cross-platform UI with strict typing and Result-based error handling

Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
2025-10-26 23:12:32 -04:00

222 lines
6.8 KiB
C#

using System.Text.Json.Serialization;
using System.Threading.RateLimiting;
using Codex.Api;
using Codex.Api.Endpoints;
using Codex.Api.Middleware;
using Codex.Dal;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS;
using OpenHarbor.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.Data;
using PoweredSoft.Data.EntityFrameworkCore;
using PoweredSoft.DynamicQuery;
using PoweredSoft.Module.Abstractions;
using OpenHarbor.CQRS.AspNetCore.Mvc;
using OpenHarbor.CQRS.DynamicQuery.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// XML documentation files for Swagger
string[] xmlFiles = { "Codex.Api.xml", "Codex.CQRS.xml", "Codex.Dal.xml" };
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
options.ForwardLimit = 2;
});
builder.Services.AddHttpContextAccessor();
// Configure CORS for Flutter and web clients
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
if (builder.Environment.IsDevelopment())
{
// Development: Allow any localhost port + Capacitor/Ionic
policy.SetIsOriginAllowed(origin =>
{
var uri = new Uri(origin);
return uri.Host == "localhost" ||
origin.StartsWith("capacitor://", StringComparison.OrdinalIgnoreCase) ||
origin.StartsWith("ionic://", StringComparison.OrdinalIgnoreCase);
})
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
else
{
// Production: Use configured origins only
var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>() ?? Array.Empty<string>();
if (allowedOrigins.Length > 0)
{
policy.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
}
});
});
// Add rate limiting (MVP: generous limits to prevent runaway loops)
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var clientId = context.User?.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: clientId,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
});
});
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Rate limit exceeded",
message = "Too many requests. Please wait before trying again.",
retryAfter = "60 seconds"
}, cancellationToken: cancellationToken);
};
});
builder.Services.AddPoweredSoftDataServices();
builder.Services.AddPoweredSoftEntityFrameworkCoreDataServices();
builder.Services.AddPoweredSoftDynamicQuery();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
builder.Services
.AddFluentValidationAutoValidation()
.AddFluentValidationClientsideAdapters();
builder.Services.AddModule<AppModule>();
var mvcBuilder = builder.Services
.AddControllers()
.AddJsonOptions(jsonOptions =>
{
jsonOptions.JsonSerializerOptions.Converters.Insert(0, new JsonStringEnumConverter());
});
mvcBuilder
.AddOpenHarborCommands();
mvcBuilder
.AddOpenHarborQueries()
.AddOpenHarborDynamicQueries();
// Register PostgreSQL DbContext
builder.Services.AddDbContext<CodexDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
if (builder.Environment.IsDevelopment())
{
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "Codex API",
Version = "v1",
Description = "CQRS-based API using OpenHarbor.CQRS framework"
});
// Include XML comments from all projects
foreach (var xmlFile in xmlFiles)
{
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
if (File.Exists(xmlPath))
{
options.IncludeXmlComments(xmlPath);
}
}
// Add authentication scheme documentation (for future use)
options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
Name = "Authorization",
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
}
var app = builder.Build();
// Global exception handler (must be first)
app.UseMiddleware<GlobalExceptionHandler>();
// Rate limiting (before CORS and routing)
app.UseRateLimiter();
// Use CORS policy configured from appsettings
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseForwardedHeaders();
if (builder.Environment.IsDevelopment() == false)
{
app.UseHttpsRedirection();
}
// Map OpenHarbor auto-generated endpoints (CreateAgent, UpdateAgent, DeleteAgent, GetAgent, Health)
app.MapControllers();
// Map manually registered endpoints (commands with return values, queries with return types)
app.MapCodexEndpoints();
// Map simple GET endpoints for lists (pragmatic MVP approach)
app.MapSimpleEndpoints();
app.Run();