diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..c2e1f89 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.8", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 53c83d9..8cbf16b 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -35,6 +35,15 @@ jobs: - name: Build run: dotnet build backend-tests/Nexus.Api.Tests.csproj --no-restore --configuration Release + - name: Verify EF migration model + run: | + dotnet tool restore + dotnet tool run dotnet-ef migrations has-pending-model-changes \ + --project backend/Nexus.Api.csproj \ + --startup-project backend/Nexus.Api.csproj \ + --configuration Release \ + --no-build + - name: Block high or critical backend vulnerabilities run: | dotnet list backend/Nexus.Api.csproj package --vulnerable --include-transitive --format json > /tmp/nexus-dotnet-vulnerabilities.json diff --git a/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs b/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs index f843410..b5ad504 100644 --- a/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs +++ b/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs @@ -106,11 +106,11 @@ public sealed class ToxiproxyAgentProvisioningIntegrationTests "/proxies/openclaw/toxics", new { - name = "drop-all-create-responses", - type = "timeout", + name = "delay-create-response", + type = "latency", stream = "downstream", toxicity = 1.0, - attributes = new { timeout = 0 } + attributes = new { latency = 1500, jitter = 0 } }, cancellationToken); toxicResponse.EnsureSuccessStatusCode(); @@ -125,7 +125,7 @@ public sealed class ToxiproxyAgentProvisioningIntegrationTests Assert.Equal(1, gateway.CreateCalls); using (var removeToxic = await toxiproxyAdmin.DeleteAsync( - "/proxies/openclaw/toxics/drop-all-create-responses")) + "/proxies/openclaw/toxics/delay-create-response")) { removeToxic.EnsureSuccessStatusCode(); } @@ -200,7 +200,7 @@ public sealed class ToxiproxyAgentProvisioningIntegrationTests # The mutation is committed before the response. Toxiproxy # blocks downstream bytes, recreating an uncertain dispatch. - time.sleep(1.5) + time.sleep(0.1) self.send_json(200, { "ok": True, "agentId": agent_id, diff --git a/backend/Data/Migrations/20260731233006_RemoveRedundantTaskParentIndex.Designer.cs b/backend/Data/Migrations/20260731233006_RemoveRedundantTaskParentIndex.Designer.cs new file mode 100644 index 0000000..2049f5e --- /dev/null +++ b/backend/Data/Migrations/20260731233006_RemoveRedundantTaskParentIndex.Designer.cs @@ -0,0 +1,977 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Nexus.Api.Data; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Nexus.Api.Migrations +{ + [DbContext(typeof(NexusDbContext))] + [Migration("20260731233006_RemoveRedundantTaskParentIndex")] + partial class RemoveRedundantTaskParentIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Nexus.Api.Data.ActivityEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("TaskId"); + + b.HasIndex("TaskId", "CreatedAt", "Id") + .IsDescending(false, true, true) + .HasDatabaseName("IX_Activity_TaskId_CreatedAt_Id") + .HasFilter("\"TaskId\" IS NOT NULL"); + + b.ToTable("Activity"); + }); + + modelBuilder.Entity("Nexus.Api.Data.AgentProposal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("Avatar") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Emoji") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LastErrorCode") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("LastErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Model") + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("OpenClawAgentId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("OpenClawWorkspace") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("RejectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RejectedBy") + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RequestedAgentId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("RequestedName") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Revision") + .IsConcurrencyToken() + .HasColumnType("integer"); + + b.Property("Role") + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("StandardFilesHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("StandardFilesJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Workspace") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("OpenClawAgentId"); + + b.HasIndex("RequestedAgentId"); + + b.HasIndex("Status", "UpdatedAt"); + + b.ToTable("AgentProposals"); + }); + + modelBuilder.Entity("Nexus.Api.Data.AgentProvisionRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("Attempt") + .HasColumnType("integer"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DispatchStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdempotencyKeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastErrorCode") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("LastErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("LeaseOwner") + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("LeaseUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenClawAgentId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProposalId") + .HasColumnType("uuid"); + + b.Property("Revision") + .IsConcurrencyToken() + .HasColumnType("integer"); + + b.Property("Stage") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TraceParent") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LeaseUntil"); + + b.HasIndex("ProposalId", "Attempt") + .IsUnique(); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("AgentProvisionRequests"); + }); + + modelBuilder.Entity("Nexus.Api.Data.NexusUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Nexus.Api.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ForUser") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Message") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("ForUser", "IsRead", "CreatedAt"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Nexus.Api.Data.OpenClawConnectionProfile", b => + { + b.Property("ProfileId") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("AdoptedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AdoptionState") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("CapabilityHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("DiscoverySource") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastProbedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastVerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ManagementEnabled") + .HasColumnType("boolean"); + + b.Property("RequiredVersion") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Revision") + .IsConcurrencyToken() + .HasColumnType("integer"); + + b.Property("TlsCertificateFingerprint") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ProfileId"); + + b.ToTable("OpenClawConnectionProfiles", null, t => + { + t.HasCheckConstraint("CK_OpenClawConnectionProfiles_Primary", "\"ProfileId\" = 'primary'"); + }); + }); + + modelBuilder.Entity("Nexus.Api.Data.OpenClawRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("AgentId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("LastGatewaySequence") + .HasColumnType("bigint"); + + b.Property("OpenClawRunId") + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Prompt") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetriedFromRunId") + .HasColumnType("uuid"); + + b.Property("Revision") + .IsConcurrencyToken() + .HasColumnType("integer"); + + b.Property("SequenceGapDetected") + .HasColumnType("boolean"); + + b.Property("SessionKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("StartIdempotencyKey") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("TraceParent") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OpenClawRunId") + .IsUnique(); + + b.HasIndex("ProjectId"); + + b.HasIndex("RetriedFromRunId"); + + b.HasIndex("SessionKey"); + + b.HasIndex("StartIdempotencyKey") + .IsUnique(); + + b.HasIndex("TaskId"); + + b.HasIndex("Status", "UpdatedAt"); + + b.ToTable("OpenClawRuns"); + }); + + modelBuilder.Entity("Nexus.Api.Data.OpenClawRunHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FromStatus") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("GatewayEventId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GatewaySequence") + .HasColumnType("bigint"); + + b.Property("IdempotencyKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResultRunId") + .HasColumnType("uuid"); + + b.Property("RunId") + .HasColumnType("uuid"); + + b.Property("SequenceGapDetected") + .HasColumnType("boolean"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TraceParent") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("GatewayEventId") + .IsUnique(); + + b.HasIndex("RunId", "OccurredAt"); + + b.HasIndex("RunId", "Action", "IdempotencyKey") + .IsUnique(); + + b.ToTable("OpenClawRunHistory"); + }); + + modelBuilder.Entity("Nexus.Api.Data.OperationClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdempotencyKeyHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Operation") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequestHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ResourceId") + .HasColumnType("uuid"); + + b.Property("ResultCode") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("Operation", "IdempotencyKeyHash") + .IsUnique(); + + b.ToTable("OperationClaims"); + }); + + modelBuilder.Entity("Nexus.Api.Data.OutboxEvent", b => + { + b.Property("Sequence") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Sequence")); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("AggregateRevision") + .HasColumnType("integer"); + + b.Property("AggregateType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("LastErrorCode") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PublishAttempts") + .HasColumnType("integer"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.HasKey("Sequence"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("OccurredAt"); + + b.HasIndex("PublishedAt", "Sequence"); + + b.ToTable("OutboxEvents"); + }); + + modelBuilder.Entity("Nexus.Api.Data.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("Progress") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FamilyId") + .HasColumnType("uuid"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId", "FamilyId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Nexus.Api.Data.SeedAudit", b => + { + b.Property("Key") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Key"); + + b.ToTable("SeedAudit"); + }); + + modelBuilder.Entity("Nexus.Api.Data.WorkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedTo") + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Detail") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedFrom") + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("IsAgentTask") + .HasColumnType("boolean"); + + b.Property("ParentTaskId") + .HasColumnType("uuid"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(240) + .HasColumnType("character varying(240)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssignedTo"); + + b.HasIndex("ExpectedFrom"); + + b.HasIndex("IsAgentTask"); + + b.HasIndex("Source"); + + b.HasIndex("ParentTaskId", "State") + .HasDatabaseName("IX_Tasks_ParentTaskId_State") + .HasFilter("\"ParentTaskId\" IS NOT NULL"); + + b.HasIndex("UpdatedAt", "Id") + .IsDescending() + .HasDatabaseName("IX_Tasks_Done_UpdatedAt_Id") + .HasFilter("\"State\" = 'Done'"); + + b.HasIndex("State", "UpdatedAt", "Id") + .IsDescending(false, true, false) + .HasDatabaseName("IX_Tasks_State_UpdatedAt_Id_Board"); + + b.HasIndex("ExpectedFrom", "State", "UpdatedAt", "Id") + .IsDescending(false, false, true, false) + .HasDatabaseName("IX_Tasks_AgentWorkflow") + .HasFilter("\"IsAgentTask\" = TRUE"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("Nexus.Api.Data.AgentProvisionRequest", b => + { + b.HasOne("Nexus.Api.Data.AgentProposal", "Proposal") + .WithMany("ProvisionRequests") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Proposal"); + }); + + modelBuilder.Entity("Nexus.Api.Data.OpenClawRun", b => + { + b.HasOne("Nexus.Api.Data.Project", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Nexus.Api.Data.WorkTask", null) + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Nexus.Api.Data.OpenClawRunHistory", b => + { + b.HasOne("Nexus.Api.Data.OpenClawRun", "Run") + .WithMany("History") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b => + { + b.HasOne("Nexus.Api.Data.NexusUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Nexus.Api.Data.WorkTask", b => + { + b.HasOne("Nexus.Api.Data.WorkTask", "ParentTask") + .WithMany("ChildTasks") + .HasForeignKey("ParentTaskId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("ParentTask"); + }); + + modelBuilder.Entity("Nexus.Api.Data.AgentProposal", b => + { + b.Navigation("ProvisionRequests"); + }); + + modelBuilder.Entity("Nexus.Api.Data.NexusUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("Nexus.Api.Data.OpenClawRun", b => + { + b.Navigation("History"); + }); + + modelBuilder.Entity("Nexus.Api.Data.WorkTask", b => + { + b.Navigation("ChildTasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Data/Migrations/20260731233006_RemoveRedundantTaskParentIndex.cs b/backend/Data/Migrations/20260731233006_RemoveRedundantTaskParentIndex.cs new file mode 100644 index 0000000..125964c --- /dev/null +++ b/backend/Data/Migrations/20260731233006_RemoveRedundantTaskParentIndex.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Nexus.Api.Migrations +{ + /// + public partial class RemoveRedundantTaskParentIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Tasks_ParentTaskId", + table: "Tasks"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Tasks_ParentTaskId", + table: "Tasks", + column: "ParentTaskId"); + } + } +} diff --git a/backend/Data/Migrations/NexusDbContextModelSnapshot.cs b/backend/Data/Migrations/NexusDbContextModelSnapshot.cs index d11b6c9..ddfd7c2 100644 --- a/backend/Data/Migrations/NexusDbContextModelSnapshot.cs +++ b/backend/Data/Migrations/NexusDbContextModelSnapshot.cs @@ -65,13 +65,13 @@ namespace Nexus.Api.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + b.Property("ApprovedBy") .HasMaxLength(240) .HasColumnType("character varying(240)"); - b.Property("ApprovedAt") - .HasColumnType("timestamp with time zone"); - b.Property("Avatar") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -110,13 +110,13 @@ namespace Nexus.Api.Migrations .HasMaxLength(2048) .HasColumnType("character varying(2048)"); + b.Property("RejectedAt") + .HasColumnType("timestamp with time zone"); + b.Property("RejectedBy") .HasMaxLength(240) .HasColumnType("character varying(240)"); - b.Property("RejectedAt") - .HasColumnType("timestamp with time zone"); - b.Property("RejectionReason") .HasMaxLength(1000) .HasColumnType("character varying(1000)"); @@ -698,12 +698,12 @@ namespace Nexus.Api.Migrations .IsRequired() .HasColumnType("jsonb"); - b.Property("PublishedAt") - .HasColumnType("timestamp with time zone"); - b.Property("PublishAttempts") .HasColumnType("integer"); + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone"); + b.Property("Type") .IsRequired() .HasMaxLength(120) @@ -870,31 +870,40 @@ namespace Nexus.Api.Migrations b.HasIndex("IsAgentTask"); - b.HasIndex("ParentTaskId"); - b.HasIndex("Source"); + b.HasIndex("ParentTaskId", "State") + .HasDatabaseName("IX_Tasks_ParentTaskId_State") + .HasFilter("\"ParentTaskId\" IS NOT NULL"); + + b.HasIndex("UpdatedAt", "Id") + .IsDescending() + .HasDatabaseName("IX_Tasks_Done_UpdatedAt_Id") + .HasFilter("\"State\" = 'Done'"); + + b.HasIndex("State", "UpdatedAt", "Id") + .IsDescending(false, true, false) + .HasDatabaseName("IX_Tasks_State_UpdatedAt_Id_Board"); + b.HasIndex("ExpectedFrom", "State", "UpdatedAt", "Id") .IsDescending(false, false, true, false) .HasDatabaseName("IX_Tasks_AgentWorkflow") .HasFilter("\"IsAgentTask\" = TRUE"); - b.HasIndex("ParentTaskId", "State") - .HasDatabaseName("IX_Tasks_ParentTaskId_State") - .HasFilter("\"ParentTaskId\" IS NOT NULL"); - - b.HasIndex("State", "UpdatedAt", "Id") - .IsDescending(false, true, false) - .HasDatabaseName("IX_Tasks_State_UpdatedAt_Id_Board"); - - b.HasIndex("UpdatedAt", "Id") - .IsDescending(true, true) - .HasDatabaseName("IX_Tasks_Done_UpdatedAt_Id") - .HasFilter("\"State\" = 'Done'"); - b.ToTable("Tasks"); }); + modelBuilder.Entity("Nexus.Api.Data.AgentProvisionRequest", b => + { + b.HasOne("Nexus.Api.Data.AgentProposal", "Proposal") + .WithMany("ProvisionRequests") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Proposal"); + }); + modelBuilder.Entity("Nexus.Api.Data.OpenClawRun", b => { b.HasOne("Nexus.Api.Data.Project", null) @@ -940,27 +949,16 @@ namespace Nexus.Api.Migrations b.Navigation("ParentTask"); }); - modelBuilder.Entity("Nexus.Api.Data.NexusUser", b => - { - b.Navigation("RefreshTokens"); - }); - - modelBuilder.Entity("Nexus.Api.Data.AgentProvisionRequest", b => - { - b.HasOne("Nexus.Api.Data.AgentProposal", "Proposal") - .WithMany("ProvisionRequests") - .HasForeignKey("ProposalId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Proposal"); - }); - modelBuilder.Entity("Nexus.Api.Data.AgentProposal", b => { b.Navigation("ProvisionRequests"); }); + modelBuilder.Entity("Nexus.Api.Data.NexusUser", b => + { + b.Navigation("RefreshTokens"); + }); + modelBuilder.Entity("Nexus.Api.Data.OpenClawRun", b => { b.Navigation("History"); diff --git a/backend/Program.cs b/backend/Program.cs index 64f920c..69da503 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -1,5 +1,6 @@ using Nexus.Api.Extensions; using Nexus.Api.Http; +using Microsoft.AspNetCore.DataProtection; using System.Reflection; var builder = WebApplication.CreateBuilder(args); @@ -11,6 +12,8 @@ if (isOpenApiGeneration) // auth services, so use an ephemeral contract-only signing value. builder.Configuration["Jwt:Key"] = "openapi-contract-generation-only-000000000000000000"; + builder.Services.AddDataProtection() + .UseEphemeralDataProtectionProvider(); } // --- Service Registration ---