Concelier — connector-owned configuration schema
Sprint: SPRINT_20260503_011_Concelier_connector_config_schema_completion
This document describes how Stella Ops surfaces operator-tunable settings for advisory sources to the UI (/ops/integrations/advisory-vex-sources) and the CLI (stella sources config …). Each connector publishes its own typed schema; the schema mechanism — not a centralised table — is the source of truth.
The contract
Each connector that exposes operator-tunable settings ships a class implementing StellaOps.Concelier.Core.Sources.Configuration.IAdvisorySourceConfigurationContributor. The class declares the source-id and the typed list of fields. The connector’s Add{Connector}Connector extension registers the contributor with DI:
services.AddAdvisorySourceConfigurationContributor<MyConnectorConfigurationContributor>();
AdvisorySourceConfigurationRegistry composes every registered contributor at boot. The Concelier WebService endpoints (GET /api/v1/advisory-sources/{id}/configuration, PUT /api/v1/advisory-sources/{id}/configuration) project the descriptor onto the JSON wire format and validate every submitted value through AdvisorySourceFieldValueValidator.
In pluginized deployments the configuration response also carries runtime state:
| Field | Meaning |
|---|---|
installed / installationState | Whether this host has a runnable connector payload for the source. installationState is installed or missing. |
enabled / operatorState | Persisted operator intent. operatorState is enabled or disabled. |
syncSupported | Backward-compatible alias for installed; true only when the scheduler has a runnable fetch job for the source. |
readiness / blockedReason | Runtime readiness after combining operator intent, installed payload, and required configuration. |
This distinction is intentional: a source can be present in the catalog and have editable settings while its connector bundle is not mounted. In that case /configuration still returns the schema, but reports installationState: "missing" and syncSupported: false so UI/CLI clients do not offer a fake sync path.
Field shape
AdvisorySourceFieldDescriptor carries:
| Property | Purpose |
|---|---|
Key | Persisted JSON key. Stored verbatim in sources.config. |
Label | Operator-facing label. |
Kind | String / Secret / Uri / Integer / Boolean / Duration / Enum / StringList / FilePath / Double |
Required | When true, missing value triggers SOURCE_CONFIG_REQUIRED. |
Sensitive | True for credentials/tokens. Hidden from GET; never echoed. |
HelpText | Long-form description. |
Placeholder | Hint for empty inputs. |
DefaultValue | Connector-supplied default (canonical form: ISO-8601 for Duration, etc.). |
EnumValues | Allowed values when Kind = Enum. |
Group | Optional UI grouping (Endpoint, Authentication, Pacing, Backfill, Trust, …). |
Aliases | Legacy keys probed by the read path. |
Validator | Optional connector-owned validator delegate. |
The validator runs after kind-level coercion (Uri → absolute http/https; Duration → ISO-8601 + shorthand; Boolean → canonicalised; Enum → matched case-insensitively; StringList → newline-canonicalised).
Vendor-specific shapes — preserved one-for-one
The schema mechanism is explicit: connectors do not flatten their Options class to a generic “URL + token” shape. Vendor-specific fields stay first-class. Examples shipped in this sprint:
- NVD:
baseEndpoint,windowSize(Duration),windowOverlap(Duration),initialBackfill(Duration). No auth. - GHSA: optional
apiToken(Secret) — vendor-specific GitHub PAT/App token used for higher REST API rate limits or enterprise routing. Public global advisories can run anonymously. - MSRC (Microsoft):
tenantId(with GUID validator),clientId,clientSecret(Secret) — Azure client-credential triplet. - Oracle:
calendarUris,advisoryUris(both StringList) — preserves multi-URI override. - Adobe:
indexUri,additionalIndexUris(StringList),seedFallbackEnabled(Boolean),seedDirectory(FilePath),seedIndexFileName(String),seedOfficialVerified(Boolean),seedTrustLevel(String) — primary/mirror URIs plus disabled-by-default operator-managed local HTML seed fallback. - Chromium:
feedUrionly. - CVE:
baseEndpoint,cveListV5Endpoint,apiOrg,apiUser,apiKey(Secret),cveListV5BaselineZipPath(FilePath),cveListV5BaselineZipSha256,seedDirectory(FilePath), pacing/backfill knobs — optional CVE Services auth triplet, operator-supplied cvelistV5 baseline zip, rolling public cvelistV5 delta, or offline seed. - EPSS:
airgapMode(Boolean),bundlePath(FilePath) — air-gap branch. - OSV:
ecosystems(StringList) — scope filter. - Distro connectors (Alpine/Debian/Ubuntu/SUSE/RedHat): each preserves its specific URIs (list + detail bases), per-fetch caps, fetch timeouts, resume overlap, request delay, and user-agent. Debian also exposes default-off full security-tracker JSON corpus controls (
enableJsonTracker,jsonTrackerEndpoint,jsonMaxAdvisoriesPerMapCycle,jsonTrackerReleases). - CERT-CC: window cursor flattened with dotted keys (
summaryWindow.windowSize, etc.). - CCCS / Acsc: composite multi-language and multi-feed scoping encoded as
StringListwith documented composite-string format (lang|uri,slug|path|enabled). - Ics.Cisa:
govDeliveryCode(Required + Sensitive Secret). - Apple:
advisoryAllowlist,advisoryBlocklist(both StringList) — trust-side filters.
Runtime cache and IOptionsMonitor invalidation
PUT /api/v1/advisory-sources/{id}/configuration:
- Validates each submitted value with the field’s
KindandValidator. Bad values produce a 400 with the field-specific error message. - Persists the canonical form to
sources.config(PostgreSQL). AdvisorySourceRuntimeSettingsCache.Upsertupdates the in-memory cache.AdvisorySourceRuntimeOptionsInvalidator.Invalidateclears the connector’sIOptionsMonitorCache,SourceHttpClientOptionscache, and any token providers.- The next time a connector instance is resolved (the Concelier model resolves
NvdConnectorand friends as transients),IPostConfigureOptions<TOptions>reads the new values fromAdvisorySourceRuntimeSettingsCacheviaAdvisorySourceRuntimeOptionsOverlayand applies them.
The currently-overlaid Options types (live DB-driven re-bind without restart): GHSA, Cisco, Microsoft, Oracle, Adobe, Chromium, NVD, Debian. Other connectors persist DB config and surface it in GET /configuration, but their live re-bind requires a service restart unless they are migrated to the overlay (B-CONNCFG-002 follow-up: per-connector overlay pattern is the planned architectural fix).
Worked example — NVD
# Describe the schema (CLI, sprint 013)
$ stella sources config describe nvd
nvd — NVD CVE Feed
Endpoint
baseEndpoint Uri default: https://services.nvd.nist.gov/rest/json/cves/2.0
Pacing
windowSize Duration default: PT4H
windowOverlap Duration default: PT5M
Backfill
initialBackfill Duration default: P7D
# Update the window (UI does the same via PUT)
$ stella sources config set nvd --field=windowSize=2h
nvd: persisted windowSize=PT2H
# Verify
$ stella sources config get nvd
{ "windowSize": "PT2H", "windowOverlap": "PT5M", ... }
Worked example — Excititor RedHat CSAF (Sprint 012 scope)
When sprint 012 unifies Excititor providers into the catalog, the same field-kind taxonomy will carry the supply-chain-sensitive shape:
| Field | Kind | Group |
|---|---|---|
metadataUri | Uri | Endpoint |
metadataCacheDuration | Duration | Pacing |
offlineSnapshotPath | FilePath | Offline snapshot |
preferOfflineSnapshot | Boolean | Offline snapshot |
persistOfflineSnapshot | Boolean | Offline snapshot |
allowBuiltInSnapshotFallback | Boolean | Offline snapshot |
trustWeight | Double | Trust |
cosignIssuer | String | Signature verification |
cosignIdentityPattern | String | Signature verification |
pgpFingerprints | StringList | Signature verification |
allowUnsigned | Boolean | Signature verification |
cosignSignatureSuffix | String | Signature verification |
cosignCertificateSuffix | String | Signature verification |
pgpSignatureSuffix | String | Signature verification |
cryptoProfile | Enum | Signature verification |
Every cosign/PGP/offline knob the connector exposes today must reach the operator surface; otherwise Stella Ops’ fail-closed signature verification posture cannot be administered.
Sprint 20260504_003 — Concelier:Throttling (adaptive host throttler)
The adaptive host throttler is configured globally (not per-connector) under Concelier:Throttling. Connectors inherit it for free through SourceFetchService and the Excititor AdaptiveThrottlingHandler — no connector-side knobs to tune. Live re-bind is honored via IOptionsMonitor.
concelier:
throttling:
enabled: true # master switch; false = fail-open everywhere
defaultClass: "default" # class applied to unmapped hosts
successIncreaseThreshold: 10 # K consecutive 200/304 before +RPS
additiveIncreaseRps: 0.1 # RPS step for the additive increase
multiplicativeDecreaseFactor: 0.5 # 429/503 multiplier (must be 0..1)
jitterEnvelope: 0.15 # ±15 % perturbation of base interval
classes:
default: { initialRps: 1.0, minRps: 0.1, maxRps: 10.0 }
bigIron: { initialRps: 1.0, minRps: 0.1, maxRps: 10.0 }
vendor: { initialRps: 0.5, minRps: 0.05, maxRps: 5.0 }
distro: { initialRps: 0.25, minRps: 0.05, maxRps: 2.0 }
cert: { initialRps: 0.5, minRps: 0.05, maxRps: 3.0 }
osvEpss: { initialRps: 2.5, minRps: 0.5, maxRps: 20.0 }
mirror: { initialRps: 25.0, minRps: 5.0, maxRps: 100.0 }
hosts:
"services.nvd.nist.gov": { class: "bigIron" }
"security-tracker.debian.org": { class: "distro" }
# …or override per host:
"api.example.test": { class: "vendor", initialRps: 0.2, maxRps: 1.0 }
Field reference
| Path | Type | Default | Notes |
|---|---|---|---|
enabled | Boolean | true | When false, the limiter permits everything immediately. |
defaultClass | String | "default" | Picked when a host is not in hosts. Logs a warning on first use of an unmapped host. |
successIncreaseThreshold | Integer | 10 | K consecutive 200/304 before additive RPS increase. |
additiveIncreaseRps | Double | 0.1 | RPS step for the additive increase. |
multiplicativeDecreaseFactor | Double | 0.5 | Multiplier applied on 429/503. Must be in (0,1) — falls back to 0.5 otherwise. |
jitterEnvelope | Double | 0.15 | ±envelope ratio applied to the base interval (e.g. 0.15 = ±15 %). |
classes.<name>.initialRps | Double | per class | Starting RPS cap for hosts assigned to this class. |
classes.<name>.minRps | Double | per class | Floor for the multiplicative decrease. |
classes.<name>.maxRps | Double | per class | Ceiling for the additive increase. |
hosts.<authority>.class | String | defaultClass | Class to apply to the host. <authority> matches Uri.Authority lowercased (e.g. services.nvd.nist.gov, host.example:8443). |
hosts.<authority>.{initialRps,minRps,maxRps} | Double | inherits from class | Per-host overrides. |
Inspecting live state
curl -H "Authorization: Bearer $(stella auth token --scope concelier.admin)" \
https://concelier.stella-ops.local/api/v1/diagnostics/throttle
Returns a JSON object keyed by host with the snapshot fields {class, currentRpsCap, minRpsCap, maxRpsCap, lastRequestUtc, quietUntilUtc, consecutiveSuccesses, totalRequests}.
Operator runbook — tuning a noisy host
- Watch
concelier.throttle.aimd_decrease_total{reason="429"}for sustained non-zero increments on the host’s tag. - Hit
/api/v1/diagnostics/throttleand confirmcurrentRpsCapis bouncing near the class minimum. - Lower
hosts.<authority>.initialRps(and optionallymaxRps) underconcelier:throttling. Re-bind is live; no restart needed. - If the host returns
Retry-Afterheaders, prefer leaving caps alone — the limiter already honorsquietUntilUtcas a hard floor.
