Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/SkillServer/Data/SkillRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ await connection.ExecuteAsync(
size_bytes AS SizeBytes,
COALESCE(artifact_sha256, sha256) AS ArtifactSha256,
COALESCE(artifact_size_bytes, size_bytes) AS ArtifactSizeBytes,
routes_to_subagent AS RoutesToSubagent,
published_at AS PublishedAt, is_latest AS IsLatest
FROM skill_versions
WHERE skill_id = @skillId AND is_latest = 1
Expand All @@ -118,6 +119,7 @@ FROM skill_versions
size_bytes AS SizeBytes,
COALESCE(artifact_sha256, sha256) AS ArtifactSha256,
COALESCE(artifact_size_bytes, size_bytes) AS ArtifactSizeBytes,
routes_to_subagent AS RoutesToSubagent,
published_at AS PublishedAt, is_latest AS IsLatest
FROM skill_versions
WHERE skill_id = @skillId AND version = @version
Expand All @@ -135,6 +137,7 @@ public async Task<IReadOnlyList<SkillVersion>> GetAllVersionsAsync(long skillId,
size_bytes AS SizeBytes,
COALESCE(artifact_sha256, sha256) AS ArtifactSha256,
COALESCE(artifact_size_bytes, size_bytes) AS ArtifactSizeBytes,
routes_to_subagent AS RoutesToSubagent,
published_at AS PublishedAt, is_latest AS IsLatest
FROM skill_versions
WHERE skill_id = @skillId
Expand All @@ -154,6 +157,7 @@ public async Task<IReadOnlyList<SkillVersion>> GetResourcefulVersionsWithoutArch
sv.size_bytes AS SizeBytes,
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
sv.routes_to_subagent AS RoutesToSubagent,
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest
FROM skill_versions sv
WHERE sv.skill_type != @archiveType
Expand All @@ -174,6 +178,7 @@ public async Task<IReadOnlyList<SkillVersionWithFileCount>> GetAllVersionsWithFi
sv.size_bytes AS SizeBytes,
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
sv.routes_to_subagent AS RoutesToSubagent,
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
(SELECT COUNT(*) FROM skill_files sf WHERE sf.skill_version_id = sv.id) AS FileCount
FROM skill_versions sv
Expand All @@ -197,6 +202,7 @@ public async Task<IReadOnlyList<SkillVersionWithMetadata>> GetAllLatestVersionsW
sv.size_bytes AS SizeBytes,
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
sv.routes_to_subagent AS RoutesToSubagent,
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
s.name AS SkillName, s.created_at AS SkillCreatedAt, s.updated_at AS SkillUpdatedAt,
(SELECT COUNT(*) FROM skill_versions sv2 WHERE sv2.skill_id = s.id) AS VersionCount
Expand Down Expand Up @@ -233,7 +239,8 @@ public async Task<long> CreateVersionAsync(
long sizeBytes,
CancellationToken ct = default,
string? artifactSha256 = null,
long? artifactSizeBytes = null)
long? artifactSizeBytes = null,
string? routesToSubagent = null)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync(ct);
Expand All @@ -252,8 +259,8 @@ await connection.ExecuteAsync(

var versionId = await connection.ExecuteScalarAsync<long>(
"""
INSERT INTO skill_versions (skill_id, version, description, category, skill_type, sha256, size_bytes, artifact_sha256, artifact_size_bytes, published_at, is_latest)
VALUES (@skillId, @version, @description, @category, @typeString, @sha256, @sizeBytes, @artifactSha256, @artifactSizeBytes, @now, 1);
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)
VALUES (@skillId, @version, @description, @category, @typeString, @sha256, @sizeBytes, @artifactSha256, @artifactSizeBytes, @routesToSubagent, @now, 1);
SELECT last_insert_rowid();
""",
new
Expand All @@ -267,6 +274,7 @@ INSERT INTO skill_versions (skill_id, version, description, category, skill_type
sizeBytes,
artifactSha256 = artifactSha256 ?? sha256,
artifactSizeBytes = artifactSizeBytes ?? sizeBytes,
routesToSubagent,
now
},
transaction);
Expand Down Expand Up @@ -346,6 +354,7 @@ public async Task<IReadOnlyList<SkillVersionWithMetadata>> SearchSkillsAsync(
sv.size_bytes AS SizeBytes,
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
sv.routes_to_subagent AS RoutesToSubagent,
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
s.name AS SkillName, s.created_at AS SkillCreatedAt, s.updated_at AS SkillUpdatedAt,
(SELECT COUNT(*) FROM skill_versions sv2 WHERE sv2.skill_id = s.id) AS VersionCount
Expand Down Expand Up @@ -383,6 +392,7 @@ private static string EscapeFtsToken(string token)
sv.size_bytes AS SizeBytes,
COALESCE(sv.artifact_sha256, sv.sha256) AS ArtifactSha256,
COALESCE(sv.artifact_size_bytes, sv.size_bytes) AS ArtifactSizeBytes,
sv.routes_to_subagent AS RoutesToSubagent,
sv.published_at AS PublishedAt, sv.is_latest AS IsLatest,
(SELECT COUNT(*) FROM skill_files sf WHERE sf.skill_version_id = sv.id) AS FileCount
FROM skill_versions sv
Expand Down
56 changes: 56 additions & 0 deletions src/SkillServer/Endpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static class Endpoints
public static WebApplication MapSkillServerEndpoints(this WebApplication app)
{
app.MapDiscoveryEndpoints();
app.MapManifestEndpoints();
app.MapSkillEndpoints();
app.MapBlobEndpoints();
app.MapApiKeyEndpoints();
Expand All @@ -33,6 +34,61 @@ private static void MapDiscoveryEndpoints(this WebApplication app)
});
}

private static void MapManifestEndpoints(this WebApplication app)
{
app.MapGet("/manifest.json", async (
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var manifest = await manifestGenerator.GenerateRootAsync(ct);
return Results.Json(manifest, SkillServerJsonContext.Default.NativeRootManifest);
});

var manifest = app.MapGroup("/manifest");

manifest.MapGet("/skills/index.json", async (
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var index = await manifestGenerator.GenerateSkillIndexAsync(ct);
return Results.Json(index, SkillServerJsonContext.Default.NativeSkillCollectionIndex);
});

manifest.MapGet("/skills/pages/{page}.json", async (
string page,
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var skillPage = await manifestGenerator.GenerateSkillPageAsync(page, ct);
return skillPage is null
? Results.NotFound()
: Results.Json(skillPage, SkillServerJsonContext.Default.NativeSkillCollectionPage);
});

manifest.MapGet("/skills/{skillName}/index.json", async (
string skillName,
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var skill = await manifestGenerator.GenerateSkillIdentityAsync(skillName, ct);
return skill is null
? Results.NotFound()
: Results.Json(skill, SkillServerJsonContext.Default.NativeSkillIdentityIndex);
});

manifest.MapGet("/skills/{skillName}/versions/{version}.json", async (
string skillName,
string version,
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var skillVersion = await manifestGenerator.GenerateSkillVersionAsync(skillName, version, ct);
return skillVersion is null
? Results.NotFound()
: Results.Json(skillVersion, SkillServerJsonContext.Default.NativeSkillVersionDetail);
});
}

private static void MapSkillEndpoints(this WebApplication app)
{
var skills = app.MapGroup("/skills");
Expand Down
185 changes: 185 additions & 0 deletions src/SkillServer/Models/NativeManifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// -----------------------------------------------------------------------
// <copyright file="NativeManifest.cs" company="Petabridge, LLC">
// Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com>
// </copyright>
// -----------------------------------------------------------------------
using System.Text.Json.Serialization;

namespace SkillServer.Models;

public sealed record NativeManifestLink
{
[JsonPropertyName("href")]
public required string Href { get; init; }
}

public sealed record NativeManifestSelfLinks
{
[JsonPropertyName("self")]
public required NativeManifestLink Self { get; init; }
}

public sealed record NativeRootManifestLinks
{
[JsonPropertyName("self")]
public required NativeManifestLink Self { get; init; }

[JsonPropertyName("rfcSkills")]
public required NativeManifestLink RfcSkills { get; init; }

[JsonPropertyName("skills")]
public required NativeManifestLink Skills { get; init; }
}

public sealed record NativeRootManifest
{
[JsonPropertyName("$schema")]
public string Schema { get; init; } = "https://schemas.netclaw.dev/skillserver/manifest/0.1.0";

[JsonPropertyName("generatedAt")]
public required DateTimeOffset GeneratedAt { get; init; }

[JsonPropertyName("links")]
public required NativeRootManifestLinks Links { get; init; }
}

public sealed record NativeManifestPageLink
{
[JsonPropertyName("range")]
public required string Range { get; init; }

[JsonPropertyName("href")]
public required string Href { get; init; }
}

public sealed record NativeSkillCollectionIndex
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "skill-index";

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }

[JsonPropertyName("pages")]
public required IReadOnlyList<NativeManifestPageLink> Pages { get; init; }
}

public sealed record NativeVersionRange
{
[JsonPropertyName("min")]
public required string Min { get; init; }

[JsonPropertyName("max")]
public required string Max { get; init; }

[JsonPropertyName("count")]
public required int Count { get; init; }
}

public sealed record NativeSkillPageItem
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("latestVersion")]
public required string LatestVersion { get; init; }

[JsonPropertyName("versionRange")]
public required NativeVersionRange VersionRange { get; init; }

[JsonPropertyName("href")]
public required string Href { get; init; }
}

public sealed record NativeSkillCollectionPage
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "skill-page";

[JsonPropertyName("range")]
public required string Range { get; init; }

[JsonPropertyName("items")]
public required IReadOnlyList<NativeSkillPageItem> Items { get; init; }

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }
}

public sealed record NativeSkillVersionLink
{
[JsonPropertyName("version")]
public required string Version { get; init; }

[JsonPropertyName("publishedAt")]
public required DateTimeOffset PublishedAt { get; init; }

[JsonPropertyName("digest")]
public required string Digest { get; init; }

[JsonPropertyName("href")]
public required string Href { get; init; }
}

public sealed record NativeSkillIdentityIndex
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "skill";

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("latestVersion")]
public required string LatestVersion { get; init; }

[JsonPropertyName("versions")]
public required IReadOnlyList<NativeSkillVersionLink> Versions { get; init; }

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }
}

public sealed record NativeSkillArtifact
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("version")]
public required string Version { get; init; }

[JsonPropertyName("type")]
public required string Type { get; init; }

[JsonPropertyName("description")]
public required string Description { get; init; }

[JsonPropertyName("url")]
public required string Url { get; init; }

[JsonPropertyName("digest")]
public required string Digest { get; init; }
}

public sealed record NativeSubagentRoute
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("href")]
public required string Href { get; init; }
}

public sealed record NativeSkillVersionDetail
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "skill-version";

[JsonPropertyName("artifact")]
public required NativeSkillArtifact Artifact { get; init; }

[JsonPropertyName("routesToSubagent")]
public NativeSubagentRoute? RoutesToSubagent { get; init; }

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }
}
3 changes: 3 additions & 0 deletions src/SkillServer/Models/Skill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public sealed record SkillVersion
public required long SizeBytes { get; init; }
public required string ArtifactSha256 { get; init; }
public required long ArtifactSizeBytes { get; init; }
public string? RoutesToSubagent { get; init; }
public required DateTimeOffset PublishedAt { get; init; }
public required bool IsLatest { get; init; }
}
Expand Down Expand Up @@ -62,6 +63,7 @@ public sealed record SkillVersionWithFileCount
public required long SizeBytes { get; init; }
public required string ArtifactSha256 { get; init; }
public required long ArtifactSizeBytes { get; init; }
public string? RoutesToSubagent { get; init; }
public required DateTimeOffset PublishedAt { get; init; }
public required bool IsLatest { get; init; }
public required int FileCount { get; init; }
Expand All @@ -82,6 +84,7 @@ public sealed record SkillVersionWithMetadata
public required long SizeBytes { get; init; }
public required string ArtifactSha256 { get; init; }
public required long ArtifactSizeBytes { get; init; }
public string? RoutesToSubagent { get; init; }
public required DateTimeOffset PublishedAt { get; init; }
public required bool IsLatest { get; init; }
public required string SkillName { get; init; }
Expand Down
5 changes: 5 additions & 0 deletions src/SkillServer/Models/SkillServerJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ namespace SkillServer.Models;
[JsonSerializable(typeof(IReadOnlyList<CheckUpdateRequestItem>))]
[JsonSerializable(typeof(IReadOnlyList<CheckUpdateResponseItem>))]
[JsonSerializable(typeof(HealthResponse))]
[JsonSerializable(typeof(NativeRootManifest))]
[JsonSerializable(typeof(NativeSkillCollectionIndex))]
[JsonSerializable(typeof(NativeSkillCollectionPage))]
[JsonSerializable(typeof(NativeSkillIdentityIndex))]
[JsonSerializable(typeof(NativeSkillVersionDetail))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
Expand Down
1 change: 1 addition & 0 deletions src/SkillServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
builder.Services.AddSingleton<SkillRepository>();
builder.Services.AddSingleton<ApiKeyRepository>();
builder.Services.AddSingleton<IndexGenerator>();
builder.Services.AddSingleton<NativeManifestGenerator>();
builder.Services.AddSingleton<SkillUploadService>();
builder.Services.AddSingleton<SkillArchiveBackfillService>();
builder.Services.AddSingleton<ApiKeyService>();
Expand Down
Loading
Loading