Skip to content

Commit cae512e

Browse files
Add native skill manifest endpoints
1 parent 0a3034c commit cae512e

10 files changed

Lines changed: 527 additions & 4 deletions

File tree

src/SkillServer/Data/SkillRepository.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ await connection.ExecuteAsync(
101101
size_bytes AS SizeBytes,
102102
COALESCE(artifact_sha256, sha256) AS ArtifactSha256,
103103
COALESCE(artifact_size_bytes, size_bytes) AS ArtifactSizeBytes,
104+
routes_to_subagent AS RoutesToSubagent,
104105
published_at AS PublishedAt, is_latest AS IsLatest
105106
FROM skill_versions
106107
WHERE skill_id = @skillId AND is_latest = 1
@@ -118,6 +119,7 @@ FROM skill_versions
118119
size_bytes AS SizeBytes,
119120
COALESCE(artifact_sha256, sha256) AS ArtifactSha256,
120121
COALESCE(artifact_size_bytes, size_bytes) AS ArtifactSizeBytes,
122+
routes_to_subagent AS RoutesToSubagent,
121123
published_at AS PublishedAt, is_latest AS IsLatest
122124
FROM skill_versions
123125
WHERE skill_id = @skillId AND version = @version
@@ -135,6 +137,7 @@ public async Task<IReadOnlyList<SkillVersion>> GetAllVersionsAsync(long skillId,
135137
size_bytes AS SizeBytes,
136138
COALESCE(artifact_sha256, sha256) AS ArtifactSha256,
137139
COALESCE(artifact_size_bytes, size_bytes) AS ArtifactSizeBytes,
140+
routes_to_subagent AS RoutesToSubagent,
138141
published_at AS PublishedAt, is_latest AS IsLatest
139142
FROM skill_versions
140143
WHERE skill_id = @skillId
@@ -154,6 +157,7 @@ public async Task<IReadOnlyList<SkillVersion>> GetResourcefulVersionsWithoutArch
154157
sv.size_bytes AS SizeBytes,
155158
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
156159
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
160+
sv.routes_to_subagent AS RoutesToSubagent,
157161
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest
158162
FROM skill_versions sv
159163
WHERE sv.skill_type != @archiveType
@@ -174,6 +178,7 @@ public async Task<IReadOnlyList<SkillVersionWithFileCount>> GetAllVersionsWithFi
174178
sv.size_bytes AS SizeBytes,
175179
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
176180
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
181+
sv.routes_to_subagent AS RoutesToSubagent,
177182
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
178183
(SELECT COUNT(*) FROM skill_files sf WHERE sf.skill_version_id = sv.id) AS FileCount
179184
FROM skill_versions sv
@@ -197,6 +202,7 @@ public async Task<IReadOnlyList<SkillVersionWithMetadata>> GetAllLatestVersionsW
197202
sv.size_bytes AS SizeBytes,
198203
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
199204
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
205+
sv.routes_to_subagent AS RoutesToSubagent,
200206
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
201207
s.name AS SkillName, s.created_at AS SkillCreatedAt, s.updated_at AS SkillUpdatedAt,
202208
(SELECT COUNT(*) FROM skill_versions sv2 WHERE sv2.skill_id = s.id) AS VersionCount
@@ -233,7 +239,8 @@ public async Task<long> CreateVersionAsync(
233239
long sizeBytes,
234240
CancellationToken ct = default,
235241
string? artifactSha256 = null,
236-
long? artifactSizeBytes = null)
242+
long? artifactSizeBytes = null,
243+
string? routesToSubagent = null)
237244
{
238245
await using var connection = new SqliteConnection(_connectionString);
239246
await connection.OpenAsync(ct);
@@ -252,8 +259,8 @@ await connection.ExecuteAsync(
252259

253260
var versionId = await connection.ExecuteScalarAsync<long>(
254261
"""
255-
INSERT INTO skill_versions (skill_id, version, description, category, skill_type, sha256, size_bytes, artifact_sha256, artifact_size_bytes, published_at, is_latest)
256-
VALUES (@skillId, @version, @description, @category, @typeString, @sha256, @sizeBytes, @artifactSha256, @artifactSizeBytes, @now, 1);
262+
INSERT INTO skill_versions (skill_id, version, description, category, skill_type, sha256, size_bytes, artifact_sha256, artifact_size_bytes, routes_to_subagent, published_at, is_latest)
263+
VALUES (@skillId, @version, @description, @category, @typeString, @sha256, @sizeBytes, @artifactSha256, @artifactSizeBytes, @routesToSubagent, @now, 1);
257264
SELECT last_insert_rowid();
258265
""",
259266
new
@@ -267,6 +274,7 @@ INSERT INTO skill_versions (skill_id, version, description, category, skill_type
267274
sizeBytes,
268275
artifactSha256 = artifactSha256 ?? sha256,
269276
artifactSizeBytes = artifactSizeBytes ?? sizeBytes,
277+
routesToSubagent,
270278
now
271279
},
272280
transaction);
@@ -346,6 +354,7 @@ public async Task<IReadOnlyList<SkillVersionWithMetadata>> SearchSkillsAsync(
346354
sv.size_bytes AS SizeBytes,
347355
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
348356
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
357+
sv.routes_to_subagent AS RoutesToSubagent,
349358
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
350359
s.name AS SkillName, s.created_at AS SkillCreatedAt, s.updated_at AS SkillUpdatedAt,
351360
(SELECT COUNT(*) FROM skill_versions sv2 WHERE sv2.skill_id = s.id) AS VersionCount
@@ -383,6 +392,7 @@ private static string EscapeFtsToken(string token)
383392
sv.size_bytes AS SizeBytes,
384393
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
385394
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
395+
sv.routes_to_subagent AS RoutesToSubagent,
386396
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
387397
(SELECT COUNT(*) FROM skill_files sf WHERE sf.skill_version_id = sv.id) AS FileCount
388398
FROM skill_versions sv

src/SkillServer/Endpoints.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public static class Endpoints
1515
public static WebApplication MapSkillServerEndpoints(this WebApplication app)
1616
{
1717
app.MapDiscoveryEndpoints();
18+
app.MapManifestEndpoints();
1819
app.MapSkillEndpoints();
1920
app.MapBlobEndpoints();
2021
app.MapApiKeyEndpoints();
@@ -33,6 +34,61 @@ private static void MapDiscoveryEndpoints(this WebApplication app)
3334
});
3435
}
3536

37+
private static void MapManifestEndpoints(this WebApplication app)
38+
{
39+
app.MapGet("/manifest.json", async (
40+
NativeManifestGenerator manifestGenerator,
41+
CancellationToken ct) =>
42+
{
43+
var manifest = await manifestGenerator.GenerateRootAsync(ct);
44+
return Results.Json(manifest, SkillServerJsonContext.Default.NativeRootManifest);
45+
});
46+
47+
var manifest = app.MapGroup("/manifest");
48+
49+
manifest.MapGet("/skills/index.json", async (
50+
NativeManifestGenerator manifestGenerator,
51+
CancellationToken ct) =>
52+
{
53+
var index = await manifestGenerator.GenerateSkillIndexAsync(ct);
54+
return Results.Json(index, SkillServerJsonContext.Default.NativeSkillCollectionIndex);
55+
});
56+
57+
manifest.MapGet("/skills/pages/{page}.json", async (
58+
string page,
59+
NativeManifestGenerator manifestGenerator,
60+
CancellationToken ct) =>
61+
{
62+
var skillPage = await manifestGenerator.GenerateSkillPageAsync(page, ct);
63+
return skillPage is null
64+
? Results.NotFound()
65+
: Results.Json(skillPage, SkillServerJsonContext.Default.NativeSkillCollectionPage);
66+
});
67+
68+
manifest.MapGet("/skills/{skillName}/index.json", async (
69+
string skillName,
70+
NativeManifestGenerator manifestGenerator,
71+
CancellationToken ct) =>
72+
{
73+
var skill = await manifestGenerator.GenerateSkillIdentityAsync(skillName, ct);
74+
return skill is null
75+
? Results.NotFound()
76+
: Results.Json(skill, SkillServerJsonContext.Default.NativeSkillIdentityIndex);
77+
});
78+
79+
manifest.MapGet("/skills/{skillName}/versions/{version}.json", async (
80+
string skillName,
81+
string version,
82+
NativeManifestGenerator manifestGenerator,
83+
CancellationToken ct) =>
84+
{
85+
var skillVersion = await manifestGenerator.GenerateSkillVersionAsync(skillName, version, ct);
86+
return skillVersion is null
87+
? Results.NotFound()
88+
: Results.Json(skillVersion, SkillServerJsonContext.Default.NativeSkillVersionDetail);
89+
});
90+
}
91+
3692
private static void MapSkillEndpoints(this WebApplication app)
3793
{
3894
var skills = app.MapGroup("/skills");
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// -----------------------------------------------------------------------
2+
// <copyright file="NativeManifest.cs" company="Petabridge, LLC">
3+
// Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com>
4+
// </copyright>
5+
// -----------------------------------------------------------------------
6+
using System.Text.Json.Serialization;
7+
8+
namespace SkillServer.Models;
9+
10+
public sealed record NativeManifestLink
11+
{
12+
[JsonPropertyName("href")]
13+
public required string Href { get; init; }
14+
}
15+
16+
public sealed record NativeManifestSelfLinks
17+
{
18+
[JsonPropertyName("self")]
19+
public required NativeManifestLink Self { get; init; }
20+
}
21+
22+
public sealed record NativeRootManifestLinks
23+
{
24+
[JsonPropertyName("self")]
25+
public required NativeManifestLink Self { get; init; }
26+
27+
[JsonPropertyName("rfcSkills")]
28+
public required NativeManifestLink RfcSkills { get; init; }
29+
30+
[JsonPropertyName("skills")]
31+
public required NativeManifestLink Skills { get; init; }
32+
}
33+
34+
public sealed record NativeRootManifest
35+
{
36+
[JsonPropertyName("$schema")]
37+
public string Schema { get; init; } = "https://schemas.netclaw.dev/skillserver/manifest/0.1.0";
38+
39+
[JsonPropertyName("generatedAt")]
40+
public required DateTimeOffset GeneratedAt { get; init; }
41+
42+
[JsonPropertyName("links")]
43+
public required NativeRootManifestLinks Links { get; init; }
44+
}
45+
46+
public sealed record NativeManifestPageLink
47+
{
48+
[JsonPropertyName("range")]
49+
public required string Range { get; init; }
50+
51+
[JsonPropertyName("href")]
52+
public required string Href { get; init; }
53+
}
54+
55+
public sealed record NativeSkillCollectionIndex
56+
{
57+
[JsonPropertyName("kind")]
58+
public string Kind { get; init; } = "skill-index";
59+
60+
[JsonPropertyName("links")]
61+
public required NativeManifestSelfLinks Links { get; init; }
62+
63+
[JsonPropertyName("pages")]
64+
public required IReadOnlyList<NativeManifestPageLink> Pages { get; init; }
65+
}
66+
67+
public sealed record NativeVersionRange
68+
{
69+
[JsonPropertyName("min")]
70+
public required string Min { get; init; }
71+
72+
[JsonPropertyName("max")]
73+
public required string Max { get; init; }
74+
75+
[JsonPropertyName("count")]
76+
public required int Count { get; init; }
77+
}
78+
79+
public sealed record NativeSkillPageItem
80+
{
81+
[JsonPropertyName("name")]
82+
public required string Name { get; init; }
83+
84+
[JsonPropertyName("latestVersion")]
85+
public required string LatestVersion { get; init; }
86+
87+
[JsonPropertyName("versionRange")]
88+
public required NativeVersionRange VersionRange { get; init; }
89+
90+
[JsonPropertyName("href")]
91+
public required string Href { get; init; }
92+
}
93+
94+
public sealed record NativeSkillCollectionPage
95+
{
96+
[JsonPropertyName("kind")]
97+
public string Kind { get; init; } = "skill-page";
98+
99+
[JsonPropertyName("range")]
100+
public required string Range { get; init; }
101+
102+
[JsonPropertyName("items")]
103+
public required IReadOnlyList<NativeSkillPageItem> Items { get; init; }
104+
105+
[JsonPropertyName("links")]
106+
public required NativeManifestSelfLinks Links { get; init; }
107+
}
108+
109+
public sealed record NativeSkillVersionLink
110+
{
111+
[JsonPropertyName("version")]
112+
public required string Version { get; init; }
113+
114+
[JsonPropertyName("publishedAt")]
115+
public required DateTimeOffset PublishedAt { get; init; }
116+
117+
[JsonPropertyName("digest")]
118+
public required string Digest { get; init; }
119+
120+
[JsonPropertyName("href")]
121+
public required string Href { get; init; }
122+
}
123+
124+
public sealed record NativeSkillIdentityIndex
125+
{
126+
[JsonPropertyName("kind")]
127+
public string Kind { get; init; } = "skill";
128+
129+
[JsonPropertyName("name")]
130+
public required string Name { get; init; }
131+
132+
[JsonPropertyName("latestVersion")]
133+
public required string LatestVersion { get; init; }
134+
135+
[JsonPropertyName("versions")]
136+
public required IReadOnlyList<NativeSkillVersionLink> Versions { get; init; }
137+
138+
[JsonPropertyName("links")]
139+
public required NativeManifestSelfLinks Links { get; init; }
140+
}
141+
142+
public sealed record NativeSkillArtifact
143+
{
144+
[JsonPropertyName("name")]
145+
public required string Name { get; init; }
146+
147+
[JsonPropertyName("version")]
148+
public required string Version { get; init; }
149+
150+
[JsonPropertyName("type")]
151+
public required string Type { get; init; }
152+
153+
[JsonPropertyName("description")]
154+
public required string Description { get; init; }
155+
156+
[JsonPropertyName("url")]
157+
public required string Url { get; init; }
158+
159+
[JsonPropertyName("digest")]
160+
public required string Digest { get; init; }
161+
}
162+
163+
public sealed record NativeSubagentRoute
164+
{
165+
[JsonPropertyName("name")]
166+
public required string Name { get; init; }
167+
168+
[JsonPropertyName("href")]
169+
public required string Href { get; init; }
170+
}
171+
172+
public sealed record NativeSkillVersionDetail
173+
{
174+
[JsonPropertyName("kind")]
175+
public string Kind { get; init; } = "skill-version";
176+
177+
[JsonPropertyName("artifact")]
178+
public required NativeSkillArtifact Artifact { get; init; }
179+
180+
[JsonPropertyName("routesToSubagent")]
181+
public NativeSubagentRoute? RoutesToSubagent { get; init; }
182+
183+
[JsonPropertyName("links")]
184+
public required NativeManifestSelfLinks Links { get; init; }
185+
}

src/SkillServer/Models/Skill.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public sealed record SkillVersion
3131
public required long SizeBytes { get; init; }
3232
public required string ArtifactSha256 { get; init; }
3333
public required long ArtifactSizeBytes { get; init; }
34+
public string? RoutesToSubagent { get; init; }
3435
public required DateTimeOffset PublishedAt { get; init; }
3536
public required bool IsLatest { get; init; }
3637
}
@@ -62,6 +63,7 @@ public sealed record SkillVersionWithFileCount
6263
public required long SizeBytes { get; init; }
6364
public required string ArtifactSha256 { get; init; }
6465
public required long ArtifactSizeBytes { get; init; }
66+
public string? RoutesToSubagent { get; init; }
6567
public required DateTimeOffset PublishedAt { get; init; }
6668
public required bool IsLatest { get; init; }
6769
public required int FileCount { get; init; }
@@ -82,6 +84,7 @@ public sealed record SkillVersionWithMetadata
8284
public required long SizeBytes { get; init; }
8385
public required string ArtifactSha256 { get; init; }
8486
public required long ArtifactSizeBytes { get; init; }
87+
public string? RoutesToSubagent { get; init; }
8588
public required DateTimeOffset PublishedAt { get; init; }
8689
public required bool IsLatest { get; init; }
8790
public required string SkillName { get; init; }

src/SkillServer/Models/SkillServerJsonContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ namespace SkillServer.Models;
2727
[JsonSerializable(typeof(IReadOnlyList<CheckUpdateRequestItem>))]
2828
[JsonSerializable(typeof(IReadOnlyList<CheckUpdateResponseItem>))]
2929
[JsonSerializable(typeof(HealthResponse))]
30+
[JsonSerializable(typeof(NativeRootManifest))]
31+
[JsonSerializable(typeof(NativeSkillCollectionIndex))]
32+
[JsonSerializable(typeof(NativeSkillCollectionPage))]
33+
[JsonSerializable(typeof(NativeSkillIdentityIndex))]
34+
[JsonSerializable(typeof(NativeSkillVersionDetail))]
3035
[JsonSourceGenerationOptions(
3136
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
3237
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]

src/SkillServer/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
builder.Services.AddSingleton<SkillRepository>();
2626
builder.Services.AddSingleton<ApiKeyRepository>();
2727
builder.Services.AddSingleton<IndexGenerator>();
28+
builder.Services.AddSingleton<NativeManifestGenerator>();
2829
builder.Services.AddSingleton<SkillUploadService>();
2930
builder.Services.AddSingleton<SkillArchiveBackfillService>();
3031
builder.Services.AddSingleton<ApiKeyService>();

0 commit comments

Comments
 (0)