Aggregation-Only Guard Library Reference

Packages: StellaOps.Aoc, StellaOps.Aoc.AspNetCore
Related tasks: WEB-AOC-19-001, WEB-AOC-19-003, DEVOPS-AOC-19-001
Audience: Concelier/Excititor service owners, Platform guild, QA

The Aggregation-Only Contract (AOC) guard library enforces the canonical ingestion rules described in docs/ingestion/aggregation-only-contract.md. Service owners should use the guard whenever raw advisory or VEX payloads are accepted so that forbidden fields are rejected long before they reach MongoDB.

Packages

StellaOps.Aoc

  • IAocGuard / AocWriteGuard — validate JSON payloads and emit AocGuardResult.
  • AocGuardOptions — toggles for signature enforcement, tenant requirements, and required top-level fields.
  • AocViolation / AocViolationCode — structured violations surfaced to callers.
  • AocError — canonical error DTO (code, message, violations[]) re-used by HTTP helpers, CLI tooling, and telemetry.
  • ServiceCollectionExtensions.AddAocGuard() — DI helper that registers the singleton guard.
  • AocGuardExtensions.ValidateOrThrow() — throws AocGuardException when validation fails.

StellaOps.Aoc.AspNetCore

  • AocGuardEndpointFilter<TRequest> — Minimal API endpoint filter that evaluates request payloads through the guard before invoking handlers.
  • AocHttpResults.Problem() — Produces a RFC 7807 payload that includes violation codes, suitable for API responses.

Minimal API integration

using StellaOps.Aoc;
using StellaOps.Aoc.AspNetCore.Routing;
using StellaOps.Aoc.AspNetCore.Results;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAocGuard();
builder.Services.Configure<AocGuardOptions>(options =>
{
    options.RequireSignatureMetadata = true;
    options.RequireTenant = true;
});

var app = builder.Build();

app.MapPost("/ingest", async (IngestionRequest request, IAocGuard guard, ILogger<Program> logger) =>
    {
        // additional application logic
        return Results.Accepted();
    })
    .AddEndpointFilter(new AocGuardEndpointFilter<IngestionRequest>(
        request => new object?[] { request.Payload },
        serializerOptions: null,
        guardOptions: null))
    .ProducesProblem(StatusCodes.Status400BadRequest)
    .WithTags("AOC");

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exceptionHandler = context.Features.Get<IExceptionHandlerFeature>();
        if (exceptionHandler?.Error is AocGuardException guardException)
        {
            var result = AocHttpResults.Problem(context, guardException);
            await result.ExecuteAsync(context);
            return;
        }

        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
    });
});

Key points:

  • Register the guard singleton before wiring repositories or worker services.
  • Use AocGuardEndpointFilter<TRequest> to protect Minimal API endpoints. The payloadSelector can yield multiple payloads (e.g. batch ingestion) and the filter will validate each one.
  • Prefer the RequireAocGuard extension when wiring endpoints; it wraps AddEndpointFilter and handles single-payload scenarios without additional boilerplate.
  • Wrap guard exceptions with AocHttpResults.Problem to ensure clients receive machine-readable codes (ERR_AOC_00x). The helper now emits the serialized AocError under the error extension for consumers that want a typed payload.

Allowed top-level fields

AocWriteGuard enforces the contract’s top-level allowlist: _id, tenant, source, upstream, content, identifiers, linkset, supersedes, createdAt/created_at, ingestedAt/ingested_at, and attributes. Unknown fields produce ERR_AOC_007 violations. When staging schema changes, extend the allowlist through AocGuardOptions.AllowedTopLevelFields:

builder.Services.Configure<AocGuardOptions>(options =>
{
    options.AllowedTopLevelFields =
        options.AllowedTopLevelFields.Add("experimental_field");
});

Worker / repository usage

Inject IAocGuard (or a module-specific wrapper such as IVexRawWriteGuard) anywhere documents are persisted. Call ValidateOrThrow before writes to guarantee fail-fast behaviour, for example:

public sealed class AdvisoryRawRepository
{
    private readonly IAocGuard _guard;

    public AdvisoryRawRepository(IAocGuard guard) => _guard = guard;

    public Task WriteAsync(JsonDocument document, CancellationToken cancellationToken)
    {
        _guard.ValidateOrThrow(document.RootElement);
        // proceed with storage logic
    }
}

Configuration tips

  • Adjust AocGuardOptions.RequiredTopLevelFields when staging new schema changes. All configured names are case-insensitive.
  • Extend AllowedTopLevelFields for temporary schema experiments so that guard runs stay clean while the contract is updated.
  • Set RequireSignatureMetadata = false for legacy feeds that do not provide signature envelopes yet; track the waiver in the module backlog.
  • Use module-specific wrappers (AddConcelierAocGuards, AddExcititorAocGuards) to combine guard registration with domain exceptions and metrics.

Testing guidance

  • Unit-test guard behaviour with fixture payloads (see src/Aoc/__Tests).
  • Service-level tests should assert that ingestion endpoints return ERR_AOC_* codes via AocHttpResults.
  • CI must run stella aoc verify once CLI support lands (DEVOPS-AOC-19-002).
  • Roslyn analyzer enforcement (WEB-AOC-19-003) will ensure the guard is registered; keep services wired through the shared extensions to prepare for that gate.

For questions or updates, coordinate with the BE‑Base Platform guild and reference WEB-AOC-19-001.