VexLens (VEX Consensus Lens) Architecture

Scope. Architecture and API contract for VexLens, the Stella Ops VEX Consensus Lens: trust modelling, deterministic consensus computation over multiple VEX statements, and the consensus/projection/issuer APIs. This dossier is for service implementers and for consumers of consensus output (Policy Engine, Vulnerability Explorer, Advisory AI, Console). Originates from Epic 7, the VEX Consensus Lens.

Related: VexHub (statement aggregation and the source catalog VexLens reads), Policy Engine (consumes consensus results), and Issuer Directory / Authority (issuer trust resolution).

1) Purpose

Compute a deterministic, reproducible consensus view over multiple VEX statements for each (artifact, advisory) tuple while preserving conflicts and provenance. Output is consumed by Policy Engine, Vulnerability Explorer, Advisory AI, and Console overlays.

2) Inputs

Note: when no host wires a concrete IIssuerDirectory / IConsensusEventEmitter / snapshot store, the library binds fail-closed Unsupported* defaults (see §5) rather than in-memory fallbacks.

Provenance field mapping (new input contract)

Excititor connectors now stamp every raw VEX document with vex.provenance.* metadata. Lens ingests these keys alongside the normalized tuples:

FieldDescriptionLens usage
vex.provenance.provider / providerName / providerKindLogical issuer identity and type supplied by the connector (e.g., excititor:ubuntu, distro).Seed issuer lookup, short-circuit Issuer Directory calls when we already trust the connector’s profile.
vex.provenance.trust.weightConnector-provided base weight (0–1).Multiplied by freshness decay & justification multipliers; overrides registry default.
vex.provenance.trust.tier & trust.noteHuman/ops tier labels (vendor, distro-trusted, etc.) plus descriptive note.Drives secondary sort (after timestamp) and Console labels; conflicts report per-tier deltas.
vex.provenance.cosign.*Cosign issuer/identity pattern (+ optional Fulcio/Rekor URIs).When present, Lens marks the statement as “cryptographically attested” and applies the higher confidence bucket immediately.
vex.provenance.pgp.fingerprintsOrdered list of PGP fingerprints used by the feed.Enables Lens to validate deterministic fingerprint sets against Issuer Directory entries and flag mismatches in conflict summaries.

Runtime note: provenance hints are not proof by themselves. Current VexLens runtime applies a cryptographically attested confidence tier only after configured signature verification and issuer trust validation succeed.

The trust engine preserves the raw metadata so downstream components can audit decisions or remap tiers without replaying ingestion.

3) Core algorithm

Consensus is computed per (vulnerabilityId, productKey) request by VexConsensusEngine (src/VexLens/StellaOps.VexLens/Consensus/VexConsensusEngine.cs). The engine is mode-driven rather than a single fixed lattice join — the lattice is one of four configurable modes.

  1. Trust weighting — each input statement is first weighted by TrustWeightEngine. The combined weight is (issuerWeight × signatureWeight × freshnessWeight) + statusSpecificityWeight + customWeight, clamped to [0.0, 2.0] (MaximumWeight was raised from the earlier 1.5 so a first-party attestation is not clamped down to a vendor’s ceiling). Issuer multipliers default to vendor 1.0 / distributor 0.9 / internal 0.8 / community 0.7 / aggregator 0.6 / unknown 0.3, plus a first-party image-specific signed-attestation multiplier of 1.3 (e.g. excititor:oci-openvex, matched by issuer id) that strictly outranks vendor/distributor. Tier bonuses apply on top (authoritative +0.2, trusted +0.1, untrusted −0.3). Freshness buckets at 7 days (fresh, ×1.0), 90 days (stale, ×0.8), 365 days (expired/older, ×0.5). Signature outcomes scale from valid (×1.0) down to no/invalid/revoked signature. Signature verification is a weight input only — never a trust-tier grant (SPRINT_20260703_001): vexhub.sources.trust_tier is operator-curated via the VexLens issuer endpoints, and the Excititor projection sink lands every new source as UNKNOWN.
  2. Threshold filter — statements below MinimumWeightThreshold (default 0.1) are dropped; if none qualify the result is NoData / under_investigation with confidence 0. When RequireJustificationForNotAffected is set (default true since SPRINT_20260703_001; OpenVEX/CSAF conformance), a not_affected statement with no justification is dropped here too — it is not cap-eligible and can never win consensus in any mode. Config escape hatch for legacy tenants: VexLens:Consensus:RequireJustificationForNotAffected=false.
  3. Mode evaluation — one of:
    • WeightedVote (default) — sum weight per status; the highest-weight status wins. Outcome is Unanimous / Majority (≥50% of weight) / Plurality. Confidence = vote fraction × weight-spread factor.
    • HighestWeight — single highest-weighted statement wins.
    • Lattice — selects the most conservative status by lattice order (see below).
    • AuthoritativeFirst — vendor / authoritative-tier issuers take precedence (confidence pinned to 0.95 when an authoritative source wins). This is the mode the score-cap bridge pins (supplier-authoritative: curated-tier issuers outrank aggregate votes), while WeightedVote stays the default for analytics/stats surfaces (SPRINT_20260703_001). Mode is selectable per request (ConsensusPolicy.Mode).
  4. Deterministic ordering & tie-breaks — statements are ordered by weight desc → statement timestamp (LastSeen ?? FirstSeen) desc → lexical source id asc → statement id asc. An unresolved tie at the top (equal weight, timestamp, and source) yields an explicit Unknown status with Indeterminate outcome and zero confidence (see §9).
  5. Conflict detection — every disagreeing pair of statuses is recorded with a severity (affected↔not_affected = Critical, affected↔fixed = High, fixed↔not_affected = Medium, anything touching under_investigation/unknown = Low) and a resolution note. Conflicts reduce confidence by a per-severity penalty (Critical −0.3 … Low −0.05).

The lattice StatusOrder (most conservative first) is affected (0) < unknown (1) < under_investigation (2) < fixed (3) < not_affected (4), with bottom affected and top not_affected. (This is the actual order in VexConsensusEngine.CreateDefaultConfiguration; the earlier doc ordering unknown < under_investigation < not_affected | affected < fixed was incorrect.)

A VexConsensusResult carries: consensusStatus, consensusJustification, confidenceScore, outcome, a rationale (summary + factors + per-status weights), contributions (per-statement weight, contribution fraction, winner flag), and optional conflicts. The :withProof variant additionally returns merge steps, conflict records with penalties, the trust-weight breakdown, the consensus mode used, and the winning issuer id + trust tier (resolution.winningIssuerId / winningIssuerTier, SPRINT_20260703_001) so the score-cap decision names who capped and under which mode. Conflicts remain visible so Policy Engine can choose a suppression strategy.

Persisted projections (see §5) record attestation_digest and input_hash columns for replay/attestation, and computation emits orchestrator-ledger events (consensus.computed, consensus.status_changed, consensus.conflict_detected, consensus.alert) via IConsensusEventEmitter when a real emitter is wired — not a single vex.consensus.updated DSSE event.

4) APIs

All VexLens HTTP endpoints are mounted under /api/v1/vexlens by StellaOps.VexLens.WebService (Program.csMapVexLensEndpoints, MapExportEndpoints). Tenancy is carried by the X-StellaOps-TenantId header (falling back to the request body tenantId where present). Authorization uses the vexlens.read / vexlens.write scope policies (AddStellaOpsScopePolicy), not generic “Authority scopes”. A named fixed-window rate limiter (vexlens, 100 req/min) is registered and UseRateLimiter is in the pipeline, but no endpoint group calls RequireRateLimiting("vexlens") and no global limiter is set — so as wired the 100 req/min limit is currently inert (defined, not enforced).

Implementation note (2026-05-29): the routes below reflect the shipped surface. Earlier /v1/vex/* paths (/v1/vex/consensus, /v1/vex/conflicts, /v1/vex/trust/weights, /v1/vex/consensus/export) were never implemented and have been corrected here. Consensus is computed on demand via POST, not fetched by GET query string.

Consensus (vexlens.read)

Projections (vexlens.read)

Delta & noise-gating (vexlens.read)

Issuer directory (vexlens.write)

Note: there is no dedicated trust/weights mutation endpoint. Trust tiers and keys are governed through the Issuer Directory CRUD endpoints above (which the WebService backs with the shared IssuerDirectory persistence and AuthorityIssuerDirectoryAdapter). Statement-level trust weighting is computed deterministically by TrustWeightEngine from issuer category/tier, signature, freshness, source format, and status — not set via an admin API.

Export (vexlens.read) — defined but not yet wired

The export route group is implemented in ExportEndpointExtensions.MapExportEndpoints (/api/v1/vexlens/export/consensus/{vulnerabilityId}/{productId}, /export/projections/{projectionId}, POST /export/batch, POST /export/combined) supporting openvex, spdx3 (SPDX 3.0.1 security profile), and csaf formats. However, as of 2026-05-29 Program.cs calls only MapVexLensEndpoints()MapExportEndpoints() is not invoked, so these routes are dormant code and not reachable in the running WebService. Treat the export surface as implemented-but-unwired until Program.cs maps it.

Consensus responses carry rationale, contributions, conflicts, and (for :withProof) the full proof. A signed consensus_digest / DSSE envelope is a forward-looking goal; the current VexConsensusResult does not emit a per-record DSSE signature.

5) Storage

Storage is PostgreSQL in the vexlens schema, created and converged by embedded startup migrations (AddStartupMigrations in Program.cs, in StellaOps.VexLens.Persistence). The live migration set on disk is:

FileAdds
001_v1_vexlens_baseline.sqlCollapsed pre-1.0 baseline (folds in the archived 001_consensus_projections.sql + 002_noise_gating_state.sql, which are under Migrations/_archived/ and are not embedded).
002_consensus_winning_source.sqlwinning_source on consensus_projections.
003_consensus_winning_issuer_trust_tier.sqlwinning_issuer_trust_tier (TRUST-CAP-0 — the Findings cap gates on the winning issuer’s curated tier).
004_consensus_outcome_check_fix.sqlRepoints the outcome CHECK constraint at the real ConsensusOutcome set (CONS-C3).

The earlier doc names (vex_consensus, vex_consensus_history, vex_conflict_queue “collections”) do not exist — they survive only as unused legacy VexLensStorageOptions.ProjectionsCollection/HistoryCollection config defaults. Actual tables:

Storage driver is postgres by default; memory and dual-write drivers carry process-local state and require explicit local/test opt-in (AddVexLensForTesting or VexLens:Storage:AllowInMemoryStorage=true).

Runtime storage and service defaults (2026-04-24)

Noise-gating runtime contract (2026-04-14)

6) Recompute strategy

7) Observability

Meter / activity source name: StellaOps.VexLens (VexLensMetrics, VexLensActivitySource).

8) Offline & export

CLI surface (corrected 2026-05-30). The wired stella vex command is built by CommandFactory.BuildVexCommand (src/Cli/StellaOps.Cli/Commands/CommandFactory.cs, registered via root.Add(BuildVexCommand(...))). Its subcommands are vex consensus list / vex consensus show, vex simulate, vex export (+ vex export verify), vex obs, vex explain, vex gen, vex providers, vex gate-scan, vex verdict, and vex unknowns.

A consensus-export CLI command does exist: stella vex export exports VEX consensus data as an NDJSON bundle (signed by default; --unsigned opts out) via CommandHandlers.HandleVexExportAsync, filtered by --vuln-id / --product-key / --purl / --status / --policy-version and written to --output. stella vex export verify checks the bundle’s --digest and --public-key signature. This is a CLI-side NDJSON consensus export and is distinct from the WebService /api/v1/vexlens/export/* OpenVEX/SPDX3/CSAF routes (which remain unwired — see §4). The earlier doc claim that “a stella vex consensus export CLI command does not exist / is roadmap” was incorrect.

The former unwired VexCommandGroup.cs sample-data group was deleted on 2026-07-29 (SPRINT_20260729_001_Cli_fabricated_reachability_verdicts). It was never referenced by the root factory. The shipped stella vex surface comes from the registered VEX plug-in path; vex lens / vex advisory entries in cli-routes.json remain legacy aliases, not evidence that the deleted sample handlers were shipped.

9) Advisory Gap Status (2026-03-04 Batch)

Status: implementation delivered in Sprint 305.

Closure sprint:

Unified provider catalog (Sprint 20260503-012)

Provider discovery now flows through the federated advisory-sources catalog (GET /api/v1/advisory-sources/catalog). VexLens consumers resolve provider trust weights via the catalog entry’s ConfigurationDescriptorId, which maps 1:1 onto Excititor’s VexProviderRuntimeSettingsCache, rather than maintaining a parallel provider list. The 7 canonical VEX providers carry the excititor:{provider} source-id namespace; entries with Backend = "excititor" and Category = "Vex" denote VEX surfaces in both UI and CLI consumers. See docs/modules/vex-hub/architecture.md §11 for the contributor wiring and operator migration note.