Platform Service (StellaOps.Platform.WebService)
Verification status (2026-09-26, DOC-2 of SPRINT_20260926_001): the security read model, legacy alias list, data model, analytics ingestion and analytics sections below were corrected against commit
942a18727c; the analytics sections were rewritten for the PCD-127 withdrawal on 2026-09-30. The remaining route-level detail was not re-verified line by line;architecture.mdand its Evidence table are authoritative.
Purpose
Provide a single, deterministic aggregation layer for cross-service UX workflows (health, quotas, onboarding, preferences and search availability) so the Console UI and CLI do not fan out to multiple modules directly.
Non-goals
- Replace module-owned APIs (Authority, Policy, Scanner, Orchestrator, etc.).
- Ingest or mutate raw evidence or policy overlays.
- Store high-volume evidence payloads (SBOMs, VEX, audit bundles).
Responsibilities
- Aggregate platform health and dependency status.
- Read tenant usage through its owning service API and distinguish measured usage from configured ceilings and unavailable contracts.
- Persist onboarding progress and tenant setup milestones.
- Persist dashboard personalization and layout preferences.
- Persist authenticated user language preference for shared Web/CLI locale selection.
- Report global search unavailable until an authoritative owner provider is supported.
- Provide global context selectors (region/environment/time window) and per-user persistence for Pack 22 top-bar context.
- Provide Pack 22 release read-model projections for list/detail/activity/approvals queue views.
- Provide Pack 22 topology inventory read-model projections for regions/environments/targets/hosts/agents/promotion paths/workflows/gate profiles.
- Surface platform metadata for UI bootstrapping (version, build, offline status).
API surface (v1)
Health aggregation
- GET
/health - GET
/health/liveness - GET
/health/readiness - GET
/healthz - GET
/readyz - GET
/api/v1/platform/health/summary - GET
/api/v1/platform/health/readiness - GET
/api/v1/platform/health/dependencies - GET
/api/v1/platform/health/incidents - GET
/api/v1/platform/health/metrics - Top-level health aliases are anonymous container/orchestrator probes; the authenticated platform readiness contract remains
/api/v1/platform/health/readiness. GET /api/v1/platform/health/readinessis the canonical readiness contract for both setup gating and post-boot diagnostics.- Required setup-blocking dependencies currently include the five converged required setup steps (
database,valkey,migrations,admin-bootstrap,crypto-profile) plus livefrontdoorandauthorityprobes. - Optional post-boot dependencies are discovered from configured
STELLAOPS_*_URLendpoints and currently includerelease-orchestrator,policy-engine,scanner,signals,notify,scheduler,registry-token,sbomservice,packsregistry, andadvisoryai.
Quota aggregation
GET
/api/v1/platform/quotas/summaryGET
/api/v1/platform/quotas/tenants/{tenantId}GET
/api/v1/platform/quotas/alertsThe two metered reads return
jobengine.runs: the JobEngine owner’s complete count of persisted run records created for the authenticated tenant in the current UTC day. The response includes the owner measurement time and half-open period bounds. Every state and retry record counts; pagination does not cap this quantity.Configure
STELLAOPS_JOBENGINE_URLto the owner service origin. The named client forwards the validated caller bearer or gateway identity envelope, and JobEngine independently requiresquota.readororch:quotaplus tenant binding. Redirects are refused. Platform validates the tenant, schema, unit, period and required fields; an unavailable, refused or malformed source returns503witherror=quota_source_unavailablerather than a default count.Platform:Quotas:Limits:jobengine.runsoptionally supplies an explicit operator ceiling. Without it,limitandremainingare JSON null while measuredusedremains available. A configured ceiling yieldsremaining=max(0,limit-used). This is a read of usage and policy configuration; it does not claim job admission enforcement or a licence entitlement.GET
/api/v1/platform/quotas/alertsreturns503: no durable quota-alert configuration source is published. Both this group’s and the Console’s alert POSTs are withdrawn; an attempted POST to the GET-only path returns405 Method Not Allowedand stores or echoes nothing.The Console’s existing GET paths
/api/v1/authority/quotas,/consumption,/dashboard,/history,/tenants,/tenants/{tenantId},/forecastand/alertsreturn503in every environment. Their entitlement, full consumption, tenant catalogue, trend, forecast and alert contracts have no complete owner sources. They direct callers to the metered Platform summary. No default tenant name, fabricated licence limits, consumption fallback, alert threshold or forecast is returned.PlatformRouteMatcherAmbiguityTestsboots the composed Platform fixture in Testing and Production and checks the shared matcher-shape assertion. Its measured route floors prevent a missing route table from passing; the architecture coverage census requires this guard and grants Platform no unguarded-fixture exemption.PlatformQuotaServicecaches the owner measurement per tenant and UTC day, preserving itsdataAsOftimestamp. A new day uses a new cache key.MeteredQuotaHttpTests,QuotaEndpointsTestsand the owner’s persistence/API tests re-verify the source, unknown-ceiling and failure contracts.POST /api/v1/authority/quotas/reportsandGET /api/v1/authority/quotas/reports/{reportId}are not served in any environment (404 Not Found): no quota report or export backend exists.GET /api/v1/gateway/rate-limitsandGET /api/v1/gateway/rate-limits/violationsanswer503 Service Unavailablewith a problem document whoseerrorisrate_limit_source_unavailable, in every environment. The router gateway enforces rate limits but publishes no read API for limiter state or violations, and Platform does not read the gateway’s limiter store, so Platform has no source for either route. Example:GET /api/v1/gateway/rate-limits/violations?limit=10 X-StellaOps-TenantId: acme HTTP/1.1 503 Service Unavailable Content-Type: application/problem+json {"type":"https://tools.ietf.org/html/rfc9110#section-15.6.4","title":"Rate-limit state unavailable","status":503,"detail":"No service publishes gateway rate-limit state or violations; Platform has no source for them.","error":"rate_limit_source_unavailable","traceId":"00-<trace-id>-<span-id>-00"}The Console quota dashboard reads the metered summary above for the signed-in tenant and shows each owner row with its unit, period bounds, source and measurement time. An operator ceiling and the remaining amount appear only when a ceiling is configured; no status or percentage is derived without one. The dashboard no longer calls the Console entitlement, consumption, dashboard, history or forecast paths that answer
503, and the Console forecast, report-export and alert-configuration pages are withdrawn (CCP-037). Each retained panel (measured usage, top tenants, recent throttle events) loads independently: a failed read renders a service-failure panel while the other panels stay visible, and a tenant switch drops what was read for the previous tenant before reading again. The throttle page reports status and violations failures separately and withholds violation-count tiles after a failed read. Empty success states appear only after a successful read returns no items; Refresh or the failure panel’s Retry re-reads the corresponding source. The dashboard admits eitherquota.readororch:quota, matching the owner read policy.Platform does not declare
/api/v1/jobengine/quotasor/api/v1/jobengine/quotas/summary. The former development/testing-only hardcoded compatibility responses were retired bySPRINT_20260809_001RAR-11; Platform is not a fallback owner for JobEngine quota operations.
Onboarding
- GET
/api/v1/platform/onboarding/status - POST
/api/v1/platform/onboarding/complete/{step} - POST
/api/v1/platform/onboarding/skip - GET
/api/v1/platform/tenants/{tenantId}/setup-status
Preferences
- GET
/api/v1/platform/preferences/dashboard - PUT
/api/v1/platform/preferences/dashboard - GET
/api/v1/platform/preferences/language - PUT
/api/v1/platform/preferences/language - GET
/api/v1/platform/dashboard/profiles - GET
/api/v1/platform/dashboard/profiles/{profileId} - POST
/api/v1/platform/dashboard/profiles
Global search
- GET
/api/v1/search(alias to/api/v1/platform/search) - GET
/api/v1/platform/search - Legacy notice: both GET endpoints emit
Deprecation,Sunset, andWarning. The unsupported successor recommendation is withdrawn; Platform advertises no replacement URI. - AdvisoryAI independently serves
POST /api/advisoryai/v1/search/querywith a JSON body containing requiredq, optionalk, and optional domain/entity filters. It requires an authenticated tenant claim andadvisory-ai:operateoradvisory-ai:admin. Its entity-card response and top-K selection differ from Platform’ssources,limit, andoffsetcontract; it is not a drop-in replacement. - Both admitted GET requests return HTTP
503withapplication/problem+jsonand stableerror=search_provider_unavailable. Platform has no authoritative search provider. The former fixed catalog, successful result model and search-cache path have been removed. - Tenant resolution and
search.readauthorization still apply.q,sources,limitandoffsetremain accepted request fields; malformed integer pagination returns400. Source filtering and pagination cannot return entity results while search is unavailable. A503is an availability failure, not an empty successful search. - PCD-454’s acceptance receipt (pruned evidence
docs/implplan/_evidence/20261007-pcd454-search-provider-truth/receipt.mdat406410e1c1) records source and registered HTTP-handler verification. No installation deployment is implied. - Re-verify the registered HTTP responses with
pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Platform/__Tests/StellaOps.Platform.WebService.Tests/StellaOps.Platform.WebService.Tests.csproj -Class "*SearchEndpointsTests" -BuildProjectReferences.
Registry image search
- GET
/api/v1/registries/images/search?q=... - GET
/api/v1/registries/images/digests?repository=project/name - GET
/api/v1/registries/images/digests?repository=project/name&digest=sha256:... - Digest entries include
tag, immutabledigest,pushedAt, and OCI label metadata when the registry exposes it through artifact labels, annotations, or manifest config labels. The optionaldigestquery filters the registry artifact list before returning labels for an image-security detail page. Release Orchestrator uses those labels to populate component-versionsourceCommitSha,builtAt, and operator-facing version labels duringsync-versions. - Production requires an explicitly configured registry backend (
STELLAOPS_HARBOR_URLorPlatform:HarborFixtureUrl). When no backend is configured, unreachable, returns non-JSON, or returns a failing status, Platform returns503 Service Unavailableinstead of fabricating an empty success result. - The Harbor fixture default is available only in
DevelopmentandTestingfor deterministic local/test harnesses.
Metadata
- GET
/api/v1/platform/metadata - Response includes a capabilities list for UI bootstrapping (
health,onboarding,preferences,quotas,search);searchhasenabled=falsebecause no authoritative provider is available. There is noanalyticscapability: the SBOM-lake analytics surface is withdrawn (see below).
Localization
- GET
/platform/i18n/{locale}.json(anonymous, cacheable UI translation bundle) - GET
/api/v1/platform/localization/bundles/{locale} - GET
/api/v1/platform/localization/bundles/{locale}/{namespace} - GET
/api/v1/platform/localization/locales(catalog used by Web and CLI locale selectors) - PUT
/api/v1/platform/localization/bundles - DELETE
/api/v1/platform/localization/strings/{locale}/{key} - Backend locale resolution contract:
X-Locale->Accept-Language-> default locale. - Runtime bundle layering consumed by backend services: shared embedded
common-> service embedded bundle -> Platform override bundle. - Platform ships locale-complete
uiandplatformnamespace bundles foren-US,de-DE,bg-BG,ru-RU,es-ES,fr-FR,uk-UA,zh-TW,zh-CN; shared localization library now providescommonbundles for the same locale set. - Bundled locales currently shipped:
en-US,de-DE,bg-BG,ru-RU,es-ES,fr-FR,uk-UA,zh-TW,zh-CN.
Stella Assistant
- GET
/api/v1/stella-assistant/tips,/api/v1/stella-assistant/glossary,/api/v1/stella-assistant/toursand/api/v1/stella-assistant/user-state(ui.preferences.read); PUT/api/v1/stella-assistant/user-state(ui.preferences.write). - Admin routes under
/api/v1/stella-assistant/admin(platform.setup.admin): tips, tours and glossary terms. - Tenant: every assistant route takes the tenant from the authenticated principal through the same request-context resolver as the other Platform routes. That is the
stellaops:tenantclaim, or the canonicalX-StellaOps-TenantIdheader for a global-admin principal only. A request without a resolvable tenant returns400witherror: tenant_missing. The legacyX-Tenant-Idheader is ignored, and there is no default tenant. - Shipped content: the
_systemtips, greetings, glossary terms and tours come from the Startup migrations060_StellaAssistant.sqland061_StellaAssistantSeedData.sql. Reads return the caller tenant’s rows together with the_systemrows: the non-admin routes return active rows only, and the admin tip and tour lists also return inactive rows. - Glossary: GET returns the caller’s terms and the system terms for the locale (falling back to
en-US), ordered by term. A tenant term with the same name as a system term does not replace it; both are returned. POST/api/v1/stella-assistant/admin/glossarycreates or updates one term owned by the caller’s tenant, keyed by tenant, term and locale, so it never changes another tenant’s row. To deactivate an own term, sendisActive: false; there is no delete route. - The shipped
_systemglossary terms are read-only through the API. A principal whose tenant resolves to_systemreceives403witherror: system_glossary_read_only. A system term changes only through a shipped migration. - Tip deactivation: DELETE
/api/v1/stella-assistant/admin/tips/{tipId}setsis_activeto false on one tip owned by the caller’s tenant and returns200. The update matches on tenant and id. AtipIdthat is not a GUID, names another tenant’s tip or names a shipped_systemtip returns404, and no row changes. The shipped_systemtips are read-only through this route: a principal whose tenant resolves to_systemreceives403witherror: system_tips_read_only.
Release Orchestrator ownership and topology projection
- Platform does not map or proxy
/api/v1/release-orchestrator/environments*,/api/v2/scripts*, or/api/v1/federation/*operations. Gateway routes those public URLs directly to the owning ReleaseOrchestrator WebApi. The former Platform EvidenceThread surface is absent; it is not a hidden proxy to dormant library code. - Platform does not reference RO Environment, Scripts, EvidenceThread, Federation, Agent, or persistence implementations. Its only RO project reference is the producer-owned, zero-reference
StellaOps.ReleaseOrchestrator.Topology.Contractswire project. - For its rebuildable topology projection, Platform enumerates active tenant UUIDs from its own
shared.tenantstable and calls RO directly atGET /internal/v1/release-orchestrator/topology-snapshot. This internal owner call has no Gateway/Router route and no database fallback. - Each request carries a tenant-bound signed identity envelope valid for two minutes and containing only
release:read.STELLAOPS_RELEASE_ORCHESTRATOR_URLis required and is the only accepted owner-base setting; transport, schema-version, payload, identity, and tenant-partition failures fail closed. - RO owns
STELLAOPS_FEATURE_FEDERATIONand the regional-federation host configuration. The public read and write routes remain routed to RO. The bundled single-region host is read-only: reads execute, while every mutation returns explicit HTTP 503 because mutation execution is not installed or implemented. There is currently no operator-selectable execution provider for those writes.
CM-6 boundary statements are verified against d3358c84319b0da835e944d70f360f7df526d9c5. Re-verify with rg -n "ProjectReference Include=.*ReleaseOrchestrator" src/Platform -g '*.csproj', rg -n "topology-snapshot" src/Platform src/ReleaseOrchestrator src/Router devops, and rg -n "release\.(regions|environments|targets|agents)" src/Platform/StellaOps.Platform.WebService -g '*.cs'.
Notification admin routing
- Platform no longer serves synthetic
/api/v1/notify/*admin compatibility payloads for quiet-hours, throttles, escalation, localization, or digest schedule management. - Core notify toolkit flows remain on
/api/v1/notify/*through the owning Notify surface. - Advanced notification admin flows use Notify’s native
/api/v2/notify/*runtime through Gateway. Suppression overrides use/api/v2/overrides/*; the old/api/v1/notifier/*rewriting aliases are retired by RAR-7. - Digest schedule CRUD remains unsupported in the live runtime; the Web console must present that surface as unavailable rather than fabricate records.
Synthetic compatibility harnesses
- Production does not map Platform-owned synthetic compatibility route groups for
/api/console/*,/api/v1/notify/*, or/api/v1/signals/*. The synthetic AOC compatibility group is not mapped in any environment (withdrawn on 2026-10-08 by SPRINT_20261007_002 CCP-004). - The Platform quota aliases under
/api/v1/authority/quotas/*and/api/v1/gateway/rate-limits*are served byPlatformEndpointsin every environment (see Quota aggregation); no synthetic quota group is mapped in any environment. - Those route groups remain available only in
DevelopmentandTestingto support deterministic compatibility harnesses and legacy UI tests. The synthetic/api/v1/jobengine/quotas*group (RAR-11) and the synthetic quota host (PCD-229) are retired in every environment. - Production callers must use the owning service APIs or receive a normal
404 Not Foundwhen targeting the removed synthetic Platform route surface.
QA fixture readback
- GET
/api/qa/fixtures/advanced-assurance-golden - This setup-admin gated route is disabled by default. Enable it with
Platform:QaFixtures:Enabled=trueorSTELLAOPS_QA_FIXTURES_ENABLED=true. - The route reads only the configured local seed artifact directory from
Platform:QaFixtures:AdvancedAssuranceGoldenSeedDirectoryorSTELLAOPS_ADVANCED_ASSURANCE_GOLDEN_SEED_DIR. Callers cannot pass filesystem paths in the request. - Required artifacts are
seed-manifest.jsonandseed-integrity.json;seed-command-output.txtis reported when present. - Platform validates the seed manifest digest recorded by
seed-integrity.jsonbefore returning readback data. Missing artifacts return503; digest drift returns409. - The response proves deterministic local seed artifact integrity and stable IDs. It does not claim downstream module databases contain equivalent live rows or that any ASSURE scenario has passed.
Advanced assurance case summary
- GET
/api/assurance/cases/ASSURANCE-GOLDEN-PROD-PAYMENTS-001 - This operator-readable route uses the same QA fixture enablement and seed directory settings as
GET /api/qa/fixtures/advanced-assurance-golden. - Platform validates the seed manifest and integrity ledger before returning the seeded environment, primary service, fixture advisory, vulnerable package, release hotfix fields, and section readiness.
- The response reports
status=fixture-backed-blocked-live-importandsource.liveRowsImported=falseuntil module-owned write-through/import exists. Requests for other case IDs return404.
Advanced assurance scenario transactions
- GET/POST
/api/assurance/cases/ASSURANCE-GOLDEN-PROD-PAYMENTS-001/scenarios/{scenarioId}/{*transactionPath} - This operator-readable route resolves ASSURE-001 through ASSURE-022 scenario transaction paths from the golden fixture contract after the same seed manifest and integrity validation as the case summary route.
- Responses include deterministic case id, scenario id, transaction id, transaction path, stable required refs, evidence refs, seed manifest digest, and explicit no-overclaim flags.
- Platform remains aggregation-only:
source.liveRowsImported=false,noOverclaim.moduleWriteThrough=false, and legal compliance/regulator/live-external-verification claims are false until owning modules provide live evidence. - Negative/tamper-style transaction paths, including
api-03, return HTTP 422 withverdict=failed_closedand a fail-closed evidence ref so reruns cannot treat a blocked scenario as a warning-only success.
API surface (v2)
Global context
- GET
/api/v2/context/regions - GET
/api/v2/context/environments?regions= - GET
/api/v2/context/preferences - PUT
/api/v2/context/preferences - PUT
/api/v2/context/regions/{regionId} - PUT
/api/v2/context/environments/{environmentId}
The context region/environment PUT routes are setup-admin convergence APIs for operator-managed installs. They let setup scripts and local lab preparation add real deployment regions and environments without direct SQL.
PUT /api/v2/context/regions/{regionId} accepts:
{
"displayName": "Local Lab",
"sortOrder": 5,
"enabled": true
}
PUT /api/v2/context/environments/{environmentId} accepts:
{
"regionId": "local-lab",
"environmentType": "development",
"displayName": "Customer TEST Lab",
"sortOrder": 6,
"enabled": true
}
Both routes require platform.context.write. Region IDs and environment IDs are normalized to lower-case route IDs. Environment upsert requires the referenced region to already exist. Validation failures use deterministic error codes such as region_id_required, region_not_found, environment_id_required, environment_type_required, and environment_type_invalid.
Releases read model
- GET
/api/v2/releases - GET
/api/v2/releases/{releaseId} - GET
/api/v2/releases/activity - GET
/api/v2/releases/approvals
Topology inventory read model
- GET
/api/v2/topology/regions - GET
/api/v2/topology/environments - GET
/api/v2/topology/targets - GET
/api/v2/topology/hosts - GET
/api/v2/topology/agents - GET
/api/v2/topology/promotion-paths - GET
/api/v2/topology/workflows - GET
/api/v2/topology/layout - GET
/api/v2/topology/gate-profiles - The PostgreSQL synchronizer obtains source topology only through RO’s schema-v1 owner snapshot. Platform’s sole local source query for this flow is the active UUID set in
shared.tenants; it does not query RO source tables in its own or a sibling database. - RO returns regions, environments, targets, and agents in deterministic order and rejects any row outside the authenticated tenant. Agent capabilities are the distinct, ordinal-sorted union of built-in and extended capabilities; canonical agent status
Staleis projected on the wire asoffline. /api/v2/topology/layoutrequires theplatform.topology.readpolicy (orch:read) and returns a tenant-scoped positioned graph. A synchronized or fresh tenant with no topology returns HTTP200with emptynodes/edges, zero counts, and a40 x 40canvas; populated topology returns positioned region/environment nodes and routed promotion edges./api/v2/topology/hostsincludesProbeStatus,ProbeType, andProbeLastHeartbeat.- Current host probe contract is projection-derived rather than live Signals state:
ProbeLastHeartbeatmirrors the latest projected host sync timestamp,ProbeStatusisactivewhen that heartbeat is within two minutes of the freshest host heartbeat in the tenant snapshot andofflineotherwise, and missing heartbeat data yieldsnot_installed. - Probe type mapping is deterministic:
winrm_host->etw;docker_host,compose_host,ssh_host,ecs_service, andnomad_job->ebpf. - Topology projections prefer explicit component-version metadata when present. The following metadata keys shape the read model:
topologyTargetId,topologyTargetName,topologyHostId,topologyHostName,topologyAgentId,topologyAgentName, andtargetType. Additional metadata such as image digest, SCM URL/commit/branch, release ID, and external dependencies is retained as release/version evidence. For that customer’s TEST environment, Oracle is represented as an external dependency metadata node (db.app.example.com:1521/<oracle-sid>) and not as a deployed container target. - Topology reads are bound to the materialized Platform release-control run. The read model uses the version ID recorded on the latest materialization run, so a later unpublished bundle version cannot accidentally reshape topology before it is materialized.
Security read model (not a Platform surface)
/api/v2/security/* and /api/risk/aggregated-status are served by StellaOps.Findings.Security, not by Platform (Program.cs no longer maps them). See the Findings dossier for the contract.
Integrations read model
- GET
/api/v2/integrations/feeds - GET
/api/v2/integrations/vex-sources
Analytics (SBOM lake) — withdrawn
Withdrawn 2026-09-30 (SPRINT_20260928_004 PCD-127). Platform no longer maps
/api/analytics/*(suppliers, licenses, vulnerabilities, backlog, attestation coverage and the two trend series) and no longer runs the analytics maintenance lane. The read model was theanalyticsschema, which Findings owns (stellaops_findings); the Findings VulnCorrelation host that ingested it was removed and its ingestion library is wired into no service, and Platform may not read another service’s schema (PCD-58), so every route could only answer503 analytics_not_configured. The Console SBOM Lake page and theanalyticsCLI command group were withdrawn in the same change. Nothing replaces the surface; reviving it needs a Findings-owned ingestion path and read API first. The sources are kept for audit undersrc/__Obsoleted/Platform/StellaOps.Platform.WebService/PCD-127/.
NIS2 telemetry effectiveness
- GET
/api/telemetry/nis2/effectiveness?tenantId=<tenantId>&window=<rolling-30d|rolling-90d> - Returns the
nis2-effectiveness-dashboard-v1contract for the tenant-scoped Console dashboard. - Requires
analytics.readand the resolved Platform tenant context. WhentenantIdis provided in the query string it must match the resolved header/context tenant; mismatches return a deterministictenant_conflict400 response. windowdefaults torolling-90dwhen omitted. Supported values arerolling-30dandrolling-90d; unsupported values return a deterministicinvalid_window400 response with the allowed set.- Response areas are always ordered
1..13and are backed byINis2AreaMetricsService.GetAreaReportAsync. Target-document and control-register snapshot hashes are nullable until the downstream N1 snapshot integrations are available. - Platform wires live threshold alerts with
AddNis2EffectivenessNotifyThresholdEmitter. Whennotify:queueis configured, live NIS2 KPI crossings enqueuenis2.effectiveness.threshold_breachedevents through the Notify-owned enqueue-only event client (AddNotifyEventClient/INotifyEventPublisher, SPRINT_20260722_026 CM-7) onto the same stream and with the same record the Notify worker reads; Testing and explicit development local harnesses can use the in-process emitter for deterministic endpoint coverage. - Outside those local harnesses, an absent Notify queue prevents the metrics service from resolving, so dashboard reads fail as well as threshold emission. This is not a tenant-resolution failure. Monthly signed effectiveness export remains tracked separately under ExportCenter.
- The local Compose source configures Platform’s existing producer with
notify__queue__Transport=Redisandnotify__queue__Redis__ConnectionString=cache.stella-ops.local:6379, matching Scheduler and the Notify consumer. It retains the existingnotify:eventsstream andnotify-workersgroup defaults. This adds no consumer or scheduled job, does not enable a harness, and does not change authentication scopes. Real future NIS2 threshold crossings and existing backup-event publication can enqueue notifications; report GETs do not enqueue anything. - With no recorded measurements, the report still describes thirteen areas with unknown/unmeasured results. It is real report plumbing, not measured effectiveness or a signed-export proof. Preserve the live process’s DataProtection key ring before its coordinated same-image configuration adoption; the source fix alone is not deployment acceptance.
The queue/metrics dependency is verified against b382f26c9fab6cb5e6b1170dd13da3495405e562 in Program.cs, Nis2EffectivenessNotifyThresholdEmitter.cs and Nis2AreaMetricsService.GetAreaReportAsync. Re-verify the candidate configuration with node tools/scripts/validate/check-platform-notify-queue.cjs --self-test and --root .; run Nis2NotifyQueueCompositionTests and adjacent Nis2TelemetryEndpointsTests through tools/scripts/test-targeted-xunit.ps1. The read-only tools/scripts/qa/verify-platform-notify-config.cjs proves the complete model delta without starting a container; its receipt deliberately does not authorize execution.
Crypto provider catalog admin (SPRINT_20260503_008)
- GET
/api/v1/admin/crypto-providers/health(probes the catalog-registered providers) - GET
/api/v1/admin/crypto-providers/preferences/ PUT / DELETE (per-tenant preference CRUD, scopecrypto:adminorops.admin) - GET
/api/v1/admin/crypto-providers/compliance-overrides?tenantId=...(effective per-tenant NIS2/DORA/CRA override state; scopecrypto:profile:adminorops.admin) - PUT
/api/v1/admin/crypto-providers/compliance-overrides/{nis2|dora|cra}(sets one explicit tenant override; emitsplatform.update_compliance_pack) - DELETE
/api/v1/admin/crypto-providers/compliance-overrides/{nis2|dora|cra}(clears one tenant override so resolution falls back to installation defaults; emitsplatform.update_compliance_pack) - Compliance override responses include
workflowReadiness(disabled,configuration_required, orevidence_pending) andreadinessGaps. An enabled NIS2 pack explicitly remainsevidence_pendingafter setup prerequisites are present until live NIS2 effectiveness and reachability evidence arrives from the upstream workflows; this API is readiness state, not a compliance proof. - GET
/api/v1/admin/crypto-providers/catalog(list all catalog rows) - GET
/api/v1/admin/crypto-providers/catalog/{providerId}(get one entry) - POST
/api/v1/admin/crypto-providers/catalog(register a new well-known provider; emitsPlatform.CreateCryptoCatalogEntryaudit) - PUT
/api/v1/admin/crypto-providers/catalog/{providerId}(partial update; emitsPlatform.UpdateCryptoCatalogEntry) - DELETE
/api/v1/admin/crypto-providers/catalog/{providerId}(refused with 409 when an active tenant preference references the row; emitsPlatform.DeleteCryptoCatalogEntry) - All catalog endpoints require the
crypto:admin(orops.admin) scope and are tenant-scoped. - Catalog rows are seeded once from
Configuration/crypto-provider-catalog.seed.yaml(embedded resource); subsequent boots leave the table alone. Operators add a new well-known provider byPOST-ing a catalog row and deploying the matching code-shippedICryptoProviderplug-in.
Legacy alias compatibility (/api/v1/*)
- GET
/api/v1/context/regions(alias of/api/v2/context/regions) - GET
/api/v1/releases(alias of/api/v2/releases) - GET
/api/v1/releases/runs,/api/v1/releases/runs/{runId}and itstimeline,gate-decision,approvals,deployments,security-inputs,evidence,rollback,replayandauditchildren (aliases of/api/v2/releases/runs/*) - GET
/api/v1/topology/regions(alias of/api/v2/topology/regions) - GET
/api/v1/integrations/feeds(alias of/api/v2/integrations/feeds) - GET
/api/v1/integrations/vex-sources(alias of/api/v2/integrations/vex-sources) - Alias usage telemetry is emitted as deterministic event keys (
alias_<method>_<route_pattern>) with tenant hash metadata only.
Data model
Tables created by the Platform migration stream (src/Platform/__Libraries/StellaOps.Platform.Persistence/Migrations/Platform/), grouped by purpose:
- Installation settings and setup:
platform.environment_settings,platform.tenant_settings,platform.setup_sessions,platform.setup_session_secrets,platform.environment_state. - Tenancy and identity:
shared.tenants,shared.actor_identity. - Console state:
platform.user_preferences,platform.ui_context_preferences,platform.context_regions,platform.context_environments,platform.translations,platform.assistant_*. - Crypto and credentials:
platform.crypto_provider_catalog,platform.tenant_crypto_preferences,platform.tenant_compliance_profile,platform.connector_credentials,platform.operator_provider_change_outbox. - Federated telemetry:
platform.telemetry_federation_*. - Backups and diagnostics:
platform.backup_policy,platform.backup_runs,platform.backup_alerts,platform.doctor_capabilities. - Release-control and topology projections:
release.control_bundles,release.control_bundle_versions,release.control_bundle_components,release.control_bundle_materialization_runs,release.topology_*_inventory,release.topology_sync_watermarks,release.trust_*.
099_v3_retire_unused_platform_tables.sql drops the retired platform.identity_provider_configs, the release.release_* and release.run_* projections, and the release.integration_* health tables. The former release.security_* projections are not part of Platform’s stream; the security read model belongs to Findings.Security.
- Schema reference:
docs/db/schemas/platform.sql(PostgreSQL; the live host owns only durable stores, whileTestingharnesses inject any required in-memory stores explicitly).
Startup migration convergence
- Platform applies the embedded
StellaOps.Platform.Persistence.Migrations.Platforminstallation/control-plane stream with migration bookkeeping inplatform.schema_migrations; a fresh database converges without manualpsqlor init-script ordering assumptions. - The pre-1.0
Migrations/Releasebaseline was never embedded and was archived on 2026-09-14 (SPRINT_20260914_005 PEF-2) underMigrations/_archived/pre_1.0/mig061/Release/v1/(README there). Therelease_app.require_current_tenant()body live estates inherited from pre-consolidation writers (plpgsql, readsapp.tenant_id) is converged to the embedded 001 body (sql, readsapp.current_tenant_id— the key Platform’s stores set) by Startup-band099_v1_release_app_tenant_function_convergence.sql. The Platform stream retains its installation state and compatibility projections while deliberately omitting authoritativerelease.environments,release.targets,release.regions,release.agents, infrastructure bindings, and deletion lifecycle tables. - ReleaseOrchestrator is the only startup migration owner for deployment topology and image-placement state. CM-6 removed
ReleaseOrchestratorMigrationModulePluginand the RO migration project reference from Platform persistence; PLT-4 (2026-09-14) then deleted the whole plugin mechanism. Platform’s topology reader consumes the owner API and never opens the RO database; therelease.topology_*projection tables it writes live in Platform’s own database, created by Platform’s Startup-band098_v1_release_topology_inventory_projection.sql.
Dependencies
- Authority (tenant/user identity, quotas, RBAC)
- Gateway (rate-limit status and request telemetry)
- Orchestrator (job quotas, SLO state)
- Notifier (alert policies and delivery status)
- Global search has no authoritative owner-provider connection. Registry image search uses its separate configured backend.
Runtime boundary policy
- Runtime read-model services (
/api/v2/releases,/api/v2/topology/*,/api/v2/integrations/*) must depend only on explicit query contracts: IReleaseControlBundleStoreIPlatformContextQueryITopologySourceReader, implemented in production by the direct owner-HTTP adapter over the closed RO topology wire contract- Current host probe enrichment is derived only from the topology projection timestamps already returned by
IReleaseControlBundleStore. - Future live runtime probe or observed inventory enrichment for
/api/v2/topology/hostsmust still arrive through an explicit query contract. Read-model services must not reach into foreign persistence directly. - Foreign module persistence references are limited to explicit allowlist surfaces: the KEK control-plane files named in the guard test.
MigrationModulePlugins, the last migration-plane entry, was deleted on 2026-09-14 (SPRINT_20260722_021 PLT-4, DC-26) with the central plugin mechanism;SeedEndpointswas deleted on 2026-08-02 by SPRINT_20260722_026 CM-2 and its allowlist entry was removed on 2026-08-23. - Runtime read endpoints must not inject foreign
*.Persistence*types,DbContextfrom other modules, or migration runners directly. - Guard tests:
src/Platform/__Tests/StellaOps.Platform.WebService.Tests/PlatformRuntimeBoundaryGuardTests.cs.
Security and scopes
- Authorization is applied to each child route. The common
/api/v1/platformgroup enforces tenant context only and must not add a module-wide scope requirement; this keeps unrelated capabilities independently least-privileged. - Health:
ops.health(summary),ops.admin(metrics) - Quotas:
quota.read(summary),quota.admin(alerts/config) - Onboarding:
onboarding.read,onboarding.write - Preferences:
ui.preferences.read,ui.preferences.write - Context:
platform.context.read,platform.context.write - Releases read model:
orch:read(platform.releasecontrol.readpolicy mapping in Platform service) - Topology read model:
orch:read(platform.topology.readpolicy mapping in Platform service) - Integrations feed read model:
advisory:read(platform.integrations.readpolicy mapping in Platform service) - Integrations VEX source read model:
vex:read(platform.integrations.vex.readpolicy mapping in Platform service) - Search availability:
search.read; Platform does not query downstream search providers. - Metadata:
platform.metadata.read - NIS2 telemetry effectiveness:
analytics.read - Federation status:
platform:federation:read - Federation operations:
platform:federation:write
Determinism and offline posture
- Stable ordering with explicit sort keys and deterministic tiebreakers.
- All timestamps in UTC ISO-8601.
- Cache last-known snapshots for offline rendering with “data as of” markers.
Analytics ingestion and maintenance
Platform hosts neither. SPRINT_20260703_001 PAC-5 relocated ingestion and vulnerability correlation to the Findings module (src/Findings/__Libraries/StellaOps.Findings.VulnCorrelation.*, wired into no service), and the analytics maintenance lane was withdrawn with the analytics surface (PCD-127). Platform binds no Platform:AnalyticsIngestion or Platform:AnalyticsMaintenance section and no Platform:Cache:AnalyticsSeconds key.
Observability
- Metrics:
platform.aggregate.latency_ms,platform.aggregate.errors_total,platform.aggregate.cache_hits_total - Logs include
traceId,tenantId,operation, and cache-hit indicators.
Gateway exposure
The Platform Service is exposed via Gateway and registered through Router discovery. It does not expose direct ingress outside Gateway in production.
Setup Wizard
The Platform Service owns the installation-scoped setup wizard used by /setup-wizard/wizard for first-run control-plane bootstrap and later reconfiguration checks.
The migrations step uses the same Platform-owned PostgreSQL connection contract as service startup: STELLAOPS_POSTGRES_PLATFORM_CONNECTION first, then Platform:Storage:PostgresConnectionString (environment spelling Platform__Storage__PostgresConnectionString). Both probe and apply fail before opening a database connection when those inputs are absent or blank. Generic, module-derived, and schema-derived aliases are unsupported. The step converges only Platform’s migration module; each other service owns its startup migrations.
Connection contract verified against source commit c9c282c305 (2026-10-03). Re-verify with pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Platform/__Tests/StellaOps.Platform.WebService.Tests/StellaOps.Platform.WebService.Tests.csproj -Class "*PlatformSetupMigrationConnectionTests" -BuildProjectReferences and the PlatformMigrationOwnershipIntegrationTests class in the same project.
Current runtime behavior:
- Authoritative wizard state is persisted in
platform.setup_sessionsvia migration063_PlatformSetupSessions.sql. - Installation-scoped environment settings and the
SetupCompletemarker now converge throughplatform.environment_settingskeyed only bykey. Migration064_EnvironmentSettingsInstallationScopeConvergence.sqlupgrades older compose-created tables that still used the legacy(tenant_id, key)primary key. - The persisted session document keeps only non-sensitive
draftValuesplus step state, timestamps, and check results. - Sensitive step inputs retained for resume/apply are stored separately in
platform.setup_session_secretsvia migration066_PlatformSetupSessionSecrets.sql. Session reads expose onlysecretDraftsmetadata (key,stepId,updatedAtUtc), never plaintext. - Probe/apply hydrate retained setup secrets server-side, and finalize deletes the retained secret records for the completed session.
- Setup session reads now also include a required-only readiness snapshot so CLI and UI status flows can show operational blockers without treating optional post-boot services as setup failures.
- Setup secret protection key precedence is
Platform:Setup:SecretProtectionKey,STELLAOPS_SECRETS_ENCRYPTION_KEY, thenSTELLAOPS_BOOTSTRAP_KEY. - The live wizard now owns the five required control-plane steps the running control plane can truthfully validate and converge plus one optional advisory bootstrap step:
database,cache,migrations,admin,crypto, andsources. - The
cryptostep is shown as Cryptography and persists regional crypto plus compliance-evidence defaults intoplatform.environment_settings. Canonical keys and soft-probe behavior are documented in Platform Cryptography and Compliance Defaults. - The optional
sourcesstep defaults to StellaOps Mirror, supports manual advisory/VEX source selection, and preserves truthful skip semantics so a fresh install can clearly show advisories as off until the operator enables them. - The
sourcesstep now performs a real reachability check before it can pass. Mirror/source connectivity failures stay on the setup step instead of being downgraded to a background warning. - Certificate or hostname validation failures are surfaced directly. Local browser automation may ignore local certs, but product advisory aggregation still requires a hostname-valid certificate.
- Tenant onboarding remains outside the bootstrap wizard. Integrations, feeds, notifications, environments, agents, branding, and related repeatable operations continue on
/setup/*and module-owned authenticated APIs. probeis diagnostic only. Onlyapplycan move a step into the converged state.
API surface (v1)
Sessions
GET /api/v1/setup/sessions- Get the current installation-scoped setup sessionGET /api/v1/setup/sessions/current- Alias for the current installation-scoped sessionGET /api/v1/setup/sessions/{sessionId}- Read a specific persisted session by IDPOST /api/v1/setup/sessions- Create a new session or force a restartPOST /api/v1/setup/sessions/resume- Resume the current session or create onePUT /api/v1/setup/sessions/{sessionId}/config- Persist non-sensitive draft valuesPOST /api/v1/setup/sessions/{sessionId}/finalize- Finalize the current session with convergence checksPOST /api/v1/setup/sessions/finalize- Compatibility finalize path
Session payloads distinguish:
draftValues- non-sensitive persisted config onlysecretDrafts- retained-secret metadata only; no plaintext secret valuesreadiness- required-only operational readiness summary for installation bootstrap
Steps
POST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/probe- Run a diagnostic probe without completing the stepPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/apply- Apply the current step and persist the new statePOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/execute- Compatibility mutation wrapper (dryRun=truemaps to probe)POST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/reset- Reset a step to pendingPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/skip- Compatibility endpoint retained for older clients; the current control-plane steps are all requiredGET /api/v1/setup/sessions/{sessionId}/steps/{stepId}/checks- Read current check resultsPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/checks/run- Re-run checks for a stepPOST /api/v1/setup/sessions/{sessionId}/steps/{stepId}/prerequisites- Evaluate prerequisites for a stepPOST /api/v1/setup/steps/{stepId}/test-connection- Compatibility probe endpoint used by older clients
Definitions
GET /api/v1/setup/definitions/steps- List the current live step definitions
Setup step identifiers
| Step ID | Title | Required | Depends On | Notes |
|---|---|---|---|---|
database | PostgreSQL Database | Yes | - | Probe verifies reachability. Apply records convergence against the current runtime connection. |
cache | Valkey / Redis Setup | Yes | - | Probe verifies cache reachability. Apply records convergence against the current runtime connection. |
migrations | Database Migrations | Yes | database | Probe reports Platform’s pending migration state. Apply can converge only Platform’s own module; ReleaseOrchestrator migrates and reports its own database from its own host. |
admin | Admin Bootstrap | Yes | migrations | Probe validates bootstrap prerequisites. Apply ensures the bootstrap admin exists. |
crypto | Cryptography | Yes | admin | Probe validates the requested regional crypto profile and emits soft warnings for unmet NIS2/DORA/CRA prerequisites. Apply records the canonical Crypto:* and Compliance:* installation defaults. |
sources | Advisory & VEX Sources | No | admin | Probe validates mirror or manual source selections. Apply persists source enablement through Concelier and schedules initial aggregation for newly enabled sources. |
The API accepts exactly the step ids it emits (the table above, plus master-key). The former aliases authority, users, cryptography, masterkey and the enum-name spelling valkey were removed on 2026-09-27; an unknown step id answers 400 Invalid Step with the list of supported steps.
Former tenant-onboarding steps such as vault, scm, registry, notify, environments, agents, telemetry, llm, and settingsstore are no longer valid setup targets. Platform returns explicit handoff guidance to the authenticated onboarding surfaces instead.
Setup session states
| Status | Description |
|---|---|
NotStarted | Setup not begun |
InProgress | Setup in progress |
Completed | All steps completed |
CompletedPartial | Required steps completed, optional skipped |
Failed | Required step failed |
Abandoned | Setup abandoned by user |
Setup step states
| Status | Description |
|---|---|
Pending | Not yet started |
Passed | Completed successfully |
Failed | Validation failed |
Skipped | Explicitly skipped (optional steps only) |
Current | Reserved contract value for active-step projections |
Blocked | Reserved contract value for dependency failures |
Anonymous posture
- Before installation setup is marked complete, setup session APIs may resolve to the installation scope without an authenticated caller.
- After setup is marked complete, anonymous setup session reads and mutations return
401and the normal authenticatedplatform.setup.*policies apply. - After setup is marked complete, an authenticated operator may still create a new installation-scoped setup session for reconfiguration on the same
/api/v1/setup/sessionscontract; the expected browser flow is anonymous401followed by authenticated session creation after sign-in. Finalizesucceeds only after every required control-plane step has converged and the required readiness dependencies are not blocked.
Security and scopes
- Health:
ops.health(summary, readiness, dependencies, incidents),ops.admin(metrics) - Read:
platform.setup.read - Write:
platform.setup.write - Admin:
platform.setup.admin
Offline posture
- Sessions include
DataAsOfUtcfor offline rendering with stale indicators - Step results cached with Doctor check pass/fail status
- Suggested fixes generated for failed checks
Related documentation
- UX flow specification:
docs/setup/setup-wizard-ux.md - CLI guide:
docs/modules/cli/guides/setup-guide.md - Local operator runbook:
docs/INSTALL_GUIDE.md - Live proof:
src/Web/StellaOps.Web/scripts/live-setup-wizard-state-truth-check.mjs
