AOC architecture

WITHDRAWN (2026-09-27, SPRINT_20260927_003 PCD-62). The AOC libraries (StellaOps.Aoc, StellaOps.Aoc.AspNetCore), the StellaOps.Aoc.Analyzers Roslyn analyzer and the CLI Aoc plug-in are no longer built or shipped. Their sources are frozen under src/__Obsoleted/Aoc/ and src/__Obsoleted/Cli/ (CODE_OF_CONDUCT 15.4). No service referenced them, and the plug-in’s direct-PostgreSQL verify form (--postgres option) read an aoc_documents table that no migration creates. This page describes the withdrawn code and is kept for reference. Still live and separate: the aoc:verify scope. The core CLI aoc command was withdrawn on 2026-10-01 (SPRINT_20260928_008 PCD-153); the Platform /api/v1/aoc/* compatibility endpoints (Development and testing only) and the Console AOC pages were withdrawn on 2026-10-08 (SPRINT_20261007_002 CCP-005 and CCP-004).

Verified against commit 942a18727c (2026-09-26). Re-verify with: find src/Aoc -name '*.csproj' && grep -rl --include=*.csproj 'StellaOps.Aoc' src | grep -v -e '^src/Aoc/' -e '^src/__Obsoleted/' && grep -rln -e IAocGuard -e RequireAocGuard src --include=*.cs | grep -v -e '^src/Aoc/' -e '^src/__Obsoleted/'

Stella Ops AOC (Aggregation-Only Contract) — enforcement via a runtime write-guard library plus a Roslyn analyzer.

Scope. Library architecture for AOC. AOC is shared libraries plus an analyzer (src/Aoc/) — it has no host, no HTTP service, no database and no compose or release-bundle entry. It ships:

  • a runtime guard (IAocGuard / AocWriteGuard) that validates ingestion documents before they are persisted,
  • an ASP.NET Core minimal-API filter over that guard, and
  • a Roslyn analyzer that catches forbidden-field writes at compile time.

Consumer status (2026-09-26). The former runtime consumers, Concelier and Excititor, were retired (services stopped 2026-08-04; sources frozen under src/__Obsoleted/Concelier/). The production ingestion path, the vulnerability hub, does not reference AOC. No live deployable enforces the guard today; see §7. The canonical term is Aggregation-Only Contract; some source comments say “Append-Only Contract” for the same concept.


0) Mission & boundaries

Mission. Enforce aggregation-only contract rules during data ingestion. Prevent connectors from writing to fields that should only be computed by downstream merge/decisioning pipelines (severity, CVSS, effective status, risk scores), and ensure every ingested document preserves its raw upstream provenance and signature metadata.

Boundaries.


1) Solution & project layout

src/Aoc/
 ├─ StellaOps.Aoc.sln
 │
 ├─ __Analyzers/
 │   └─ StellaOps.Aoc.Analyzers/           # Roslyn DiagnosticAnalyzers
 │       └─ AocForbiddenFieldAnalyzer.cs   # Single analyzer (AOC0001/0002/0003)
 │
 ├─ __Libraries/
 │   ├─ StellaOps.Aoc/                     # Core runtime guard + abstractions
 │   │   ├─ IAocGuard.cs                   # interface: Validate(JsonElement, options)
 │   │   ├─ AocWriteGuard*.cs              # partial impl (TopLevel/Content/Upstream/Signature/Base64)
 │   │   ├─ AocViolationCode.cs            # enum → ERR_AOC_001..010
 │   │   ├─ AocViolationCodeExtensions.cs  # enum → "ERR_AOC_00x" mapping
 │   │   ├─ AocForbiddenKeys.cs            # forbidden + derived key sets
 │   │   ├─ AocGuardOptions.cs             # required/allowed fields, signature/tenant policy
 │   │   ├─ AocGuardResult.cs / AocViolation.cs / AocError.cs
 │   │   ├─ AocGuardException.cs / AocGuardExtensions.cs (ValidateOrThrow)
 │   │   └─ ServiceCollectionExtensions.cs # AddAocGuard()
 │   │
 │   └─ StellaOps.Aoc.AspNetCore/          # Minimal-API integration
 │       ├─ Routing/AocGuardEndpointFilter*.cs   # IEndpointFilter + RequireAocGuard<T>()
 │       └─ Results/AocHttpResults.cs             # RFC 7807 problem responses
 │
 └─ __Tests/
     ├─ StellaOps.Aoc.Analyzers.Tests/
     ├─ StellaOps.Aoc.AspNetCore.Tests/
     └─ StellaOps.Aoc.Tests/

There is no StellaOps.Aoc.Cli project, and the CLI has no aoc command. See §8.

Both libraries target net10.0 with TreatWarningsAsErrors. StellaOps.Aoc depends only on Microsoft.Extensions.DependencyInjection.Abstractions; StellaOps.Aoc.AspNetCore adds a FrameworkReference to Microsoft.AspNetCore.App. The analyzer targets netstandard2.0 (IncludeBuildOutput=false, EnforceExtendedAnalyzerRules) and is not published as a NuGet package.


2) Core concept

2.1 Forbidden fields (runtime guard)

The runtime guard (AocForbiddenKeys.IsForbiddenTopLevel) treats these top-level keys as forbidden — they are computed by the decisioning pipeline and must never appear in an ingested document:

FieldReason
severityComputed from CVSS + context
cvssNormalized from multiple sources
cvss_vectorParsed/validated post-merge
merged_fromProvenance tracking
consensus_providerVEX provider selection
reachabilityRuntime analysis result
asset_criticalityPolicy engine computation
risk_scoreFinal risk calculation

A forbidden top-level field emits AocViolationCode.ForbiddenField → ERR_AOC_001.

2.2 Derived fields

Any field prefixed with effective_(AocForbiddenKeys.IsDerivedField) is treated as a derived field. The runtime guard reports it separately as AocViolationCode.DerivedFindingDetected → ERR_AOC_006, with its own message (“must not be written during ingestion”). It is not in the forbidden-key set above.

Runtime vs. analyzer field sets differ. The Roslyn analyzer’s ForbiddenTopLevel set additionally lists effective_status and effective_range explicitly (alongside the effective_ prefix rule), whereas the runtime guard relies solely on the effective_ prefix to catch derived fields. Both produce equivalent verdicts in practice; the difference is an implementation detail of the two checkers.

2.3 Document shape enforced by the guard

Beyond forbidden/derived fields, AocGuardOptions.Default enforces a full ingestion-document shape:

The guard returns an AocGuardResult (IsValid + an immutable list of AocViolation). It collects all violations rather than throwing; IAocGuard.ValidateOrThrow(...) (extension) wraps a failing result in AocGuardException.


3) Violation codes

3.1 Runtime guard codes (AocViolationCode → ERR_AOC_00x)

The runtime guard is the authoritative code space. AocViolationCodeExtensions.ToErrorCode maps the enum to stable string codes:

Enum valueError codeRaised when
ForbiddenFieldERR_AOC_001A forbidden top-level field is present
MergeAttemptERR_AOC_002Reserved (enum member; not emitted by AocWriteGuard)
IdempotencyViolationERR_AOC_003Reserved (enum member; not emitted by AocWriteGuard)
MissingProvenanceERR_AOC_004upstream.content_hash / upstream.signature / content.raw missing
SignatureInvalidERR_AOC_005Signature marked present but sig payload missing/blank
DerivedFindingDetectedERR_AOC_006An effective_* (derived) field is present
UnknownFieldERR_AOC_007A top-level field outside the allow-list
MissingRequiredFieldERR_AOC_008A required top-level field (or content / linkset) is missing
InvalidTenantERR_AOC_009tenant is absent or not a non-empty string
InvalidSignatureMetadataERR_AOC_010Signature metadata shape invalid (present/format/sig encoding/key_id)

(None maps to the fallback ERR_AOC_000.) Codes ERR_AOC_002/003 exist in the enum but are not produced by the current AocWriteGuard validation path.

3.2 Compile-time analyzer diagnostics (AocForbiddenFieldAnalyzer)

The single Roslyn analyzer (StellaOps.Aoc.Analyzers) defines three diagnostics. These IDs are AOC000x, a separate namespace from the runtime ERR_AOC_00x codes (the analyzer messages cross-reference the runtime code for context):

IDSeverityDescription (referenced runtime code)
AOC0001ErrorForbidden field write detected (refs ERR_AOC_001)
AOC0002ErrorDerived effective_* field write detected (refs ERR_AOC_006)
AOC0003WarningUnguarded database write without IAocGuard validation (refs ERR_AOC_007)

AOC0003 recognises EF Core (DbContext.SaveChanges*, DbSet.Add/Update*), ADO.NET (DbCommand/NpgsqlCommand.ExecuteNonQuery*), and MongoDB (IMongoCollection.InsertOne/UpdateOne/ReplaceOne/BulkWrite…) write calls, and suppresses the warning when the write is lexically within an IAocGuard.Validate/ValidateOrThrow scope or the enclosing method takes an IAocGuard parameter.


4) Usage

4.1 Analyzer Reference

The analyzer is not packaged; an ingestion project consumes it as an analyzer-only project reference (adjust the relative path):

<ItemGroup>
  <ProjectReference Include="..\..\Aoc\__Analyzers\StellaOps.Aoc.Analyzers\StellaOps.Aoc.Analyzers.csproj"
                    OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

No live project carries this reference at 942a18727c.

4.2 Runtime guard usage

Register the guard in DI (AddAocGuard() binds IAocGuard → AocWriteGuard as a singleton), then validate the document before persisting. The guard is synchronous and operates on a System.Text.Json.JsonElement — there is no ValidateAsync:

services.AddAocGuard();   // StellaOps.Aoc.ServiceCollectionExtensions

public sealed class MyConnector
{
    private readonly IAocGuard _aocGuard;

    public void Ingest(JsonElement document)
    {
        // Validate-and-throw: AocGuardException carries the AocGuardResult/violations.
        _aocGuard.ValidateOrThrow(document);   // or: var result = _aocGuard.Validate(document);
        _repository.Insert(document);
    }
}

Validate(JsonElement, AocGuardOptions?) returns an AocGuardResult listing all violations. Use the ValidateOrThrow extension when you want a fail-closed AocGuardException instead of inspecting the result.

4.3 ASP.NET Core minimal-API integration

StellaOps.Aoc.AspNetCore adds an endpoint filter so a host can guard a route without touching the handler body:

// Generic illustration — the selector projects the request to the payload(s) to validate.
app.MapPost("/ingest/advisory", Handler)
   .RequireAocGuard<TRequest>(req => req.Payload);   // payload selector

There are two overloads of RequireAocGuard<TRequest>(...): one taking Func<TRequest, object?> (single payload) and one taking Func<TRequest, IEnumerable<object?>> (multiple payloads); both accept optional JsonSerializerOptions and AocGuardOptions. The selector returns any CLR object — the filter serializes it to a JsonElement before validating, so it does not require a pre-built JsonElement/Document field. No live host calls RequireAocGuard at 942a18727c.

RequireAocGuard<TRequest>(...) (in AocGuardEndpointFilterExtensions) resolves IAocGuard from DI, resolves AocGuardOptions (preferring the per-call guardOptions, then IOptions<AocGuardOptions>, else AocGuardOptions.Default), extracts one or more payloads via the selector, validates each, and on the first failing payload short-circuits with an RFC 7807 problem response built by AocHttpResults.Problem(...). Error-code → HTTP status mapping (AocHttpResults.MapErrorCodeToStatus):

Error codeHTTP status
ERR_AOC_003409 Conflict
ERR_AOC_004, ERR_AOC_005422 Unprocessable Entity
ERR_AOC_006403 Forbidden
all others400 Bad Request

The status is keyed off the response’s top-level code, which AocError.FromResult(...) selects as the first violation after sorting all violations by error code (ordinal), then path, then message. So when a payload has multiple violations, the lowest-sorted ERR_AOC_00x (e.g. ERR_AOC_001 before ERR_AOC_004) drives the HTTP status. The problem response carries code, a violations array (each {code, path, message}), and the full error object in its extensions; the title is “Aggregation-Only Contract violation” and the type URI is https://stella-ops.org/problems/aoc-violation. A status/type/title/detail/extensions override can be supplied to AocHttpResults.Problem(...) directly.


5) Configuration

5.1 Runtime guard (AocGuardOptions)

The guard’s behaviour is driven by AocGuardOptions (AocGuardOptions.Default used when none supplied):

OptionDefaultEffect
RequiredTopLevelFieldstenant, source, upstream, content, linksetMissing → ERR_AOC_008
AllowedTopLevelFieldsrequired set + _id, identifiers, attributes, supersedes, createdAt, created_at, ingestedAt, ingested_at, links, advisory_key, statementsOthers → ERR_AOC_007
RequireTenanttrueEnforces non-empty string tenant (ERR_AOC_009)
RequireSignatureMetadatatrueEnforces upstream.signature shape (ERR_AOC_005/ERR_AOC_010)
AllowedSignatureFormatsempty (any)When non-empty, restricts upstream.signature.format

5.2 Compile-time analyzer activation

AocForbiddenFieldAnalyzer only runs in an ingestion context. A symbol is treated as ingestion code when any of these hold (test/analyzer assemblies — *.Tests/*.Test/*.Testing/*.Analyzers — are always excluded):


6) Lineage invariant (historical)

The analyzer cannot enforce that the bytes that were signed are the bytes that flow into normalization. The retired Excititor service closed that gap with a VEX-specific lineage gate (IVexRawWriteGuard.EnsureLineage, error code aoc.lineage.broken). That code is frozen under src/__Obsoleted/Concelier/, and no live service carries an equivalent AOC lineage gate. The vulnerability hub documents its own ingest-time verification in ../vulnerabilities/architecture.md.


7) Enforcement points (who consumes AOC)

At 942a18727c no live deployable references or calls the guard:

7.1 /aoc/verify and /api/v1/aoc/* (withdrawn 2026-10-08)

AOC does not host an endpoint. The service-hosted POST /aoc/verify endpoints belonged to the retired Concelier and Excititor WebServices and no longer exist.

No service maps an /api/v1/aoc route in any environment. The Platform WebService used to map an /api/v1/aoc compatibility group only in Development and testing environments, gated by the aoc:verify scope; it was withdrawn in two steps (SPRINT_20261007_002):

A request to any former /api/v1/aoc route receives normal 404. The sources are frozen under src/__Obsoleted/Platform/StellaOps.Platform.WebService/CCP-004/ and src/__Obsoleted/Web/StellaOps.Web/CCP-004/ (with the CCP-005 pages under CCP-005/).

The canonical scope remains aoc:verify (StellaOpsScopes.AocVerify); no Console route or Platform endpoint requires it any more.


8) CLI surface

There is no CLI project inside src/Aoc/ and no aoc command in the shipped CLI. Both former aoc verify implementations are withdrawn, and neither ever called the AOC guard:

The Console’s /api/v1/aoc client was withdrawn with the Console AOC pages on 2026-10-08 (CCP-004, §7.1).