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:

FieldMeaning
installed / installationStateWhether this host has a runnable connector payload for the source. installationState is installed or missing.
enabled / operatorStatePersisted operator intent. operatorState is enabled or disabled.
syncSupportedBackward-compatible alias for installed; true only when the scheduler has a runnable fetch job for the source.
readiness / blockedReasonRuntime 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:

PropertyPurpose
KeyPersisted JSON key. Stored verbatim in sources.config.
LabelOperator-facing label.
KindString / Secret / Uri / Integer / Boolean / Duration / Enum / StringList / FilePath / Double
RequiredWhen true, missing value triggers SOURCE_CONFIG_REQUIRED.
SensitiveTrue for credentials/tokens. Hidden from GET; never echoed.
HelpTextLong-form description.
PlaceholderHint for empty inputs.
DefaultValueConnector-supplied default (canonical form: ISO-8601 for Duration, etc.).
EnumValuesAllowed values when Kind = Enum.
GroupOptional UI grouping (Endpoint, Authentication, Pacing, Backfill, Trust, …).
AliasesLegacy keys probed by the read path.
ValidatorOptional 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:

Runtime cache and IOptionsMonitor invalidation

PUT /api/v1/advisory-sources/{id}/configuration:

  1. Validates each submitted value with the field’s Kind and Validator. Bad values produce a 400 with the field-specific error message.
  2. Persists the canonical form to sources.config (PostgreSQL).
  3. AdvisorySourceRuntimeSettingsCache.Upsert updates the in-memory cache.
  4. AdvisorySourceRuntimeOptionsInvalidator.Invalidate clears the connector’s IOptionsMonitorCache, SourceHttpClientOptions cache, and any token providers.
  5. The next time a connector instance is resolved (the Concelier model resolves NvdConnector and friends as transients), IPostConfigureOptions<TOptions> reads the new values from AdvisorySourceRuntimeSettingsCache via AdvisorySourceRuntimeOptionsOverlay and 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:

FieldKindGroup
metadataUriUriEndpoint
metadataCacheDurationDurationPacing
offlineSnapshotPathFilePathOffline snapshot
preferOfflineSnapshotBooleanOffline snapshot
persistOfflineSnapshotBooleanOffline snapshot
allowBuiltInSnapshotFallbackBooleanOffline snapshot
trustWeightDoubleTrust
cosignIssuerStringSignature verification
cosignIdentityPatternStringSignature verification
pgpFingerprintsStringListSignature verification
allowUnsignedBooleanSignature verification
cosignSignatureSuffixStringSignature verification
cosignCertificateSuffixStringSignature verification
pgpSignatureSuffixStringSignature verification
cryptoProfileEnumSignature 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

PathTypeDefaultNotes
enabledBooleantrueWhen false, the limiter permits everything immediately.
defaultClassString"default"Picked when a host is not in hosts. Logs a warning on first use of an unmapped host.
successIncreaseThresholdInteger10K consecutive 200/304 before additive RPS increase.
additiveIncreaseRpsDouble0.1RPS step for the additive increase.
multiplicativeDecreaseFactorDouble0.5Multiplier applied on 429/503. Must be in (0,1) — falls back to 0.5 otherwise.
jitterEnvelopeDouble0.15±envelope ratio applied to the base interval (e.g. 0.15 = ±15 %).
classes.<name>.initialRpsDoubleper classStarting RPS cap for hosts assigned to this class.
classes.<name>.minRpsDoubleper classFloor for the multiplicative decrease.
classes.<name>.maxRpsDoubleper classCeiling for the additive increase.
hosts.<authority>.classStringdefaultClassClass to apply to the host. <authority> matches Uri.Authority lowercased (e.g. services.nvd.nist.gov, host.example:8443).
hosts.<authority>.{initialRps,minRps,maxRps}Doubleinherits from classPer-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

  1. Watch concelier.throttle.aimd_decrease_total{reason="429"} for sustained non-zero increments on the host’s tag.
  2. Hit /api/v1/diagnostics/throttle and confirm currentRpsCap is bouncing near the class minimum.
  3. Lower hosts.<authority>.initialRps (and optionally maxRps) under concelier:throttling. Re-bind is live; no restart needed.
  4. If the host returns Retry-After headers, prefer leaving caps alone — the limiter already honors quietUntilUtc as a hard floor.