This commit is contained in:
StellaOps Bot
2025-12-13 02:22:15 +02:00
parent 564df71bfb
commit 999e26a48e
395 changed files with 25045 additions and 2224 deletions

View File

@@ -0,0 +1,134 @@
using StellaOps.Symbols.Core.Models;
namespace StellaOps.Symbols.Server.Contracts;
/// <summary>
/// Request to upload a symbol manifest.
/// </summary>
public sealed record UploadSymbolManifestRequest(
string DebugId,
string BinaryName,
string? CodeId,
string? Platform,
BinaryFormat Format,
IReadOnlyList<SymbolEntryDto> Symbols,
IReadOnlyList<SourceMappingDto>? SourceMappings);
/// <summary>
/// Symbol entry DTO for API.
/// </summary>
public sealed record SymbolEntryDto(
ulong Address,
ulong Size,
string MangledName,
string? DemangledName,
SymbolType Type,
SymbolBinding Binding,
string? SourceFile,
int? SourceLine,
string? ContentHash);
/// <summary>
/// Source mapping DTO for API.
/// </summary>
public sealed record SourceMappingDto(
string CompiledPath,
string SourcePath,
string? ContentHash);
/// <summary>
/// Response from manifest upload.
/// </summary>
public sealed record UploadSymbolManifestResponse(
string ManifestId,
string DebugId,
string BinaryName,
string? BlobUri,
int SymbolCount,
DateTimeOffset CreatedAt);
/// <summary>
/// Request to resolve symbols.
/// </summary>
public sealed record ResolveSymbolsRequest(
string DebugId,
IReadOnlyList<ulong> Addresses);
/// <summary>
/// Response from symbol resolution.
/// </summary>
public sealed record ResolveSymbolsResponse(
string DebugId,
IReadOnlyList<SymbolResolutionDto> Resolutions);
/// <summary>
/// Symbol resolution DTO.
/// </summary>
public sealed record SymbolResolutionDto(
ulong Address,
bool Found,
string? MangledName,
string? DemangledName,
ulong Offset,
string? SourceFile,
int? SourceLine,
double Confidence);
/// <summary>
/// Symbol manifest list response.
/// </summary>
public sealed record SymbolManifestListResponse(
IReadOnlyList<SymbolManifestSummary> Manifests,
int TotalCount,
int Offset,
int Limit);
/// <summary>
/// Summary of a symbol manifest.
/// </summary>
public sealed record SymbolManifestSummary(
string ManifestId,
string DebugId,
string? CodeId,
string BinaryName,
string? Platform,
BinaryFormat Format,
int SymbolCount,
bool HasDsse,
DateTimeOffset CreatedAt);
/// <summary>
/// Detailed manifest response.
/// </summary>
public sealed record SymbolManifestDetailResponse(
string ManifestId,
string DebugId,
string? CodeId,
string BinaryName,
string? Platform,
BinaryFormat Format,
string TenantId,
string? BlobUri,
string? DsseDigest,
long? RekorLogIndex,
int SymbolCount,
IReadOnlyList<SymbolEntryDto> Symbols,
IReadOnlyList<SourceMappingDto>? SourceMappings,
DateTimeOffset CreatedAt);
/// <summary>
/// Health check response.
/// </summary>
public sealed record SymbolsHealthResponse(
string Status,
string Version,
DateTimeOffset Timestamp,
SymbolsHealthMetrics? Metrics);
/// <summary>
/// Health metrics.
/// </summary>
public sealed record SymbolsHealthMetrics(
long TotalManifests,
long TotalSymbols,
long TotalBlobBytes);

View File

@@ -0,0 +1,323 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Symbols.Core.Abstractions;
using StellaOps.Symbols.Core.Models;
using StellaOps.Symbols.Infrastructure;
using StellaOps.Symbols.Server.Contracts;
var builder = WebApplication.CreateBuilder(args);
// Authentication and Authorization
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configure: options =>
{
options.RequiredScopes.Clear();
});
builder.Services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.FallbackPolicy = options.DefaultPolicy;
});
// Symbols services (in-memory for development)
builder.Services.AddSymbolsInMemory();
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
const string SymbolsReadPolicy = "symbols:read";
const string SymbolsWritePolicy = "symbols:write";
// Health endpoint (anonymous)
app.MapGet("/health", () =>
{
return TypedResults.Ok(new SymbolsHealthResponse(
Status: "healthy",
Version: "1.0.0",
Timestamp: DateTimeOffset.UtcNow,
Metrics: null));
})
.AllowAnonymous()
.WithName("GetHealth")
.WithSummary("Health check endpoint");
// Upload symbol manifest
app.MapPost("/v1/symbols/manifests", async Task<Results<Created<UploadSymbolManifestResponse>, ProblemHttpResult>> (
HttpContext httpContext,
UploadSymbolManifestRequest request,
ISymbolRepository repository,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var symbols = request.Symbols.Select(s => new SymbolEntry
{
Address = s.Address,
Size = s.Size,
MangledName = s.MangledName,
DemangledName = s.DemangledName,
Type = s.Type,
Binding = s.Binding,
SourceFile = s.SourceFile,
SourceLine = s.SourceLine,
ContentHash = s.ContentHash
}).ToList();
var sourceMappings = request.SourceMappings?.Select(m => new SourceMapping
{
CompiledPath = m.CompiledPath,
SourcePath = m.SourcePath,
ContentHash = m.ContentHash
}).ToList();
var manifestId = ComputeManifestId(request.DebugId, tenantId, symbols);
var manifest = new SymbolManifest
{
ManifestId = manifestId,
DebugId = request.DebugId,
CodeId = request.CodeId,
BinaryName = request.BinaryName,
Platform = request.Platform,
Format = request.Format,
Symbols = symbols,
SourceMappings = sourceMappings,
TenantId = tenantId,
CreatedAt = DateTimeOffset.UtcNow
};
await repository.StoreManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
var response = new UploadSymbolManifestResponse(
ManifestId: manifestId,
DebugId: request.DebugId,
BinaryName: request.BinaryName,
BlobUri: manifest.BlobUri,
SymbolCount: symbols.Count,
CreatedAt: manifest.CreatedAt);
return TypedResults.Created($"/v1/symbols/manifests/{manifestId}", response);
})
.RequireAuthorization()
.WithName("UploadSymbolManifest")
.WithSummary("Upload a symbol manifest")
.Produces(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Get manifest by ID
app.MapGet("/v1/symbols/manifests/{manifestId}", async Task<Results<Ok<SymbolManifestDetailResponse>, NotFound, ProblemHttpResult>> (
string manifestId,
ISymbolRepository repository,
CancellationToken cancellationToken) =>
{
var manifest = await repository.GetManifestAsync(manifestId, cancellationToken).ConfigureAwait(false);
if (manifest is null)
{
return TypedResults.NotFound();
}
var response = MapToDetailResponse(manifest);
return TypedResults.Ok(response);
})
.RequireAuthorization()
.WithName("GetSymbolManifest")
.WithSummary("Get symbol manifest by ID")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Query manifests
app.MapGet("/v1/symbols/manifests", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
HttpContext httpContext,
ISymbolRepository repository,
string? debugId,
string? codeId,
string? binaryName,
string? platform,
int? limit,
int? offset,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var query = new SymbolQuery
{
TenantId = tenantId,
DebugId = debugId,
CodeId = codeId,
BinaryName = binaryName,
Platform = platform,
Limit = limit ?? 50,
Offset = offset ?? 0
};
var result = await repository.QueryManifestsAsync(query, cancellationToken).ConfigureAwait(false);
var summaries = result.Manifests.Select(m => new SymbolManifestSummary(
ManifestId: m.ManifestId,
DebugId: m.DebugId,
CodeId: m.CodeId,
BinaryName: m.BinaryName,
Platform: m.Platform,
Format: m.Format,
SymbolCount: m.Symbols.Count,
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
CreatedAt: m.CreatedAt)).ToList();
return TypedResults.Ok(new SymbolManifestListResponse(
Manifests: summaries,
TotalCount: result.TotalCount,
Offset: result.Offset,
Limit: result.Limit));
})
.RequireAuthorization()
.WithName("QuerySymbolManifests")
.WithSummary("Query symbol manifests")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Resolve symbols
app.MapPost("/v1/symbols/resolve", async Task<Results<Ok<ResolveSymbolsResponse>, ProblemHttpResult>> (
HttpContext httpContext,
ResolveSymbolsRequest request,
ISymbolResolver resolver,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var resolutions = await resolver.ResolveBatchAsync(
request.DebugId,
request.Addresses,
tenantId,
cancellationToken).ConfigureAwait(false);
var dtos = resolutions.Select(r => new SymbolResolutionDto(
Address: r.Address,
Found: r.Found,
MangledName: r.Symbol?.MangledName,
DemangledName: r.Symbol?.DemangledName,
Offset: r.Offset,
SourceFile: r.Symbol?.SourceFile,
SourceLine: r.Symbol?.SourceLine,
Confidence: r.Confidence)).ToList();
return TypedResults.Ok(new ResolveSymbolsResponse(
DebugId: request.DebugId,
Resolutions: dtos));
})
.RequireAuthorization()
.WithName("ResolveSymbols")
.WithSummary("Resolve symbol addresses")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Get manifests by debug ID
app.MapGet("/v1/symbols/by-debug-id/{debugId}", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
HttpContext httpContext,
string debugId,
ISymbolRepository repository,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var manifests = await repository.GetManifestsByDebugIdAsync(debugId, tenantId, cancellationToken)
.ConfigureAwait(false);
var summaries = manifests.Select(m => new SymbolManifestSummary(
ManifestId: m.ManifestId,
DebugId: m.DebugId,
CodeId: m.CodeId,
BinaryName: m.BinaryName,
Platform: m.Platform,
Format: m.Format,
SymbolCount: m.Symbols.Count,
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
CreatedAt: m.CreatedAt)).ToList();
return TypedResults.Ok(new SymbolManifestListResponse(
Manifests: summaries,
TotalCount: summaries.Count,
Offset: 0,
Limit: summaries.Count));
})
.RequireAuthorization()
.WithName("GetManifestsByDebugId")
.WithSummary("Get manifests by debug ID")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.Run();
static bool TryGetTenant(HttpContext httpContext, out ProblemHttpResult? problem, out string tenantId)
{
tenantId = string.Empty;
if (!httpContext.Request.Headers.TryGetValue("X-Stella-Tenant", out var tenantValues) ||
string.IsNullOrWhiteSpace(tenantValues))
{
problem = TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_tenant");
return false;
}
tenantId = tenantValues.ToString();
problem = null;
return true;
}
static string ComputeManifestId(string debugId, string tenantId, IReadOnlyList<SymbolEntry> symbols)
{
// Simplified hash computation (should use BLAKE3 in production)
var combined = $"{debugId}:{tenantId}:{symbols.Count}:{DateTimeOffset.UtcNow.Ticks}";
using var sha = System.Security.Cryptography.SHA256.Create();
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combined));
return Convert.ToHexString(hash).ToLowerInvariant()[..32];
}
static SymbolManifestDetailResponse MapToDetailResponse(SymbolManifest manifest)
{
return new SymbolManifestDetailResponse(
ManifestId: manifest.ManifestId,
DebugId: manifest.DebugId,
CodeId: manifest.CodeId,
BinaryName: manifest.BinaryName,
Platform: manifest.Platform,
Format: manifest.Format,
TenantId: manifest.TenantId,
BlobUri: manifest.BlobUri,
DsseDigest: manifest.DsseDigest,
RekorLogIndex: manifest.RekorLogIndex,
SymbolCount: manifest.Symbols.Count,
Symbols: manifest.Symbols.Select(s => new SymbolEntryDto(
s.Address, s.Size, s.MangledName, s.DemangledName,
s.Type, s.Binding, s.SourceFile, s.SourceLine, s.ContentHash)).ToList(),
SourceMappings: manifest.SourceMappings?.Select(m => new SourceMappingDto(
m.CompiledPath, m.SourcePath, m.ContentHash)).ToList(),
CreatedAt: manifest.CreatedAt);
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
<ProjectReference Include="..\StellaOps.Symbols.Infrastructure\StellaOps.Symbols.Infrastructure.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
</ItemGroup>
</Project>