AOC architecture
WITHDRAWN (2026-09-27, SPRINT_20260927_003 PCD-62). The AOC libraries (
StellaOps.Aoc,StellaOps.Aoc.AspNetCore), theStellaOps.Aoc.AnalyzersRoslyn analyzer and the CLI Aoc plug-in are no longer built or shipped. Their sources are frozen undersrc/__Obsoleted/Aoc/andsrc/__Obsoleted/Cli/(CODE_OF_CONDUCT 15.4). No service referenced them, and the plug-in’s direct-PostgreSQL verify form (--postgresoption) read anaoc_documentstable that no migration creates. This page describes the withdrawn code and is kept for reference. Still live and separate: theaoc:verifyscope. The core CLIaoccommand 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.
- AOC provides enforcement at two layers:
- runtime —
AocWriteGuard.Validate(JsonElement)rejects non-conforming documents at the persistence boundary; - compile-time — Roslyn analyzers flag forbidden writes in ingestion code.
- runtime —
- AOC enforcement targets ingestion code (Connectors, Ingestion assemblies).
- AOC does not run on merge/decisioning code (those are allowed to write derived fields).
- AOC owns no schema and no service; it is a dependency of code that ingests data.
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.Cliproject, and the CLI has noaoccommand. 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:
| Field | Reason |
|---|---|
severity | Computed from CVSS + context |
cvss | Normalized from multiple sources |
cvss_vector | Parsed/validated post-merge |
merged_from | Provenance tracking |
consensus_provider | VEX provider selection |
reachability | Runtime analysis result |
asset_criticality | Policy engine computation |
risk_score | Final 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
ForbiddenTopLevelset additionally listseffective_statusandeffective_rangeexplicitly (alongside theeffective_prefix rule), whereas the runtime guard relies solely on theeffective_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:
- Required top-level fields:
tenant,source,upstream,content,linkset(missing →ERR_AOC_008; missingcontent/linksetare reported viaMissingRequiredField). - Allowed top-level fields: the required set plus
_id,identifiers,attributes,supersedes,createdAt/created_at,ingestedAt/ingested_at,links,advisory_key,statements. Any other unknown top-level key →ERR_AOC_007(UnknownField). - Tenant (when
RequireTenant, default on): must be a non-empty string, elseERR_AOC_009(InvalidTenant). - Provenance:
upstream.content_hashandcontent.rawmust be present, elseERR_AOC_004(MissingProvenance). - Signature (when
RequireSignatureMetadata, default on): theupstream.signatureobject must be present — a missing/non-object signature is reported asERR_AOC_004(MissingProvenance), notERR_AOC_005(seeAocWriteGuard.Upstream.cs). When the object is present andpresent:true, the guard requires apresentboolean, a non-emptyformat(optionally allow-listed viaAllowedSignatureFormats), a base64/base64url-encodedsig, and akey_id. Shape problems (missing/invalidpresent/format/key_id, or asigthat is present but not valid base64) →ERR_AOC_010(InvalidSignatureMetadata); a missing/blanksigpayload →ERR_AOC_005(SignatureInvalid). Whenpresent:false, no further signature checks run.
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 value | Error code | Raised when |
|---|---|---|
ForbiddenField | ERR_AOC_001 | A forbidden top-level field is present |
MergeAttempt | ERR_AOC_002 | Reserved (enum member; not emitted by AocWriteGuard) |
IdempotencyViolation | ERR_AOC_003 | Reserved (enum member; not emitted by AocWriteGuard) |
MissingProvenance | ERR_AOC_004 | upstream.content_hash / upstream.signature / content.raw missing |
SignatureInvalid | ERR_AOC_005 | Signature marked present but sig payload missing/blank |
DerivedFindingDetected | ERR_AOC_006 | An effective_* (derived) field is present |
UnknownField | ERR_AOC_007 | A top-level field outside the allow-list |
MissingRequiredField | ERR_AOC_008 | A required top-level field (or content / linkset) is missing |
InvalidTenant | ERR_AOC_009 | tenant is absent or not a non-empty string |
InvalidSignatureMetadata | ERR_AOC_010 | Signature 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):
| ID | Severity | Description (referenced runtime code) |
|---|---|---|
AOC0001 | Error | Forbidden field write detected (refs ERR_AOC_001) |
AOC0002 | Error | Derived effective_* field write detected (refs ERR_AOC_006) |
AOC0003 | Warning | Unguarded 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 code | HTTP status |
|---|---|
ERR_AOC_003 | 409 Conflict |
ERR_AOC_004, ERR_AOC_005 | 422 Unprocessable Entity |
ERR_AOC_006 | 403 Forbidden |
| all others | 400 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):
| Option | Default | Effect |
|---|---|---|
RequiredTopLevelFields | tenant, source, upstream, content, linkset | Missing → ERR_AOC_008 |
AllowedTopLevelFields | required set + _id, identifiers, attributes, supersedes, createdAt, created_at, ingestedAt, ingested_at, links, advisory_key, statements | Others → ERR_AOC_007 |
RequireTenant | true | Enforces non-empty string tenant (ERR_AOC_009) |
RequireSignatureMetadata | true | Enforces upstream.signature shape (ERR_AOC_005/ERR_AOC_010) |
AllowedSignatureFormats | empty (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):
- It (or its type/assembly) carries an
[AocIngestion]/[AocIngestionContext]attribute marker. - Assembly name contains
.Connector., ends with.Connector, or contains.Ingestion; or namespace contains.Connector.or.Ingestion. - MSBuild/
.editorconfigopt-in:stellaops_aoc_ingestion/build_property.StellaOpsAocIngestion=trueorall(enable for the whole compilation),stellaops_aoc_ingestion_assemblies/build_property.StellaOpsAocIngestionAssemblies(semicolon/comma/space list, or*/all),stellaops_aoc_ingestion_namespace_prefixes/build_property.StellaOpsAocIngestionNamespacePrefixes(prefix list).
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:
- No live project references
StellaOps.Aoc.AspNetCoreorStellaOps.Aoc.Analyzers, and no live source callsAddAocGuard,IAocGuardorRequireAocGuard. - The only live
ProjectReferencetoStellaOps.Aocis the CLI pluginsrc/Cli/__Libraries/StellaOps.Cli.Plugins.Aoc/, which uses no guard type (§8). - Historical consumers (retired). Concelier (
AddAocGuard(),RequireAocGuardon/ingest/advisory,AdvisoryRawWriteGuard) and Excititor (AddExcititorAocGuards(),VexRawWriteGuard) wired the guard until the 2026-08-04 cutover. Their sources are undersrc/__Obsoleted/Concelier/and are not built or shipped; see archived Concelier README and archived Excititor README.
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):
- CCP-005 (2026-10-08) unmapped its actions,
POST verify,POST compliance/violations/{violationId}/retry,POST provenance/validateandPOST compliance/reports, which answered fixed success, a fixed complete/valid chain and fixed totals, and bound the remaining reads’ tenant to the authenticated envelope. The Console’s Provenance Validator and Compliance Report pages and its Retry controls were withdrawn with them. - CCP-004 (2026-10-08) withdrew the remaining reads,
GET metrics,GET compliance/dashboard,GET compliance/violationsandGET ingestion/flow. They answered fixed sample data (12,870 documents with 23 guard violations, 99.1% provenance completeness, P95 301 ms, two fixed ingestion sources at 5.2/min and 3.7/min with an 18% error rate, three fixed violation rows) with timestamps computed back from the request time, and never called the guard. No service owns AOC metrics, guard violations or ingestion flow. The Console’s AOC Compliance pages (/ops/operations/aoc,/violations,/ingestion), their/ops/aocand/platform/ops/aocredirects and the Console AOC client were withdrawn in the same change; each former page path resolves to the Console’s not-found page.
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:
- Built-in command — withdrawn on 2026-10-01 (SPRINT_20260928_008 PCD-153) with
sources ingest --dry-run. They posted toapi/aoc/verifyandapi/aoc/ingest/dry-run, routes no host serves (§7.1). Sources:src/__Obsoleted/Cli/StellaOps.Cli/PCD-153/. - Plugin module — withdrawn on 2026-09-27 (SPRINT_20260927_003 PCD-62) to
src/__Obsoleted/Cli/__Libraries/StellaOps.Cli.Plugins.Aoc/. It readid, hash, previous_hashfrom anaoc_documentstable that no runtime migration creates.
The Console’s /api/v1/aoc client was withdrawn with the Console AOC pages on 2026-10-08 (CCP-004, §7.1).
Related Documentation
- Guides:
./guides/aoc-guardrails.md,./guides/guard-library.md - Security invariants:
../../security/aoc-invariants.md - Policy linting design:
../policy/design/policy-aoc-linting-rules.md - CLI command guide:
../cli/guides/commands/aoc.md - Production ingestion:
../vulnerabilities/architecture.md - Historical consumers (retired): archived Concelier architecture, archived Excititor architecture
- ADR (Excititor-era ingest gate):
../../architecture/decisions/ADR-019-excititor-vex-signature-verification-ingest-gate.md
