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 ---