feat(concelier): blocked-readiness state for credential-gated sources (SRC-CREDS-005)

Closes the last open task in SPRINT_20260422_003. Persisted operator
enablement is now separated from runtime readiness so credential-gated
sources can show an explicit blocked state instead of collapsing into a
generic failed/disabled shape.

Readiness model:
- new SourceReadiness constants class: Disabled | Unsupported | Blocked | Ready
- ConfiguredAdvisorySourceStatus gains Readiness + BlockedReason alongside
  existing SyncState (kept as backward-compatible alias)
- enabled = persisted operator intent (untouched)
- readiness = blocked when persisted-enabled and credentials/URIs missing
- blockedReason = free-form list of missing fields
- blockingReason.errorCode = SOURCE_CONFIG_REQUIRED for structured drill-down

Endpoint propagation:
- /status: persisted enabled=true kept; readiness=blocked; readyForSync=false
- /{id}/enable: 200 with readiness=blocked; sourceRegistry left disabled
  until credentials land (pre-existing behaviour retained)
- /{id}/sync: 422 readiness=blocked + SOURCE_CONFIG_REQUIRED;
  **connector never invoked**, no job run created
- /sync (batch): per-result outcome=blocked with readiness/errorCode/
  blockedReason; excluded from totalTriggered; other sources proceed
- Transition: PUT /{id}/configuration with missing credential →
  runtimeOptionsInvalidator.Invalidate → next /status flips to ready.
  No disable/re-enable cycle needed.

Tests: 8 targeted xUnit methods via scripts/test-targeted-xunit.ps1,
8/8 pass. Includes: blocked status exposure, blocked-to-ready transition
on persisted credential, connector-not-invoked-when-blocked, plus 4
pre-existing SRC-CREDS-002 regression tests.

Docs:
- docs/modules/concelier/connectors.md — new "Blocked / sleeping
  readiness state" section with field contract, per-endpoint behaviour
  table, UI/CLI rendering guidance, resolution flow
- docs/modules/cli/guides/commands/db.md — short note under
  `db connectors configure` cross-linking the connectors.md contract

Sprint SPRINT_20260422_003 archived — all 5 tasks DONE.

New fields are additive; existing UI types in
source-management.api.ts ignore unknown fields so no UI breakage. A
future FE pass can wire explicit readiness/blockedReason rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-22 16:28:33 +03:00
parent 06a8558b0f
commit 838257245a
6 changed files with 441 additions and 50 deletions

View File

@@ -9,6 +9,50 @@ Operator configuration note:
- Oracle, Adobe, and Chromium use public defaults and only need UI or CLI input when you override or mirror the upstream endpoints.
- See [source-credentials.md](docs/modules/concelier/operations/source-credentials.md).
---
## Blocked / sleeping readiness state
Each advisory source has two independent flags in its status response:
| Field | Meaning |
| --- | --- |
| `enabled` | Persisted operator intent. `true` means "the operator asked for this source to run". Survives restarts, backfills, and connectivity checks. |
| `readiness` | Runtime readiness. One of `ready`, `blocked`, `disabled`, or `unsupported`. Computed live from connector configuration. |
The `blocked` state is reserved for **credential-gated or URI-gated sources that are persisted-enabled but missing required configuration**. In this state:
- `enabled` remains `true` — the operator's intent is preserved across restarts.
- `readiness` (alias `syncState`) is `blocked`.
- `blockedReason` is a free-form human-readable message naming the missing field(s) (for example, `"GitHub Security Advisories requires an API token before sync can run."`).
- `blockingReason` carries the structured diagnostics object: `errorCode = SOURCE_CONFIG_REQUIRED`, `possibleReasons`, and ordered `remediationSteps`.
- The scheduler and the manual `/sync` and `/sync-all` endpoints **short-circuit** — the connector is never invoked, so the operator does not see a generic scheduler failure or a misleading "last run succeeded" state.
### Endpoint-by-endpoint contract
| Endpoint | Blocked behaviour |
| --- | --- |
| `GET /api/v1/advisory-sources/status` | Per-source `readiness = "blocked"`, `blockedReason` populated, `readyForSync = false`, `enabled = true`. |
| `POST /api/v1/advisory-sources/{sourceId}/enable` | Returns `200 OK` with `{ enabled: true, readiness: "blocked", blockingReason, blockedReason }`. The persisted row is enabled but the source registry is left disabled until credentials land. |
| `POST /api/v1/advisory-sources/{sourceId}/sync` | Returns `422 Unprocessable Entity` with `{ error: "source_config_required", readiness: "blocked", code: "SOURCE_CONFIG_REQUIRED", blockedReason }`. The connector is **not** invoked and no job run is created. |
| `POST /api/v1/advisory-sources/sync` | Each blocked source is reported inside `results[]` with `outcome: "blocked"`, `readiness: "blocked"`, `errorCode: "SOURCE_CONFIG_REQUIRED"`, `blockedReason`; it is excluded from `totalTriggered`. Other sources in the batch still run normally. |
| `POST /api/v1/advisory-sources/check` | Blocked sources keep their persisted `enabled` value instead of being auto-disabled by the periodic connectivity check, so the status continues to reflect operator intent until credentials are supplied. |
### Resolving the blocked state
The operator resolves a blocked source by supplying the missing configuration through either entry path:
- Web UI: `Integrations -> Advisory sources`, open the source card, fill in the fields under **Configuration**, save.
- CLI: `stella db connectors configure <source> --set <field>=<value>` (see [`docs/modules/cli/guides/commands/db.md`](/C:/dev/New%20folder/git.stella-ops.org/docs/modules/cli/guides/commands/db.md)).
On the next `/status` call the source's `readiness` flips to `ready`, `blockedReason` becomes `null`, and `readyForSync` becomes `true`. No disable/re-enable dance is required — the runtime settings cache picks up the persisted change through the options-invalidator and the connector runs on the next scheduler tick or manual trigger.
UI/CLI rendering guidance:
- Render `enabled` and `readiness` as two separate indicators. An `enabled` toggle that silently collapses to a "disabled" or "failed" visualisation hides operator intent from the next operator on shift.
- Prefer `blockedReason` (short human sentence) for the visible row and fall back to `blockingReason.possibleReasons` / `remediationSteps` for an expanded drawer.
- Do not treat `blocked` as an error state for alerting purposes — it is an expected "sleeping" state on fresh installs and on hosts that have not yet received the credential set.
The catalog currently contains **78 source definitions** across **14 categories**. The authoritative source list is defined in `src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs`.
Canonical runtime note: the operator-facing source IDs in this index are the only scheduler/catalog IDs that should be used for Concelier jobs and setup. Legacy connector aliases such as `ics-cisa`, `ics-kaspersky`, `ru-bdu`, `ru-nkcki`, `vndr-adobe`, `vndr-apple`, `vndr-chromium`, `vndr-cisco`, `vndr-oracle`, and `vndr.msrc` remain compatibility-only aliases inside normalization paths and must not appear as primary runtime job keys.