diff --git a/.gitignore b/.gitignore
index 6e11a0b9..86af3a96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,5 @@ TestResults/
seed-data/ics-cisa/*.csv
seed-data/ics-cisa/*.xlsx
seed-data/ics-cisa/*.sha256
+seed-data/cert-bund/**/*.json
+seed-data/cert-bund/**/*.sha256
diff --git a/SPRINTS.md b/SPRINTS.md
index b4cfc435..9365279e 100644
--- a/SPRINTS.md
+++ b/SPRINTS.md
@@ -105,8 +105,8 @@
| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Feedser.Merge/TASKS.md | DONE (2025-10-15) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-005 | Connector coordination for new advisory fields
GHSA/NVD/OSV connectors now ship description, CWE, and canonical metric data with refreshed fixtures; merge coordination log updated and exporters notified. |
| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Feedser.Exporter.Json/TASKS.md | DONE (2025-10-15) | Team Exporters – JSON | FEEDEXPORT-JSON-04-001 | Surface new advisory fields in JSON exporter
Update schemas/offline bundle + fixtures once model/core parity lands.
2025-10-15: `dotnet test src/StellaOps.Feedser.Exporter.Json.Tests` validated canonical metric/CWE emission. |
| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Feedser.Exporter.TrivyDb/TASKS.md | DONE (2025-10-15) | Team Exporters – Trivy DB | FEEDEXPORT-TRIVY-04-001 | Propagate new advisory fields into Trivy DB package
Extend Bolt builder, metadata, and regression tests for the expanded schema.
2025-10-15: `dotnet test src/StellaOps.Feedser.Exporter.TrivyDb.Tests` confirmed canonical metric/CWE propagation. |
-| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Feedser.Source.Ghsa/TASKS.md | TODO | Team Connector Regression Fixtures | FEEDCONN-GHSA-04-004 | Harden CVSS fallback so canonical metric ids persist when GitHub omits vectors; extend fixtures and document severity precedence hand-off to Merge. |
-| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Feedser.Source.Osv/TASKS.md | TODO | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-04-005 | Map OSV advisories lacking CVSS vectors to canonical metric ids/notes and document CWE provenance quirks; schedule parity fixture updates. |
+| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Feedser.Source.Ghsa/TASKS.md | DONE (2025-10-16) | Team Connector Regression Fixtures | FEEDCONN-GHSA-04-004 | Harden CVSS fallback so canonical metric ids persist when GitHub omits vectors; extend fixtures and document severity precedence hand-off to Merge. |
+| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Feedser.Source.Osv/TASKS.md | DONE (2025-10-16) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-04-005 | Map OSV advisories lacking CVSS vectors to canonical metric ids/notes and document CWE provenance quirks; schedule parity fixture updates. |
| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Core/TASKS.md | DONE (2025-10-15) | Team Vexer Core & Policy | VEXER-CORE-01-001 | Stand up canonical VEX claim/consensus records with deterministic serializers so Storage/Exports share a stable contract. |
| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Core/TASKS.md | DONE (2025-10-15) | Team Vexer Core & Policy | VEXER-CORE-01-002 | Implement trust-weighted consensus resolver with baseline policy weights, justification gates, telemetry output, and majority/tie handling. |
| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Core/TASKS.md | DONE (2025-10-15) | Team Vexer Core & Policy | VEXER-CORE-01-003 | Publish shared connector/exporter/attestation abstractions and deterministic query signature utilities for cache/attestation workflows. |
@@ -116,29 +116,35 @@
| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | DONE (2025-10-16) | Team Vexer Policy | VEXER-POLICY-01-004 | Implement YAML/JSON schema validation and deterministic diagnostics for operator bundles. |
| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Policy/TASKS.md | DONE (2025-10-16) | Team Vexer Policy | VEXER-POLICY-01-005 | Add policy change tracking, snapshot digests, and telemetry/logging hooks. |
| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | DONE (2025-10-15) | Team Vexer Storage | VEXER-STORAGE-01-001 | Mongo mapping registry plus raw/export entities and DI extensions in place. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | TODO | Team Vexer Storage | VEXER-STORAGE-01-004 | Build provider/consensus/cache class maps and related collections. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | DONE (2025-10-16) | Team Vexer Storage | VEXER-STORAGE-01-004 | Build provider/consensus/cache class maps and related collections. |
| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Export/TASKS.md | DONE (2025-10-15) | Team Vexer Export | VEXER-EXPORT-01-001 | Export engine delivers cache lookup, manifest creation, and policy integration. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Export/TASKS.md | TODO | Team Vexer Export | VEXER-EXPORT-01-004 | Connect export engine to attestation client and persist Rekor metadata. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Attestation/TASKS.md | TODO | Team Vexer Attestation | VEXER-ATTEST-01-001 | Implement in-toto predicate + DSSE builder providing envelopes for export attestation. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Connectors.Abstractions/TASKS.md | TODO | Team Vexer Connectors | VEXER-CONN-ABS-01-001 | Deliver shared connector context/base classes so provider plug-ins can be activated via WebService/Worker. |
-| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.WebService/TASKS.md | TODO | Team Vexer WebService | VEXER-WEB-01-001 | Scaffold minimal API host, DI, and `/vexer/status` endpoint integrating policy, storage, export, and attestation services. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Worker/TASKS.md | TODO | Team Vexer Worker | VEXER-WORKER-01-001 | Create Worker host with provider scheduling and logging to drive recurring pulls/reconciliation. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Formats.CSAF/TASKS.md | TODO | Team Vexer Formats | VEXER-FMT-CSAF-01-001 | Implement CSAF normalizer foundation translating provider documents into `VexClaim` entries. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Formats.CycloneDX/TASKS.md | TODO | Team Vexer Formats | VEXER-FMT-CYCLONE-01-001 | Implement CycloneDX VEX normalizer capturing `analysis` state and component references. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Formats.OpenVEX/TASKS.md | TODO | Team Vexer Formats | VEXER-FMT-OPENVEX-01-001 | Implement OpenVEX normalizer to ingest attestations into canonical claims with provenance. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.RedHat.CSAF/TASKS.md | TODO | Team Vexer Connectors – Red Hat | VEXER-CONN-RH-01-001 | Ship Red Hat CSAF provider metadata discovery enabling incremental pulls. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Cisco.CSAF/TASKS.md | TODO | Team Vexer Connectors – Cisco | VEXER-CONN-CISCO-01-001 | Implement Cisco CSAF endpoint discovery/auth to unlock paginated pulls. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.SUSE.RancherVEXHub/TASKS.md | TODO | Team Vexer Connectors – SUSE | VEXER-CONN-SUSE-01-001 | Build Rancher VEX Hub discovery/subscription path with offline snapshot support. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.MSRC.CSAF/TASKS.md | TODO | Team Vexer Connectors – MSRC | VEXER-CONN-MS-01-001 | Deliver AAD onboarding/token cache for MSRC CSAF ingestion. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Oracle.CSAF/TASKS.md | TODO | Team Vexer Connectors – Oracle | VEXER-CONN-ORACLE-01-001 | Implement Oracle CSAF catalogue discovery with CPU calendar awareness. |
-| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Ubuntu.CSAF/TASKS.md | TODO | Team Vexer Connectors – Ubuntu | VEXER-CONN-UBUNTU-01-001 | Implement Ubuntu CSAF discovery and channel selection for USN ingestion. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Export/TASKS.md | DONE (2025-10-17) | Team Vexer Export | VEXER-EXPORT-01-004 | Connect export engine to attestation client and persist Rekor metadata. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Attestation/TASKS.md | DONE (2025-10-16) | Team Vexer Attestation | VEXER-ATTEST-01-001 | Implement in-toto predicate + DSSE builder providing envelopes for export attestation. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.Connectors.Abstractions/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors | VEXER-CONN-ABS-01-001 | Deliver shared connector context/base classes so provider plug-ins can be activated via WebService/Worker. |
+| Sprint 5 | Vexer Core Foundations | src/StellaOps.Vexer.WebService/TASKS.md | DONE (2025-10-17) | Team Vexer WebService | VEXER-WEB-01-001 | Scaffold minimal API host, DI, and `/vexer/status` endpoint integrating policy, storage, export, and attestation services. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Worker/TASKS.md | DONE (2025-10-17) | Team Vexer Worker | VEXER-WORKER-01-001 | Create Worker host with provider scheduling and logging to drive recurring pulls/reconciliation. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Formats.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Formats | VEXER-FMT-CSAF-01-001 | Implement CSAF normalizer foundation translating provider documents into `VexClaim` entries. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Formats.CycloneDX/TASKS.md | DONE (2025-10-17) | Team Vexer Formats | VEXER-FMT-CYCLONE-01-001 | Implement CycloneDX VEX normalizer capturing `analysis` state and component references. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Formats.OpenVEX/TASKS.md | DONE (2025-10-17) | Team Vexer Formats | VEXER-FMT-OPENVEX-01-001 | Implement OpenVEX normalizer to ingest attestations into canonical claims with provenance. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Red Hat | VEXER-CONN-RH-01-001 | Ship Red Hat CSAF provider metadata discovery enabling incremental pulls. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Red Hat | VEXER-CONN-RH-01-002 | Fetch CSAF windows with ETag handling, resume tokens, quarantine on schema errors, and persist raw docs. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Red Hat | VEXER-CONN-RH-01-003 | Populate provider trust overrides (cosign issuer, identity regex) and provenance hints for policy evaluation/logging. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Red Hat | VEXER-CONN-RH-01-004 | Persist resume cursors (last updated timestamp/document hashes) in storage and reload during fetch to avoid duplicates. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Red Hat | VEXER-CONN-RH-01-005 | Register connector in Worker/WebService DI, add scheduled jobs, and document CLI triggers for Red Hat CSAF pulls. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Red Hat | VEXER-CONN-RH-01-006 | Add CSAF normalization parity fixtures ensuring RHSA-specific metadata is preserved. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Cisco.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Cisco | VEXER-CONN-CISCO-01-001 | Implement Cisco CSAF endpoint discovery/auth to unlock paginated pulls. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Cisco.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Cisco | VEXER-CONN-CISCO-01-002 | Implement Cisco CSAF paginated fetch loop with dedupe and raw persistence support. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.SUSE.RancherVEXHub/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – SUSE | VEXER-CONN-SUSE-01-001 | Build Rancher VEX Hub discovery/subscription path with offline snapshot support. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.MSRC.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – MSRC | VEXER-CONN-MS-01-001 | Deliver AAD onboarding/token cache for MSRC CSAF ingestion. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Oracle.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Oracle | VEXER-CONN-ORACLE-01-001 | Implement Oracle CSAF catalogue discovery with CPU calendar awareness. |
+| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.Ubuntu.CSAF/TASKS.md | DONE (2025-10-17) | Team Vexer Connectors – Ubuntu | VEXER-CONN-UBUNTU-01-001 | Implement Ubuntu CSAF discovery and channel selection for USN ingestion. |
| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Vexer.Connectors.OCI.OpenVEX.Attest/TASKS.md | TODO | Team Vexer Connectors – OCI | VEXER-CONN-OCI-01-001 | Wire OCI discovery/auth to fetch OpenVEX attestations for configured images. |
| Sprint 6 | Vexer Ingest & Formats | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | VEXER-CLI-01-001 | Add `vexer` CLI verbs bridging to WebService with consistent auth and offline UX. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Core/TASKS.md | TODO | Team Vexer Core & Policy | VEXER-CORE-02-001 | Context signal schema prep – extend consensus models with severity/KEV/EPSS fields and update canonical serializers. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Policy/TASKS.md | TODO | Team Vexer Policy | VEXER-POLICY-02-001 | Scoring coefficients & weight ceilings – add α/β options, weight boosts, and validation guidance. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Storage.Mongo/TASKS.md | TODO | Team Vexer Storage | VEXER-STORAGE-02-001 | Statement events & scoring signals – create immutable VEX statement store plus consensus extensions with indexes/migrations. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.WebService/TASKS.md | TODO | Team Vexer WebService | VEXER-WEB-01-004 | Resolve API & signed responses – expose `/vexer/resolve`, return signed consensus/score envelopes, document auth. |
-| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Attestation/TASKS.md | TODO | Team Vexer Attestation | VEXER-ATTEST-01-002 | Rekor v2 client integration – ship transparency log client with retries and offline queue. |
+| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Attestation/TASKS.md | DONE (2025-10-16) | Team Vexer Attestation | VEXER-ATTEST-01-002 | Rekor v2 client integration – ship transparency log client with retries and offline queue. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Worker/TASKS.md | TODO | Team Vexer Worker | VEXER-WORKER-01-004 | TTL refresh & stability damper – schedule re-resolve loops and guard against status flapping. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Vexer.Export/TASKS.md | TODO | Team Vexer Export | VEXER-EXPORT-01-005 | Score & resolve envelope surfaces – include signed consensus/score artifacts in exports. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Feedser.Core/TASKS.md | TODO | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-001 | Advisory event log & asOf queries – surface immutable statements and replay capability. |
diff --git a/seed-data/cert-bund/README.md b/seed-data/cert-bund/README.md
new file mode 100644
index 00000000..28c5c642
--- /dev/null
+++ b/seed-data/cert-bund/README.md
@@ -0,0 +1,52 @@
+# CERT-Bund Offline Kit Seed Data
+
+This directory stores **offline snapshots** for the CERT-Bund connector.
+The artefacts mirror the public JSON search and export endpoints so
+air‑gapped deployments can hydrate the connector without contacting the
+portal.
+
+> ⚠️ **Distribution notice** – CERT-Bund advisories are published by BSI
+> (Federal Office for Information Security, Germany). Review the portal
+> terms of use before redistributing the snapshots. Always keep the JSON
+> payloads and accompanying SHA-256 sums together.
+
+## Recommended layout
+
+```
+seed-data/cert-bund/
+├── search/ # paginated search JSON files
+│ ├── certbund-search-page-00.json
+│ └── …
+├── export/ # yearly export JSON files
+│ ├── certbund-export-2014.json
+│ └── …
+├── manifest/
+│ └── certbund-offline-manifest.json
+└── certbund-offline-manifest.sha256
+```
+
+Use `certbund-offline-manifest.json` to feed the Offline Kit build: every
+entry contains `source`, `from`, `to`, `sha256`, `capturedAt`, and the
+relative file path. The manifest is deterministic when regenerated with
+the tooling described below.
+
+## Tooling
+
+Run the helper under `tools/` to capture fresh snapshots or regenerate
+the manifest:
+
+```
+python tools/certbund_offline_snapshot.py --output seed-data/cert-bund
+```
+
+See the connector operations guide
+(`docs/ops/feedser-certbund-operations.md`) for detailed usage,
+including how to provide cookies/tokens when the portal requires manual
+authentication.
+
+## Git hygiene
+
+- JSON payloads and checksums are **ignored by Git**. Generate them
+ locally when preparing an Offline Kit bundle.
+- Commit documentation, scripts, and manifest templates only – never the
+ exported advisory data itself.
diff --git a/src/StellaOps.Feedser.Source.CertBund/TASKS.md b/src/StellaOps.Feedser.Source.CertBund/TASKS.md
index 0fb691ed..48c152f6 100644
--- a/src/StellaOps.Feedser.Source.CertBund/TASKS.md
+++ b/src/StellaOps.Feedser.Source.CertBund/TASKS.md
@@ -9,4 +9,4 @@
|FEEDCONN-CERTBUND-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-15)** – Added `CertBundDiagnostics` (meter `StellaOps.Feedser.Source.CertBund`) with fetch/parse/map counters + histograms, recorded coverage days, wired stage summary logs, and published the ops runbook (`docs/ops/feedser-certbund-operations.md`).|
|FEEDCONN-CERTBUND-02-007 Feed history & locale assessment|BE-Conn-CERTBUND|Research|**DONE (2025-10-15)** – Measured RSS retention (~6 days/≈250 items), captured connector-driven backfill guidance in the runbook, and aligned locale guidance (preserve `language=de`, Docs glossary follow-up). **Next:** coordinate with Tools to land the state-seeding helper so scripted backfills replace manual Mongo tweaks.|
|FEEDCONN-CERTBUND-02-008 Session bootstrap & cookie strategy|BE-Conn-CERTBUND|Source.Common|**DONE (2025-10-14)** – Feed client primes the portal session (cookie container via `SocketsHttpHandler`), shares cookies across detail requests, and documents bootstrap behaviour in options (`PortalBootstrapUri`).|
-|FEEDCONN-CERTBUND-02-009 Offline Kit export packaging|BE-Conn-CERTBUND, Docs|Offline Kit|**TODO** – Capture JSON search/export snapshots (per-year splits), generate manifest fields (`source`,`from`,`to`,`sha256`,`capturedAt`), and update Offline Kit docs so air-gapped deployments can seed historical CERT-Bund advisories without live fetching. **Remark:** follow the interim workflow documented in `docs/ops/feedser-certbund-operations.md` §3.3 until the packaged artefacts ship.|
+|FEEDCONN-CERTBUND-02-009 Offline Kit export packaging|BE-Conn-CERTBUND, Docs|Offline Kit|**DONE (2025-10-17)** – Added `tools/certbund_offline_snapshot.py` to capture search/export JSON, emit deterministic manifests + SHA files, and refreshed docs (`docs/ops/feedser-certbund-operations.md`, `docs/24_OFFLINE_KIT.md`) with offline-kit instructions and manifest layout guidance. Seed data README/ignore rules cover local snapshot hygiene.|
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/AGENTS.md b/src/StellaOps.Vexer.Connectors.Abstractions/AGENTS.md
index 6d4e8e97..dadb0d0d 100644
--- a/src/StellaOps.Vexer.Connectors.Abstractions/AGENTS.md
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/AGENTS.md
@@ -5,12 +5,12 @@ Defines shared connector infrastructure for Vexer, including base contexts, resu
- `IVexConnector` context implementation, raw store helpers, verification hooks, and telemetry utilities.
- Configuration primitives (YAML parsing, secrets handling guidelines) and options validation.
- Connector lifecycle helpers for retries, paging, `.well-known` discovery, and resume markers.
-- Documentation for connector packaging, plugin manifest metadata, and DI registration.
+- Documentation for connector packaging, plugin manifest metadata, and DI registration (see `docs/dev/30_VEXER_CONNECTOR_GUIDE.md` and `docs/dev/templates/vexer-connector/`).
## Participants
- All Vexer connector projects reference this module to obtain base classes and context services.
- WebService/Worker instantiate connectors via plugin loader leveraging abstractions defined here.
## Interfaces & contracts
-- Connector context, result, and telemetry interfaces; `ConnectorDescriptor`, `ConnectorOptions`, authentication helpers.
+- Connector context, result, and telemetry interfaces; `VexConnectorDescriptor`, `VexConnectorBase`, options binder/validators, authentication helpers.
- Utility classes for HTTP clients, throttling, and deterministic logging.
## In/Out of scope
In: shared abstractions, helper utilities, configuration binding, documentation for connector authors.
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/IVexConnectorOptionsValidator.cs b/src/StellaOps.Vexer.Connectors.Abstractions/IVexConnectorOptionsValidator.cs
new file mode 100644
index 00000000..007004d7
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/IVexConnectorOptionsValidator.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace StellaOps.Vexer.Connectors.Abstractions;
+
+///
+/// Custom validator hook executed after connector options are bound.
+///
+/// Connector-specific options type.
+public interface IVexConnectorOptionsValidator
+{
+ void Validate(VexConnectorDescriptor descriptor, TOptions options, IList errors);
+}
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/StellaOps.Vexer.Connectors.Abstractions.csproj b/src/StellaOps.Vexer.Connectors.Abstractions/StellaOps.Vexer.Connectors.Abstractions.csproj
new file mode 100644
index 00000000..63f633c3
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/StellaOps.Vexer.Connectors.Abstractions.csproj
@@ -0,0 +1,17 @@
+
+
+ net10.0
+ preview
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/TASKS.md b/src/StellaOps.Vexer.Connectors.Abstractions/TASKS.md
index 408ff364..696fe975 100644
--- a/src/StellaOps.Vexer.Connectors.Abstractions/TASKS.md
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/TASKS.md
@@ -2,6 +2,6 @@ If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
-|VEXER-CONN-ABS-01-001 – Connector context & base classes|Team Vexer Connectors|VEXER-CORE-01-003|TODO – Implement `VexConnectorContext`, result types, helper base classes, and deterministic logging helpers for connectors.|
-|VEXER-CONN-ABS-01-002 – YAML options & validation|Team Vexer Connectors|VEXER-CONN-ABS-01-001|TODO – Provide strongly-typed options binding/validation for connector YAML definitions with offline-safe defaults.|
-|VEXER-CONN-ABS-01-003 – Plugin packaging & docs|Team Vexer Connectors|VEXER-CONN-ABS-01-001|TODO – Document connector packaging (NuGet manifest, plugin loader metadata) and supply reference templates for downstream connector modules.|
+|VEXER-CONN-ABS-01-001 – Connector context & base classes|Team Vexer Connectors|VEXER-CORE-01-003|**DONE (2025-10-17)** – Added `StellaOps.Vexer.Connectors.Abstractions` project with `VexConnectorBase`, deterministic logging scopes, metadata builder helpers, and connector descriptors; docs updated to highlight the shared abstractions.|
+|VEXER-CONN-ABS-01-002 – YAML options & validation|Team Vexer Connectors|VEXER-CONN-ABS-01-001|**DONE (2025-10-17)** – Delivered `VexConnectorOptionsBinder` + binder options/validators, environment-variable expansion, data-annotation checks, and custom validation hooks with documentation updates covering the workflow.|
+|VEXER-CONN-ABS-01-003 – Plugin packaging & docs|Team Vexer Connectors|VEXER-CONN-ABS-01-001|**DONE (2025-10-17)** – Authored `docs/dev/30_VEXER_CONNECTOR_GUIDE.md`, added quick-start template under `docs/dev/templates/vexer-connector/`, and updated module docs to reference the packaging workflow.|
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorBase.cs b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorBase.cs
new file mode 100644
index 00000000..ba312419
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorBase.cs
@@ -0,0 +1,99 @@
+using System.Collections.Immutable;
+using System.Security.Cryptography;
+using Microsoft.Extensions.Logging;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Connectors.Abstractions;
+
+///
+/// Convenience base class for implementing .
+///
+public abstract class VexConnectorBase : IVexConnector
+{
+ protected VexConnectorBase(VexConnectorDescriptor descriptor, ILogger logger, TimeProvider? timeProvider = null)
+ {
+ Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ TimeProvider = timeProvider ?? TimeProvider.System;
+ }
+
+ ///
+ public string Id => Descriptor.Id;
+
+ ///
+ public VexProviderKind Kind => Descriptor.Kind;
+
+ protected VexConnectorDescriptor Descriptor { get; }
+
+ protected ILogger Logger { get; }
+
+ protected TimeProvider TimeProvider { get; }
+
+ protected DateTimeOffset UtcNow() => TimeProvider.GetUtcNow();
+
+ protected VexRawDocument CreateRawDocument(
+ VexDocumentFormat format,
+ Uri sourceUri,
+ ReadOnlyMemory content,
+ ImmutableDictionary? metadata = null)
+ {
+ if (sourceUri is null)
+ {
+ throw new ArgumentNullException(nameof(sourceUri));
+ }
+
+ var digest = ComputeSha256(content.Span);
+ var captured = TimeProvider.GetUtcNow();
+ return new VexRawDocument(
+ Descriptor.Id,
+ format,
+ sourceUri,
+ captured,
+ digest,
+ content,
+ metadata ?? ImmutableDictionary.Empty);
+ }
+
+ protected IDisposable BeginConnectorScope(string operation, IReadOnlyDictionary? metadata = null)
+ => VexConnectorLogScope.Begin(Logger, Descriptor, operation, metadata);
+
+ protected void LogConnectorEvent(LogLevel level, string eventName, string message, IReadOnlyDictionary? metadata = null, Exception? exception = null)
+ {
+ using var scope = BeginConnectorScope(eventName, metadata);
+ if (exception is null)
+ {
+ Logger.Log(level, "{Message}", message);
+ }
+ else
+ {
+ Logger.Log(level, exception, "{Message}", message);
+ }
+ }
+
+ protected ImmutableDictionary BuildMetadata(Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+ var builder = new VexConnectorMetadataBuilder();
+ configure(builder);
+ return builder.Build();
+ }
+
+ private static string ComputeSha256(ReadOnlySpan content)
+ {
+ Span buffer = stackalloc byte[32];
+ if (SHA256.TryHashData(content, buffer, out _))
+ {
+ return "sha256:" + Convert.ToHexString(buffer).ToLowerInvariant();
+ }
+
+ using var sha = SHA256.Create();
+ var hash = sha.ComputeHash(content.ToArray());
+ return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
+ }
+
+ public abstract ValueTask ValidateAsync(VexConnectorSettings settings, CancellationToken cancellationToken);
+
+ public abstract IAsyncEnumerable FetchAsync(VexConnectorContext context, CancellationToken cancellationToken);
+
+ public abstract ValueTask NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken);
+}
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorDescriptor.cs b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorDescriptor.cs
new file mode 100644
index 00000000..4e7eb211
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorDescriptor.cs
@@ -0,0 +1,54 @@
+using System.Collections.Immutable;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Connectors.Abstractions;
+
+///
+/// Static descriptor for a Vexer connector plug-in.
+///
+public sealed record VexConnectorDescriptor
+{
+ public VexConnectorDescriptor(string id, VexProviderKind kind, string displayName)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentException("Connector id must be provided.", nameof(id));
+ }
+
+ Id = id;
+ Kind = kind;
+ DisplayName = string.IsNullOrWhiteSpace(displayName) ? id : displayName;
+ }
+
+ ///
+ /// Stable connector identifier (matches provider id).
+ ///
+ public string Id { get; }
+
+ ///
+ /// Provider kind served by the connector.
+ ///
+ public VexProviderKind Kind { get; }
+
+ ///
+ /// Human friendly name used in logs/diagnostics.
+ ///
+ public string DisplayName { get; }
+
+ ///
+ /// Optional friendly description.
+ ///
+ public string? Description { get; init; }
+
+ ///
+ /// Document formats the connector is expected to emit.
+ ///
+ public ImmutableArray SupportedFormats { get; init; } = ImmutableArray.Empty;
+
+ ///
+ /// Optional tags surfaced in diagnostics (e.g. "beta", "offline").
+ ///
+ public ImmutableArray Tags { get; init; } = ImmutableArray.Empty;
+
+ public override string ToString() => $"{Id} ({Kind})";
+}
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorLogScope.cs b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorLogScope.cs
new file mode 100644
index 00000000..5cc8da82
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorLogScope.cs
@@ -0,0 +1,50 @@
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Connectors.Abstractions;
+
+///
+/// Helper to establish deterministic logging scopes for connector operations.
+///
+public static class VexConnectorLogScope
+{
+ public static IDisposable Begin(ILogger logger, VexConnectorDescriptor descriptor, string operation, IReadOnlyDictionary? metadata = null)
+ {
+ ArgumentNullException.ThrowIfNull(logger);
+ ArgumentNullException.ThrowIfNull(descriptor);
+ ArgumentException.ThrowIfNullOrEmpty(operation);
+
+ var scopeValues = new List>
+ {
+ new("vex.connector.id", descriptor.Id),
+ new("vex.connector.kind", descriptor.Kind.ToString()),
+ new("vex.connector.operation", operation),
+ };
+
+ if (!string.Equals(descriptor.DisplayName, descriptor.Id, StringComparison.Ordinal))
+ {
+ scopeValues.Add(new KeyValuePair("vex.connector.displayName", descriptor.DisplayName));
+ }
+
+ if (!string.IsNullOrWhiteSpace(descriptor.Description))
+ {
+ scopeValues.Add(new KeyValuePair("vex.connector.description", descriptor.Description));
+ }
+
+ if (!descriptor.Tags.IsDefaultOrEmpty)
+ {
+ scopeValues.Add(new KeyValuePair("vex.connector.tags", string.Join(",", descriptor.Tags)));
+ }
+
+ if (metadata is not null)
+ {
+ foreach (var kvp in metadata.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
+ {
+ scopeValues.Add(new KeyValuePair($"vex.{kvp.Key}", kvp.Value));
+ }
+ }
+
+ return logger.BeginScope(scopeValues)!;
+ }
+}
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorMetadataBuilder.cs b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorMetadataBuilder.cs
new file mode 100644
index 00000000..8404ab94
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorMetadataBuilder.cs
@@ -0,0 +1,37 @@
+using System.Collections.Immutable;
+
+namespace StellaOps.Vexer.Connectors.Abstractions;
+
+///
+/// Builds deterministic metadata dictionaries for raw documents and logging scopes.
+///
+public sealed class VexConnectorMetadataBuilder
+{
+ private readonly SortedDictionary _values = new(StringComparer.Ordinal);
+
+ public VexConnectorMetadataBuilder Add(string key, string? value)
+ {
+ if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value))
+ {
+ _values[key] = value!;
+ }
+
+ return this;
+ }
+
+ public VexConnectorMetadataBuilder Add(string key, DateTimeOffset value)
+ => Add(key, value.ToUniversalTime().ToString("O"));
+
+ public VexConnectorMetadataBuilder AddRange(IEnumerable> items)
+ {
+ foreach (var item in items)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ return this;
+ }
+
+ public ImmutableDictionary Build()
+ => _values.ToImmutableDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
+}
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorOptionsBinder.cs b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorOptionsBinder.cs
new file mode 100644
index 00000000..d307fbca
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorOptionsBinder.cs
@@ -0,0 +1,157 @@
+using System.Collections.Immutable;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using StellaOps.Vexer.Core;
+
+namespace StellaOps.Vexer.Connectors.Abstractions;
+
+///
+/// Provides strongly typed binding and validation for connector options.
+///
+public static class VexConnectorOptionsBinder
+{
+ public static TOptions Bind(
+ VexConnectorDescriptor descriptor,
+ VexConnectorSettings settings,
+ VexConnectorOptionsBinderOptions? options = null,
+ IEnumerable>? validators = null)
+ where TOptions : class, new()
+ {
+ ArgumentNullException.ThrowIfNull(descriptor);
+ ArgumentNullException.ThrowIfNull(settings);
+
+ var binderSettings = options ?? new VexConnectorOptionsBinderOptions();
+ var transformed = TransformValues(settings, binderSettings);
+
+ var configuration = BuildConfiguration(transformed);
+
+ var result = new TOptions();
+ var errors = new List();
+
+ try
+ {
+ configuration.Bind(
+ result,
+ binderOptions => binderOptions.ErrorOnUnknownConfiguration = !binderSettings.AllowUnknownKeys);
+ }
+ catch (InvalidOperationException ex) when (!binderSettings.AllowUnknownKeys)
+ {
+ errors.Add(ex.Message);
+ }
+
+ binderSettings.PostConfigure?.Invoke(result);
+
+ if (binderSettings.ValidateDataAnnotations)
+ {
+ ValidateDataAnnotations(result, errors);
+ }
+
+ if (validators is not null)
+ {
+ foreach (var validator in validators)
+ {
+ validator?.Validate(descriptor, result, errors);
+ }
+ }
+
+ if (errors.Count > 0)
+ {
+ throw new VexConnectorOptionsValidationException(descriptor.Id, errors);
+ }
+
+ return result;
+ }
+
+ private static ImmutableDictionary TransformValues(
+ VexConnectorSettings settings,
+ VexConnectorOptionsBinderOptions binderOptions)
+ {
+ var builder = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase);
+ foreach (var kvp in settings.Values)
+ {
+ var value = kvp.Value;
+ if (binderOptions.TrimWhitespace && value is not null)
+ {
+ value = value.Trim();
+ }
+
+ if (binderOptions.TreatEmptyAsNull && string.IsNullOrEmpty(value))
+ {
+ value = null;
+ }
+
+ if (value is not null && binderOptions.ExpandEnvironmentVariables)
+ {
+ value = Environment.ExpandEnvironmentVariables(value);
+ }
+
+ if (binderOptions.ValueTransformer is not null)
+ {
+ value = binderOptions.ValueTransformer.Invoke(kvp.Key, value);
+ }
+
+ builder[kvp.Key] = value;
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static IConfiguration BuildConfiguration(ImmutableDictionary values)
+ {
+ var sources = new List>();
+ foreach (var kvp in values)
+ {
+ if (kvp.Value is not null)
+ {
+ sources.Add(new KeyValuePair(kvp.Key, kvp.Value));
+ }
+ }
+
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.Add(new DictionaryConfigurationSource(sources));
+ return configurationBuilder.Build();
+ }
+
+ private static void ValidateDataAnnotations(TOptions options, IList errors)
+ {
+ var validationResults = new List();
+ var validationContext = new ValidationContext(options!);
+ if (!Validator.TryValidateObject(options!, validationContext, validationResults, validateAllProperties: true))
+ {
+ foreach (var validationResult in validationResults)
+ {
+ if (!string.IsNullOrWhiteSpace(validationResult.ErrorMessage))
+ {
+ errors.Add(validationResult.ErrorMessage);
+ }
+ }
+ }
+ }
+
+ private sealed class DictionaryConfigurationSource : IConfigurationSource
+ {
+ private readonly IReadOnlyList> _data;
+
+ public DictionaryConfigurationSource(IEnumerable> data)
+ {
+ _data = data?.ToList() ?? new List>();
+ }
+
+ public IConfigurationProvider Build(IConfigurationBuilder builder) => new DictionaryConfigurationProvider(_data);
+ }
+
+ private sealed class DictionaryConfigurationProvider : ConfigurationProvider
+ {
+ public DictionaryConfigurationProvider(IEnumerable> data)
+ {
+ foreach (var pair in data)
+ {
+ if (pair.Value is not null)
+ {
+ Data[pair.Key] = pair.Value;
+ }
+ }
+ }
+ }
+}
diff --git a/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorOptionsBinderOptions.cs b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorOptionsBinderOptions.cs
new file mode 100644
index 00000000..c4710999
--- /dev/null
+++ b/src/StellaOps.Vexer.Connectors.Abstractions/VexConnectorOptionsBinderOptions.cs
@@ -0,0 +1,45 @@
+namespace StellaOps.Vexer.Connectors.Abstractions;
+
+///
+/// Customisation options for connector options binding.
+///
+public sealed class VexConnectorOptionsBinderOptions
+{
+ ///
+ /// Indicates whether environment variables should be expanded in option values.
+ /// Defaults to true.
+ ///
+ public bool ExpandEnvironmentVariables { get; set; } = true;
+
+ ///
+ /// When true the binder trims whitespace around option values.
+ ///
+ public bool TrimWhitespace { get; set; } = true;
+
+ ///
+ /// Converts empty strings to null before binding. Default: true.
+ ///
+ public bool TreatEmptyAsNull { get; set; } = true;
+
+ ///
+ /// When false, binding fails if unknown configuration keys are provided.
+ /// Default: true (permitting unknown keys).
+ ///
+ public bool AllowUnknownKeys { get; set; } = true;
+
+ ///
+ /// Enables validation after binding.
+ /// Default: true.
+ ///
+ public bool ValidateDataAnnotations { get; set; } = true;
+
+ ///
+ /// Optional post-configuration callback executed after binding.
+ ///
+ public Action