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
+135
View File
@@ -0,0 +1,135 @@
namespace Nexus.Api.Data;
/// <summary>
/// Durable, secret-free proposal for creating one OpenClaw-owned agent.
/// Nexus owns the approval workflow only; the resulting agent remains owned by
/// OpenClaw.
/// </summary>
public sealed class AgentProposal
{
public Guid Id { get; init; } = Guid.NewGuid();
public required string Source { get; set; }
public required string RequestedName { get; set; }
public required string RequestedAgentId { get; set; }
public string? Role { get; set; }
public string? Description { get; set; }
public string? Model { get; set; }
public string? Emoji { get; set; }
public string? Avatar { get; set; }
public required string Workspace { get; set; }
public required string StandardFilesJson { get; set; }
public required string StandardFilesHash { get; set; }
public string Status { get; set; } = AgentProposalStates.AwaitingApproval;
public required string RequestedBy { get; set; }
public string? ApprovedBy { get; set; }
public string? RejectedBy { get; set; }
public string? RejectionReason { get; set; }
public string? OpenClawAgentId { get; set; }
public string? OpenClawWorkspace { get; set; }
public string? LastErrorCode { get; set; }
public string? LastErrorMessage { get; set; }
public int Revision { get; set; } = 1;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? ApprovedAt { get; set; }
public DateTimeOffset? RejectedAt { get; set; }
public DateTimeOffset? CompletedAt { get; set; }
public ICollection<AgentProvisionRequest> ProvisionRequests { get; set; } =
new List<AgentProvisionRequest>();
}
/// <summary>
/// One explicitly authorized provisioning attempt. A request that reached the
/// dispatch boundary is never silently re-queued after a restart.
/// </summary>
public sealed class AgentProvisionRequest
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid ProposalId { get; set; }
public AgentProposal Proposal { get; set; } = null!;
public int Attempt { get; set; }
public required string Stage { get; set; }
public string Status { get; set; } = AgentProvisionRequestStates.Queued;
public required string IdempotencyKeyHash { get; set; }
public required string Actor { get; set; }
public required string CorrelationId { get; set; }
public string? TraceParent { get; set; }
public string? OpenClawAgentId { get; set; }
public string? LastErrorCode { get; set; }
public string? LastErrorMessage { get; set; }
public string? LeaseOwner { get; set; }
public DateTimeOffset? LeaseUntil { get; set; }
public int Revision { get; set; } = 1;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? DispatchStartedAt { get; set; }
public DateTimeOffset? CompletedAt { get; set; }
}
/// <summary>
/// Hash-only idempotency claim. Raw idempotency keys and request content are
/// deliberately not persisted.
/// </summary>
public sealed class OperationClaim
{
public Guid Id { get; init; } = Guid.NewGuid();
public required string Operation { get; set; }
public required string IdempotencyKeyHash { get; set; }
public required string RequestHash { get; set; }
public Guid? ResourceId { get; set; }
public required string State { get; set; }
public string? ResultCode { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? CompletedAt { get; set; }
public DateTimeOffset ExpiresAt { get; set; } =
DateTimeOffset.UtcNow.AddDays(7);
}
/// <summary>
/// Transactional, content-minimized domain event. The payload may contain
/// identifiers and states, but never proposal markdown or credentials.
/// </summary>
public sealed class OutboxEvent
{
public long Sequence { get; init; }
public Guid EventId { get; init; } = Guid.NewGuid();
public required string Type { get; set; }
public required string AggregateType { get; set; }
public required string AggregateId { get; set; }
public int AggregateRevision { get; set; }
public required string PayloadJson { get; set; }
public DateTimeOffset OccurredAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? PublishedAt { get; set; }
public int PublishAttempts { get; set; }
public string? LastErrorCode { get; set; }
}
public static class AgentProposalStates
{
public const string Draft = "draft";
public const string AwaitingApproval = "awaiting_approval";
public const string Provisioning = "provisioning";
public const string Ready = "ready";
public const string Partial = "partial";
public const string Failed = "failed";
public const string InDoubt = "in_doubt";
public const string Rejected = "rejected";
}
public static class AgentProvisionStages
{
public const string CreateAgent = "create_agent";
public const string ReconcileAgent = "reconcile_agent";
public const string FinalizeFiles = "finalize_files";
}
public static class AgentProvisionRequestStates
{
public const string Queued = "queued";
public const string Dispatching = "dispatching";
public const string Completed = "completed";
public const string Failed = "failed";
public const string Partial = "partial";
public const string InDoubt = "in_doubt";
public const string Blocked = "blocked";
}
@@ -0,0 +1,552 @@
// <auto-generated />
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("20260730130442_AddOpenClawRunProjection")]
partial class AddOpenClawRunProjection
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("TaskId");
b.ToTable("Activity");
});
modelBuilder.Entity("Nexus.Api.Data.NexusUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<DateTimeOffset?>("LastLoginAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ForUser")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<bool>("IsRead")
.HasColumnType("boolean");
b.Property<string>("Message")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.HasKey("Id");
b.HasIndex("ForUser", "IsRead", "CreatedAt");
b.ToTable("Notifications");
});
modelBuilder.Entity("Nexus.Api.Data.OpenClawRun", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Actor")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("AgentId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("CorrelationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("FinishedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<long?>("LastGatewaySequence")
.HasColumnType("bigint");
b.Property<string>("OpenClawRunId")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<Guid?>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("RetriedFromRunId")
.HasColumnType("uuid");
b.Property<int>("Revision")
.IsConcurrencyToken()
.HasColumnType("integer");
b.Property<bool>("SequenceGapDetected")
.HasColumnType("boolean");
b.Property<string>("SessionKey")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("StartIdempotencyKey")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset?>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<string>("TraceParent")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Actor")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("CorrelationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("FromStatus")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("GatewayEventId")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<long?>("GatewaySequence")
.HasColumnType("bigint");
b.Property<string>("IdempotencyKey")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("ResultRunId")
.HasColumnType("uuid");
b.Property<Guid>("RunId")
.HasColumnType("uuid");
b.Property<bool>("SequenceGapDetected")
.HasColumnType("boolean");
b.Property<string>("ToStatus")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("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.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<int>("Progress")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Projects");
});
modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("FamilyId")
.HasColumnType("uuid");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTimeOffset?>("RevokedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("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<string>("Key")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Key");
b.ToTable("SeedAudit");
});
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AssignedTo")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Detail")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTimeOffset?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExpectedFrom")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<bool>("IsAgentTask")
.HasColumnType("boolean");
b.Property<Guid?>("ParentTaskId")
.HasColumnType("uuid");
b.Property<string>("Priority")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssignedTo");
b.HasIndex("ExpectedFrom");
b.HasIndex("IsAgentTask");
b.HasIndex("ParentTaskId");
b.HasIndex("Source");
b.ToTable("Tasks");
});
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.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
}
}
}
@@ -0,0 +1,156 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Nexus.Api.Migrations
{
/// <inheritdoc />
public partial class AddOpenClawRunProjection : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OpenClawRuns",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Title = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
Prompt = table.Column<string>(type: "text", nullable: false),
AgentId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
SessionKey = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
Status = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
TaskId = table.Column<Guid>(type: "uuid", nullable: true),
ProjectId = table.Column<Guid>(type: "uuid", nullable: true),
OpenClawRunId = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: true),
RetriedFromRunId = table.Column<Guid>(type: "uuid", nullable: true),
StartIdempotencyKey = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
CorrelationId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Actor = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
TraceParent = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
LastError = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
LastGatewaySequence = table.Column<long>(type: "bigint", nullable: true),
SequenceGapDetected = table.Column<bool>(type: "boolean", nullable: false),
Revision = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
StartedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
FinishedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenClawRuns", x => x.Id);
table.ForeignKey(
name: "FK_OpenClawRuns_Projects_ProjectId",
column: x => x.ProjectId,
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_OpenClawRuns_Tasks_TaskId",
column: x => x.TaskId,
principalTable: "Tasks",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "OpenClawRunHistory",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RunId = table.Column<Guid>(type: "uuid", nullable: false),
Action = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
FromStatus = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
ToStatus = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
Message = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
Actor = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
CorrelationId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
IdempotencyKey = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
TraceParent = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
GatewayEventId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
GatewaySequence = table.Column<long>(type: "bigint", nullable: true),
SequenceGapDetected = table.Column<bool>(type: "boolean", nullable: false),
ResultRunId = table.Column<Guid>(type: "uuid", nullable: true),
OccurredAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenClawRunHistory", x => x.Id);
table.ForeignKey(
name: "FK_OpenClawRunHistory_OpenClawRuns_RunId",
column: x => x.RunId,
principalTable: "OpenClawRuns",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OpenClawRunHistory_GatewayEventId",
table: "OpenClawRunHistory",
column: "GatewayEventId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenClawRunHistory_RunId_Action_IdempotencyKey",
table: "OpenClawRunHistory",
columns: new[] { "RunId", "Action", "IdempotencyKey" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenClawRunHistory_RunId_OccurredAt",
table: "OpenClawRunHistory",
columns: new[] { "RunId", "OccurredAt" });
migrationBuilder.CreateIndex(
name: "IX_OpenClawRuns_OpenClawRunId",
table: "OpenClawRuns",
column: "OpenClawRunId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenClawRuns_ProjectId",
table: "OpenClawRuns",
column: "ProjectId");
migrationBuilder.CreateIndex(
name: "IX_OpenClawRuns_RetriedFromRunId",
table: "OpenClawRuns",
column: "RetriedFromRunId");
migrationBuilder.CreateIndex(
name: "IX_OpenClawRuns_SessionKey",
table: "OpenClawRuns",
column: "SessionKey");
migrationBuilder.CreateIndex(
name: "IX_OpenClawRuns_StartIdempotencyKey",
table: "OpenClawRuns",
column: "StartIdempotencyKey",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenClawRuns_Status_UpdatedAt",
table: "OpenClawRuns",
columns: new[] { "Status", "UpdatedAt" });
migrationBuilder.CreateIndex(
name: "IX_OpenClawRuns_TaskId",
table: "OpenClawRuns",
column: "TaskId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OpenClawRunHistory");
migrationBuilder.DropTable(
name: "OpenClawRuns");
}
}
}
@@ -0,0 +1,619 @@
// <auto-generated />
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.Data.Migrations
{
[DbContext(typeof(NexusDbContext))]
[Migration("20260730191613_AddOpenClawConnectionProfile")]
partial class AddOpenClawConnectionProfile
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("TaskId");
b.ToTable("Activity");
});
modelBuilder.Entity("Nexus.Api.Data.NexusUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<DateTimeOffset?>("LastLoginAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ForUser")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<bool>("IsRead")
.HasColumnType("boolean");
b.Property<string>("Message")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("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<string>("ProfileId")
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<DateTimeOffset?>("AdoptedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("AdoptionState")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("CapabilityHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DeviceId")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("DiscoverySource")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Endpoint")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<DateTimeOffset?>("LastProbedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("LastVerifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("ManagementEnabled")
.HasColumnType("boolean");
b.Property<string>("RequiredVersion")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<int>("Revision")
.IsConcurrencyToken()
.HasColumnType("integer");
b.Property<string>("TlsCertificateFingerprint")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Actor")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("AgentId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("CorrelationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("FinishedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<long?>("LastGatewaySequence")
.HasColumnType("bigint");
b.Property<string>("OpenClawRunId")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<Guid?>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("RetriedFromRunId")
.HasColumnType("uuid");
b.Property<int>("Revision")
.IsConcurrencyToken()
.HasColumnType("integer");
b.Property<bool>("SequenceGapDetected")
.HasColumnType("boolean");
b.Property<string>("SessionKey")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("StartIdempotencyKey")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset?>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<string>("TraceParent")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Actor")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("CorrelationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("FromStatus")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("GatewayEventId")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<long?>("GatewaySequence")
.HasColumnType("bigint");
b.Property<string>("IdempotencyKey")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("ResultRunId")
.HasColumnType("uuid");
b.Property<Guid>("RunId")
.HasColumnType("uuid");
b.Property<bool>("SequenceGapDetected")
.HasColumnType("boolean");
b.Property<string>("ToStatus")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("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.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<int>("Progress")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Projects");
});
modelBuilder.Entity("Nexus.Api.Data.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("FamilyId")
.HasColumnType("uuid");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTimeOffset?>("RevokedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("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<string>("Key")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Key");
b.ToTable("SeedAudit");
});
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AssignedTo")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Detail")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTimeOffset?>("DueDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExpectedFrom")
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<bool>("IsAgentTask")
.HasColumnType("boolean");
b.Property<Guid?>("ParentTaskId")
.HasColumnType("uuid");
b.Property<string>("Priority")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("character varying(60)");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AssignedTo");
b.HasIndex("ExpectedFrom");
b.HasIndex("IsAgentTask");
b.HasIndex("ParentTaskId");
b.HasIndex("Source");
b.ToTable("Tasks");
});
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.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
}
}
}
@@ -0,0 +1,48 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Nexus.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddOpenClawConnectionProfile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OpenClawConnectionProfiles",
columns: table => new
{
ProfileId = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
Endpoint = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
DiscoverySource = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
RequiredVersion = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
TlsCertificateFingerprint = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AdoptionState = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
ManagementEnabled = table.Column<bool>(type: "boolean", nullable: false),
CapabilityHash = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
DeviceId = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: true),
Revision = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
LastProbedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LastVerifiedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
AdoptedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenClawConnectionProfiles", x => x.ProfileId);
table.CheckConstraint("CK_OpenClawConnectionProfiles_Primary", "\"ProfileId\" = 'primary'");
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OpenClawConnectionProfiles");
}
}
}
@@ -0,0 +1,261 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Nexus.Api.Data;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Nexus.Api.Migrations
{
/// <inheritdoc />
[DbContext(typeof(NexusDbContext))]
[Migration("20260730224500_AddAgentProvisioningAndBoardIndexes")]
public partial class AddAgentProvisioningAndBoardIndexes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AgentProposals",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Source = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
RequestedName = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
RequestedAgentId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Role = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: true),
Description = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: true),
Model = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: true),
Emoji = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
Avatar = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
Workspace = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
StandardFilesJson = table.Column<string>(type: "jsonb", nullable: false),
StandardFilesHash = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Status = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
RequestedBy = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
ApprovedBy = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: true),
RejectedBy = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: true),
RejectionReason = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
OpenClawAgentId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
OpenClawWorkspace = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
LastErrorCode = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
LastErrorMessage = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
Revision = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ApprovedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
RejectedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
CompletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AgentProposals", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OperationClaims",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Operation = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
IdempotencyKeyHash = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
RequestHash = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
ResourceId = table.Column<Guid>(type: "uuid", nullable: true),
State = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
ResultCode = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
CompletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OperationClaims", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OutboxEvents",
columns: table => new
{
Sequence = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
EventId = table.Column<Guid>(type: "uuid", nullable: false),
Type = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
AggregateType = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
AggregateId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
AggregateRevision = table.Column<int>(type: "integer", nullable: false),
PayloadJson = table.Column<string>(type: "jsonb", nullable: false),
OccurredAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
PublishedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
PublishAttempts = table.Column<int>(type: "integer", nullable: false),
LastErrorCode = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OutboxEvents", x => x.Sequence);
});
migrationBuilder.CreateTable(
name: "AgentProvisionRequests",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ProposalId = table.Column<Guid>(type: "uuid", nullable: false),
Attempt = table.Column<int>(type: "integer", nullable: false),
Stage = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
Status = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false),
IdempotencyKeyHash = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Actor = table.Column<string>(type: "character varying(240)", maxLength: 240, nullable: false),
CorrelationId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
TraceParent = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
OpenClawAgentId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
LastErrorCode = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
LastErrorMessage = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
LeaseOwner = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: true),
LeaseUntil = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
Revision = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
DispatchStartedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
CompletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AgentProvisionRequests", x => x.Id);
table.ForeignKey(
name: "FK_AgentProvisionRequests_AgentProposals_ProposalId",
column: x => x.ProposalId,
principalTable: "AgentProposals",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Activity_TaskId_CreatedAt_Id",
table: "Activity",
columns: new[] { "TaskId", "CreatedAt", "Id" },
descending: new[] { false, true, true },
filter: "\"TaskId\" IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_AgentProposals_OpenClawAgentId",
table: "AgentProposals",
column: "OpenClawAgentId");
migrationBuilder.CreateIndex(
name: "IX_AgentProposals_RequestedAgentId",
table: "AgentProposals",
column: "RequestedAgentId");
migrationBuilder.CreateIndex(
name: "IX_AgentProposals_Status_UpdatedAt",
table: "AgentProposals",
columns: new[] { "Status", "UpdatedAt" });
migrationBuilder.CreateIndex(
name: "IX_AgentProvisionRequests_LeaseUntil",
table: "AgentProvisionRequests",
column: "LeaseUntil");
migrationBuilder.CreateIndex(
name: "IX_AgentProvisionRequests_ProposalId_Attempt",
table: "AgentProvisionRequests",
columns: new[] { "ProposalId", "Attempt" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AgentProvisionRequests_Status_CreatedAt",
table: "AgentProvisionRequests",
columns: new[] { "Status", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_OperationClaims_ExpiresAt",
table: "OperationClaims",
column: "ExpiresAt");
migrationBuilder.CreateIndex(
name: "IX_OperationClaims_Operation_IdempotencyKeyHash",
table: "OperationClaims",
columns: new[] { "Operation", "IdempotencyKeyHash" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OutboxEvents_EventId",
table: "OutboxEvents",
column: "EventId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OutboxEvents_OccurredAt",
table: "OutboxEvents",
column: "OccurredAt");
migrationBuilder.CreateIndex(
name: "IX_OutboxEvents_PublishedAt_Sequence",
table: "OutboxEvents",
columns: new[] { "PublishedAt", "Sequence" });
migrationBuilder.CreateIndex(
name: "IX_Tasks_Done_UpdatedAt_Id",
table: "Tasks",
columns: new[] { "UpdatedAt", "Id" },
descending: new[] { true, true },
filter: "\"State\" = 'Done'");
migrationBuilder.CreateIndex(
name: "IX_Tasks_AgentWorkflow",
table: "Tasks",
columns: new[] { "ExpectedFrom", "State", "UpdatedAt", "Id" },
descending: new[] { false, false, true, false },
filter: "\"IsAgentTask\" = TRUE");
migrationBuilder.CreateIndex(
name: "IX_Tasks_ParentTaskId_State",
table: "Tasks",
columns: new[] { "ParentTaskId", "State" },
filter: "\"ParentTaskId\" IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_Tasks_State_UpdatedAt_Id_Board",
table: "Tasks",
columns: new[] { "State", "UpdatedAt", "Id" },
descending: new[] { false, true, false });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Activity_TaskId_CreatedAt_Id",
table: "Activity");
migrationBuilder.DropIndex(
name: "IX_Tasks_Done_UpdatedAt_Id",
table: "Tasks");
migrationBuilder.DropIndex(
name: "IX_Tasks_AgentWorkflow",
table: "Tasks");
migrationBuilder.DropIndex(
name: "IX_Tasks_ParentTaskId_State",
table: "Tasks");
migrationBuilder.DropIndex(
name: "IX_Tasks_State_UpdatedAt_Id_Board",
table: "Tasks");
migrationBuilder.DropTable(
name: "AgentProvisionRequests");
migrationBuilder.DropTable(
name: "OperationClaims");
migrationBuilder.DropTable(
name: "OutboxEvents");
migrationBuilder.DropTable(
name: "AgentProposals");
}
}
}
@@ -51,9 +51,225 @@ namespace Nexus.Api.Migrations
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ApprovedBy")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<DateTimeOffset?>("ApprovedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Avatar")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<DateTimeOffset?>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.Property<string>("Emoji")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("LastErrorCode")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("LastErrorMessage")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("Model")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("OpenClawAgentId")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("OpenClawWorkspace")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("RejectedBy")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<DateTimeOffset?>("RejectedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("RejectionReason")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("RequestedAgentId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("RequestedBy")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("RequestedName")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<int>("Revision")
.IsConcurrencyToken()
.HasColumnType("integer");
b.Property<string>("Role")
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("StandardFilesHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("StandardFilesJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Actor")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<int>("Attempt")
.HasColumnType("integer");
b.Property<DateTimeOffset?>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CorrelationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("DispatchStartedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("IdempotencyKeyHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LastErrorCode")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("LastErrorMessage")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<string>("LeaseOwner")
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<DateTimeOffset?>("LeaseUntil")
.HasColumnType("timestamp with time zone");
b.Property<string>("OpenClawAgentId")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<Guid>("ProposalId")
.HasColumnType("uuid");
b.Property<int>("Revision")
.IsConcurrencyToken()
.HasColumnType("integer");
b.Property<string>("Stage")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("TraceParent")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("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<Guid>("Id")
@@ -141,6 +357,370 @@ namespace Nexus.Api.Migrations
b.ToTable("Notifications");
});
modelBuilder.Entity("Nexus.Api.Data.OpenClawConnectionProfile", b =>
{
b.Property<string>("ProfileId")
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<DateTimeOffset?>("AdoptedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("AdoptionState")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("CapabilityHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DeviceId")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("DiscoverySource")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Endpoint")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<DateTimeOffset?>("LastProbedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("LastVerifiedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("ManagementEnabled")
.HasColumnType("boolean");
b.Property<string>("RequiredVersion")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<int>("Revision")
.IsConcurrencyToken()
.HasColumnType("integer");
b.Property<string>("TlsCertificateFingerprint")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Actor")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("AgentId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("CorrelationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("FinishedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<long?>("LastGatewaySequence")
.HasColumnType("bigint");
b.Property<string>("OpenClawRunId")
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<Guid?>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("RetriedFromRunId")
.HasColumnType("uuid");
b.Property<int>("Revision")
.IsConcurrencyToken()
.HasColumnType("integer");
b.Property<bool>("SequenceGapDetected")
.HasColumnType("boolean");
b.Property<string>("SessionKey")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("StartIdempotencyKey")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTimeOffset?>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<Guid?>("TaskId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<string>("TraceParent")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTimeOffset>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Actor")
.IsRequired()
.HasMaxLength(240)
.HasColumnType("character varying(240)");
b.Property<string>("CorrelationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("FromStatus")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("GatewayEventId")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<long?>("GatewaySequence")
.HasColumnType("bigint");
b.Property<string>("IdempotencyKey")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("ResultRunId")
.HasColumnType("uuid");
b.Property<Guid>("RunId")
.HasColumnType("uuid");
b.Property<bool>("SequenceGapDetected")
.HasColumnType("boolean");
b.Property<string>("ToStatus")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("IdempotencyKeyHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Operation")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("RequestHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid?>("ResourceId")
.HasColumnType("uuid");
b.Property<string>("ResultCode")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("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<long>("Sequence")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Sequence"));
b.Property<string>("AggregateId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<int>("AggregateRevision")
.HasColumnType("integer");
b.Property<string>("AggregateType")
.IsRequired()
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<Guid>("EventId")
.HasColumnType("uuid");
b.Property<string>("LastErrorCode")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PayloadJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTimeOffset?>("PublishedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("PublishAttempts")
.HasColumnType("integer");
b.Property<string>("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<Guid>("Id")
@@ -294,9 +874,51 @@ namespace Nexus.Api.Migrations
b.HasIndex("Source");
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.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")
@@ -323,6 +945,27 @@ namespace Nexus.Api.Migrations
b.Navigation("RefreshTokens");
});
modelBuilder.Entity("Nexus.Api.Data.AgentProposal", b =>
{
b.Navigation("ProvisionRequests");
});
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.Navigation("History");
});
modelBuilder.Entity("Nexus.Api.Data.WorkTask", b =>
{
b.Navigation("ChildTasks");
+158
View File
@@ -11,9 +11,20 @@ public sealed class NexusDbContext(DbContextOptions<NexusDbContext> options) : D
public DbSet<NexusUser> Users => Set<NexusUser>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<SeedAudit> SeedAudits => Set<SeedAudit>();
public DbSet<OpenClawRun> OpenClawRuns => Set<OpenClawRun>();
public DbSet<OpenClawRunHistory> OpenClawRunHistory => Set<OpenClawRunHistory>();
public DbSet<OpenClawConnectionProfile> OpenClawConnectionProfiles =>
Set<OpenClawConnectionProfile>();
public DbSet<AgentProposal> AgentProposals => Set<AgentProposal>();
public DbSet<AgentProvisionRequest> AgentProvisionRequests =>
Set<AgentProvisionRequest>();
public DbSet<OperationClaim> OperationClaims => Set<OperationClaim>();
public DbSet<OutboxEvent> OutboxEvents => Set<OutboxEvent>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new OpenClawConnectionProfileConfiguration());
ConfigureAgentProvisioning(modelBuilder);
modelBuilder.Entity<Project>().Property(x => x.Name).HasMaxLength(160);
modelBuilder.Entity<WorkTask>(entity =>
{
@@ -26,6 +37,20 @@ public sealed class NexusDbContext(DbContextOptions<NexusDbContext> options) : D
entity.HasIndex(x => x.AssignedTo);
entity.HasIndex(x => x.IsAgentTask);
entity.HasIndex(x => x.ExpectedFrom);
entity.HasIndex(x => new { x.State, x.UpdatedAt, x.Id })
.IsDescending(false, true, false)
.HasDatabaseName("IX_Tasks_State_UpdatedAt_Id_Board");
entity.HasIndex(x => new { x.UpdatedAt, x.Id })
.IsDescending(true, true)
.HasFilter("\"State\" = 'Done'")
.HasDatabaseName("IX_Tasks_Done_UpdatedAt_Id");
entity.HasIndex(x => new { x.ParentTaskId, x.State })
.HasFilter("\"ParentTaskId\" IS NOT NULL")
.HasDatabaseName("IX_Tasks_ParentTaskId_State");
entity.HasIndex(x => new { x.ExpectedFrom, x.State, x.UpdatedAt, x.Id })
.IsDescending(false, false, true, false)
.HasFilter("\"IsAgentTask\" = TRUE")
.HasDatabaseName("IX_Tasks_AgentWorkflow");
entity.HasOne(x => x.ParentTask)
.WithMany(x => x.ChildTasks)
.HasForeignKey(x => x.ParentTaskId)
@@ -44,6 +69,10 @@ public sealed class NexusDbContext(DbContextOptions<NexusDbContext> options) : D
{
entity.Property(x => x.Message).HasMaxLength(1000);
entity.HasIndex(x => x.TaskId);
entity.HasIndex(x => new { x.TaskId, x.CreatedAt, x.Id })
.IsDescending(false, true, true)
.HasFilter("\"TaskId\" IS NOT NULL")
.HasDatabaseName("IX_Activity_TaskId_CreatedAt_Id");
});
modelBuilder.Entity<NexusUser>().HasIndex(u => u.NormalizedEmail).IsUnique();
modelBuilder.Entity<RefreshToken>().HasIndex(r => r.TokenHash).IsUnique();
@@ -56,5 +85,134 @@ public sealed class NexusDbContext(DbContextOptions<NexusDbContext> options) : D
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ActivityEvent>().HasIndex(x => x.CreatedAt);
modelBuilder.Entity<OpenClawRun>(entity =>
{
entity.Property(x => x.Title).HasMaxLength(160);
entity.Property(x => x.Prompt).HasColumnType("text");
entity.Property(x => x.AgentId).HasMaxLength(200);
entity.Property(x => x.SessionKey).HasMaxLength(500);
entity.Property(x => x.Status).HasMaxLength(40);
entity.Property(x => x.OpenClawRunId).HasMaxLength(240);
entity.Property(x => x.StartIdempotencyKey).HasMaxLength(200);
entity.Property(x => x.CorrelationId).HasMaxLength(200);
entity.Property(x => x.Actor).HasMaxLength(240);
entity.Property(x => x.TraceParent).HasMaxLength(128);
entity.Property(x => x.LastError).HasMaxLength(2000);
entity.Property(x => x.Revision).IsConcurrencyToken();
entity.HasIndex(x => x.StartIdempotencyKey).IsUnique();
entity.HasIndex(x => x.OpenClawRunId).IsUnique();
entity.HasIndex(x => new { x.Status, x.UpdatedAt });
entity.HasIndex(x => x.TaskId);
entity.HasIndex(x => x.ProjectId);
entity.HasIndex(x => x.SessionKey);
entity.HasIndex(x => x.RetriedFromRunId);
entity.HasOne<WorkTask>()
.WithMany()
.HasForeignKey(x => x.TaskId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne<Project>()
.WithMany()
.HasForeignKey(x => x.ProjectId)
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity<OpenClawRunHistory>(entity =>
{
entity.Property(x => x.Action).HasMaxLength(80);
entity.Property(x => x.FromStatus).HasMaxLength(40);
entity.Property(x => x.ToStatus).HasMaxLength(40);
entity.Property(x => x.Message).HasMaxLength(2000);
entity.Property(x => x.Actor).HasMaxLength(240);
entity.Property(x => x.CorrelationId).HasMaxLength(200);
entity.Property(x => x.IdempotencyKey).HasMaxLength(200);
entity.Property(x => x.TraceParent).HasMaxLength(128);
entity.Property(x => x.GatewayEventId).HasMaxLength(120);
entity.HasIndex(x => new { x.RunId, x.OccurredAt });
entity.HasIndex(x => new { x.RunId, x.Action, x.IdempotencyKey }).IsUnique();
entity.HasIndex(x => x.GatewayEventId).IsUnique();
entity.HasOne(x => x.Run)
.WithMany(x => x.History)
.HasForeignKey(x => x.RunId)
.OnDelete(DeleteBehavior.Cascade);
});
}
private static void ConfigureAgentProvisioning(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AgentProposal>(entity =>
{
entity.Property(x => x.Source).HasMaxLength(40);
entity.Property(x => x.RequestedName).HasMaxLength(80);
entity.Property(x => x.RequestedAgentId).HasMaxLength(64);
entity.Property(x => x.Role).HasMaxLength(160);
entity.Property(x => x.Description).HasMaxLength(4000);
entity.Property(x => x.Model).HasMaxLength(240);
entity.Property(x => x.Emoji).HasMaxLength(32);
entity.Property(x => x.Avatar).HasMaxLength(2048);
entity.Property(x => x.Workspace).HasMaxLength(2048);
entity.Property(x => x.StandardFilesJson).HasColumnType("jsonb");
entity.Property(x => x.StandardFilesHash).HasMaxLength(64);
entity.Property(x => x.Status).HasMaxLength(40);
entity.Property(x => x.RequestedBy).HasMaxLength(240);
entity.Property(x => x.ApprovedBy).HasMaxLength(240);
entity.Property(x => x.RejectedBy).HasMaxLength(240);
entity.Property(x => x.RejectionReason).HasMaxLength(1000);
entity.Property(x => x.OpenClawAgentId).HasMaxLength(128);
entity.Property(x => x.OpenClawWorkspace).HasMaxLength(2048);
entity.Property(x => x.LastErrorCode).HasMaxLength(120);
entity.Property(x => x.LastErrorMessage).HasMaxLength(2000);
entity.Property(x => x.Revision).IsConcurrencyToken();
entity.HasIndex(x => new { x.Status, x.UpdatedAt });
entity.HasIndex(x => x.RequestedAgentId);
entity.HasIndex(x => x.OpenClawAgentId);
});
modelBuilder.Entity<AgentProvisionRequest>(entity =>
{
entity.Property(x => x.Stage).HasMaxLength(40);
entity.Property(x => x.Status).HasMaxLength(40);
entity.Property(x => x.IdempotencyKeyHash).HasMaxLength(64);
entity.Property(x => x.Actor).HasMaxLength(240);
entity.Property(x => x.CorrelationId).HasMaxLength(200);
entity.Property(x => x.TraceParent).HasMaxLength(128);
entity.Property(x => x.OpenClawAgentId).HasMaxLength(128);
entity.Property(x => x.LastErrorCode).HasMaxLength(120);
entity.Property(x => x.LastErrorMessage).HasMaxLength(2000);
entity.Property(x => x.LeaseOwner).HasMaxLength(160);
entity.Property(x => x.Revision).IsConcurrencyToken();
entity.HasIndex(x => new { x.ProposalId, x.Attempt }).IsUnique();
entity.HasIndex(x => new { x.Status, x.CreatedAt });
entity.HasIndex(x => x.LeaseUntil);
entity.HasOne(x => x.Proposal)
.WithMany(x => x.ProvisionRequests)
.HasForeignKey(x => x.ProposalId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<OperationClaim>(entity =>
{
entity.Property(x => x.Operation).HasMaxLength(100);
entity.Property(x => x.IdempotencyKeyHash).HasMaxLength(64);
entity.Property(x => x.RequestHash).HasMaxLength(64);
entity.Property(x => x.State).HasMaxLength(40);
entity.Property(x => x.ResultCode).HasMaxLength(120);
entity.HasIndex(x => new { x.Operation, x.IdempotencyKeyHash }).IsUnique();
entity.HasIndex(x => x.ExpiresAt);
});
modelBuilder.Entity<OutboxEvent>(entity =>
{
entity.HasKey(x => x.Sequence);
entity.Property(x => x.Sequence).ValueGeneratedOnAdd();
entity.Property(x => x.Type).HasMaxLength(120);
entity.Property(x => x.AggregateType).HasMaxLength(80);
entity.Property(x => x.AggregateId).HasMaxLength(128);
entity.Property(x => x.PayloadJson).HasColumnType("jsonb");
entity.Property(x => x.LastErrorCode).HasMaxLength(120);
entity.HasIndex(x => x.EventId).IsUnique();
entity.HasIndex(x => new { x.PublishedAt, x.Sequence });
entity.HasIndex(x => x.OccurredAt);
});
}
}
+76
View File
@@ -0,0 +1,76 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Nexus.Api.Models;
namespace Nexus.Api.Data;
/// <summary>
/// Secret-free metadata for Nexus' one active OpenClaw connection.
/// Device private keys, device tokens, bootstrap tokens and provider credentials
/// are intentionally outside this entity.
/// </summary>
[Table("OpenClawConnectionProfiles")]
public sealed class OpenClawConnectionProfile
{
public const string PrimaryProfileId = "primary";
[Key]
[MaxLength(40)]
public string ProfileId { get; set; } = PrimaryProfileId;
[MaxLength(2048)]
public required string Endpoint { get; set; }
[MaxLength(80)]
public required string DiscoverySource { get; set; }
[MaxLength(120)]
public string? RequiredVersion { get; set; }
[MaxLength(128)]
public string? TlsCertificateFingerprint { get; set; }
[MaxLength(40)]
public string AdoptionState { get; set; } = OpenClawAdoptionStates.None;
public bool ManagementEnabled { get; set; }
[MaxLength(128)]
public string? CapabilityHash { get; set; }
[MaxLength(240)]
public string? DeviceId { get; set; }
public int Revision { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? LastProbedAt { get; set; }
public DateTimeOffset? LastVerifiedAt { get; set; }
public DateTimeOffset? AdoptedAt { get; set; }
}
/// <summary>
/// Kept next to the entity so the root integration only has to apply this
/// configuration from NexusDbContext.OnModelCreating.
/// </summary>
public sealed class OpenClawConnectionProfileConfiguration
: IEntityTypeConfiguration<OpenClawConnectionProfile>
{
public void Configure(EntityTypeBuilder<OpenClawConnectionProfile> entity)
{
entity.ToTable(
"OpenClawConnectionProfiles",
table => table.HasCheckConstraint(
"CK_OpenClawConnectionProfiles_Primary",
"\"ProfileId\" = 'primary'"));
entity.HasKey(profile => profile.ProfileId);
entity.Property(profile => profile.Revision).IsConcurrencyToken();
}
}
+70
View File
@@ -0,0 +1,70 @@
namespace Nexus.Api.Data;
/// <summary>
/// Durable Nexus projection of one OpenClaw chat run. Nexus owns the
/// correlation and audit metadata; OpenClaw remains the execution authority.
/// </summary>
public sealed class OpenClawRun
{
public Guid Id { get; init; } = Guid.NewGuid();
public required string Title { get; set; }
public required string Prompt { get; set; }
public required string AgentId { get; set; }
public required string SessionKey { get; set; }
public string Status { get; set; } = OpenClawRunStates.Dispatching;
public Guid? TaskId { get; set; }
public Guid? ProjectId { get; set; }
public string? OpenClawRunId { get; set; }
public Guid? RetriedFromRunId { get; set; }
public required string StartIdempotencyKey { get; set; }
public required string CorrelationId { get; set; }
public required string Actor { get; set; }
public string? TraceParent { get; set; }
public string? LastError { get; set; }
public long? LastGatewaySequence { get; set; }
public bool SequenceGapDetected { get; set; }
public int Revision { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? StartedAt { get; set; }
public DateTimeOffset? FinishedAt { get; set; }
public ICollection<OpenClawRunHistory> History { get; set; } = new List<OpenClawRunHistory>();
}
/// <summary>
/// Append-only action and state-transition ledger for an OpenClaw run.
/// </summary>
public sealed class OpenClawRunHistory
{
public long Id { get; init; }
public Guid RunId { get; set; }
public OpenClawRun Run { get; set; } = null!;
public required string Action { get; set; }
public required string FromStatus { get; set; }
public required string ToStatus { get; set; }
public required string Message { get; set; }
public required string Actor { get; set; }
public required string CorrelationId { get; set; }
public string? IdempotencyKey { get; set; }
public string? TraceParent { get; set; }
public string? GatewayEventId { get; set; }
public long? GatewaySequence { get; set; }
public bool SequenceGapDetected { get; set; }
public Guid? ResultRunId { get; set; }
public DateTimeOffset OccurredAt { get; set; } = DateTimeOffset.UtcNow;
}
public static class OpenClawRunStates
{
public const string Dispatching = "dispatching";
public const string Running = "running";
public const string Stopping = "stopping";
public const string Stopped = "stopped";
public const string Completed = "completed";
public const string Failed = "failed";
public const string Blocked = "blocked";
public const string Unsupported = "unsupported";
public static bool IsTerminal(string state)
=> state is Stopped or Completed or Failed or Blocked or Unsupported;
}