diff --git a/.changeset/two-app-persistence.md b/.changeset/two-app-persistence.md new file mode 100644 index 000000000..86e86944f --- /dev/null +++ b/.changeset/two-app-persistence.md @@ -0,0 +1,5 @@ +--- +"agentweaver": minor +--- + +Add durable, redacted identity, authorization, repository-grant, automation, run-snapshot, and audit records for the two-GitHub-App model. diff --git a/apps/Agentweaver.Api.Data/Memory/MemoryDbContext.cs b/apps/Agentweaver.Api.Data/Memory/MemoryDbContext.cs index 10b8bd75f..594813922 100644 --- a/apps/Agentweaver.Api.Data/Memory/MemoryDbContext.cs +++ b/apps/Agentweaver.Api.Data/Memory/MemoryDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Agentweaver.Api.Runs; using Agentweaver.Api.Auth.OAuth; using Agentweaver.Api.Coordinator; @@ -32,6 +33,15 @@ public sealed class MemoryDbContext(DbContextOptions options) : public DbSet WebSessionExchangeCodes => Set(); public DbSet IntegrationBuildLocks => Set(); public DbSet DismissedNotifications => Set(); + public DbSet GitHubAuthorizations => Set(); + public DbSet GitHubAppAuthorizations => Set(); + public DbSet GitHubInstallations => Set(); + public DbSet GitHubRepositoryGrants => Set(); + public DbSet ProjectCopilotBindings => Set(); + public DbSet AutomationActivations => Set(); + public DbSet AutomationInvocations => Set(); + public DbSet RunGitHubIdentitySnapshots => Set(); + public DbSet GitHubAuditRecords => Set(); // Replica-safe per-pod / per-run singleton state moved out of process memory. public DbSet PendingRequests => Set(); @@ -181,6 +191,7 @@ protected override void OnModelCreating(ModelBuilder model) }); model.Entity().HasKey(l => l.ProjectId); + ConfigureTwoAppPersistence(model); model.Entity(e => { e.ToTable("dismissed_notifications"); @@ -214,9 +225,16 @@ protected override void OnModelCreating(ModelBuilder model) // and EF does not report pending model changes for the memory.db migrations. if (!Database.IsNpgsql()) { + // The SQLite companion database keeps a project projection solely as the principal + // for project-scoped durable two-App records, matching PostgreSQL FK semantics. + model.Entity(e => + { + e.ToTable("projects"); + e.HasKey(x => x.ProjectId); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + }); model.Ignore(); model.Ignore(); - model.Ignore(); model.Ignore(); model.Ignore(); model.Ignore(); @@ -555,4 +573,170 @@ protected override void OnModelCreating(ModelBuilder model) }); } + + private void ConfigureTwoAppPersistence(ModelBuilder model) + { + model.Entity(e => + { + e.ToTable("github_authorizations").HasKey(x => x.State); + e.Property(x => x.State).HasColumnName("state"); + e.Property(x => x.AppKind).HasColumnName("app_kind"); + e.Property(x => x.Purpose).HasColumnName("purpose"); + e.Property(x => x.EntraObjectId).HasColumnName("entra_object_id"); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + e.Property(x => x.ExpiresAtUnixMilliseconds).HasColumnName("expires_at_unix_ms"); + e.Property(x => x.ReturnRouteKey).HasColumnName("return_route_key"); + e.Property(x => x.PkceVerifierProtected).HasColumnName("pkce_verifier_protected"); + e.Property(x => x.CallbackCookieHash).HasColumnName("callback_cookie_hash"); + e.Property(x => x.Status).HasColumnName("status"); + e.Property(x => x.CreatedAt).HasColumnName("created_at"); + e.Property(x => x.CompletedAt).HasColumnName("completed_at"); + e.HasIndex(x => new { x.EntraObjectId, x.State }).IsUnique(); + e.HasIndex(x => x.ExpiresAtUnixMilliseconds); + ConfigureProjectForeignKey(e, "FK_github_authorizations_projects_project_id"); + }); + + model.Entity(e => + { + e.ToTable("github_app_authorizations").HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.EntraObjectId).HasColumnName("entra_object_id"); + e.Property(x => x.AppKind).HasColumnName("app_kind"); + e.Property(x => x.Purpose).HasColumnName("purpose"); + e.Property(x => x.CredentialReference).HasColumnName("credential_reference"); + e.Property(x => x.CredentialVersion).HasColumnName("credential_version"); + e.Property(x => x.GrantDigest).HasColumnName("grant_digest"); + e.Property(x => x.CreatedAt).HasColumnName("created_at"); + e.Property(x => x.RevokedAt).HasColumnName("revoked_at"); + e.HasIndex(x => new { x.EntraObjectId, x.AppKind, x.Purpose }); + }); + + model.Entity(e => + { + e.ToTable("github_installations").HasKey(x => x.InstallationId); + e.Property(x => x.InstallationId).HasColumnName("installation_id").ValueGeneratedNever(); + e.Property(x => x.AppKind).HasColumnName("app_kind"); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + e.Property(x => x.CreatedAt).HasColumnName("created_at"); + e.Property(x => x.RevokedAt).HasColumnName("revoked_at"); + ConfigureProjectForeignKey(e, "FK_github_installations_projects_project_id"); + }); + + model.Entity(e => + { + e.ToTable("github_repository_grants").HasKey(x => new { x.InstallationId, x.RepositoryId }); + e.Property(x => x.InstallationId).HasColumnName("installation_id"); + e.Property(x => x.RepositoryId).HasColumnName("repository_id"); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + e.Property(x => x.FullNameDisplay).HasColumnName("full_name_display"); + e.Property(x => x.PermissionDigest).HasColumnName("permission_digest"); + e.Property(x => x.GrantedAt).HasColumnName("granted_at"); + e.Property(x => x.RevokedAt).HasColumnName("revoked_at"); + e.HasIndex(x => new { x.InstallationId, x.RepositoryId }).IsUnique(); + e.HasOne().WithMany().HasForeignKey(x => x.InstallationId) + .OnDelete(DeleteBehavior.Cascade).HasConstraintName("FK_github_repository_grants_installations_installation_id"); + ConfigureProjectForeignKey(e, "FK_github_repository_grants_projects_project_id"); + }); + + model.Entity(e => + { + e.ToTable("project_copilot_bindings").HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + e.Property(x => x.EntraObjectId).HasColumnName("entra_object_id"); + e.Property(x => x.CredentialReference).HasColumnName("credential_reference"); + e.Property(x => x.CredentialVersion).HasColumnName("credential_version"); + e.Property(x => x.GrantDigest).HasColumnName("grant_digest"); + e.Property(x => x.Status).HasColumnName("status"); + e.Property(x => x.BoundAt).HasColumnName("bound_at"); + e.Property(x => x.DeactivatedAt).HasColumnName("deactivated_at"); + e.HasIndex(x => x.ProjectId).IsUnique().HasFilter("status = 0") + .HasDatabaseName("UX_project_copilot_bindings_active_project"); + ConfigureProjectForeignKey(e, "FK_project_copilot_bindings_projects_project_id"); + }); + + model.Entity(e => + { + e.ToTable("automation_activations").HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + e.Property(x => x.InstallationId).HasColumnName("installation_id"); + e.Property(x => x.RepositoryId).HasColumnName("repository_id"); + e.Property(x => x.AutomationKey).HasColumnName("automation_key"); + e.Property(x => x.Status).HasColumnName("status"); + e.Property(x => x.ActivatedAt).HasColumnName("activated_at"); + e.Property(x => x.InvalidatedAt).HasColumnName("invalidated_at"); + e.HasIndex(x => new { x.ProjectId, x.InstallationId, x.RepositoryId, x.AutomationKey }).IsUnique(); + e.HasOne().WithMany() + .HasForeignKey(x => new { x.InstallationId, x.RepositoryId }) + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_automation_activations_repository_grants_installation_id_repository_id"); + ConfigureProjectForeignKey(e, "FK_automation_activations_projects_project_id"); + }); + + model.Entity(e => + { + e.ToTable("automation_invocations").HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + e.Property(x => x.ActivationId).HasColumnName("activation_id"); + e.Property(x => x.OccurrenceKey).HasColumnName("occurrence_key"); + e.Property(x => x.DeliveryId).HasColumnName("delivery_id"); + e.Property(x => x.EventName).HasColumnName("event_name"); + e.Property(x => x.InstallationId).HasColumnName("installation_id"); + e.Property(x => x.RepositoryId).HasColumnName("repository_id"); + e.Property(x => x.Outcome).HasColumnName("outcome"); + e.Property(x => x.ReceivedAt).HasColumnName("received_at"); + e.Property(x => x.CompletedAt).HasColumnName("completed_at"); + e.HasIndex(x => new { x.ActivationId, x.OccurrenceKey }).IsUnique(); + e.HasIndex(x => new { x.DeliveryId, x.EventName }).IsUnique(); + e.HasOne().WithMany().HasForeignKey(x => x.ActivationId) + .OnDelete(DeleteBehavior.Cascade).HasConstraintName("FK_automation_invocations_activations_activation_id"); + ConfigureProjectForeignKey(e, "FK_automation_invocations_projects_project_id"); + }); + + model.Entity(e => + { + e.ToTable("run_github_identity_snapshots").HasKey(x => x.RunId); + e.Property(x => x.RunId).HasColumnName("run_id"); + e.Property(x => x.ProjectId).HasColumnName("project_id"); + e.Property(x => x.AppKind).HasColumnName("app_kind"); + e.Property(x => x.Purpose).HasColumnName("purpose"); + e.Property(x => x.CredentialReference).HasColumnName("credential_reference"); + e.Property(x => x.CredentialVersion).HasColumnName("credential_version"); + e.Property(x => x.GrantDigest).HasColumnName("grant_digest"); + e.Property(x => x.InstallationId).HasColumnName("installation_id"); + e.Property(x => x.RepositoryId).HasColumnName("repository_id"); + e.Property(x => x.EntraObjectId).HasColumnName("entra_object_id"); + e.Property(x => x.CapturedAt).HasColumnName("captured_at"); + ConfigureProjectForeignKey(e, "FK_run_github_identity_snapshots_projects_project_id"); + }); + + model.Entity(e => + { + e.ToTable("github_audit_records").HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id").ValueGeneratedOnAdd(); + e.Property(x => x.EntraObjectId).HasColumnName("entra_object_id"); + e.Property(x => x.ActorKind).HasColumnName("actor_kind"); + e.Property(x => x.Action).HasColumnName("action"); + e.Property(x => x.ResourceId).HasColumnName("resource_id"); + e.Property(x => x.AppKind).HasColumnName("app_kind"); + e.Property(x => x.Purpose).HasColumnName("purpose"); + e.Property(x => x.Outcome).HasColumnName("outcome"); + e.Property(x => x.ReasonCode).HasColumnName("reason_code"); + e.Property(x => x.CorrelationId).HasColumnName("correlation_id"); + e.Property(x => x.OccurredAt).HasColumnName("occurred_at"); + e.Property(x => x.CredentialVersionOrDigest).HasColumnName("credential_version_or_digest"); + e.HasIndex(x => x.OccurredAt); + }); + } + + private void ConfigureProjectForeignKey( + EntityTypeBuilder entity, + string constraintName) + where TEntity : class + { + entity.HasOne().WithMany().HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade).HasConstraintName(constraintName); + } } diff --git a/apps/Agentweaver.Api.Data/Memory/TwoAppPersistenceRecords.cs b/apps/Agentweaver.Api.Data/Memory/TwoAppPersistenceRecords.cs new file mode 100644 index 000000000..eb63cc2d0 --- /dev/null +++ b/apps/Agentweaver.Api.Data/Memory/TwoAppPersistenceRecords.cs @@ -0,0 +1,132 @@ +namespace Agentweaver.Api.Memory; + +public enum GitHubAppKind { Repo, Copilot } +public enum GitHubAuthorizationPurpose { InteractiveRepository, InteractiveCopilot, UnattendedRepository, UnattendedCopilot } +public enum GitHubAuthorizationStatus { Pending, Redeeming, Completed, Failed } +public enum GitHubBindingStatus { Active, Inactive, Revoked } +public enum AutomationActivationStatus { Active, Inactive, Invalidated } +public enum AutomationInvocationOutcome { Claimed, Duplicate, Completed, Failed } +public enum GitHubAuditActorKind { HumanEntraSubject, GitHubWebhook } +public enum GitHubAuditAction { AuthorizationCompleted, BindingChanged, InstallationChanged, GrantChanged, AutomationActivated, AutomationInvoked, RunSnapshotValidated } +public enum GitHubAuditOutcome { Succeeded, Denied, Failed } +public enum GitHubAuditReasonCode { None, BindingUnavailable, InstallationUnavailable, TransactionInvalid, TransactionConsumed, RotationMismatch, DuplicateDelivery } + +public sealed class GitHubAuthorizationRecord +{ + public string State { get; set; } = ""; + public GitHubAppKind AppKind { get; set; } + public GitHubAuthorizationPurpose Purpose { get; set; } + public string EntraObjectId { get; set; } = ""; + public string? ProjectId { get; set; } + public long ExpiresAtUnixMilliseconds { get; set; } + public string ReturnRouteKey { get; set; } = ""; + public string PkceVerifierProtected { get; set; } = ""; + public string CallbackCookieHash { get; set; } = ""; + public GitHubAuthorizationStatus Status { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset? CompletedAt { get; set; } +} + +public sealed class GitHubAppAuthorizationRecord +{ + public string Id { get; set; } = ""; + public string EntraObjectId { get; set; } = ""; + public GitHubAppKind AppKind { get; set; } + public GitHubAuthorizationPurpose Purpose { get; set; } + public string CredentialReference { get; set; } = ""; + public string CredentialVersion { get; set; } = ""; + public string GrantDigest { get; set; } = ""; + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset? RevokedAt { get; set; } +} + +public sealed class GitHubInstallationRecord +{ + public long InstallationId { get; set; } + public GitHubAppKind AppKind { get; set; } + public string? ProjectId { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset? RevokedAt { get; set; } +} + +public sealed class GitHubRepositoryGrantRecord +{ + public long InstallationId { get; set; } + public long RepositoryId { get; set; } + public string ProjectId { get; set; } = ""; + public string FullNameDisplay { get; set; } = ""; + public string PermissionDigest { get; set; } = ""; + public DateTimeOffset GrantedAt { get; set; } + public DateTimeOffset? RevokedAt { get; set; } +} + +public sealed class ProjectCopilotBindingRecord +{ + public string Id { get; set; } = ""; + public string ProjectId { get; set; } = ""; + public string EntraObjectId { get; set; } = ""; + public string CredentialReference { get; set; } = ""; + public string CredentialVersion { get; set; } = ""; + public string GrantDigest { get; set; } = ""; + public GitHubBindingStatus Status { get; set; } + public DateTimeOffset BoundAt { get; set; } + public DateTimeOffset? DeactivatedAt { get; set; } +} + +public sealed class AutomationActivationRecord +{ + public string Id { get; set; } = ""; + public string ProjectId { get; set; } = ""; + public long InstallationId { get; set; } + public long RepositoryId { get; set; } + public string AutomationKey { get; set; } = ""; + public AutomationActivationStatus Status { get; set; } + public DateTimeOffset ActivatedAt { get; set; } + public DateTimeOffset? InvalidatedAt { get; set; } +} + +public sealed class AutomationInvocationRecord +{ + public string Id { get; set; } = ""; + public string ProjectId { get; set; } = ""; + public string ActivationId { get; set; } = ""; + public string OccurrenceKey { get; set; } = ""; + public string? DeliveryId { get; set; } + public string? EventName { get; set; } + public long? InstallationId { get; set; } + public long? RepositoryId { get; set; } + public AutomationInvocationOutcome Outcome { get; set; } + public DateTimeOffset ReceivedAt { get; set; } + public DateTimeOffset? CompletedAt { get; set; } +} + +public sealed class RunGitHubIdentitySnapshotRecord +{ + public string RunId { get; set; } = ""; + public string ProjectId { get; set; } = ""; + public GitHubAppKind AppKind { get; set; } + public GitHubAuthorizationPurpose Purpose { get; set; } + public string CredentialReference { get; set; } = ""; + public string CredentialVersion { get; set; } = ""; + public string GrantDigest { get; set; } = ""; + public long? InstallationId { get; set; } + public long? RepositoryId { get; set; } + public string? EntraObjectId { get; set; } + public DateTimeOffset CapturedAt { get; set; } +} + +public sealed class GitHubAuditRecord +{ + public long Id { get; set; } + public string? EntraObjectId { get; set; } + public GitHubAuditActorKind ActorKind { get; set; } + public GitHubAuditAction Action { get; set; } + public string ResourceId { get; set; } = ""; + public GitHubAppKind? AppKind { get; set; } + public GitHubAuthorizationPurpose? Purpose { get; set; } + public GitHubAuditOutcome Outcome { get; set; } + public GitHubAuditReasonCode ReasonCode { get; set; } + public string CorrelationId { get; set; } = ""; + public DateTimeOffset OccurredAt { get; set; } + public string? CredentialVersionOrDigest { get; set; } +} diff --git a/apps/Agentweaver.Api.Migrations.Postgres/Migrations/20260827112924_AddTwoAppPersistence.Designer.cs b/apps/Agentweaver.Api.Migrations.Postgres/Migrations/20260827112924_AddTwoAppPersistence.Designer.cs new file mode 100644 index 000000000..75a346afc --- /dev/null +++ b/apps/Agentweaver.Api.Migrations.Postgres/Migrations/20260827112924_AddTwoAppPersistence.Designer.cs @@ -0,0 +1,2826 @@ +// +using System; +using Agentweaver.Api.Memory; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Agentweaver.Api.Migrations.Postgres.Migrations +{ + [DbContext(typeof(MemoryDbContext))] + [Migration("20260827112924_AddTwoAppPersistence")] + partial class AddTwoAppPersistence + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.EntraOAuthState", b => + { + b.Property("State") + .HasColumnType("text"); + + b.Property("CodeVerifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("EntraOAuthStates"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpAuthorizationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CodeChallenge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GithubLogin") + .IsRequired() + .HasColumnType("text"); + + b.Property("RedirectUri") + .IsRequired() + .HasColumnType("text"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ExpiresAt"); + + b.ToTable("McpAuthorizationCodes"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpClientRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RedirectUris") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("McpClientRegistrations"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpPendingAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientState") + .HasColumnType("text"); + + b.Property("CodeChallenge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RedirectUri") + .IsRequired() + .HasColumnType("text"); + + b.Property("Resource") + .HasColumnType("text"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("State") + .IsUnique(); + + b.ToTable("McpPendingAuthorizations"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ChainId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GithubLogin") + .IsRequired() + .HasColumnType("text"); + + b.Property("Org") + .HasColumnType("text"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ChainId"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("Subject", "ClientId"); + + b.ToTable("McpRefreshTokens"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpRevokedJti", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Jti") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("Jti") + .IsUnique(); + + b.ToTable("McpRevokedJtis"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.OAuthState", b => + { + b.Property("State") + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("OAuthStates"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.WebSessionExchangeCode", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Login") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("WebSessionExchangeCodes"); + }); + + modelBuilder.Entity("Agentweaver.Api.Coordinator.CoordinatorAssemblyReviewRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateTreeHash") + .HasColumnType("text"); + + b.Property("CoordinatorFailedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinatorFailureReason") + .HasColumnType("text"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DecisionJson") + .HasColumnType("text"); + + b.Property("DecisionSubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IntegrationBranch") + .HasColumnType("text"); + + b.Property("OwnerUser") + .HasColumnType("text"); + + b.Property("Reviewer") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CoordinatorRunId") + .IsUnique(); + + b.ToTable("AssemblyReviews"); + }); + + modelBuilder.Entity("Agentweaver.Api.Diagnostics.HeartbeatStatusRecord", b => + { + b.Property("PodName") + .HasColumnType("text"); + + b.Property("ActedCount") + .HasColumnType("integer"); + + b.Property("DurationMs") + .HasColumnType("bigint"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("ErrorCount") + .HasColumnType("integer"); + + b.Property("IntervalSeconds") + .HasColumnType("integer"); + + b.Property("LastTickUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PodName"); + + b.ToTable("HeartbeatStatuses"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AgentMemory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Importance") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SourceIdentity") + .HasColumnType("text"); + + b.Property("SourceKind") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("legacy"); + + b.Property("SourceRunId") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("TrustState") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("legacy"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId", "AgentName"); + + b.HasIndex("ProjectId", "Type"); + + b.ToTable("AgentMemory"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AuthModeEpochRecord", b => + { + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("AuthMode") + .IsRequired() + .HasColumnType("text") + .HasColumnName("auth_mode"); + + b.Property("Epoch") + .HasColumnType("bigint") + .HasColumnName("epoch"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Key"); + + b.ToTable("auth_mode_epochs", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("AutomationKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("automation_key"); + + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("InvalidatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("invalidated_at"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId", "InstallationId", "RepositoryId", "AutomationKey") + .IsUnique(); + + b.ToTable("automation_activations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("ActivationId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("activation_id"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("DeliveryId") + .HasColumnType("text") + .HasColumnName("delivery_id"); + + b.Property("EventName") + .HasColumnType("text") + .HasColumnName("event_name"); + + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("OccurrenceKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("occurrence_key"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("received_at"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ActivationId", "OccurrenceKey") + .IsUnique(); + + b.HasIndex("DeliveryId", "EventName") + .IsUnique(); + + b.ToTable("automation_invocations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.BacklogTaskDependencyRecord", b => + { + b.Property("TaskId") + .HasColumnType("text") + .HasColumnName("task_id"); + + b.Property("DependsOnTaskId") + .HasColumnType("text") + .HasColumnName("depends_on_task_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.HasKey("TaskId", "DependsOnTaskId"); + + b.HasIndex("DependsOnTaskId") + .HasDatabaseName("IX_backlog_task_dependencies_prerequisite"); + + b.HasIndex("ProjectId", "TaskId") + .HasDatabaseName("IX_backlog_task_dependencies_project_task"); + + b.ToTable("backlog_task_dependencies", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.BacklogTaskRecord", b => + { + b.Property("TaskId") + .HasColumnType("text") + .HasColumnName("task_id"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("archived_at"); + + b.Property("CapturedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("captured_by"); + + b.Property("CapturedByUserId") + .HasColumnType("text") + .HasColumnName("captured_by_user_id"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("claimed_at"); + + b.Property("CommittedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("committed_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("OrderKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("order_key"); + + b.Property("ParentPrdRunId") + .HasColumnType("text") + .HasColumnName("parent_prd_run_id"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("PromotionKey") + .HasColumnType("text") + .HasColumnName("promotion_key"); + + b.Property("PromotionReason") + .HasColumnType("text") + .HasColumnName("promotion_reason"); + + b.Property("RunId") + .HasColumnType("text") + .HasColumnName("run_id"); + + b.Property("SourceFilePath") + .HasColumnType("text") + .HasColumnName("source_file_path"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("WorkflowOverrideId") + .HasColumnType("text") + .HasColumnName("workflow_override_id"); + + b.HasKey("TaskId"); + + b.HasIndex("RunId") + .IsUnique() + .HasDatabaseName("IX_backlog_tasks_run") + .HasFilter("run_id IS NOT NULL"); + + b.HasIndex("ParentPrdRunId", "PromotionKey") + .IsUnique() + .HasDatabaseName("IX_backlog_tasks_parent_promotion_key") + .HasFilter("parent_prd_run_id IS NOT NULL AND promotion_key IS NOT NULL"); + + b.HasIndex("ProjectId", "State", "OrderKey") + .IsUnique() + .HasDatabaseName("IX_backlog_tasks_orderkey_unique") + .HasFilter("state IN ('backlog','ready') AND archived_at IS NULL"); + + b.ToTable("backlog_tasks", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.BlueprintPackageAcquisitionRecord", b => + { + b.Property("OwnerId") + .HasColumnType("text") + .HasColumnName("owner_id"); + + b.Property("PackageId") + .HasColumnType("text") + .HasColumnName("package_id"); + + b.Property("CanonicalVersionKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("canonical_version_key"); + + b.Property("Ordinal") + .HasColumnType("integer") + .HasColumnName("ordinal"); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_at"); + + b.Property("CanonicalVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("canonical_version"); + + b.Property("Producer") + .HasColumnType("text") + .HasColumnName("producer"); + + b.Property("Repository") + .HasColumnType("text") + .HasColumnName("repository"); + + b.Property("RequestedRef") + .HasColumnType("text") + .HasColumnName("requested_ref"); + + b.Property("Revision") + .HasColumnType("text") + .HasColumnName("revision"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("source"); + + b.HasKey("OwnerId", "PackageId", "CanonicalVersionKey", "Ordinal"); + + b.ToTable("blueprint_package_acquisitions", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.BlueprintPackageLibraryRecord", b => + { + b.Property("OwnerId") + .HasColumnType("text") + .HasColumnName("owner_id"); + + b.Property("PackageId") + .HasColumnType("text") + .HasColumnName("package_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.HasKey("OwnerId", "PackageId"); + + b.ToTable("blueprint_package_library", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.BlueprintPackagePayloadRecord", b => + { + b.Property("OwnerId") + .HasColumnType("text") + .HasColumnName("owner_id"); + + b.Property("PackageId") + .HasColumnType("text") + .HasColumnName("package_id"); + + b.Property("CanonicalVersionKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("canonical_version_key"); + + b.Property("Path") + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("Bytes") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("bytes"); + + b.Property("CanonicalVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("canonical_version"); + + b.HasKey("OwnerId", "PackageId", "CanonicalVersionKey", "Path"); + + b.ToTable("blueprint_package_payloads", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.BlueprintPackageVersionRecord", b => + { + b.Property("OwnerId") + .HasColumnType("text") + .HasColumnName("owner_id"); + + b.Property("PackageId") + .HasColumnType("text") + .HasColumnName("package_id"); + + b.Property("CanonicalVersionKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("canonical_version_key"); + + b.Property("CanonicalVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("canonical_version"); + + b.Property("ContainerSha256") + .HasColumnType("text") + .HasColumnName("container_sha256"); + + b.Property("ContentDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content_digest"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("PayloadSetDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("payload_set_digest"); + + b.Property("RawManifest") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("raw_manifest"); + + b.Property("RawManifestSha256") + .IsRequired() + .HasColumnType("text") + .HasColumnName("raw_manifest_sha256"); + + b.HasKey("OwnerId", "PackageId", "CanonicalVersionKey"); + + b.ToTable("blueprint_package_versions", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.CastProposalRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Owner") + .IsRequired() + .HasColumnType("text") + .HasColumnName("owner"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("ProposalJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("proposal_json"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId") + .HasDatabaseName("IX_cast_proposals_project_id"); + + b.ToTable("cast_proposals", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Decision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedBy") + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rationale") + .HasColumnType("text"); + + b.Property("SourceIdentity") + .HasColumnType("text"); + + b.Property("SourceKind") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("legacy"); + + b.Property("SourceRunId") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupersededById") + .HasColumnType("integer"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("TrustState") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("legacy"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SupersededById"); + + b.HasIndex("ProjectId", "AgentName"); + + b.HasIndex("ProjectId", "Status"); + + b.ToTable("Decisions"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.DecisionInboxEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DecisionId") + .HasColumnType("integer"); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rationale") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceIdentity") + .HasColumnType("text"); + + b.Property("SourceKind") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("legacy"); + + b.Property("SourceRunId") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DecisionId"); + + b.HasIndex("ProjectId", "Slug") + .IsUnique(); + + b.HasIndex("ProjectId", "Status"); + + b.ToTable("DecisionInbox"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.DismissedNotification", b => + { + b.Property("User") + .HasColumnType("text") + .HasColumnName("user"); + + b.Property("NotificationId") + .HasColumnType("text") + .HasColumnName("notification_id"); + + b.Property("DismissedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("dismissed_at"); + + b.HasKey("User", "NotificationId"); + + b.ToTable("dismissed_notifications", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAccountLinkStateRecord", b => + { + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("EntraUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("entra_user_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("github_account_link_states", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAppAuthorizationRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("grant_digest"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.HasKey("Id"); + + b.HasIndex("EntraObjectId", "AppKind", "Purpose"); + + b.ToTable("github_app_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasColumnName("action"); + + b.Property("ActorKind") + .HasColumnType("integer") + .HasColumnName("actor_kind"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("CredentialVersionOrDigest") + .HasColumnType("text") + .HasColumnName("credential_version_or_digest"); + + b.Property("EntraObjectId") + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("ReasonCode") + .HasColumnType("integer") + .HasColumnName("reason_code"); + + b.Property("ResourceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("resource_id"); + + b.HasKey("Id"); + + b.HasIndex("OccurredAt"); + + b.ToTable("github_audit_records", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CallbackCookieHash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("callback_cookie_hash"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("ExpiresAtUnixMilliseconds") + .HasColumnType("bigint") + .HasColumnName("expires_at_unix_ms"); + + b.Property("PkceVerifierProtected") + .IsRequired() + .HasColumnType("text") + .HasColumnName("pkce_verifier_protected"); + + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("ReturnRouteKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("return_route_key"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAtUnixMilliseconds"); + + b.HasIndex("ProjectId"); + + b.HasIndex("EntraObjectId", "State") + .IsUnique(); + + b.ToTable("github_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId"); + + b.HasIndex("ProjectId"); + + b.ToTable("github_installations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.Property("FullNameDisplay") + .IsRequired() + .HasColumnType("text") + .HasColumnName("full_name_display"); + + b.Property("GrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("granted_at"); + + b.Property("PermissionDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("permission_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId", "RepositoryId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("InstallationId", "RepositoryId") + .IsUnique(); + + b.ToTable("github_repository_grants", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.IntegrationBuildLockRecord", b => + { + b.Property("ProjectId") + .HasColumnType("text"); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerPodId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerToken") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ProjectId"); + + b.ToTable("IntegrationBuildLocks"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.OutcomeSpec", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowTaskPromotion") + .HasColumnType("boolean"); + + b.Property("Assumptions") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClarifyingQuestions") + .HasColumnType("text"); + + b.Property("ConfirmedBy") + .HasColumnType("text"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DesiredOutcome") + .IsRequired() + .HasColumnType("text"); + + b.Property("Goal") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId", "CoordinatorRunId"); + + b.ToTable("OutcomeSpecs"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("BoundAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("bound_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_version"); + + b.Property("DeactivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deactivated_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("grant_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId") + .IsUnique() + .HasDatabaseName("UX_project_copilot_bindings_active_project") + .HasFilter("status = 0"); + + b.ToTable("project_copilot_bindings", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectGitHubIdentityOverrideRecord", b => + { + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("EntraUserId") + .HasColumnType("text") + .HasColumnName("entra_user_id"); + + b.Property("GitHubLogin") + .IsRequired() + .HasColumnType("text") + .HasColumnName("github_login"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("ProjectId", "EntraUserId"); + + b.HasIndex("EntraUserId", "GitHubLogin") + .HasDatabaseName("IX_project_github_identity_overrides_user_login"); + + b.ToTable("project_github_identity_overrides", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectRecord", b => + { + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("ActiveReviewPolicyName") + .HasColumnType("text") + .HasColumnName("active_review_policy_name"); + + b.Property("AllowedWorkflowIds") + .HasColumnType("text") + .HasColumnName("allowed_workflow_ids"); + + b.Property("BlueprintGenerationModel") + .HasColumnType("text") + .HasColumnName("blueprint_generation_model"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DefaultBranch") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("main") + .HasColumnName("default_branch"); + + b.Property("DefaultModelCopilot") + .HasColumnType("text") + .HasColumnName("default_model_copilot"); + + b.Property("DefaultModelFoundry") + .HasColumnType("text") + .HasColumnName("default_model_foundry"); + + b.Property("DefaultProvider") + .IsRequired() + .HasColumnType("text") + .HasColumnName("default_provider"); + + b.Property("DefaultWorkflowId") + .HasColumnType("text") + .HasColumnName("default_workflow_id"); + + b.Property("MaxReadyPerHeartbeat") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3) + .HasColumnName("max_ready_per_heartbeat"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OriginKind") + .IsRequired() + .HasColumnType("text") + .HasColumnName("origin_kind"); + + b.Property("OutcomeSpecGenerationModel") + .HasColumnType("text") + .HasColumnName("outcome_spec_generation_model"); + + b.Property("Owner") + .IsRequired() + .HasColumnType("text") + .HasColumnName("owner"); + + b.Property("PickupAutoApproveTools") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("pickup_auto_approve_tools"); + + b.Property("PickupAutopilot") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("pickup_autopilot"); + + b.Property("PreviewApprovalTimeoutMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasColumnName("preview_approval_timeout_minutes"); + + b.Property("SandboxProfile") + .HasColumnType("text") + .HasColumnName("sandbox_profile"); + + b.Property("SourceBlueprintId") + .HasColumnType("text") + .HasColumnName("source_blueprint_id"); + + b.Property("SourceBlueprintType") + .HasColumnType("text") + .HasColumnName("source_blueprint_type"); + + b.Property("SourceRepository") + .HasColumnType("text") + .HasColumnName("source_repository"); + + b.Property("State") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("active") + .HasColumnName("state"); + + b.Property("TeamRevision") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L) + .HasColumnName("team_revision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WebhookSecret") + .HasColumnType("text") + .HasColumnName("webhook_secret"); + + b.Property("WorkflowGenerationModel") + .HasColumnType("text") + .HasColumnName("workflow_generation_model"); + + b.Property("WorkingDirectory") + .IsRequired() + .HasColumnType("text") + .HasColumnName("working_directory"); + + b.HasKey("ProjectId"); + + b.HasIndex("State") + .HasDatabaseName("IX_projects_state"); + + b.ToTable("projects", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectRoleAssignmentRecord", b => + { + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("PrincipalId") + .HasColumnType("text") + .HasColumnName("principal_id"); + + b.Property("GrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("granted_at"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("granted_by"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role"); + + b.HasKey("ProjectId", "PrincipalId"); + + b.HasIndex("PrincipalId") + .HasDatabaseName("IX_project_role_assignments_principal_id"); + + b.HasIndex("ProjectId", "Role") + .HasDatabaseName("IX_project_role_assignments_project_role"); + + b.ToTable("project_role_assignments", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunAuthorshipCapability", b => + { + b.Property("RunId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("run_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("token_hash"); + + b.HasKey("RunId"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("run_authorship_capabilities", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.Property("RunId") + .HasColumnType("text") + .HasColumnName("run_id"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CapturedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("captured_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("grant_digest"); + + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.HasKey("RunId"); + + b.HasIndex("ProjectId"); + + b.ToTable("run_github_identity_snapshots", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunRecord", b => + { + b.Property("RunId") + .HasColumnType("text") + .HasColumnName("run_id"); + + b.Property("AgentCharter") + .HasColumnType("text") + .HasColumnName("agent_charter"); + + b.Property("AgentName") + .HasColumnType("text") + .HasColumnName("agent_name"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("archived_at"); + + b.Property("Attempt") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("attempt"); + + b.Property("Diff") + .HasColumnType("text") + .HasColumnName("diff"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("FencingToken") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L) + .HasColumnName("fencing_token"); + + b.Property("HeartbeatAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("heartbeat_at"); + + b.Property("LeaseExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lease_expires_at"); + + b.Property("MergeConflicts") + .HasColumnType("text") + .HasColumnName("merge_conflicts"); + + b.Property("MergedCommitHash") + .HasColumnType("text") + .HasColumnName("merged_commit_hash"); + + b.Property("ModelId") + .HasColumnType("text") + .HasColumnName("model_id"); + + b.Property("ModelSource") + .IsRequired() + .HasColumnType("text") + .HasColumnName("model_source"); + + b.Property("Origin") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("interactive") + .HasColumnName("origin"); + + b.Property("OriginatingBranch") + .IsRequired() + .HasColumnType("text") + .HasColumnName("originating_branch"); + + b.Property("OwnerId") + .HasColumnType("text") + .HasColumnName("owner_id"); + + b.Property("ParentRunId") + .HasColumnType("text") + .HasColumnName("parent_run_id"); + + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("RepositoryPath") + .IsRequired() + .HasColumnType("text") + .HasColumnName("repository_path"); + + b.Property("Result") + .HasColumnType("text") + .HasColumnName("result"); + + b.Property("RetriedFrom") + .HasColumnType("text") + .HasColumnName("retried_from"); + + b.Property("ReviewReadyAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("review_ready_at"); + + b.Property("ReviewedBy") + .HasColumnType("text") + .HasColumnName("reviewed_by"); + + b.Property("SandboxBackend") + .HasColumnType("text") + .HasColumnName("sandbox_backend"); + + b.Property("SandboxClaimName") + .HasColumnType("text") + .HasColumnName("sandbox_claim_name"); + + b.Property("SandboxNamespace") + .HasColumnType("text") + .HasColumnName("sandbox_namespace"); + + b.Property("SandboxPodName") + .HasColumnType("text") + .HasColumnName("sandbox_pod_name"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("SubmittingUser") + .IsRequired() + .HasColumnType("text") + .HasColumnName("submitting_user"); + + b.Property("SubtaskId") + .HasColumnType("text") + .HasColumnName("subtask_id"); + + b.Property("Task") + .IsRequired() + .HasColumnType("text") + .HasColumnName("task"); + + b.Property("TreeHash") + .HasColumnType("text") + .HasColumnName("tree_hash"); + + b.Property("WorkflowRunId") + .HasColumnType("text") + .HasColumnName("workflow_run_id"); + + b.Property("WorkflowSelectionReason") + .HasColumnType("text") + .HasColumnName("workflow_selection_reason"); + + b.Property("WorktreeBranch") + .HasColumnType("text") + .HasColumnName("worktree_branch"); + + b.Property("WorktreePath") + .HasColumnType("text") + .HasColumnName("worktree_path"); + + b.HasKey("RunId"); + + b.HasIndex("WorkflowRunId") + .HasDatabaseName("IX_runs_workflow_run_id"); + + b.HasIndex("Origin", "Status") + .HasDatabaseName("IX_runs_origin_status"); + + b.HasIndex("ParentRunId", "SubtaskId") + .HasDatabaseName("IX_runs_parent_subtask"); + + b.HasIndex("ProjectId", "Status") + .HasDatabaseName("IX_runs_project_status"); + + b.ToTable("runs", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunRevisionRecord", b => + { + b.Property("RunId") + .HasColumnType("text") + .HasColumnName("run_id"); + + b.Property("RevisionNumber") + .HasColumnType("integer") + .HasColumnName("revision_number"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("PreviousTreeHash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("previous_tree_hash"); + + b.Property("RawComment") + .IsRequired() + .HasColumnType("text") + .HasColumnName("raw_comment"); + + b.Property("ReviewerUser") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reviewer_user"); + + b.Property("SanitizedComment") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sanitized_comment"); + + b.HasKey("RunId", "RevisionNumber"); + + b.ToTable("run_revisions", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SessionContext", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveIssues") + .HasColumnType("text"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FocusArea") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SerializedState") + .HasColumnType("text"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Summary") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId", "EndedAt"); + + b.HasIndex("ProjectId", "SessionId") + .IsUnique(); + + b.ToTable("SessionContexts"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SkillAssignmentRecord", b => + { + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("SkillId") + .HasColumnType("text") + .HasColumnName("skill_id"); + + b.Property("AgentName") + .HasColumnType("text") + .HasColumnName("agent_name"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.HasKey("ProjectId", "SkillId", "AgentName"); + + b.HasIndex("ProjectId", "AgentName") + .HasDatabaseName("IX_skill_assignments_agent"); + + b.ToTable("skill_assignments", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SkillMarketplaceSourceRecord", b => + { + b.Property("SourceId") + .HasColumnType("text") + .HasColumnName("source_id"); + + b.Property("Branch") + .HasColumnType("text") + .HasColumnName("branch"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ParseStrategy") + .HasColumnType("text") + .HasColumnName("parse_strategy"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Repository") + .IsRequired() + .HasColumnType("text") + .HasColumnName("repository"); + + b.Property("Subpath") + .HasColumnType("text") + .HasColumnName("subpath"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("SourceId"); + + b.HasIndex("ProjectId"); + + b.ToTable("skill_marketplace_sources", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SkillRecord", b => + { + b.Property("SkillId") + .HasColumnType("text") + .HasColumnName("skill_id"); + + b.Property("ContentHash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content_hash"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Instructions") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instructions"); + + b.Property("MarketplaceName") + .HasColumnType("text") + .HasColumnName("marketplace_name"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Provenance") + .IsRequired() + .HasColumnType("text") + .HasColumnName("provenance"); + + b.Property("Resources") + .HasColumnType("text") + .HasColumnName("resources"); + + b.Property("SourceLocation") + .HasColumnType("text") + .HasColumnName("source_location"); + + b.Property("SourceRepository") + .HasColumnType("text") + .HasColumnName("source_repository"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("active") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("SkillId"); + + b.HasAlternateKey("ProjectId", "SkillId") + .HasName("AK_skills_project_id_skill_id"); + + b.ToTable("skills", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SteeringDirective", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionAttempt") + .HasColumnType("integer"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("DecidedAction") + .HasColumnType("text"); + + b.Property("ExecStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutionAttempts") + .HasColumnType("integer"); + + b.Property("Instruction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("text"); + + b.Property("RelayedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Severity") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetChildRunId") + .HasColumnType("text"); + + b.Property("TargetScopeJson") + .HasColumnType("text"); + + b.Property("TreeHash") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CoordinatorRunId", "Status"); + + b.ToTable("SteeringDirectives"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SteeringRevisionExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionAttempt") + .HasColumnType("integer"); + + b.Property("CheckpointWatermark") + .HasColumnType("integer"); + + b.Property("ConfirmedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectState") + .IsRequired() + .HasColumnType("text"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SteeringDirectiveId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SteeringDirectiveId", "ActionAttempt", "RunId") + .IsUnique(); + + b.ToTable("SteeringRevisionExecutions"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Subtask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentCharter") + .HasColumnType("text"); + + b.Property("AssignedAgent") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChildRunId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeclaredOutputPathsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("InfrastructureRetryCount") + .HasColumnType("integer"); + + b.Property("InfrastructureRetryEligibleAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsolationStrategy") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastResetAttempt") + .HasColumnType("integer"); + + b.Property("LastResetDirectiveId") + .HasColumnType("integer"); + + b.Property("LockedOutAgents") + .HasColumnType("text"); + + b.Property("Phase") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriorChildRunId") + .HasColumnType("text"); + + b.Property("RecoveryAttempts") + .HasColumnType("integer"); + + b.Property("RecoveryGuidance") + .HasColumnType("text"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.Property("SelectedModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("SteeringRetentionUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkPlanId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorkPlanId"); + + b.ToTable("Subtasks"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SubtaskDependency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DependsOnSubtaskId") + .HasColumnType("integer"); + + b.Property("SubtaskId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DependsOnSubtaskId"); + + b.HasIndex("SubtaskId"); + + b.ToTable("SubtaskDependencies"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.WorkPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssemblyStage") + .HasColumnType("text"); + + b.Property("AssemblyStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssemblyStatusReason") + .HasColumnType("text"); + + b.Property("AssemblyTerminalStage") + .HasColumnType("text"); + + b.Property("CoordinatorPodId") + .HasColumnType("text"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HumanReviewRoundTrips") + .HasColumnType("integer"); + + b.Property("IntegrationBranch") + .HasColumnType("text"); + + b.Property("IsolationSummary") + .HasColumnType("text"); + + b.Property("OutcomeSpecId") + .HasColumnType("integer"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("SteeringIterations") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkflowId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CoordinatorRunId"); + + b.HasIndex("OutcomeSpecId"); + + b.ToTable("WorkPlans"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.WorkflowCheckpointRecord", b => + { + b.Property("StoreName") + .HasColumnType("text") + .HasColumnName("store_name"); + + b.Property("SessionId") + .HasColumnType("text") + .HasColumnName("session_id"); + + b.Property("CheckpointId") + .HasColumnType("text") + .HasColumnName("checkpoint_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("HasParentMetadata") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("has_parent_metadata"); + + b.Property("ParentCheckpointId") + .HasColumnType("text") + .HasColumnName("parent_checkpoint_id"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("StoreName", "SessionId", "CheckpointId"); + + b.HasIndex("StoreName", "SessionId") + .HasDatabaseName("IX_workflow_checkpoints_store_session"); + + b.ToTable("workflow_checkpoints", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.WorkflowRunRecord", b => + { + b.Property("WorkflowRunId") + .HasColumnType("text") + .HasColumnName("workflow_run_id"); + + b.Property("OrchestrationWorktreePath") + .HasColumnType("text") + .HasColumnName("orchestration_worktree_path"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("SubmittingUser") + .IsRequired() + .HasColumnType("text") + .HasColumnName("submitting_user"); + + b.Property("Task") + .IsRequired() + .HasColumnType("text") + .HasColumnName("task"); + + b.HasKey("WorkflowRunId"); + + b.HasIndex("ProjectId") + .HasDatabaseName("IX_workflow_runs_project_id"); + + b.ToTable("workflow_runs", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Runs.CoordinatorDeferredDecisionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DecisionJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("DeferredDecisions"); + }); + + modelBuilder.Entity("Agentweaver.Api.Runs.PendingRequestRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerUser") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequestJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("PendingRequests"); + }); + + modelBuilder.Entity("Agentweaver.Api.Runs.RunEventRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("text"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sequence") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RunId"); + + b.HasIndex("RunId", "Sequence") + .IsUnique(); + + b.ToTable("RunEvents"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_activations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.AutomationActivationRecord", null) + .WithMany() + .HasForeignKey("ActivationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_activations_activation_id"); + + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Decision", b => + { + b.HasOne("Agentweaver.Api.Memory.Decision", null) + .WithMany() + .HasForeignKey("SupersededById"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.DecisionInboxEntry", b => + { + b.HasOne("Agentweaver.Api.Memory.Decision", null) + .WithMany() + .HasForeignKey("DecisionId"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_authorizations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_installations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_github_repository_grants_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_project_copilot_bindings_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_run_github_identity_snapshots_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SkillAssignmentRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_skill_assignments_projects_project_id"); + + b.HasOne("Agentweaver.Api.Memory.SkillRecord", null) + .WithMany() + .HasForeignKey("ProjectId", "SkillId") + .HasPrincipalKey("ProjectId", "SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_skill_assignments_skills_project_id_skill_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SkillMarketplaceSourceRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_skill_marketplace_sources_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SkillRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_skills_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Subtask", b => + { + b.HasOne("Agentweaver.Api.Memory.WorkPlan", null) + .WithMany() + .HasForeignKey("WorkPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SubtaskDependency", b => + { + b.HasOne("Agentweaver.Api.Memory.Subtask", null) + .WithMany() + .HasForeignKey("DependsOnSubtaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Agentweaver.Api.Memory.Subtask", null) + .WithMany() + .HasForeignKey("SubtaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.WorkPlan", b => + { + b.HasOne("Agentweaver.Api.Memory.OutcomeSpec", null) + .WithMany() + .HasForeignKey("OutcomeSpecId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/Agentweaver.Api.Migrations.Postgres/Migrations/20260827112924_AddTwoAppPersistence.cs b/apps/Agentweaver.Api.Migrations.Postgres/Migrations/20260827112924_AddTwoAppPersistence.cs new file mode 100644 index 000000000..012226dce --- /dev/null +++ b/apps/Agentweaver.Api.Migrations.Postgres/Migrations/20260827112924_AddTwoAppPersistence.cs @@ -0,0 +1,96 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Agentweaver.Api.Migrations.Postgres.Migrations; + +public partial class AddTwoAppPersistence : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + CREATE TABLE github_authorizations ( + state text PRIMARY KEY, app_kind integer NOT NULL, purpose integer NOT NULL, + entra_object_id text NOT NULL, project_id text NULL REFERENCES projects(project_id) ON DELETE CASCADE, + expires_at_unix_ms bigint NOT NULL, return_route_key text NOT NULL, + pkce_verifier_protected text NOT NULL, callback_cookie_hash text NOT NULL, + status integer NOT NULL, created_at timestamp with time zone NOT NULL, + completed_at timestamp with time zone NULL); + CREATE INDEX "IX_github_authorizations_entra_object_id_state" ON github_authorizations(entra_object_id, state); + CREATE INDEX "IX_github_authorizations_expires_at_unix_ms" ON github_authorizations(expires_at_unix_ms); + + CREATE TABLE github_app_authorizations ( + id text PRIMARY KEY, entra_object_id text NOT NULL, app_kind integer NOT NULL, purpose integer NOT NULL, + credential_reference text NOT NULL, credential_version text NOT NULL, grant_digest text NOT NULL, + created_at timestamp with time zone NOT NULL, revoked_at timestamp with time zone NULL); + CREATE INDEX "IX_github_app_authorizations_entra_object_id_app_kind_purpose" + ON github_app_authorizations(entra_object_id, app_kind, purpose); + + CREATE TABLE github_installations ( + installation_id bigint PRIMARY KEY, app_kind integer NOT NULL, + project_id text NULL REFERENCES projects(project_id) ON DELETE CASCADE, + created_at timestamp with time zone NOT NULL, revoked_at timestamp with time zone NULL); + + CREATE TABLE github_repository_grants ( + installation_id bigint NOT NULL REFERENCES github_installations(installation_id) ON DELETE CASCADE, + repository_id bigint NOT NULL, project_id text NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, + full_name_display text NOT NULL, permission_digest text NOT NULL, + granted_at timestamp with time zone NOT NULL, revoked_at timestamp with time zone NULL, + PRIMARY KEY(installation_id, repository_id)); + + CREATE TABLE project_copilot_bindings ( + id text PRIMARY KEY, project_id text NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, + entra_object_id text NOT NULL, credential_reference text NOT NULL, credential_version text NOT NULL, + grant_digest text NOT NULL, status integer NOT NULL, bound_at timestamp with time zone NOT NULL, + deactivated_at timestamp with time zone NULL); + CREATE UNIQUE INDEX "UX_project_copilot_bindings_active_project" + ON project_copilot_bindings(project_id) WHERE status = 0; + + CREATE TABLE automation_activations ( + id text PRIMARY KEY, project_id text NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, + installation_id bigint NOT NULL, repository_id bigint NOT NULL, automation_key text NOT NULL, + status integer NOT NULL, activated_at timestamp with time zone NOT NULL, + invalidated_at timestamp with time zone NULL, + UNIQUE(project_id, installation_id, repository_id, automation_key), + FOREIGN KEY(installation_id, repository_id) + REFERENCES github_repository_grants(installation_id, repository_id) ON DELETE CASCADE); + + CREATE TABLE automation_invocations ( + id text PRIMARY KEY, project_id text NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, + activation_id text NOT NULL REFERENCES automation_activations(id) ON DELETE CASCADE, + occurrence_key text NOT NULL, delivery_id text NULL, event_name text NULL, + installation_id bigint NULL, repository_id bigint NULL, outcome integer NOT NULL, + received_at timestamp with time zone NOT NULL, completed_at timestamp with time zone NULL, + UNIQUE(activation_id, occurrence_key), UNIQUE(delivery_id, event_name)); + + CREATE TABLE run_github_identity_snapshots ( + run_id text PRIMARY KEY, project_id text NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, + app_kind integer NOT NULL, purpose integer NOT NULL, credential_reference text NOT NULL, + credential_version text NOT NULL, grant_digest text NOT NULL, installation_id bigint NULL, + repository_id bigint NULL, entra_object_id text NULL, captured_at timestamp with time zone NOT NULL); + + CREATE TABLE github_audit_records ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, entra_object_id text NULL, + actor_kind integer NOT NULL CHECK (actor_kind IN (0, 1)), action integer NOT NULL, + resource_id text NOT NULL, app_kind integer NULL, purpose integer NULL, outcome integer NOT NULL, + reason_code integer NOT NULL, correlation_id text NOT NULL, + occurred_at timestamp with time zone NOT NULL, credential_version_or_digest text NULL); + CREATE INDEX "IX_github_audit_records_occurred_at" ON github_audit_records(occurred_at); + """); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + DROP TABLE IF EXISTS github_audit_records; + DROP TABLE IF EXISTS run_github_identity_snapshots; + DROP TABLE IF EXISTS automation_invocations; + DROP TABLE IF EXISTS automation_activations; + DROP TABLE IF EXISTS project_copilot_bindings; + DROP TABLE IF EXISTS github_repository_grants; + DROP TABLE IF EXISTS github_installations; + DROP TABLE IF EXISTS github_app_authorizations; + DROP TABLE IF EXISTS github_authorizations; + """); + } +} diff --git a/apps/Agentweaver.Api.Migrations.Postgres/Migrations/MemoryDbContextModelSnapshot.cs b/apps/Agentweaver.Api.Migrations.Postgres/Migrations/MemoryDbContextModelSnapshot.cs index d8d783826..f689e1e80 100644 --- a/apps/Agentweaver.Api.Migrations.Postgres/Migrations/MemoryDbContextModelSnapshot.cs +++ b/apps/Agentweaver.Api.Migrations.Postgres/Migrations/MemoryDbContextModelSnapshot.cs @@ -22,6 +22,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.EntraOAuthState", b => + { + b.Property("State") + .HasColumnType("text"); + + b.Property("CodeVerifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("EntraOAuthStates"); + }); + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpAuthorizationCode", b => { b.Property("Id") @@ -238,25 +257,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("McpRevokedJtis"); }); - modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.EntraOAuthState", b => - { - b.Property("State") - .HasColumnType("text"); - - b.Property("CodeVerifier") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("State"); - - b.HasIndex("ExpiresAt"); - - b.ToTable("EntraOAuthStates"); - }); - modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.OAuthState", b => { b.Property("State") @@ -449,29 +449,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AgentMemory"); }); - modelBuilder.Entity("Agentweaver.Api.Memory.RunAuthorshipCapability", b => - { - b.Property("RunId") - .HasMaxLength(128) - .HasColumnType("character varying(128)") - .HasColumnName("run_id"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("TokenHash") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("token_hash"); - - b.HasKey("RunId"); - - b.HasIndex("ExpiresAt"); - - b.ToTable("run_authorship_capabilities", (string)null); - }); - modelBuilder.Entity("Agentweaver.Api.Memory.AuthModeEpochRecord", b => { b.Property("Key") @@ -496,6 +473,114 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("auth_mode_epochs", (string)null); }); + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("AutomationKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("automation_key"); + + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("InvalidatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("invalidated_at"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId", "RepositoryId"); + + b.HasIndex("ProjectId", "InstallationId", "RepositoryId", "AutomationKey") + .IsUnique(); + + b.ToTable("automation_activations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("ActivationId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("activation_id"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("DeliveryId") + .HasColumnType("text") + .HasColumnName("delivery_id"); + + b.Property("EventName") + .HasColumnType("text") + .HasColumnName("event_name"); + + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("OccurrenceKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("occurrence_key"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("received_at"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ActivationId", "OccurrenceKey") + .IsUnique(); + + b.HasIndex("DeliveryId", "EventName") + .IsUnique(); + + b.ToTable("automation_invocations", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.BacklogTaskDependencyRecord", b => { b.Property("TaskId") @@ -1020,6 +1105,255 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("github_account_link_states", (string)null); }); + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAppAuthorizationRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("grant_digest"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.HasKey("Id"); + + b.HasIndex("EntraObjectId", "AppKind", "Purpose"); + + b.ToTable("github_app_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasColumnName("action"); + + b.Property("ActorKind") + .HasColumnType("integer") + .HasColumnName("actor_kind"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("CredentialVersionOrDigest") + .HasColumnType("text") + .HasColumnName("credential_version_or_digest"); + + b.Property("EntraObjectId") + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("occurred_at"); + + b.Property("Outcome") + .HasColumnType("integer") + .HasColumnName("outcome"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("ReasonCode") + .HasColumnType("integer") + .HasColumnName("reason_code"); + + b.Property("ResourceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("resource_id"); + + b.HasKey("Id"); + + b.HasIndex("OccurredAt"); + + b.ToTable("github_audit_records", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.Property("State") + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CallbackCookieHash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("callback_cookie_hash"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("ExpiresAtUnixMilliseconds") + .HasColumnType("bigint") + .HasColumnName("expires_at_unix_ms"); + + b.Property("PkceVerifierProtected") + .IsRequired() + .HasColumnType("text") + .HasColumnName("pkce_verifier_protected"); + + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("ReturnRouteKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("return_route_key"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAtUnixMilliseconds"); + + b.HasIndex("ProjectId"); + + b.HasIndex("EntraObjectId", "State") + .IsUnique(); + + b.ToTable("github_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ProjectId") + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId"); + + b.HasIndex("ProjectId"); + + b.ToTable("github_installations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.Property("FullNameDisplay") + .IsRequired() + .HasColumnType("text") + .HasColumnName("full_name_display"); + + b.Property("GrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("granted_at"); + + b.Property("PermissionDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("permission_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId", "RepositoryId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("InstallationId", "RepositoryId") + .IsUnique(); + + b.ToTable("github_repository_grants", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.IntegrationBuildLockRecord", b => { b.Property("ProjectId") @@ -1099,6 +1433,59 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OutcomeSpecs"); }); + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("BoundAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("bound_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_version"); + + b.Property("DeactivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deactivated_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("grant_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId") + .IsUnique() + .HasDatabaseName("UX_project_copilot_bindings_active_project") + .HasFilter("status = 0"); + + b.ToTable("project_copilot_bindings", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectGitHubIdentityOverrideRecord", b => { b.Property("ProjectId") @@ -1304,6 +1691,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("project_role_assignments", (string)null); }); + modelBuilder.Entity("Agentweaver.Api.Memory.RunAuthorshipCapability", b => + { + b.Property("RunId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("run_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("token_hash"); + + b.HasKey("RunId"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("run_authorship_capabilities", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.Property("RunId") + .HasColumnType("text") + .HasColumnName("run_id"); + + b.Property("AppKind") + .HasColumnType("integer") + .HasColumnName("app_kind"); + + b.Property("CapturedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("captured_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .HasColumnType("text") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("text") + .HasColumnName("grant_digest"); + + b.Property("InstallationId") + .HasColumnType("bigint") + .HasColumnName("installation_id"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("integer") + .HasColumnName("purpose"); + + b.Property("RepositoryId") + .HasColumnType("bigint") + .HasColumnName("repository_id"); + + b.HasKey("RunId"); + + b.HasIndex("ProjectId"); + + b.ToTable("run_github_identity_snapshots", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.RunRecord", b => { b.Property("RunId") @@ -2193,6 +2660,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RunEvents"); }); + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_activations_projects_project_id"); + + b.HasOne("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", null) + .WithMany() + .HasForeignKey("InstallationId", "RepositoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_activations_repository_grants_installation_id_repository_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.AutomationActivationRecord", null) + .WithMany() + .HasForeignKey("ActivationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_activations_activation_id"); + + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_projects_project_id"); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.Decision", b => { b.HasOne("Agentweaver.Api.Memory.Decision", null) @@ -2207,6 +2708,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("DecisionId"); }); + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_authorizations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_installations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.GitHubInstallationRecord", null) + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_github_repository_grants_installations_installation_id"); + + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_github_repository_grants_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_project_copilot_bindings_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_run_github_identity_snapshots_projects_project_id"); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.SkillAssignmentRecord", b => { b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) diff --git a/apps/Agentweaver.Api/Auth/KeyVaultGitHubTokenStore.cs b/apps/Agentweaver.Api/Auth/KeyVaultGitHubTokenStore.cs index ba0813cb9..7035100e3 100644 --- a/apps/Agentweaver.Api/Auth/KeyVaultGitHubTokenStore.cs +++ b/apps/Agentweaver.Api/Auth/KeyVaultGitHubTokenStore.cs @@ -23,26 +23,20 @@ namespace Agentweaver.Api.Auth; /// is provided, the on-disk token is lazily read and written through to KV. /// A KV tombstone (signed-out) always wins over any disk value. /// -/// Disk mirror: after every successful SetAsync, the token is also written to -/// so pods that read the shared filesystem file remain -/// functional in phase-1 (before they are updated to call the API). /// public sealed class KeyVaultGitHubTokenStore : IMultiIdentityGitHubTokenStore, IDistributedGitHubTokenRefreshLeaseStore, IGitHubTokenScopeEnumerable { private readonly ISecretStore _secretStore; private readonly FileSystemGitHubTokenStore? _diskFallback; // lazy migration source - private readonly FileSystemGitHubTokenStore? _diskMirror; // post-write mirror private static readonly JsonSerializerOptions _json = new() { WriteIndented = false }; public KeyVaultGitHubTokenStore( ISecretStore secretStore, - FileSystemGitHubTokenStore? diskFallback = null, - FileSystemGitHubTokenStore? diskMirror = null) + FileSystemGitHubTokenStore? diskFallback = null) { _secretStore = secretStore; _diskFallback = diskFallback; - _diskMirror = diskMirror; } // ── IGitHubTokenStore ──────────────────────────────────────────────────── @@ -116,12 +110,6 @@ public async Task SetAsync(GitHubTokenScope scope, GitHubToken token, Cancellati return; } - // Mirror to disk so pods reading the shared filesystem file still work. - if (_diskMirror is not null) - { - try { await _diskMirror.SetAsync(scope, token, ct).ConfigureAwait(false); } - catch (Exception) { /* best effort */ } - } } public async Task GetIdentityAsync(GitHubTokenScope scope, CancellationToken ct = default) @@ -149,11 +137,6 @@ public async Task SignOutAsync(GitHubTokenScope scope, CancellationToken ct = de // No ETag check for sign-out: the tombstone must always win. await _secretStore.SetSecretAsync(scope.Key, json, etag: null, ct).ConfigureAwait(false); - if (_diskMirror is not null) - { - try { await _diskMirror.SignOutAsync(scope, ct).ConfigureAwait(false); } - catch (Exception) { /* best effort */ } - } } public async Task> ListLinkedIdentitiesAsync( diff --git a/apps/Agentweaver.Api/Auth/TwoAppPersistenceStore.cs b/apps/Agentweaver.Api/Auth/TwoAppPersistenceStore.cs new file mode 100644 index 000000000..6c2fe5b46 --- /dev/null +++ b/apps/Agentweaver.Api/Auth/TwoAppPersistenceStore.cs @@ -0,0 +1,178 @@ +using Agentweaver.Api.Memory; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Npgsql; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Agentweaver.Api.Auth; + +public enum AuthorizationClaimResult { Claimed, Invalid, Consumed } +public enum BindingWriteResult { Bound, Unavailable } +public enum InvocationClaimResult { Claimed, Duplicate } + +/// +/// Persistence boundary for the two GitHub App model. It accepts only opaque credential +/// references and exposes guarded state transitions rather than mutable entity access. +/// +public sealed class TwoAppPersistenceStore(MemoryDbContext db) +{ + private static readonly Regex CredentialPattern = new( + @"(?:gh[ups]_|github_pat_|-----BEGIN [A-Z ]+-----|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + public async Task AddAuthorizationAsync(GitHubAuthorizationRecord authorization, CancellationToken ct = default) + { + EnsureSafe(authorization); + db.GitHubAuthorizations.Add(authorization); + await db.SaveChangesAsync(ct).ConfigureAwait(false); + } + + public async Task AddAppAuthorizationAsync(GitHubAppAuthorizationRecord authorization, CancellationToken ct = default) + { + EnsureSafe(authorization); + db.GitHubAppAuthorizations.Add(authorization); + await db.SaveChangesAsync(ct).ConfigureAwait(false); + } + + public async Task ClaimAuthorizationAsync( + string state, + string entraObjectId, + DateTimeOffset now, + CancellationToken ct = default) + { + var changed = await db.GitHubAuthorizations + .Where(x => x.State == state && + x.EntraObjectId == entraObjectId && + x.Status == GitHubAuthorizationStatus.Pending && + x.ExpiresAtUnixMilliseconds >= now.ToUnixTimeMilliseconds()) + .ExecuteUpdateAsync(s => s + .SetProperty(x => x.Status, GitHubAuthorizationStatus.Redeeming), ct) + .ConfigureAwait(false); + if (changed == 1) + return AuthorizationClaimResult.Claimed; + + var status = await db.GitHubAuthorizations.AsNoTracking() + .Where(x => x.State == state && x.EntraObjectId == entraObjectId) + .Select(x => (GitHubAuthorizationStatus?)x.Status) + .SingleOrDefaultAsync(ct) + .ConfigureAwait(false); + return status is GitHubAuthorizationStatus.Redeeming or GitHubAuthorizationStatus.Completed or GitHubAuthorizationStatus.Failed + ? AuthorizationClaimResult.Consumed + : AuthorizationClaimResult.Invalid; + } + + public Task CompleteAuthorizationAsync( + string state, + bool succeeded, + CancellationToken ct = default) => + db.GitHubAuthorizations + .Where(x => x.State == state && x.Status == GitHubAuthorizationStatus.Redeeming) + .ExecuteUpdateAsync(s => s + .SetProperty(x => x.Status, succeeded ? GitHubAuthorizationStatus.Completed : GitHubAuthorizationStatus.Failed) + .SetProperty(x => x.CompletedAt, DateTimeOffset.UtcNow), ct); + + public async Task ReplaceCopilotBindingAsync( + ProjectCopilotBindingRecord binding, + CancellationToken ct = default) + { + EnsureSafe(binding); + var now = DateTimeOffset.UtcNow; + await using var transaction = await db.Database.BeginTransactionAsync(ct).ConfigureAwait(false); + await db.ProjectCopilotBindings + .Where(x => x.ProjectId == binding.ProjectId && x.Status == GitHubBindingStatus.Active) + .ExecuteUpdateAsync(s => s + .SetProperty(x => x.Status, GitHubBindingStatus.Inactive) + .SetProperty(x => x.DeactivatedAt, now), ct) + .ConfigureAwait(false); + db.ChangeTracker.Clear(); + + db.ProjectCopilotBindings.Add(binding); + try + { + await db.SaveChangesAsync(ct).ConfigureAwait(false); + await transaction.CommitAsync(ct).ConfigureAwait(false); + return BindingWriteResult.Bound; + } + catch (DbUpdateException ex) when (IsUniqueViolation(ex)) + { + await transaction.RollbackAsync(CancellationToken.None).ConfigureAwait(false); + db.ChangeTracker.Clear(); + return BindingWriteResult.Unavailable; + } + } + + public async Task ClaimInvocationAsync( + AutomationInvocationRecord invocation, + CancellationToken ct = default) + { + EnsureSafe(invocation); + db.AutomationInvocations.Add(invocation); + try + { + await db.SaveChangesAsync(ct).ConfigureAwait(false); + return InvocationClaimResult.Claimed; + } + catch (DbUpdateException ex) when (IsUniqueViolation(ex)) + { + db.ChangeTracker.Clear(); + return InvocationClaimResult.Duplicate; + } + } + + public async Task AddRunIdentitySnapshotAsync( + RunGitHubIdentitySnapshotRecord snapshot, + CancellationToken ct = default) + { + EnsureSafe(snapshot); + db.RunGitHubIdentitySnapshots.Add(snapshot); + try + { + await db.SaveChangesAsync(ct).ConfigureAwait(false); + return true; + } + catch (DbUpdateException ex) when (IsUniqueViolation(ex)) + { + db.ChangeTracker.Clear(); + return false; + } + } + + public async Task HasPinnedSnapshotVersionAsync( + string runId, + string credentialVersion, + CancellationToken ct = default) => + await db.RunGitHubIdentitySnapshots.AsNoTracking() + .AnyAsync(x => x.RunId == runId && x.CredentialVersion == credentialVersion, ct) + .ConfigureAwait(false); + + public async Task AppendAuditAsync(GitHubAuditRecord audit, CancellationToken ct = default) + { + EnsureSafe(audit); + if (audit.ActorKind == GitHubAuditActorKind.HumanEntraSubject && string.IsNullOrWhiteSpace(audit.EntraObjectId)) + throw new ArgumentException("Human audit records require an Entra subject.", nameof(audit)); + if (audit.ActorKind == GitHubAuditActorKind.GitHubWebhook && audit.EntraObjectId is not null) + throw new ArgumentException("Webhook audit records cannot carry an Entra subject.", nameof(audit)); + + db.GitHubAuditRecords.Add(audit); + await db.SaveChangesAsync(ct).ConfigureAwait(false); + } + + private static bool IsUniqueViolation(DbUpdateException exception) => + exception.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation } || + exception.InnerException is SqliteException + { + SqliteErrorCode: 19, + SqliteExtendedErrorCode: 1555 or 2067 + }; + + private static void EnsureSafe(object record) + { + foreach (var property in record.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.PropertyType == typeof(string))) + { + if (property.GetValue(record) is string value && CredentialPattern.IsMatch(value)) + throw new ArgumentException("Two-App persistence accepts only redacted credential references and metadata.", property.Name); + } + } +} diff --git a/apps/Agentweaver.Api/MemoryDbContextDesignFactory.cs b/apps/Agentweaver.Api/MemoryDbContextDesignFactory.cs new file mode 100644 index 000000000..75c30d868 --- /dev/null +++ b/apps/Agentweaver.Api/MemoryDbContextDesignFactory.cs @@ -0,0 +1,25 @@ +using Agentweaver.Api.Memory; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Agentweaver.Api; + +public sealed class MemoryDbContextDesignFactory : IDesignTimeDbContextFactory +{ + public MemoryDbContext CreateDbContext(string[] args) + { + var options = new DbContextOptionsBuilder(); + if (args.Contains("--postgres-migrations", StringComparer.Ordinal)) + { + options.UseNpgsql( + "Host=localhost;Database=agentweaver_design;Username=postgres;Password=postgres", + npg => npg.MigrationsAssembly("Agentweaver.Api.Migrations.Postgres")); + } + else + { + options.UseSqlite("Data Source=agentweaver-design.db", sqlite => sqlite.MigrationsAssembly("Agentweaver.Api")); + } + + return new MemoryDbContext(options.Options); + } +} diff --git a/apps/Agentweaver.Api/Migrations/20260827115552_AddTwoAppPersistence.Designer.cs b/apps/Agentweaver.Api/Migrations/20260827115552_AddTwoAppPersistence.Designer.cs new file mode 100644 index 000000000..3eb2d8730 --- /dev/null +++ b/apps/Agentweaver.Api/Migrations/20260827115552_AddTwoAppPersistence.Designer.cs @@ -0,0 +1,1896 @@ +// +using System; +using Agentweaver.Api.Memory; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Agentweaver.Api.Migrations +{ + [DbContext(typeof(MemoryDbContext))] + [Migration("20260827115552_AddTwoAppPersistence")] + partial class AddTwoAppPersistence + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.7"); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.EntraOAuthState", b => + { + b.Property("State") + .HasColumnType("TEXT"); + + b.Property("CodeVerifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("EntraOAuthStates"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpAuthorizationCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodeChallenge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("GithubLogin") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RedirectUri") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ExpiresAt"); + + b.ToTable("McpAuthorizationCodes"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpClientRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClientName") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RedirectUris") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("McpClientRegistrations"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpPendingAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClientState") + .HasColumnType("TEXT"); + + b.Property("CodeChallenge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("RedirectUri") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Resource") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("State") + .IsUnique(); + + b.ToTable("McpPendingAuthorizations"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConsumedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("GithubLogin") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Org") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChainId"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("Subject", "ClientId"); + + b.ToTable("McpRefreshTokens"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.McpRevokedJti", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Jti") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("Jti") + .IsUnique(); + + b.ToTable("McpRevokedJtis"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.OAuthState", b => + { + b.Property("State") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("OAuthStates"); + }); + + modelBuilder.Entity("Agentweaver.Api.Auth.OAuth.WebSessionExchangeCode", b => + { + b.Property("Code") + .HasColumnType("TEXT"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Login") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Code"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("WebSessionExchangeCodes"); + }); + + modelBuilder.Entity("Agentweaver.Api.Coordinator.CoordinatorAssemblyReviewRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AggregateTreeHash") + .HasColumnType("TEXT"); + + b.Property("CoordinatorFailedAt") + .HasColumnType("TEXT"); + + b.Property("CoordinatorFailureReason") + .HasColumnType("TEXT"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DecisionJson") + .HasColumnType("TEXT"); + + b.Property("DecisionSubmittedAt") + .HasColumnType("TEXT"); + + b.Property("IntegrationBranch") + .HasColumnType("TEXT"); + + b.Property("OwnerUser") + .HasColumnType("TEXT"); + + b.Property("Reviewer") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CoordinatorRunId") + .IsUnique(); + + b.ToTable("AssemblyReviews"); + }); + + modelBuilder.Entity("Agentweaver.Api.Diagnostics.HeartbeatStatusRecord", b => + { + b.Property("PodName") + .HasColumnType("TEXT"); + + b.Property("ActedCount") + .HasColumnType("INTEGER"); + + b.Property("DurationMs") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ErrorCount") + .HasColumnType("INTEGER"); + + b.Property("IntervalSeconds") + .HasColumnType("INTEGER"); + + b.Property("LastTickUtc") + .HasColumnType("TEXT"); + + b.HasKey("PodName"); + + b.ToTable("HeartbeatStatuses"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AgentMemory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgentName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ApprovedAt") + .HasColumnType("TEXT"); + + b.Property("ApprovedBy") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Importance") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasColumnType("TEXT"); + + b.Property("SourceIdentity") + .HasColumnType("TEXT"); + + b.Property("SourceKind") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("legacy"); + + b.Property("SourceRunId") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TrustState") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("legacy"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId", "AgentName"); + + b.HasIndex("ProjectId", "Type"); + + b.ToTable("AgentMemory"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AuthModeEpochRecord", b => + { + b.Property("Key") + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("AuthMode") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("auth_mode"); + + b.Property("Epoch") + .HasColumnType("INTEGER") + .HasColumnName("epoch"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Key"); + + b.ToTable("auth_mode_epochs", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("TEXT") + .HasColumnName("activated_at"); + + b.Property("AutomationKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("automation_key"); + + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("InvalidatedAt") + .HasColumnType("TEXT") + .HasColumnName("invalidated_at"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId", "RepositoryId"); + + b.HasIndex("ProjectId", "InstallationId", "RepositoryId", "AutomationKey") + .IsUnique(); + + b.ToTable("automation_activations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ActivationId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("activation_id"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("DeliveryId") + .HasColumnType("TEXT") + .HasColumnName("delivery_id"); + + b.Property("EventName") + .HasColumnType("TEXT") + .HasColumnName("event_name"); + + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("OccurrenceKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("occurrence_key"); + + b.Property("Outcome") + .HasColumnType("INTEGER") + .HasColumnName("outcome"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("ReceivedAt") + .HasColumnType("TEXT") + .HasColumnName("received_at"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ActivationId", "OccurrenceKey") + .IsUnique(); + + b.HasIndex("DeliveryId", "EventName") + .IsUnique(); + + b.ToTable("automation_invocations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Decision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgentName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ApprovedAt") + .HasColumnType("TEXT"); + + b.Property("ApprovedBy") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Rationale") + .HasColumnType("TEXT"); + + b.Property("SourceIdentity") + .HasColumnType("TEXT"); + + b.Property("SourceKind") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("legacy"); + + b.Property("SourceRunId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SupersededById") + .HasColumnType("INTEGER"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TrustState") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("legacy"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SupersededById"); + + b.HasIndex("ProjectId", "AgentName"); + + b.HasIndex("ProjectId", "Status"); + + b.ToTable("Decisions"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.DecisionInboxEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgentName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DecisionId") + .HasColumnType("INTEGER"); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Rationale") + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SourceIdentity") + .HasColumnType("TEXT"); + + b.Property("SourceKind") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("legacy"); + + b.Property("SourceRunId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DecisionId"); + + b.HasIndex("ProjectId", "Slug") + .IsUnique(); + + b.HasIndex("ProjectId", "Status"); + + b.ToTable("DecisionInbox"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.DismissedNotification", b => + { + b.Property("User") + .HasColumnType("TEXT") + .HasColumnName("user"); + + b.Property("NotificationId") + .HasColumnType("TEXT") + .HasColumnName("notification_id"); + + b.Property("DismissedAt") + .HasColumnType("TEXT") + .HasColumnName("dismissed_at"); + + b.HasKey("User", "NotificationId"); + + b.ToTable("dismissed_notifications", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAccountLinkStateRecord", b => + { + b.Property("State") + .HasColumnType("TEXT") + .HasColumnName("state"); + + b.Property("EntraUserId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("entra_user_id"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires_at"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("github_account_link_states", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAppAuthorizationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("grant_digest"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked_at"); + + b.HasKey("Id"); + + b.HasIndex("EntraObjectId", "AppKind", "Purpose"); + + b.ToTable("github_app_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Action") + .HasColumnType("INTEGER") + .HasColumnName("action"); + + b.Property("ActorKind") + .HasColumnType("INTEGER") + .HasColumnName("actor_kind"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("correlation_id"); + + b.Property("CredentialVersionOrDigest") + .HasColumnType("TEXT") + .HasColumnName("credential_version_or_digest"); + + b.Property("EntraObjectId") + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("OccurredAt") + .HasColumnType("TEXT") + .HasColumnName("occurred_at"); + + b.Property("Outcome") + .HasColumnType("INTEGER") + .HasColumnName("outcome"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("ReasonCode") + .HasColumnType("INTEGER") + .HasColumnName("reason_code"); + + b.Property("ResourceId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("resource_id"); + + b.HasKey("Id"); + + b.HasIndex("OccurredAt"); + + b.ToTable("github_audit_records", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.Property("State") + .HasColumnType("TEXT") + .HasColumnName("state"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CallbackCookieHash") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("callback_cookie_hash"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("ExpiresAtUnixMilliseconds") + .HasColumnType("INTEGER") + .HasColumnName("expires_at_unix_ms"); + + b.Property("PkceVerifierProtected") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("pkce_verifier_protected"); + + b.Property("ProjectId") + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("ReturnRouteKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("return_route_key"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasColumnName("status"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAtUnixMilliseconds"); + + b.HasIndex("ProjectId"); + + b.HasIndex("EntraObjectId", "State") + .IsUnique(); + + b.ToTable("github_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("ProjectId") + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId"); + + b.HasIndex("ProjectId"); + + b.ToTable("github_installations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.Property("FullNameDisplay") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("full_name_display"); + + b.Property("GrantedAt") + .HasColumnType("TEXT") + .HasColumnName("granted_at"); + + b.Property("PermissionDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("permission_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId", "RepositoryId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("InstallationId", "RepositoryId") + .IsUnique(); + + b.ToTable("github_repository_grants", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.IntegrationBuildLockRecord", b => + { + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("AcquiredAt") + .HasColumnType("TEXT"); + + b.Property("OwnerPodId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ProjectId"); + + b.ToTable("IntegrationBuildLocks"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.OutcomeSpec", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowTaskPromotion") + .HasColumnType("INTEGER"); + + b.Property("Assumptions") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClarifyingQuestions") + .HasColumnType("TEXT"); + + b.Property("ConfirmedBy") + .HasColumnType("TEXT"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DesiredOutcome") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Goal") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId", "CoordinatorRunId"); + + b.ToTable("OutcomeSpecs"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("BoundAt") + .HasColumnType("TEXT") + .HasColumnName("bound_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_version"); + + b.Property("DeactivatedAt") + .HasColumnType("TEXT") + .HasColumnName("deactivated_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("grant_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId") + .IsUnique() + .HasDatabaseName("UX_project_copilot_bindings_active_project") + .HasFilter("status = 0"); + + b.ToTable("project_copilot_bindings", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectGitHubIdentityOverrideRecord", b => + { + b.Property("ProjectId") + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("EntraUserId") + .HasColumnType("TEXT") + .HasColumnName("entra_user_id"); + + b.Property("GitHubLogin") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("github_login"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("ProjectId", "EntraUserId"); + + b.HasIndex("EntraUserId", "GitHubLogin") + .HasDatabaseName("IX_project_github_identity_overrides_user_login"); + + b.ToTable("project_github_identity_overrides", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectRecord", b => + { + b.Property("ProjectId") + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("ActiveReviewPolicyName") + .HasColumnType("TEXT"); + + b.Property("AllowedWorkflowIds") + .HasColumnType("TEXT"); + + b.Property("BlueprintGenerationModel") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DefaultBranch") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultModelCopilot") + .HasColumnType("TEXT"); + + b.Property("DefaultModelFoundry") + .HasColumnType("TEXT"); + + b.Property("DefaultProvider") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultWorkflowId") + .HasColumnType("TEXT"); + + b.Property("MaxReadyPerHeartbeat") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OriginKind") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OutcomeSpecGenerationModel") + .HasColumnType("TEXT"); + + b.Property("Owner") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PickupAutoApproveTools") + .HasColumnType("INTEGER"); + + b.Property("PickupAutopilot") + .HasColumnType("INTEGER"); + + b.Property("PreviewApprovalTimeoutMinutes") + .HasColumnType("INTEGER"); + + b.Property("SandboxProfile") + .HasColumnType("TEXT"); + + b.Property("SourceBlueprintId") + .HasColumnType("TEXT"); + + b.Property("SourceBlueprintType") + .HasColumnType("TEXT"); + + b.Property("SourceRepository") + .HasColumnType("TEXT"); + + b.Property("State") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TeamRevision") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WebhookSecret") + .HasColumnType("TEXT"); + + b.Property("WorkflowGenerationModel") + .HasColumnType("TEXT"); + + b.Property("WorkingDirectory") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ProjectId"); + + b.ToTable("projects", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunAuthorshipCapability", b => + { + b.Property("RunId") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("run_id"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires_at"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("token_hash"); + + b.HasKey("RunId"); + + b.HasIndex("ExpiresAt"); + + b.ToTable("run_authorship_capabilities", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.Property("RunId") + .HasColumnType("TEXT") + .HasColumnName("run_id"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CapturedAt") + .HasColumnType("TEXT") + .HasColumnName("captured_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("grant_digest"); + + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.HasKey("RunId"); + + b.HasIndex("ProjectId"); + + b.ToTable("run_github_identity_snapshots", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SessionContext", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActiveIssues") + .HasColumnType("TEXT"); + + b.Property("EndedAt") + .HasColumnType("TEXT"); + + b.Property("FocusArea") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SerializedState") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedAt") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId", "EndedAt"); + + b.HasIndex("ProjectId", "SessionId") + .IsUnique(); + + b.ToTable("SessionContexts"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SteeringDirective", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActionAttempt") + .HasColumnType("INTEGER"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DecidedAction") + .HasColumnType("TEXT"); + + b.Property("ExecStartedAt") + .HasColumnType("TEXT"); + + b.Property("ExecutionAttempts") + .HasColumnType("INTEGER"); + + b.Property("Instruction") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelayedAt") + .HasColumnType("TEXT"); + + b.Property("Severity") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TargetChildRunId") + .HasColumnType("TEXT"); + + b.Property("TargetScopeJson") + .HasColumnType("TEXT"); + + b.Property("TreeHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CoordinatorRunId", "Status"); + + b.ToTable("SteeringDirectives"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SteeringRevisionExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActionAttempt") + .HasColumnType("INTEGER"); + + b.Property("CheckpointWatermark") + .HasColumnType("INTEGER"); + + b.Property("ConfirmedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EffectState") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SteeringDirectiveId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SteeringDirectiveId", "ActionAttempt", "RunId") + .IsUnique(); + + b.ToTable("SteeringRevisionExecutions"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Subtask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgentCharter") + .HasColumnType("TEXT"); + + b.Property("AssignedAgent") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ChildRunId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeclaredOutputPathsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InfrastructureRetryCount") + .HasColumnType("INTEGER"); + + b.Property("InfrastructureRetryEligibleAt") + .HasColumnType("TEXT"); + + b.Property("IsolationStrategy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastResetAttempt") + .HasColumnType("INTEGER"); + + b.Property("LastResetDirectiveId") + .HasColumnType("INTEGER"); + + b.Property("LockedOutAgents") + .HasColumnType("TEXT"); + + b.Property("Phase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PriorChildRunId") + .HasColumnType("TEXT"); + + b.Property("RecoveryAttempts") + .HasColumnType("INTEGER"); + + b.Property("RecoveryGuidance") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SelectedModelId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SteeringRetentionUntil") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WorkPlanId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("WorkPlanId"); + + b.ToTable("Subtasks"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SubtaskDependency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DependsOnSubtaskId") + .HasColumnType("INTEGER"); + + b.Property("SubtaskId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DependsOnSubtaskId"); + + b.HasIndex("SubtaskId"); + + b.ToTable("SubtaskDependencies"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.WorkPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AssemblyStage") + .HasColumnType("TEXT"); + + b.Property("AssemblyStartedAt") + .HasColumnType("TEXT"); + + b.Property("AssemblyStatusReason") + .HasColumnType("TEXT"); + + b.Property("AssemblyTerminalStage") + .HasColumnType("TEXT"); + + b.Property("CoordinatorPodId") + .HasColumnType("TEXT"); + + b.Property("CoordinatorRunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("HumanReviewRoundTrips") + .HasColumnType("INTEGER"); + + b.Property("IntegrationBranch") + .HasColumnType("TEXT"); + + b.Property("IsolationSummary") + .HasColumnType("TEXT"); + + b.Property("OutcomeSpecId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SteeringIterations") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WorkflowId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CoordinatorRunId"); + + b.HasIndex("OutcomeSpecId"); + + b.ToTable("WorkPlans"); + }); + + modelBuilder.Entity("Agentweaver.Api.Runs.CoordinatorDeferredDecisionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DecisionJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("DeferredDecisions"); + }); + + modelBuilder.Entity("Agentweaver.Api.Runs.PendingRequestRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("OwnerUser") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RequestJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("RunId") + .IsUnique(); + + b.ToTable("PendingRequests"); + }); + + modelBuilder.Entity("Agentweaver.Api.Runs.RunEventRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Sequence") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RunId"); + + b.HasIndex("RunId", "Sequence") + .IsUnique(); + + b.ToTable("RunEvents"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_activations_projects_project_id"); + + b.HasOne("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", null) + .WithMany() + .HasForeignKey("InstallationId", "RepositoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_activations_repository_grants_installation_id_repository_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.AutomationActivationRecord", null) + .WithMany() + .HasForeignKey("ActivationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_activations_activation_id"); + + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Decision", b => + { + b.HasOne("Agentweaver.Api.Memory.Decision", null) + .WithMany() + .HasForeignKey("SupersededById"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.DecisionInboxEntry", b => + { + b.HasOne("Agentweaver.Api.Memory.Decision", null) + .WithMany() + .HasForeignKey("DecisionId"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_authorizations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_installations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.GitHubInstallationRecord", null) + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_github_repository_grants_installations_installation_id"); + + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_github_repository_grants_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_project_copilot_bindings_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_run_github_identity_snapshots_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.Subtask", b => + { + b.HasOne("Agentweaver.Api.Memory.WorkPlan", null) + .WithMany() + .HasForeignKey("WorkPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.SubtaskDependency", b => + { + b.HasOne("Agentweaver.Api.Memory.Subtask", null) + .WithMany() + .HasForeignKey("DependsOnSubtaskId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Agentweaver.Api.Memory.Subtask", null) + .WithMany() + .HasForeignKey("SubtaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.WorkPlan", b => + { + b.HasOne("Agentweaver.Api.Memory.OutcomeSpec", null) + .WithMany() + .HasForeignKey("OutcomeSpecId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/Agentweaver.Api/Migrations/20260827115552_AddTwoAppPersistence.cs b/apps/Agentweaver.Api/Migrations/20260827115552_AddTwoAppPersistence.cs new file mode 100644 index 000000000..6eb86fc63 --- /dev/null +++ b/apps/Agentweaver.Api/Migrations/20260827115552_AddTwoAppPersistence.cs @@ -0,0 +1,404 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Agentweaver.Api.Migrations +{ + /// + public partial class AddTwoAppPersistence : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "github_app_authorizations", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + entra_object_id = table.Column(type: "TEXT", nullable: false), + app_kind = table.Column(type: "INTEGER", nullable: false), + purpose = table.Column(type: "INTEGER", nullable: false), + credential_reference = table.Column(type: "TEXT", nullable: false), + credential_version = table.Column(type: "TEXT", nullable: false), + grant_digest = table.Column(type: "TEXT", nullable: false), + created_at = table.Column(type: "TEXT", nullable: false), + revoked_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_github_app_authorizations", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "github_audit_records", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + entra_object_id = table.Column(type: "TEXT", nullable: true), + actor_kind = table.Column(type: "INTEGER", nullable: false), + action = table.Column(type: "INTEGER", nullable: false), + resource_id = table.Column(type: "TEXT", nullable: false), + app_kind = table.Column(type: "INTEGER", nullable: true), + purpose = table.Column(type: "INTEGER", nullable: true), + outcome = table.Column(type: "INTEGER", nullable: false), + reason_code = table.Column(type: "INTEGER", nullable: false), + correlation_id = table.Column(type: "TEXT", nullable: false), + occurred_at = table.Column(type: "TEXT", nullable: false), + credential_version_or_digest = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_github_audit_records", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "projects", + columns: table => new + { + project_id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + OriginKind = table.Column(type: "TEXT", nullable: false), + SourceRepository = table.Column(type: "TEXT", nullable: true), + WorkingDirectory = table.Column(type: "TEXT", nullable: false), + DefaultBranch = table.Column(type: "TEXT", nullable: false), + Owner = table.Column(type: "TEXT", nullable: false), + DefaultProvider = table.Column(type: "TEXT", nullable: false), + DefaultModelCopilot = table.Column(type: "TEXT", nullable: true), + DefaultModelFoundry = table.Column(type: "TEXT", nullable: true), + State = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + WebhookSecret = table.Column(type: "TEXT", nullable: true), + TeamRevision = table.Column(type: "INTEGER", nullable: false), + MaxReadyPerHeartbeat = table.Column(type: "INTEGER", nullable: false), + PickupAutopilot = table.Column(type: "INTEGER", nullable: false), + PickupAutoApproveTools = table.Column(type: "INTEGER", nullable: false), + PreviewApprovalTimeoutMinutes = table.Column(type: "INTEGER", nullable: false), + DefaultWorkflowId = table.Column(type: "TEXT", nullable: true), + ActiveReviewPolicyName = table.Column(type: "TEXT", nullable: true), + SandboxProfile = table.Column(type: "TEXT", nullable: true), + SourceBlueprintId = table.Column(type: "TEXT", nullable: true), + SourceBlueprintType = table.Column(type: "TEXT", nullable: true), + BlueprintGenerationModel = table.Column(type: "TEXT", nullable: true), + WorkflowGenerationModel = table.Column(type: "TEXT", nullable: true), + OutcomeSpecGenerationModel = table.Column(type: "TEXT", nullable: true), + AllowedWorkflowIds = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_projects", x => x.project_id); + }); + + migrationBuilder.CreateTable( + name: "github_authorizations", + columns: table => new + { + state = table.Column(type: "TEXT", nullable: false), + app_kind = table.Column(type: "INTEGER", nullable: false), + purpose = table.Column(type: "INTEGER", nullable: false), + entra_object_id = table.Column(type: "TEXT", nullable: false), + project_id = table.Column(type: "TEXT", nullable: true), + expires_at_unix_ms = table.Column(type: "INTEGER", nullable: false), + return_route_key = table.Column(type: "TEXT", nullable: false), + pkce_verifier_protected = table.Column(type: "TEXT", nullable: false), + callback_cookie_hash = table.Column(type: "TEXT", nullable: false), + status = table.Column(type: "INTEGER", nullable: false), + created_at = table.Column(type: "TEXT", nullable: false), + completed_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_github_authorizations", x => x.state); + table.ForeignKey( + name: "FK_github_authorizations_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "project_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "github_installations", + columns: table => new + { + installation_id = table.Column(type: "INTEGER", nullable: false), + app_kind = table.Column(type: "INTEGER", nullable: false), + project_id = table.Column(type: "TEXT", nullable: true), + created_at = table.Column(type: "TEXT", nullable: false), + revoked_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_github_installations", x => x.installation_id); + table.ForeignKey( + name: "FK_github_installations_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "project_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "project_copilot_bindings", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + project_id = table.Column(type: "TEXT", nullable: false), + entra_object_id = table.Column(type: "TEXT", nullable: false), + credential_reference = table.Column(type: "TEXT", nullable: false), + credential_version = table.Column(type: "TEXT", nullable: false), + grant_digest = table.Column(type: "TEXT", nullable: false), + status = table.Column(type: "INTEGER", nullable: false), + bound_at = table.Column(type: "TEXT", nullable: false), + deactivated_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_project_copilot_bindings", x => x.id); + table.ForeignKey( + name: "FK_project_copilot_bindings_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "project_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "run_github_identity_snapshots", + columns: table => new + { + run_id = table.Column(type: "TEXT", nullable: false), + project_id = table.Column(type: "TEXT", nullable: false), + app_kind = table.Column(type: "INTEGER", nullable: false), + purpose = table.Column(type: "INTEGER", nullable: false), + credential_reference = table.Column(type: "TEXT", nullable: false), + credential_version = table.Column(type: "TEXT", nullable: false), + grant_digest = table.Column(type: "TEXT", nullable: false), + installation_id = table.Column(type: "INTEGER", nullable: true), + repository_id = table.Column(type: "INTEGER", nullable: true), + entra_object_id = table.Column(type: "TEXT", nullable: true), + captured_at = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_run_github_identity_snapshots", x => x.run_id); + table.ForeignKey( + name: "FK_run_github_identity_snapshots_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "project_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "github_repository_grants", + columns: table => new + { + installation_id = table.Column(type: "INTEGER", nullable: false), + repository_id = table.Column(type: "INTEGER", nullable: false), + project_id = table.Column(type: "TEXT", nullable: false), + full_name_display = table.Column(type: "TEXT", nullable: false), + permission_digest = table.Column(type: "TEXT", nullable: false), + granted_at = table.Column(type: "TEXT", nullable: false), + revoked_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_github_repository_grants", x => new { x.installation_id, x.repository_id }); + table.ForeignKey( + name: "FK_github_repository_grants_installations_installation_id", + column: x => x.installation_id, + principalTable: "github_installations", + principalColumn: "installation_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_github_repository_grants_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "project_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "automation_activations", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + project_id = table.Column(type: "TEXT", nullable: false), + installation_id = table.Column(type: "INTEGER", nullable: false), + repository_id = table.Column(type: "INTEGER", nullable: false), + automation_key = table.Column(type: "TEXT", nullable: false), + status = table.Column(type: "INTEGER", nullable: false), + activated_at = table.Column(type: "TEXT", nullable: false), + invalidated_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_automation_activations", x => x.id); + table.ForeignKey( + name: "FK_automation_activations_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "project_id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_automation_activations_repository_grants_installation_id_repository_id", + columns: x => new { x.installation_id, x.repository_id }, + principalTable: "github_repository_grants", + principalColumns: new[] { "installation_id", "repository_id" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "automation_invocations", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + project_id = table.Column(type: "TEXT", nullable: false), + activation_id = table.Column(type: "TEXT", nullable: false), + occurrence_key = table.Column(type: "TEXT", nullable: false), + delivery_id = table.Column(type: "TEXT", nullable: true), + event_name = table.Column(type: "TEXT", nullable: true), + installation_id = table.Column(type: "INTEGER", nullable: true), + repository_id = table.Column(type: "INTEGER", nullable: true), + outcome = table.Column(type: "INTEGER", nullable: false), + received_at = table.Column(type: "TEXT", nullable: false), + completed_at = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_automation_invocations", x => x.id); + table.ForeignKey( + name: "FK_automation_invocations_activations_activation_id", + column: x => x.activation_id, + principalTable: "automation_activations", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_automation_invocations_projects_project_id", + column: x => x.project_id, + principalTable: "projects", + principalColumn: "project_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_automation_activations_installation_id_repository_id", + table: "automation_activations", + columns: new[] { "installation_id", "repository_id" }); + + migrationBuilder.CreateIndex( + name: "IX_automation_activations_project_id_installation_id_repository_id_automation_key", + table: "automation_activations", + columns: new[] { "project_id", "installation_id", "repository_id", "automation_key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_automation_invocations_activation_id_occurrence_key", + table: "automation_invocations", + columns: new[] { "activation_id", "occurrence_key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_automation_invocations_delivery_id_event_name", + table: "automation_invocations", + columns: new[] { "delivery_id", "event_name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_automation_invocations_project_id", + table: "automation_invocations", + column: "project_id"); + + migrationBuilder.CreateIndex( + name: "IX_github_app_authorizations_entra_object_id_app_kind_purpose", + table: "github_app_authorizations", + columns: new[] { "entra_object_id", "app_kind", "purpose" }); + + migrationBuilder.CreateIndex( + name: "IX_github_audit_records_occurred_at", + table: "github_audit_records", + column: "occurred_at"); + + migrationBuilder.CreateIndex( + name: "IX_github_authorizations_entra_object_id_state", + table: "github_authorizations", + columns: new[] { "entra_object_id", "state" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_github_authorizations_expires_at_unix_ms", + table: "github_authorizations", + column: "expires_at_unix_ms"); + + migrationBuilder.CreateIndex( + name: "IX_github_authorizations_project_id", + table: "github_authorizations", + column: "project_id"); + + migrationBuilder.CreateIndex( + name: "IX_github_installations_project_id", + table: "github_installations", + column: "project_id"); + + migrationBuilder.CreateIndex( + name: "IX_github_repository_grants_installation_id_repository_id", + table: "github_repository_grants", + columns: new[] { "installation_id", "repository_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_github_repository_grants_project_id", + table: "github_repository_grants", + column: "project_id"); + + migrationBuilder.CreateIndex( + name: "UX_project_copilot_bindings_active_project", + table: "project_copilot_bindings", + column: "project_id", + unique: true, + filter: "status = 0"); + + migrationBuilder.CreateIndex( + name: "IX_run_github_identity_snapshots_project_id", + table: "run_github_identity_snapshots", + column: "project_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "automation_invocations"); + + migrationBuilder.DropTable( + name: "github_app_authorizations"); + + migrationBuilder.DropTable( + name: "github_audit_records"); + + migrationBuilder.DropTable( + name: "github_authorizations"); + + migrationBuilder.DropTable( + name: "project_copilot_bindings"); + + migrationBuilder.DropTable( + name: "run_github_identity_snapshots"); + + migrationBuilder.DropTable( + name: "automation_activations"); + + migrationBuilder.DropTable( + name: "github_repository_grants"); + + migrationBuilder.DropTable( + name: "github_installations"); + + migrationBuilder.DropTable( + name: "projects"); + } + } +} diff --git a/apps/Agentweaver.Api/Migrations/MemoryDbContextModelSnapshot.cs b/apps/Agentweaver.Api/Migrations/MemoryDbContextModelSnapshot.cs index 382ab8bf1..b3e1f7780 100644 --- a/apps/Agentweaver.Api/Migrations/MemoryDbContextModelSnapshot.cs +++ b/apps/Agentweaver.Api/Migrations/MemoryDbContextModelSnapshot.cs @@ -454,6 +454,114 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("auth_mode_epochs", (string)null); }); + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("TEXT") + .HasColumnName("activated_at"); + + b.Property("AutomationKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("automation_key"); + + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("InvalidatedAt") + .HasColumnType("TEXT") + .HasColumnName("invalidated_at"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId", "RepositoryId"); + + b.HasIndex("ProjectId", "InstallationId", "RepositoryId", "AutomationKey") + .IsUnique(); + + b.ToTable("automation_activations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ActivationId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("activation_id"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("DeliveryId") + .HasColumnType("TEXT") + .HasColumnName("delivery_id"); + + b.Property("EventName") + .HasColumnType("TEXT") + .HasColumnName("event_name"); + + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("OccurrenceKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("occurrence_key"); + + b.Property("Outcome") + .HasColumnType("INTEGER") + .HasColumnName("outcome"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("ReceivedAt") + .HasColumnType("TEXT") + .HasColumnName("received_at"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ActivationId", "OccurrenceKey") + .IsUnique(); + + b.HasIndex("DeliveryId", "EventName") + .IsUnique(); + + b.ToTable("automation_invocations", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.Decision", b => { b.Property("Id") @@ -648,6 +756,253 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("github_account_link_states", (string)null); }); + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAppAuthorizationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("grant_digest"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked_at"); + + b.HasKey("Id"); + + b.HasIndex("EntraObjectId", "AppKind", "Purpose"); + + b.ToTable("github_app_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Action") + .HasColumnType("INTEGER") + .HasColumnName("action"); + + b.Property("ActorKind") + .HasColumnType("INTEGER") + .HasColumnName("actor_kind"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("correlation_id"); + + b.Property("CredentialVersionOrDigest") + .HasColumnType("TEXT") + .HasColumnName("credential_version_or_digest"); + + b.Property("EntraObjectId") + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("OccurredAt") + .HasColumnType("TEXT") + .HasColumnName("occurred_at"); + + b.Property("Outcome") + .HasColumnType("INTEGER") + .HasColumnName("outcome"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("ReasonCode") + .HasColumnType("INTEGER") + .HasColumnName("reason_code"); + + b.Property("ResourceId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("resource_id"); + + b.HasKey("Id"); + + b.HasIndex("OccurredAt"); + + b.ToTable("github_audit_records", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.Property("State") + .HasColumnType("TEXT") + .HasColumnName("state"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CallbackCookieHash") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("callback_cookie_hash"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("ExpiresAtUnixMilliseconds") + .HasColumnType("INTEGER") + .HasColumnName("expires_at_unix_ms"); + + b.Property("PkceVerifierProtected") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("pkce_verifier_protected"); + + b.Property("ProjectId") + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("ReturnRouteKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("return_route_key"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasColumnName("status"); + + b.HasKey("State"); + + b.HasIndex("ExpiresAtUnixMilliseconds"); + + b.HasIndex("ProjectId"); + + b.HasIndex("EntraObjectId", "State") + .IsUnique(); + + b.ToTable("github_authorizations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("ProjectId") + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId"); + + b.HasIndex("ProjectId"); + + b.ToTable("github_installations", (string)null); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.Property("FullNameDisplay") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("full_name_display"); + + b.Property("GrantedAt") + .HasColumnType("TEXT") + .HasColumnName("granted_at"); + + b.Property("PermissionDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("permission_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked_at"); + + b.HasKey("InstallationId", "RepositoryId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("InstallationId", "RepositoryId") + .IsUnique(); + + b.ToTable("github_repository_grants", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.IntegrationBuildLockRecord", b => { b.Property("ProjectId") @@ -725,6 +1080,59 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OutcomeSpecs"); }); + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("BoundAt") + .HasColumnType("TEXT") + .HasColumnName("bound_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_version"); + + b.Property("DeactivatedAt") + .HasColumnType("TEXT") + .HasColumnName("deactivated_at"); + + b.Property("EntraObjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("grant_digest"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId") + .IsUnique() + .HasDatabaseName("UX_project_copilot_bindings_active_project") + .HasFilter("status = 0"); + + b.ToTable("project_copilot_bindings", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectGitHubIdentityOverrideRecord", b => { b.Property("ProjectId") @@ -752,6 +1160,105 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("project_github_identity_overrides", (string)null); }); + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectRecord", b => + { + b.Property("ProjectId") + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("ActiveReviewPolicyName") + .HasColumnType("TEXT"); + + b.Property("AllowedWorkflowIds") + .HasColumnType("TEXT"); + + b.Property("BlueprintGenerationModel") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DefaultBranch") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultModelCopilot") + .HasColumnType("TEXT"); + + b.Property("DefaultModelFoundry") + .HasColumnType("TEXT"); + + b.Property("DefaultProvider") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DefaultWorkflowId") + .HasColumnType("TEXT"); + + b.Property("MaxReadyPerHeartbeat") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OriginKind") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OutcomeSpecGenerationModel") + .HasColumnType("TEXT"); + + b.Property("Owner") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PickupAutoApproveTools") + .HasColumnType("INTEGER"); + + b.Property("PickupAutopilot") + .HasColumnType("INTEGER"); + + b.Property("PreviewApprovalTimeoutMinutes") + .HasColumnType("INTEGER"); + + b.Property("SandboxProfile") + .HasColumnType("TEXT"); + + b.Property("SourceBlueprintId") + .HasColumnType("TEXT"); + + b.Property("SourceBlueprintType") + .HasColumnType("TEXT"); + + b.Property("SourceRepository") + .HasColumnType("TEXT"); + + b.Property("State") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TeamRevision") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("WebhookSecret") + .HasColumnType("TEXT"); + + b.Property("WorkflowGenerationModel") + .HasColumnType("TEXT"); + + b.Property("WorkingDirectory") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ProjectId"); + + b.ToTable("projects", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.RunAuthorshipCapability", b => { b.Property("RunId") @@ -775,6 +1282,63 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("run_authorship_capabilities", (string)null); }); + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.Property("RunId") + .HasColumnType("TEXT") + .HasColumnName("run_id"); + + b.Property("AppKind") + .HasColumnType("INTEGER") + .HasColumnName("app_kind"); + + b.Property("CapturedAt") + .HasColumnType("TEXT") + .HasColumnName("captured_at"); + + b.Property("CredentialReference") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_reference"); + + b.Property("CredentialVersion") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("credential_version"); + + b.Property("EntraObjectId") + .HasColumnType("TEXT") + .HasColumnName("entra_object_id"); + + b.Property("GrantDigest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("grant_digest"); + + b.Property("InstallationId") + .HasColumnType("INTEGER") + .HasColumnName("installation_id"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("project_id"); + + b.Property("Purpose") + .HasColumnType("INTEGER") + .HasColumnName("purpose"); + + b.Property("RepositoryId") + .HasColumnType("INTEGER") + .HasColumnName("repository_id"); + + b.HasKey("RunId"); + + b.HasIndex("ProjectId"); + + b.ToTable("run_github_identity_snapshots", (string)null); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.SessionContext", b => { b.Property("Id") @@ -1188,6 +1752,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RunEvents"); }); + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationActivationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_activations_projects_project_id"); + + b.HasOne("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", null) + .WithMany() + .HasForeignKey("InstallationId", "RepositoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_activations_repository_grants_installation_id_repository_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.AutomationInvocationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.AutomationActivationRecord", null) + .WithMany() + .HasForeignKey("ActivationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_activations_activation_id"); + + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_automation_invocations_projects_project_id"); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.Decision", b => { b.HasOne("Agentweaver.Api.Memory.Decision", null) @@ -1202,6 +1800,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("DecisionId"); }); + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubAuthorizationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_authorizations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubInstallationRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_github_installations_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.GitHubRepositoryGrantRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.GitHubInstallationRecord", null) + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_github_repository_grants_installations_installation_id"); + + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_github_repository_grants_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.ProjectCopilotBindingRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_project_copilot_bindings_projects_project_id"); + }); + + modelBuilder.Entity("Agentweaver.Api.Memory.RunGitHubIdentitySnapshotRecord", b => + { + b.HasOne("Agentweaver.Api.Memory.ProjectRecord", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_run_github_identity_snapshots_projects_project_id"); + }); + modelBuilder.Entity("Agentweaver.Api.Memory.Subtask", b => { b.HasOne("Agentweaver.Api.Memory.WorkPlan", null) diff --git a/apps/Agentweaver.Api/Program.cs b/apps/Agentweaver.Api/Program.cs index 3c7ede990..4b21d5ee4 100644 --- a/apps/Agentweaver.Api/Program.cs +++ b/apps/Agentweaver.Api/Program.cs @@ -210,7 +210,7 @@ var secretClient = new SecretClient(new Uri(kvUri), new DefaultAzureCredential()); var kvSecretStore = new KeyVaultSecretStore(secretClient); var diskFs = new FileSystemGitHubTokenStore(); // migration source only - var kvTokenStore = new KeyVaultGitHubTokenStore(kvSecretStore, diskFallback: diskFs, diskMirror: null); + var kvTokenStore = new KeyVaultGitHubTokenStore(kvSecretStore, diskFallback: diskFs); var cachedTokenStore = new CachingGitHubTokenStore(kvTokenStore); // Outermost decorator: rewrite legacy per-user scopes onto the caller's ACTIVE linked GitHub // identity so Entra-signed-in users (whose credentials live under user-link:{oid}:{login}) @@ -259,6 +259,7 @@ builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton _factory; private readonly IConfiguration _configuration; private readonly ILogger _logger; + private readonly Func? _beforeTwoAppCommit; public SqliteToPostgresMigrator( IDbContextFactory factory, IConfiguration configuration, - ILogger logger) + ILogger logger, + Func? beforeTwoAppCommit = null) { _factory = factory; _configuration = configuration; _logger = logger; + _beforeTwoAppCommit = beforeTwoAppCommit; } public async Task RunAsync(CancellationToken ct = default) @@ -43,13 +46,121 @@ public async Task RunAsync(CancellationToken ct = default) // Migrate agentweaver.db tables await MigrateAgentweaverDbAsync(agentweaverDbPath, db, ct); - // Note: memory.db EF entities (AgentMemory, Decisions, etc.) are managed by EF migrations - // and don't need data migration for fresh Postgres deployments. If existing memory.db data - // must be preserved, extend this migrator with table-by-table reads from memory.db. + await MigrateTwoAppRecordsAsync(memoryDbPath, db, ct); _logger.LogInformation("Migration complete."); } + private async Task MigrateTwoAppRecordsAsync(string memoryDbPath, MemoryDbContext destination, CancellationToken ct) + { + if (!File.Exists(memoryDbPath)) + return; + + var sourceOptions = new DbContextOptionsBuilder() + .UseSqlite($"Data Source={memoryDbPath}") + .Options; + await using var source = new MemoryDbContext(sourceOptions); + + List authorizations; + List installations; + List grants; + List bindings; + List activations; + List invocations; + List snapshots; + List appAuthorizations; + List audits; + try + { + authorizations = await source.GitHubAuthorizations.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + installations = await source.GitHubInstallations.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + grants = await source.GitHubRepositoryGrants.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + bindings = await source.ProjectCopilotBindings.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + activations = await source.AutomationActivations.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + invocations = await source.AutomationInvocations.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + snapshots = await source.RunGitHubIdentitySnapshots.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + appAuthorizations = await source.GitHubAppAuthorizations.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + audits = await source.GitHubAuditRecords.AsNoTracking().ToListAsync(ct).ConfigureAwait(false); + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 1 && + ex.Message.Contains("no such table", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("SQLite source predates two-App persistence; no two-App records to migrate."); + return; + } + + if (authorizations.Count + installations.Count + grants.Count + bindings.Count + activations.Count + + invocations.Count + snapshots.Count + appAuthorizations.Count + audits.Count == 0) + return; + + var projectIds = authorizations.Where(x => x.ProjectId is not null).Select(x => x.ProjectId!) + .Concat(installations.Where(x => x.ProjectId is not null).Select(x => x.ProjectId!)) + .Concat(grants.Select(x => x.ProjectId)) + .Concat(bindings.Select(x => x.ProjectId)) + .Concat(activations.Select(x => x.ProjectId)) + .Concat(invocations.Select(x => x.ProjectId)) + .Concat(snapshots.Select(x => x.ProjectId)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + var targetProjectIds = await destination.Projects.AsNoTracking() + .Where(x => projectIds.Contains(x.ProjectId)) + .Select(x => x.ProjectId) + .ToListAsync(ct) + .ConfigureAwait(false); + if (targetProjectIds.Count != projectIds.Length) + throw new InvalidOperationException( + "Two-App persistence transfer aborted: a project is missing from the destination, so no binding can become usable partially."); + + await using var transaction = await destination.Database.BeginTransactionAsync(ct).ConfigureAwait(false); + try + { + foreach (var item in authorizations) + if (!await destination.GitHubAuthorizations.AnyAsync(x => x.State == item.State, ct).ConfigureAwait(false)) + destination.GitHubAuthorizations.Add(item); + foreach (var item in installations) + if (!await destination.GitHubInstallations.AnyAsync(x => x.InstallationId == item.InstallationId, ct).ConfigureAwait(false)) + destination.GitHubInstallations.Add(item); + foreach (var item in grants) + if (!await destination.GitHubRepositoryGrants.AnyAsync(x => x.InstallationId == item.InstallationId && x.RepositoryId == item.RepositoryId, ct).ConfigureAwait(false)) + destination.GitHubRepositoryGrants.Add(item); + foreach (var item in bindings) + if (!await destination.ProjectCopilotBindings.AnyAsync(x => x.Id == item.Id, ct).ConfigureAwait(false)) + destination.ProjectCopilotBindings.Add(item); + foreach (var item in activations) + if (!await destination.AutomationActivations.AnyAsync(x => x.Id == item.Id, ct).ConfigureAwait(false)) + destination.AutomationActivations.Add(item); + foreach (var item in invocations) + if (!await destination.AutomationInvocations.AnyAsync(x => x.Id == item.Id, ct).ConfigureAwait(false)) + destination.AutomationInvocations.Add(item); + foreach (var item in snapshots) + if (!await destination.RunGitHubIdentitySnapshots.AnyAsync(x => x.RunId == item.RunId, ct).ConfigureAwait(false)) + destination.RunGitHubIdentitySnapshots.Add(item); + foreach (var item in appAuthorizations) + if (!await destination.GitHubAppAuthorizations.AnyAsync(x => x.Id == item.Id, ct).ConfigureAwait(false)) + destination.GitHubAppAuthorizations.Add(item); + foreach (var item in audits) + if (!await destination.GitHubAuditRecords.AnyAsync(x => x.Id == item.Id, ct).ConfigureAwait(false)) + destination.GitHubAuditRecords.Add(item); + + await destination.SaveChangesAsync(ct).ConfigureAwait(false); + if (destination.Database.IsNpgsql() && audits.Count > 0) + { + await destination.Database.ExecuteSqlRawAsync( + "SELECT setval(pg_get_serial_sequence('github_audit_records', 'id'), " + + "(SELECT COALESCE(MAX(id), 1) FROM github_audit_records), true);", ct) + .ConfigureAwait(false); + } + if (_beforeTwoAppCommit is not null) + await _beforeTwoAppCommit(ct).ConfigureAwait(false); + await transaction.CommitAsync(ct).ConfigureAwait(false); + } + catch + { + await transaction.RollbackAsync(CancellationToken.None).ConfigureAwait(false); + throw; + } + } + private async Task MigrateAgentweaverDbAsync(string dbPath, MemoryDbContext db, CancellationToken ct) { if (!File.Exists(dbPath)) diff --git a/tests/Agentweaver.Tests/Auth/KeyVaultGitHubTokenStoreTests.cs b/tests/Agentweaver.Tests/Auth/KeyVaultGitHubTokenStoreTests.cs index 0332482d4..ac40338ca 100644 --- a/tests/Agentweaver.Tests/Auth/KeyVaultGitHubTokenStoreTests.cs +++ b/tests/Agentweaver.Tests/Auth/KeyVaultGitHubTokenStoreTests.cs @@ -14,11 +14,10 @@ public sealed class KeyVaultGitHubTokenStoreTests // ── Helpers ─────────────────────────────────────────────────────────────── private static (InMemorySecretStore secrets, KeyVaultGitHubTokenStore store) MakeStore( - FileSystemGitHubTokenStore? diskFallback = null, - FileSystemGitHubTokenStore? diskMirror = null) + FileSystemGitHubTokenStore? diskFallback = null) { var secrets = new InMemorySecretStore(); - var store = new KeyVaultGitHubTokenStore(secrets, diskFallback, diskMirror); + var store = new KeyVaultGitHubTokenStore(secrets, diskFallback); return (secrets, store); } @@ -444,37 +443,6 @@ public async Task GetAsync_WhenKvAbsent_MigratesDiskTokenToKv() kvResult.Value.Should().Contain("disk_access"); } - [Fact] - public async Task SetAsync_MirrorsTokenToDisk() - { - using var dir = new TempDir(); - var diskStore = new FileSystemGitHubTokenStore(dir.Path); - var (_, store) = MakeStore(diskMirror: diskStore); - - var token = SampleToken(access: "kv_access"); - await store.SetAsync(GitHubTokenScope.Installation, token); - - // Disk must also have the token. - var diskEntry = await diskStore.GetAsync(GitHubTokenScope.Installation); - diskEntry.Status.Should().Be(GitHubTokenStatus.SignedIn); - diskEntry.AccessToken.Should().Be("kv_access", "SetAsync must mirror signed-in tokens to disk"); - } - - [Fact] - public async Task SignOutAsync_MirrorsTombstoneToDisk() - { - using var dir = new TempDir(); - var diskStore = new FileSystemGitHubTokenStore(dir.Path); - var (_, store) = MakeStore(diskMirror: diskStore); - - await store.SetAsync(GitHubTokenScope.Installation, SampleToken()); - await store.SignOutAsync(GitHubTokenScope.Installation); - - var diskEntry = await diskStore.GetAsync(GitHubTokenScope.Installation); - diskEntry.Status.Should().Be(GitHubTokenStatus.SignedOut, - "SignOut must mirror tombstone to disk"); - } - // ── Per-user scope ──────────────────────────────────────────────────────── [Fact] diff --git a/tests/Agentweaver.Tests/Auth/TwoAppPersistenceStoreTests.cs b/tests/Agentweaver.Tests/Auth/TwoAppPersistenceStoreTests.cs new file mode 100644 index 000000000..75725dcd1 --- /dev/null +++ b/tests/Agentweaver.Tests/Auth/TwoAppPersistenceStoreTests.cs @@ -0,0 +1,231 @@ +using Agentweaver.Api.Auth; +using Agentweaver.Api.Memory; +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Agentweaver.Tests.Auth; + +public sealed class TwoAppPersistenceStoreTests +{ + [Fact] + public async Task AuthorizationClaim_IsSingleUseAndExpiresInDatabasePredicate() + { + await using var connection = await OpenDatabaseAsync(); + var options = Options(connection); + await using (var setup = new MemoryDbContext(options)) + { + await new TwoAppPersistenceStore(setup).AddAuthorizationAsync(new GitHubAuthorizationRecord + { + State = "state", + AppKind = GitHubAppKind.Copilot, + Purpose = GitHubAuthorizationPurpose.InteractiveCopilot, + EntraObjectId = "entra", + ExpiresAtUnixMilliseconds = DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds(), + ReturnRouteKey = "projects", + PkceVerifierProtected = "protected", + CallbackCookieHash = "hash", + Status = GitHubAuthorizationStatus.Pending, + CreatedAt = DateTimeOffset.UtcNow, + }); + } + + await using var first = new MemoryDbContext(options); + await using var second = new MemoryDbContext(options); + (await new TwoAppPersistenceStore(first).ClaimAuthorizationAsync("state", "entra", DateTimeOffset.UtcNow)) + .Should().Be(AuthorizationClaimResult.Claimed); + (await new TwoAppPersistenceStore(second).ClaimAuthorizationAsync("state", "entra", DateTimeOffset.UtcNow)) + .Should().Be(AuthorizationClaimResult.Consumed); + } + + [Fact] + public async Task BindingReplacement_DeactivatesBeforeInsertAndLeavesOneActiveBinding() + { + await using var connection = await OpenDatabaseAsync(); + var options = Options(connection); + await using var db = new MemoryDbContext(options); + var store = new TwoAppPersistenceStore(db); + + (await store.ReplaceCopilotBindingAsync(Binding("first"))).Should().Be(BindingWriteResult.Bound); + (await store.ReplaceCopilotBindingAsync(Binding("second"))).Should().Be(BindingWriteResult.Bound); + + var bindings = await db.ProjectCopilotBindings.AsNoTracking().ToListAsync(); + bindings.Should().HaveCount(2); + bindings.Count(x => x.Status == GitHubBindingStatus.Active).Should().Be(1); + bindings.Single(x => x.Status == GitHubBindingStatus.Active).Id.Should().Be("second"); + bindings.Single(x => x.Id == "first").DeactivatedAt.Should().NotBeNull(); + } + + [Fact] + public async Task ActiveBindingUniqueIndexRejectsConcurrentInsert() + { + await using var connection = await OpenDatabaseAsync(); + var options = Options(connection); + await using (var first = new MemoryDbContext(options)) + { + first.ProjectCopilotBindings.Add(Binding("first")); + await first.SaveChangesAsync(); + } + + await using var second = new MemoryDbContext(options); + second.ProjectCopilotBindings.Add(Binding("second")); + var action = () => second.SaveChangesAsync(); + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task SQLiteProjectDeletion_CascadesTwoAppRecords() + { + await using var connection = await OpenDatabaseAsync(); + var options = Options(connection); + await using var db = new MemoryDbContext(options); + (await new TwoAppPersistenceStore(db).ReplaceCopilotBindingAsync(Binding("binding"))) + .Should().Be(BindingWriteResult.Bound); + + db.Projects.Remove(new ProjectRecord { ProjectId = "project" }); + await db.SaveChangesAsync(); + (await db.ProjectCopilotBindings.CountAsync()).Should().Be(0); + } + + [Fact] + public async Task InvocationClaim_UsesDatabaseUniqueConstraintInsteadOfPrecheck() + { + await using var connection = await OpenDatabaseAsync(); + var options = Options(connection); + await using (var db = new MemoryDbContext(options)) + { + db.GitHubInstallations.Add(new GitHubInstallationRecord + { + InstallationId = 101, + AppKind = GitHubAppKind.Repo, + ProjectId = "project", + CreatedAt = DateTimeOffset.UtcNow, + }); + db.GitHubRepositoryGrants.Add(new GitHubRepositoryGrantRecord + { + InstallationId = 101, + RepositoryId = 202, + ProjectId = "project", + FullNameDisplay = "owner/repository", + PermissionDigest = "digest", + GrantedAt = DateTimeOffset.UtcNow, + }); + db.AutomationActivations.Add(new AutomationActivationRecord + { + Id = "activation", + ProjectId = "project", + InstallationId = 101, + RepositoryId = 202, + AutomationKey = "nightly", + Status = AutomationActivationStatus.Active, + ActivatedAt = DateTimeOffset.UtcNow, + }); + await db.SaveChangesAsync(); + } + + await using var first = new MemoryDbContext(options); + await using var second = new MemoryDbContext(options); + (await new TwoAppPersistenceStore(first).ClaimInvocationAsync(Invocation("one"))).Should().Be(InvocationClaimResult.Claimed); + (await new TwoAppPersistenceStore(second).ClaimInvocationAsync(Invocation("two"))).Should().Be(InvocationClaimResult.Duplicate); + } + + [Fact] + public async Task SnapshotIsWriteOnceAndFailsClosedOnCredentialRotation() + { + await using var connection = await OpenDatabaseAsync(); + var options = Options(connection); + await using var db = new MemoryDbContext(options); + var store = new TwoAppPersistenceStore(db); + var snapshot = new RunGitHubIdentitySnapshotRecord + { + RunId = "run", + ProjectId = "project", + AppKind = GitHubAppKind.Copilot, + Purpose = GitHubAuthorizationPurpose.UnattendedCopilot, + CredentialReference = "kv-copilot-project", + CredentialVersion = "etag-1", + GrantDigest = "digest", + CapturedAt = DateTimeOffset.UtcNow, + }; + + (await store.AddRunIdentitySnapshotAsync(snapshot)).Should().BeTrue(); + (await store.AddRunIdentitySnapshotAsync(snapshot)).Should().BeFalse(); + (await store.HasPinnedSnapshotVersionAsync("run", "etag-1")).Should().BeTrue(); + (await store.HasPinnedSnapshotVersionAsync("run", "etag-2")).Should().BeFalse(); + } + + [Theory] + [InlineData("ghu_sensitive")] + [InlineData("github_pat_sensitive")] + [InlineData("-----BEGIN PRIVATE KEY-----")] + [InlineData("eyJheader.payload.")] + public async Task PersistenceBoundaryRejectsCredentialMaterial(string credential) + { + await using var connection = await OpenDatabaseAsync(); + await using var db = new MemoryDbContext(Options(connection)); + var binding = Binding("binding"); + binding.CredentialReference = credential; + + var action = () => new TwoAppPersistenceStore(db).ReplaceCopilotBindingAsync(binding); + await action.Should().ThrowAsync(); + } + + [Fact] + public void AuditActorEnumCannotRepresentInternalPrincipal() + { + Enum.GetNames().Should().BeEquivalentTo( + [nameof(GitHubAuditActorKind.HumanEntraSubject), nameof(GitHubAuditActorKind.GitHubWebhook)]); + } + + private static async Task OpenDatabaseAsync() + { + var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + await using var db = new MemoryDbContext(Options(connection)); + await db.Database.EnsureCreatedAsync(); + db.Projects.Add(Project("project")); + await db.SaveChangesAsync(); + return connection; + } + + private static DbContextOptions Options(SqliteConnection connection) => + new DbContextOptionsBuilder().UseSqlite(connection).Options; + + private static ProjectCopilotBindingRecord Binding(string id) => new() + { + Id = id, + ProjectId = "project", + EntraObjectId = "entra", + CredentialReference = "kv-copilot-project", + CredentialVersion = "version", + GrantDigest = "digest", + Status = GitHubBindingStatus.Active, + BoundAt = DateTimeOffset.UtcNow, + }; + + private static AutomationInvocationRecord Invocation(string id) => new() + { + Id = id, + ProjectId = "project", + ActivationId = "activation", + OccurrenceKey = "occurrence", + DeliveryId = "delivery", + EventName = "schedule", + InstallationId = 101, + RepositoryId = 202, + Outcome = AutomationInvocationOutcome.Claimed, + ReceivedAt = DateTimeOffset.UtcNow, + }; + + private static ProjectRecord Project(string id) => new() + { + ProjectId = id, + Name = "Project", + OriginKind = "blank", + WorkingDirectory = "C:\\project", + Owner = "owner", + DefaultProvider = "github_copilot", + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + }; +} diff --git a/tests/Agentweaver.Tests/PostgresIntegration/DataMigratorTests.cs b/tests/Agentweaver.Tests/PostgresIntegration/DataMigratorTests.cs index 809029518..82ed71647 100644 --- a/tests/Agentweaver.Tests/PostgresIntegration/DataMigratorTests.cs +++ b/tests/Agentweaver.Tests/PostgresIntegration/DataMigratorTests.cs @@ -152,11 +152,55 @@ await migrate.Should().ThrowAsync() acquisition => acquisition.PackageId == _seededPackageId)).Should().Be(0); } + [PostgresFact] + public async Task Migrator_TwoAppFailure_RollsBackEveryTwoAppRecord() + { + var options = new DbContextOptionsBuilder() + .UseSqlite($"Data Source={_memoryDbPath}") + .Options; + await using (var source = new MemoryDbContext(options)) + { + await source.Database.EnsureCreatedAsync(); + source.Projects.Add(new ProjectRecord + { + ProjectId = _seededProjectId, + Name = "Source project", + OriginKind = "blank", + WorkingDirectory = "source-worktree", + Owner = "owner", + DefaultProvider = "github_copilot", + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + }); + source.ProjectCopilotBindings.Add(new ProjectCopilotBindingRecord + { + Id = "binding-" + Guid.NewGuid().ToString("N"), + ProjectId = _seededProjectId, + EntraObjectId = "entra", + CredentialReference = "kv-copilot", + CredentialVersion = "version-1", + GrantDigest = "digest", + Status = GitHubBindingStatus.Active, + BoundAt = DateTimeOffset.UtcNow, + }); + await source.SaveChangesAsync(); + } + + var migrator = BuildMigrator(_ => Task.FromException( + new InvalidOperationException("Injected two-App transfer failure."))); + Func migrate = () => migrator.RunAsync(); + await migrate.Should().ThrowAsync() + .WithMessage("Injected two-App transfer failure."); + + await using var verify = await _pg.CreateDbContextAsync(); + (await verify.ProjectCopilotBindings.CountAsync(x => x.ProjectId == _seededProjectId)).Should().Be(0); + } + // ───────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────── - private SqliteToPostgresMigrator BuildMigrator() + private SqliteToPostgresMigrator BuildMigrator(Func? beforeTwoAppCommit = null) { var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -169,7 +213,8 @@ private SqliteToPostgresMigrator BuildMigrator() return new SqliteToPostgresMigrator( _pg.Factory, config, - NullLogger.Instance); + NullLogger.Instance, + beforeTwoAppCommit); } /// diff --git a/tests/Agentweaver.Tests/PostgresIntegration/TwoAppPersistencePostgresTests.cs b/tests/Agentweaver.Tests/PostgresIntegration/TwoAppPersistencePostgresTests.cs new file mode 100644 index 000000000..1aa790f2b --- /dev/null +++ b/tests/Agentweaver.Tests/PostgresIntegration/TwoAppPersistencePostgresTests.cs @@ -0,0 +1,62 @@ +using Agentweaver.Api.Auth; +using Agentweaver.Api.Memory; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; + +namespace Agentweaver.Tests.PostgresIntegration; + +[Collection("PostgresIntegration")] +[Trait("Category", "PostgresIntegration")] +public sealed class TwoAppPersistencePostgresTests(PostgresFixture postgres) +{ + [PostgresFact] + public async Task ProjectCascadeAndActiveBindingConstraintMatchSqlite() + { + var projectId = $"two-app-{Guid.NewGuid():N}"; + await using (var db = await postgres.CreateDbContextAsync()) + { + db.Projects.Add(new ProjectRecord + { + ProjectId = projectId, + Name = "Two App test", + OriginKind = "blank", + WorkingDirectory = "two-app-worktree", + Owner = "owner", + DefaultProvider = "github_copilot", + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + }); + await db.SaveChangesAsync(); + var store = new TwoAppPersistenceStore(db); + (await store.ReplaceCopilotBindingAsync(Binding("first", projectId))).Should().Be(BindingWriteResult.Bound); + } + + await using (var conflicting = await postgres.CreateDbContextAsync()) + { + conflicting.ProjectCopilotBindings.Add(Binding("second", projectId)); + var save = () => conflicting.SaveChangesAsync(); + await save.Should().ThrowAsync(); + } + + await using (var delete = await postgres.CreateDbContextAsync()) + { + delete.Projects.Remove(new ProjectRecord { ProjectId = projectId }); + await delete.SaveChangesAsync(); + } + + await using var verify = await postgres.CreateDbContextAsync(); + (await verify.ProjectCopilotBindings.CountAsync(x => x.ProjectId == projectId)).Should().Be(0); + } + + private static ProjectCopilotBindingRecord Binding(string id, string projectId) => new() + { + Id = id, + ProjectId = projectId, + EntraObjectId = "entra", + CredentialReference = "kv-copilot", + CredentialVersion = "version", + GrantDigest = "digest", + Status = GitHubBindingStatus.Active, + BoundAt = DateTimeOffset.UtcNow, + }; +}