fad476f115
Skills move from a global Git-only registry to a per-company library that orgs author and
version in-app — Git stays as the shared *starter* library.
Domain & persistence:
- Skill gains OrganizationId (null = shared builtin, visible to every org), Origin
(Builtin | Authored | Installed), AuthoredByMemberId. Identity is now
(OrganizationId, SkillKey, Version); the unique index uses NULLS NOT DISTINCT so builtins
stay unique by key+version while each org gets its own namespace (and can fork a builtin).
AddSkillOwnership migration backfills existing rows as Builtin.
- Owned GoldenExample rows are cloned in Skill.Index so a fork can't re-parent the source's
tracked entities.
Authoring (tenant, dynamic):
- POST /api/skills/authored — structured fields → same indexer pipeline (embedding +
publish gate apply identically), tagged org + author. POST /api/skills/{key}/fork copies a
builtin/global skill into your org as an editable Authored draft. List/Get are org-scoped
(your org + shared builtins). New Capability.ManageSkills (Owner + TeamOwner), audited.
- GET /api/skills/marketplace: read-only seam listing public skills across orgs (install is
the next step).
Security (from adversarial review — two confirmed criticals):
- Managing shared builtins is an operator action, not a tenant one. /index (posts arbitrary
content as a global builtin) and /sync (re-indexes the shared library) now require a
platform admin key (X-Skills-Admin-Key, fixed-time compare, fail-closed when unset) via
SkillAdminOptions — previously any authenticated user of any org could inject/poison global
skills. New test asserts an authenticated Owner without the key gets 403 on both.
UI: new /skills library page — browse shared + org skills grouped by key with their versions,
create / new-version / fork, golden-test editor + body, Draft/Published badge and the
publish-gate hint (needs roles + ≥1 golden test).
Verified: ArchitectureTests 8/8, IntegrationTests 46/46 (new SkillLibraryTests: org
isolation, version coexistence, fork, publish gate, Member 403, admin-gate 403), client build
green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
204 lines
7.2 KiB
C#
204 lines
7.2 KiB
C#
// <auto-generated />
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
using Pgvector;
|
|
using TeamUp.Modules.Skills.Persistence;
|
|
|
|
#nullable disable
|
|
|
|
namespace TeamUp.Modules.Skills.Persistence.Migrations
|
|
{
|
|
[DbContext(typeof(SkillsDbContext))]
|
|
[Migration("20260610180442_AddSkillOwnership")]
|
|
partial class AddSkillOwnership
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
{
|
|
#pragma warning disable 612, 618
|
|
modelBuilder
|
|
.HasDefaultSchema("skills")
|
|
.HasAnnotation("ProductVersion", "10.0.8")
|
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
|
|
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
|
|
|
modelBuilder.Entity("TeamUp.Modules.Skills.Domain.Skill", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("uuid");
|
|
|
|
b.Property<Guid?>("AuthoredByMemberId")
|
|
.HasColumnType("uuid");
|
|
|
|
b.Property<string>("Body")
|
|
.IsRequired()
|
|
.HasColumnType("text");
|
|
|
|
b.Property<string>("ContentHash")
|
|
.IsRequired()
|
|
.HasMaxLength(64)
|
|
.HasColumnType("character varying(64)");
|
|
|
|
b.PrimitiveCollection<List<string>>("Context")
|
|
.IsRequired()
|
|
.HasColumnType("text[]");
|
|
|
|
b.Property<Vector>("Embedding")
|
|
.HasColumnType("vector(384)");
|
|
|
|
b.Property<DateTimeOffset>("IndexedAtUtc")
|
|
.HasColumnType("timestamp with time zone");
|
|
|
|
b.Property<string>("Inputs")
|
|
.HasMaxLength(2000)
|
|
.HasColumnType("character varying(2000)");
|
|
|
|
b.Property<string>("MinTier")
|
|
.IsRequired()
|
|
.HasMaxLength(20)
|
|
.HasColumnType("character varying(20)");
|
|
|
|
b.Property<string>("Name")
|
|
.IsRequired()
|
|
.HasMaxLength(200)
|
|
.HasColumnType("character varying(200)");
|
|
|
|
b.Property<Guid?>("OrganizationId")
|
|
.HasColumnType("uuid");
|
|
|
|
b.Property<string>("Origin")
|
|
.IsRequired()
|
|
.HasMaxLength(20)
|
|
.HasColumnType("character varying(20)");
|
|
|
|
b.Property<string>("Outputs")
|
|
.HasMaxLength(2000)
|
|
.HasColumnType("character varying(2000)");
|
|
|
|
b.PrimitiveCollection<List<string>>("Roles")
|
|
.IsRequired()
|
|
.HasColumnType("text[]");
|
|
|
|
b.Property<string>("SkillKey")
|
|
.IsRequired()
|
|
.HasMaxLength(128)
|
|
.HasColumnType("character varying(128)");
|
|
|
|
b.Property<string>("SourceCommit")
|
|
.HasColumnType("text");
|
|
|
|
b.Property<string>("SourcePath")
|
|
.HasColumnType("text");
|
|
|
|
b.Property<string>("SourceRepo")
|
|
.HasColumnType("text");
|
|
|
|
b.Property<string>("Status")
|
|
.IsRequired()
|
|
.HasMaxLength(20)
|
|
.HasColumnType("character varying(20)");
|
|
|
|
b.Property<string>("Summary")
|
|
.HasMaxLength(1000)
|
|
.HasColumnType("character varying(1000)");
|
|
|
|
b.PrimitiveCollection<List<string>>("Tools")
|
|
.IsRequired()
|
|
.HasColumnType("text[]");
|
|
|
|
b.Property<DateTimeOffset>("UpdatedAtUtc")
|
|
.HasColumnType("timestamp with time zone");
|
|
|
|
b.Property<string>("Version")
|
|
.IsRequired()
|
|
.HasMaxLength(32)
|
|
.HasColumnType("character varying(32)");
|
|
|
|
b.Property<string>("Visibility")
|
|
.IsRequired()
|
|
.HasMaxLength(20)
|
|
.HasColumnType("character varying(20)");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("OrganizationId");
|
|
|
|
b.HasIndex("Status");
|
|
|
|
b.HasIndex("OrganizationId", "SkillKey", "Version")
|
|
.IsUnique();
|
|
|
|
NpgsqlIndexBuilderExtensions.AreNullsDistinct(b.HasIndex("OrganizationId", "SkillKey", "Version"), false);
|
|
|
|
b.ToTable("skills", "skills");
|
|
});
|
|
|
|
modelBuilder.Entity("TeamUp.Modules.Skills.Domain.Skill", b =>
|
|
{
|
|
b.OwnsMany("TeamUp.Modules.Skills.Domain.GoldenExample", "GoldenTests", b1 =>
|
|
{
|
|
b1.Property<Guid>("SkillId");
|
|
|
|
b1.Property<int>("__synthesizedOrdinal")
|
|
.ValueGeneratedOnAdd();
|
|
|
|
b1.Property<string>("Expected")
|
|
.IsRequired();
|
|
|
|
b1.Property<string>("Input")
|
|
.IsRequired();
|
|
|
|
b1.HasKey("SkillId", "__synthesizedOrdinal");
|
|
|
|
b1.ToTable("skills", "skills");
|
|
|
|
b1
|
|
.ToJson("GoldenTests")
|
|
.HasColumnType("jsonb");
|
|
|
|
b1.WithOwner()
|
|
.HasForeignKey("SkillId");
|
|
});
|
|
|
|
b.OwnsMany("TeamUp.Modules.Skills.Domain.SkillAction", "Actions", b1 =>
|
|
{
|
|
b1.Property<Guid>("SkillId");
|
|
|
|
b1.Property<int>("__synthesizedOrdinal")
|
|
.ValueGeneratedOnAdd();
|
|
|
|
b1.Property<string>("Description");
|
|
|
|
b1.Property<string>("Name")
|
|
.IsRequired();
|
|
|
|
b1.Property<int>("Risk");
|
|
|
|
b1.HasKey("SkillId", "__synthesizedOrdinal");
|
|
|
|
b1.ToTable("skills", "skills");
|
|
|
|
b1
|
|
.ToJson("Actions")
|
|
.HasColumnType("jsonb");
|
|
|
|
b1.WithOwner()
|
|
.HasForeignKey("SkillId");
|
|
});
|
|
|
|
b.Navigation("Actions");
|
|
|
|
b.Navigation("GoldenTests");
|
|
});
|
|
#pragma warning restore 612, 618
|
|
}
|
|
}
|
|
}
|