Dynamic per-org skill library: in-app authoring, versioning, fork (+ marketplace seam)
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>
This commit is contained in:
@@ -42,15 +42,18 @@ public sealed class SkillRegistryTests(PostgresFixture postgres) : IClassFixture
|
||||
|
||||
private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId);
|
||||
|
||||
private sealed record ActionDto(string Name, string Risk);
|
||||
private sealed record ActionDto(string Name, string Risk, string? Description);
|
||||
|
||||
private sealed record GoldenTestDto(string Input, string Expected);
|
||||
|
||||
private sealed record SkillSummary(
|
||||
string SkillKey, string Name, string Version, string? Summary, List<string> Roles,
|
||||
string Visibility, string MinTier, string Status, List<ActionDto> Actions);
|
||||
string Visibility, string MinTier, string Status, string Origin, Guid? OrganizationId,
|
||||
int GoldenTestCount, List<ActionDto> Actions);
|
||||
|
||||
private sealed record SkillDetail(
|
||||
SkillSummary Skill, string? Inputs, string? Outputs, List<string> Tools,
|
||||
List<string> Context, int GoldenTestCount, string Body);
|
||||
List<string> Context, List<GoldenTestDto> GoldenTests, string Body);
|
||||
|
||||
[Fact]
|
||||
public async Task Index_publishes_and_makes_skill_queryable_by_role()
|
||||
@@ -73,6 +76,7 @@ public sealed class SkillRegistryTests(PostgresFixture postgres) : IClassFixture
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", owner!.Token);
|
||||
client.DefaultRequestHeaders.Add("X-Skills-Admin-Key", TeamUpWebFactory.PlatformAdminKey);
|
||||
|
||||
// Index the SKILL.md.
|
||||
var indexResponse = await client.PostAsJsonAsync("/api/skills/index", new { content = SpecWritingSkill });
|
||||
@@ -81,7 +85,9 @@ public sealed class SkillRegistryTests(PostgresFixture postgres) : IClassFixture
|
||||
Assert.NotNull(indexed);
|
||||
Assert.Equal("spec-writing", indexed!.Skill.SkillKey);
|
||||
Assert.Equal("Published", indexed.Skill.Status); // has roles + a golden test
|
||||
Assert.Equal(1, indexed.GoldenTestCount);
|
||||
Assert.Equal("Builtin", indexed.Skill.Origin);
|
||||
Assert.Null(indexed.Skill.OrganizationId);
|
||||
Assert.Single(indexed.GoldenTests);
|
||||
Assert.Contains(indexed.Skill.Actions, a => a.Name == "write-spec" && a.Risk == "Draft");
|
||||
|
||||
// Queryable by its role…
|
||||
|
||||
Reference in New Issue
Block a user