Integrations Architecture

The Integrations module is the central integration catalog and connector hub of the Stella Ops release control plane: the single, tenant-scoped place where connections to external tools — SCM providers, container registries, secrets managers, and CI systems — are registered, health-checked, and resolved for downstream modules.

Audience: engineers extending the connector catalog or consuming the integrations API, and operators onboarding external tools. This dossier covers the service runtime, data model, API surface, plugin admission, and the Secret Authority boundary. For the shared plugin runtime see the Plugin Framework architecture.

Overview

The Integrations module provides a unified service for managing connections to external tools (SCM providers, container registries, CI systems). It uses a plugin-based architecture where each external provider is implemented as a connector plugin, enabling new integrations to be added without modifying core logic. All integration state is persisted in PostgreSQL with tenant-scoped isolation.

Mounted connector runtime (updated 2026-06-04). External connector bundles are mounted read-only from devops/plugins/integrations/<profile>/<plugin-id>/ to /app/plugins/integrations/<profile>/<plugin-id>/; config mounts at /app/etc/plugins/integrations, trust roots at /app/trust-roots/plugins/integrations, and scratch/probe output belongs under /var/lib/stellaops/plugin-scratch/integrations. Cloud, proprietary, network-heavy, and SCM/CI provider sets are opt-in profile overlays, not base image payload.

The base mirror/storage connectors that stay in the WebService image are host-owned bootstrap behavior for offline and local registry operations. Their probe rows use profile image, remain visible for diagnostics, and are non-required for mounted-plugin acceptance. Profile-mounted connectors remain required once declared by compose and must pass manifest, signature, load, and probe admission.

Design Principles

  1. Plugin extensibility - New providers are added as plugins, not core code changes
  2. Credential indirection - Secrets are referenced via AuthRef URIs pointing to external vaults; the integration catalog never stores raw credentials
  3. Tenant isolation - Every integration record is scoped to a tenant; queries are filtered at the data layer
  4. Health-first - Every integration has a health check contract; the latest probe result is durable on the catalog row

Components

Integrations/
├── StellaOps.Integrations.WebService/             # HTTP API (Minimal API), IntegrationService, plugin loader, secret broker
├── StellaOps.Integrations.PluginHost.ProcessRunner/ # Out-of-process child runner for sandboxed external connectors
├── __Libraries/
│   ├── StellaOps.Integrations.Contracts/          # Shared DTOs, IIntegrationConnectorPlugin, IIntegrationCatalog seam
│   ├── StellaOps.Integrations.Core/               # Enums, IntegrationConfig, lifecycle events, DORA RoI metadata
│   ├── StellaOps.Integrations.Persistence/        # PostgreSQL via IntegrationDbContext + embedded SQL migrations
│   ├── StellaOps.Integrations.Services/           # Cross-host service helpers
│   └── StellaOps.Integrations.DockerV2/           # Docker Registry v2 client primitives
├── __Plugins/                                     # Connector implementation source projects
│   ├── StellaOps.Integrations.Plugin.GitHubApp/   # Profile-bundled GitHub App / GitHub Enterprise connector
│   ├── StellaOps.Integrations.Plugin.GitLab/      # Profile-bundled GitLab Server / GitLab CI connector
│   ├── StellaOps.Integrations.Plugin.Gitea/       # Profile-bundled Gitea SCM connector
│   ├── StellaOps.Integrations.Plugin.Harbor/      # Profile-bundled Harbor registry connector
│   ├── StellaOps.Integrations.Plugin.DockerRegistry/ # Profile-bundled Docker Registry / GitLab Container Registry connector
│   ├── StellaOps.Integrations.Plugin.Jenkins/     # Jenkins CI connector
│   ├── StellaOps.Integrations.Plugin.Nexus/       # Nexus repository connector
│   ├── StellaOps.Integrations.Plugin.Vault/       # HashiCorp Vault secrets-manager connector
│   ├── StellaOps.Integrations.Plugin.Consul/      # Consul config connector
│   ├── StellaOps.Integrations.Plugin.EbpfAgent/   # eBPF runtime-host agent connector
│   └── StellaOps.Integrations.Plugin.InMemory/    # In-memory test double (NOT in the built-in catalog; test fixtures only)
├── __Extensions/                                  # Non-.NET IDE extensions (see "IDE Extensions" below)
│   ├── vscode-stella-ops/                         # VS Code extension (TypeScript)
│   └── jetbrains-stella-ops/                      # JetBrains plugin (Kotlin)
└── __Tests/                                       # xUnit test projects + process-isolation plugin fixtures
    ├── StellaOps.Integrations.Tests/
    ├── StellaOps.Integrations.Plugin.Tests/
    └── StellaOps.Integrations.DockerV2.Tests/

Note: the InMemory connector lives under __Plugins/ but is deliberately excluded from IntegrationBuiltInPluginCatalog.GetAssemblies(); the running WebService never loads it. It is referenced only by explicit test harnesses. Docker Registry / GitLab Container Registry now follows the same runtime-composition rule as the other external connectors: the source project can be built as a signed bundle, but integrations-web does not compile or load it from the image.

Data Flow

[External Tool] <── health check ─────────── [Integrations WebService]
                                                    │
[Client Module] ── GET /api/v1/integrations ──>    │
                                                    │
[Admin/CLI] ── POST /api/v1/integrations ─────>    │ ── [IntegrationDbContext] ── [PostgreSQL]
                                                    │
[Plugin Loader] ── load built-in + admit external ─│ ── [Plugin Framework]
  1. Registration: An administrator or automated onboarding flow creates an integration record via POST /api/v1/integrations, providing type/provider, endpoint, optional extendedConfig, and — at most one of — an inline secret (inlineSecret + credentialKind) or an external authRefUri. Registry-typed integrations must carry credentials.
  2. Discovery: The IntegrationPluginLoader registers the host-owned provider catalog (WebService-local feed/object-store probes only) and, only when enabled, verifies external directory candidates before retaining provider descriptors in memory for lookup and failure reporting. In production, external directory admission requires signature trust, declared connector capabilities, a healthy stellaops.pluginhost.process.v1 child-runner handshake, and child-runner metadata validation. External DLLs are not loaded into the WebService process; accepted external candidates are exposed as process-backed connector proxies.
  3. Health checking: The API invokes each integration’s provider-specific health check and persists the latest result (last_health_status, last_health_check_at) even when the status is unchanged. Background polling ships and is enabled by default: IntegrationHealthMonitor (a BackgroundService registered at Program.cs:407) sweeps every Status==Active && !IsDeleted integration across all tenants via IGlobalIntegrationRepository and calls the same CheckHealthAsync per row. Defaults (IntegrationHealthMonitorOptions, bound from Integrations:HealthMonitor): Enabled=true, Interval=5m (clamped to a floor), InitialDelay=1m. Offline/air-gap note: because health probes are outbound calls to the configured provider endpoints, a sealed site that does not want periodic egress attempts must set Integrations:HealthMonitor:Enabled=false. Health-history tables and alert emission remain target capabilities (the monitor overwrites the latest status and emits IntegrationHealthChangedEvent in-process; it does not append history rows).
  4. Consumption: Downstream modules query the catalog filtered by tenant: human/operator callers list via GET /api/v1/integrations?type={type} (there is no /integrations/{type} path segment), and cross-module consumers that hold the integrations data (e.g. Scanner’s bulk-import) resolve a single integration through the in-process read-only IIntegrationCatalog seam rather than HTTP. Release-orchestrator (and Scanner.Worker / SbomService) fetch the credential via POST /api/v1/integrations/{id}/resolve-credentials, which always returns it AEAD-sealed (sealedSecret) and unseal it in memory — no plaintext credential travels on the wire (SPRINT_20260704_002 / CUS-3).
  5. Registry auto-scan (Sprint 20260607_001, AUTO-001/004/005): when a registry integration becomes Active (on add/connect, and via a periodic catch-up sweep) its repositories/tags are enumerated, each tag is resolved to its image digest, and a discovery row is appended to integrations.registry_image_discoveries. The Scanner.Worker RegistryImageDiscoveredBridgeService consumes that outbox and submits a readiness-gated scan per digest — so SBOM-readiness auto-populates and the deploy gate stops blocking on missing SBOMs, with no manual POST /api/v1/scans. See “Registry auto-scan” below.

Registry auto-scan (close the SBOM-readiness gap)

RegistryImageEnumerator is provider-aware:

Enumeration is bounded (MaxReposPerRegistry, MaxTagsPerRepo), deterministically ordered (repo asc, tag asc), honours ExtendedConfig.scanRepoPattern / scanTagPattern globs, and is fail-soft (a single repo/tag failure is logged and skipped). A registry that is Failed/anonymous/credential-unresolvable is never enumerated (S046-001).

The on-connect path (RegistryAutoScanOnConnectTrigger) is fire-and-forget off the request hot path (a detached scoped task) so it never changes the create/test latency or status. The periodic sweep (RegistryAutoScanSweepService, modelled on RegistryWriteDriftMonitor) enumerates active registries cross-tenant via IGlobalIntegrationRepository.GetActiveByTypeAcrossAllTenantsAsync; it is disabled by default (opt-in) and capped by MaxIntegrationsPerSweep + a global MaxImagesPerSweep so one large registry cannot flood the scan queue (remaining images roll to the next sweep). Dedupe across the on-connect path and the sweep is owned by the Scanner-side deterministic scan id, not by the outbox.

Config keys — service options under Integrations:RegistryAutoScan: Enabled (on-connect default, default true), MaxReposPerRegistry, MaxTagsPerRepo, DefaultRepoPattern, DefaultTagPattern, and Sweep:{Enabled (default false), Interval, InitialDelay, MaxIntegrationsPerSweep, MaxImagesPerSweep}. Per-integration ExtendedConfig overrides: autoScanOnConnect (bool, overrides the service default both ways), scanRepoPattern, scanTagPattern, gitlabGroupPath/gitlabGroupId (GitLab only).

For Artifactory storage API fallback, per-integration ExtendedConfig may include artifactoryRepository and/or artifactoryStorageApiBase; these are non-secret routing hints and are forwarded to the process-isolated connector runner.

Registry provider descriptors and matrix

Registry onboarding is descriptor-driven. StellaOps.Integrations.Core.RegistryProviderDescriptor is the source contract for registry tiles and IRegistryProvider is the marker implemented by registry connector plugins. The descriptor carries the stable descriptorId, provider enum, friendly display name, default endpoint and hint, accepted authModes, discovery strategy, OCI write-probe flag, setup-document link, local test image hint, connector name, and optional cloud token-exchange note.

GET /api/v1/integrations/providers projects these descriptors into ProviderInfo. The console merges that backend metadata onto its presentation definitions keyed by descriptorId, so auth fields and setup links come from the descriptor catalog instead of hard-coded FE guesses. Auth modes are wire-compatible with the existing credential contract: anonymous sends no credential, basic composes username:secret, and token sends the bare bearer/PAT value.

Descriptor idDisplay nameProvider enumAuth modesDiscovery strategyConnectorNotes
acrAzure Container RegistryAcrbasicprovider_apiacr-registryBCL-only AAD client-credentials plus ACR token exchange; endpoints are overridable for offline/local proof.
dockerhubDocker HubDockerHubanonymous, basic, tokenprovider_apidocker-registryShares the generic Docker connector but remains a distinct tile by descriptor id.
ecrAWS ECREcrbasicprovider_apiecr-registryBCL-only SigV4 GetAuthorizationToken; region and endpoint are overrideable through ExtendedConfig.
gcrGoogle Artifact Registry / GCRGcrbasicprovider_apigcr-registryBCL-only service-account JWT to OAuth2 exchange; token endpoint is overrideable through ExtendedConfig.
ghcrGitHub Container Registry (GHCR)GitHubContainerRegistryanonymous, basic, tokenprovider_apighcr-registryProvider-native catalog metadata; OCI operations still use the registry connector path.
gitlab-crGitLab Container RegistryGitLabContainerRegistrybasic, tokenoci_cataloggitlab-container-registryGitLab-bundled registry connector; group/project API enumeration is used where _catalog is not available.
harborHarborHarborbasic, tokenoci_catalogharbor-registryRobot-account friendly; uses OCI catalog/tag enumeration for image discovery.
oci-distributionGeneric OCI Distribution (registry:2 / distribution)DockerHubanonymous, basic, tokenoci_catalogdocker-registryLocal/self-hosted registry tile; supports the non-destructive OCI write probe.
quayQuayQuaybasic, tokenoci_catalogquay-registryRobot-account friendly; shares generic OCI behavior.
artifactoryJFrog ArtifactoryArtifactorybasic, tokenoci_catalog with Artifactory storage API fallbackartifactory-registryDocker repository endpoint supplied by the operator; set artifactoryRepository and/or artifactoryStorageApiBase when _catalog is unavailable.
nexusSonatype NexusNexusbasic, tokenoci_catalognexus-registryDocker repository endpoint supplied by the operator.
zotZot (CNCF OCI-native)Zotanonymous, basic, tokenoci_catalogzot-registryLocal Stella Ops registry proof path.

The cloud rows are implemented without AWS, Google, or Azure SDK dependencies. The token endpoints are overrideable so local labs can attempt provider-compatible emulators and so unit tests can assert the exact request/signature shapes. When no provider-compatible emulator or cloud credential is available, the strongest local runtime proof is an authenticated OCI Distribution endpoint using a provider-shaped, already-exchanged Docker-login credential (AWS:<password>, oauth2accesstoken:<token>, or the ACR null-guid username). That proves the Stella Ops descriptor, credential materialization, OCI read, discovery, and auto-scan paths, but it is not AWS/GCP/Azure managed control-plane proof. The S020 retry against the local test-ecr LocalStack container returned HTTP 501 for GetAuthorizationToken, so no ECR managed-control-plane proof is claimed.

The local compose QA fixtures include S020 OCI Distribution services under devops/compose/docker-compose.integration-fixtures.yml with htpasswd files in devops/compose/fixtures/integration-fixtures/registry-providers/. They provide the test-harbor, s020-harbor-registry, s020-dockerhub-oci, s020-ghcr-oci, s020-ecr-oci, s020-gcr-oci, and s020-acr-oci network aliases used by the provider matrix evidence. Seed them with real image manifests before discovery; the S020 proof copied library/alpine:latest from the existing local test-registry-basic:5000 registry. s020-harbor-registry runs the real Harbor registry-photon binary for the OCI Distribution component only. Full Harbor application proof is covered separately in docs/qa/feature-checks/runs/2026-06-23-s020-harbor-full-local-app-proof/: the run pulled official goharbor/prepare:v2.11.0, generated a disposable Harbor v2.11 compose/config under ignored local runtime/, started core/portal/db/jobservice/redis/nginx/registry/registryctl/log, seeded s020/alpine:full-local, and verified Stella Ops Harbor /test, /discover, and image browse against http://host.docker.internal:18085. The committed evidence keeps command output and summaries only; generated DB, key, and test-credential material remains ignored.

Database Schema

PostgreSQL. The default connection string targets database stellaops_integrations, and EF Core / the embedded SQL migrations own the integrations schema (IntegrationDbContext.DefaultSchemaName = "integrations"). Migrations are applied on startup via AddStartupMigrations("integrations", "Integrations.Persistence", …).

TablePurpose
integrations.integrationsPrimary module-owned catalog table. Columns: id (UUID PK), name, description, type, provider, status, endpoint, auth_ref_uri, organization_id, config_json (JSONB), last_health_status, last_health_check_at, created_at, updated_at, created_by, updated_by, tenant_id, tags (JSONB), is_deleted. Inline-credential columns (migrations 003/004): credential_backend, credential_kind, credential_ciphertext, credential_nonce, credential_kek_id, credential_kek_version, credential_alg, credential_metadata. Registry-policy column (migration 006): requires_credentials_resolution. The type, provider, status, and last_health_status enum columns are stored as TEXT (the enum member name, e.g. Scm/GitLabServer/Active), not as integers.
integrations.registry_image_discoveriesRegistry auto-scan outbox (migration 002_v1_registry_image_discoveries.sql, Sprint 20260607_001). Append-only rows of (integration_id, tenant_id, registry_host, repository_path, tag, image_digest, discovered_at, source). Written by the auto-scan enumeration on registry connect + the periodic sweep; polled cross-schema by Scanner.Worker RegistryImageDiscoveredBridgeService to submit readiness-gated scans. A unique index on (tenant_id, registry_host, repository_path, image_digest) + ON CONFLICT DO NOTHING collapses repeat discovery of the same image; dedupe of the resulting scans is owned by the deterministic Scanner scan id, not by this table.

Indexes: ix_integrations_type, ix_integrations_provider, ix_integrations_status, ix_integrations_tenant_id, a partial unique index ix_integrations_tenant_name over (tenant_id, name) WHERE is_deleted = false, and a partial ix_integrations_requires_creds_resolution. Delete is soft (is_deleted = true). Migration 004_v1_registry_image_discovery_integration_stamp.sql adds discovery browse indexes on (tenant_id, integration_id, repository_path) and (tenant_id, image_digest) for the registry/image browser read paths.

Tenant-isolation invariant (SPRINT_20260531_035, class C1 fix)

Every read / mutate against integrations.integrations MUST be tenant-scoped. This is enforced in two layers:

  1. Application layer (primary defence) — IIntegrationRepository. Every method that touches a tenant-owned row carries a required tenantId parameter that is applied as a WHERE tenant_id = @tenantId predicate in SQL. BuildQuery rejects a null/empty IntegrationQuery.TenantId with ArgumentException (no silent cross-tenant fallthrough). Bypass for genuinely system-wide scans (e.g. health workers) is the explicit IGlobalIntegrationRepository seam, so cross-tenant intent is auditable in DI rather than smuggled through a magic-null id. A cross-tenant UpdateAsync throws IntegrationNotFoundException (mapped to HTTP 404 at the API surface) — never an InvalidOperationException that would leak row existence.
  2. Database layer (defence-in-depth) — migration 007_tenant_isolation_rls.sql. PostgreSQL Row-Level Security is enabled (not forced) on integrations.integrations, with a single policy integrations_tenant_isolation keyed on current_setting('app.current_tenant_id', true). Table owners and BYPASSRLS roles continue to bypass per the standard pattern (mirrors release.integrations reference at Migrations/Release/001_IntegrationHub.sql:196), so the migration is safe to land on running services — only future non-owner callers that bypass the repository would meet the policy. The app.current_tenant_id session variable is the same GUC used elsewhere in the platform (see shared.actor_identity in 076_ActorIdentity.sql).

Why both layers? Hunter #1 (multi-tenancy audit, 2026-05-31) confirmed that the application-layer filter had been missing on 7 methods (GetByIdAsync, UpdateAsync, DeleteAsync, GetByProviderAsync, GetActiveByTypeAsync, UpdateHealthStatusAsync, BuildQuery’s optional-tenant fallthrough). For the highest-severity instance — UpdateAsync — tenant A could rewrite tenant B’s config_json/auth_ref_uri/credential_ciphertext columns and trigger health-checks under their own worker simply by knowing the row id. The single-layer fix would have been adequate but would re-expose the same class the next time a caller appears that doesn’t go through the repository wrapper. Belt + braces.

Regression coverage: PostgresIntegrationRepositoryTenantIsolationTests (Testcontainers Postgres, one test per fixed method, asserts returned-row tenant_id matches the caller’s, never != null).

The KEK control plane (crypto.kek_versions) and the credential re-seal progress tables are owned by StellaOps.Cryptography.CredentialStore and registered for this service via AddCryptoKekVersionsMigrations("Integrations") / AddCredentialReSealMigrations("Integrations"); they are shared infrastructure, not Integrations-local tables.

The /internal/crypto/* routes are installation-wide control-plane operations, not tenant catalog APIs. They still require an authenticated principal: reads accept crypto:kek:read, crypto:kek:rotate, or ops.admin; start/abort accept crypto:kek:rotate or ops.admin. Platform forwards a validated operator Bearer token or relays the gateway-signed identity envelope after Platform verified it as StellaRouterEnvelope; Integrations verifies/re-authorizes the identity again. Network bypass alone cannot authorize these routes. Base compose accepts both api://integrations and first-party stellaops audiences and reaches the service on port 8080. The default compose transport remains internal HTTP, so this boundary must not be described as mTLS until direct certificate-bound listeners and clients are provisioned.

DORA Register of Information metadata

Connection profiles can carry optional doraRoiMetadata for DORA Register of Information projections. The metadata is stored as JSONB on the catalog row and is included in create/update/read DTOs so downstream RoI encoders can project integrations without scraping provider-specific configuration.

Fields:

Provenance is explicit. operatorFilled is valid for operator-entered fields and all deeper subcontracting ranks. autoCapturedDepth1 is valid only for the direct provider at subContractingChain.rank == 1; ranks greater than 1 must remain operator-filled. This implements the approved DORA-Q3 decision that Stella auto-discovers only direct integrations and must not imply automatic knowledge of deeper supply chains.

The Web Integration Hub treats this as optional module metadata, not as a global product mode. During integration onboarding, operators can opt in to attach DORA RoI metadata on the discovery-scope step. The Console sends scalar criticality and contract-date fields with operatorFilled provenance, allows autoCapturedDepth1 only for rank-1 subcontracting rows, forces deeper ranks to operatorFilled, and shows stored metadata read-only on the integration overview. Profiles without RoI metadata remain unchanged.

The shipped schema does not include module-owned health_checks, audit_logs, or plugin_metadata tables. The service persists the latest health result on integrations; mutable endpoint audit is emitted through the shared unified audit pipeline and structured service logs. Historical health retention and an Integrations-local audit store require separate schema work.

Endpoints

Registry browse endpoints are additive under /api/v1/integrations/registries, require tenant context, and use Integration.Registry.Read -> registry:read (StellaOpsScopes.RegistryRead).

All endpoints below are mapped under the /api/v1/integrations group in IntegrationEndpoints.cs. The group requires authorization and a tenant context (RequireAuthorization(IntegrationPolicies.Read).RequireTenant()), and each route additionally pins a per-operation policy. The three policies map to canonical scopes: Integration.Read → integration:read, Integration.Write → integration:write, Integration.Operate → integration:operate (see Program.cs AddStellaOpsScopePolicy wiring and StellaOpsScopes.IntegrationRead/Write/Operate). Route IDs use {id:guid} constraints.

Integration CRUD (/api/v1/integrations)

Operations

Registry browse (/api/v1/integrations/registries)

When a Scanner storage connection string can be resolved from ScannerStorage:Postgres:ConnectionString, ConnectionStrings:Default, or ConnectionStrings:IntegrationsDb, the WebService registers ScannerRegistryImageVulnStateProvider instead of the policy-only fallback. Only this Scanner-DSN branch registers IScanRuntimeStateRepository, IArtifactBomRepository, and the validated Scanner:SbomReadiness:SuccessfulScanMaxAge option (positive, default 14.00:00:00). The read-only provider joins the latest runtime attempt, latest durable BOM, public reachability verdicts, severity counts, and policy decision by tenant + image digest. Per-digest Scanner reads execute with a fixed concurrency bound (ScannerRegistryImageVulnStateProvider.MaxConcurrentDigestLookups) and results are reassembled in requested-digest order; this prevents database-latency multiplication without allowing unbounded connection fan-out. Durable BOM age maps to v2 readiness=ready|stale; a successful lookup with no BOM maps to absent. Exact attempt aliases map to pending, running, succeeded, failed, or cancelled, with the attempt timestamp kept separate from lastSuccessfulScanAt. A BOM-only row remains visible. Legacy sbomReadiness and lastScanAt retain their previous attempt-derived mapping byte-for-byte. A non-cancellation durable-BOM repository failure is logged and degrades only v2 readiness to null (unknown/unavailable), while runtime attempt, policy, severity, and reachability enrichments remain usable; request cancellation still propagates.

Policy gate decisions are joined through a separate read-only adapter over policy.gate_decisions, resolved from PolicyStorage:Postgres:ConnectionString, Policy:Postgres:ConnectionString, ConnectionStrings:PolicyDb, ConnectionStrings:Default, or the Integrations connection string fallback. The adapter resolves tenant slugs through shared.tenants, sets app.current_tenant_id, and reads the latest row per lower-cased image_digest by evaluated_at/decision_id. Policy statuses map to registry-browser verdicts as pass/allow -> allow, warn/review -> warn, and fail/block/deny -> block; unknown statuses remain unknown. When Scanner storage is unavailable, PolicyOnlyRegistryImageVulnStateProvider still exposes hasDecision=true and verdict for matching digest rows while scan and reachability fields stay null. This is an explicit short-term cross-domain read projection for the image browser; a future signed OCI referrer or Policy-owned projection may supersede it without changing the browser response shape.

Credential backends (explicit, post-Sprint-032)

Every integration row carries an explicit credentialBackend discriminator in one of three canonical shapes. The legacy credential_backend='none' + non-null auth_ref_uri shape was removed in Sprint 032 (no released version carried it); registration rejects it with HTTP 400 and migration 005 backfilled any residual rows to authref.

BackendRequest shapeRow shapeWhen to use
inlineinlineSecret + credentialKind, no authRefUriAEAD-sealed ciphertext (credential_* columns), auth_ref_uri NULLStella-managed credential storage. Canonical for the WS-7 onboarding flow (see 01b-register-inline-integrations.sh).
authrefauthRefUri only, no inlineSecretauth_ref_uri set, inline ciphertext NULLOperator wants the secret to live in an external vault (HashiCorp Vault, Azure Key Vault, etc.) and resolve at runtime.
noneNeither inlineSecret nor authRefUricredential_backend='none' + all credential columns + auth_ref_uri NULLGenuine anonymous-access integration (no credentials configured).

Resolution dispatch is unified across both code paths (IntegrationService.BuildConfigAsync for test/health/discover, and IntegrationCredentialResolver.ResolveAsync for release-orchestrator’s sync-versions): both run the same explicit-backend switch with no default back-compat dispatch. Release-orchestrator’s HttpRegistryCredentialResolver always dispatches to integrations-web’s /resolve-credentials (the none-backend short-circuit was removed in Sprint 032 because it defeated the integrations-web back-compat for sync-versions).

DB invariant: ck_integrations_credential_backend CHECK constraint (migration 005) rejects any row that doesn’t match one of the three shapes above. ADR-004 forward-only; downgrade is via PostgreSQL snapshot restore.

Health (/api/v1/integrations)

Runtime health

All three runtime aliases call the same IntegrationHealth handler and return { status: "Healthy", timestamp }. They are anonymous (AllowAnonymous) container/orchestrator probes and do NOT trigger provider checks; provider health remains under /api/v1/integrations/{id}/health.

Provider discovery

JSON wire shape (request + response)

Sprint SPRINT_20260528_033 (commits 6cb43f2711 + a3a7384bab) restored the legacy operator contract for enum fields on POST/PUT /api/v1/integrations:

Bulk SCM import

Sprint SPRINT_20260514_001 adds a bulk import primitive so an operator can onboard an entire SCM org in one call. The endpoints live in the Scanner WebService (not Integrations), because the work product is a fan-out of SBOM source registrations + scan triggers:

IIntegrationCatalog seam

Scanner.WebService references StellaOps.Integrations.Contracts only - not Persistence / WebService. To classify an integration without a write-side dependency, Contracts now exposes a read-only IIntegrationCatalog (GetIntegrationAsync(id) / IsScmIntegrationAsync(id)). The Postgres-backed IntegrationCatalog implementation lives in StellaOps.Integrations.Persistence and is registered by hosts that own the integrations data (AddIntegrationCatalog()). Scanner.WebService registers a fail-safe NullIntegrationCatalog default via TryAddSingleton, so a host wiring the real catalog wins; on its own the bulk-import endpoint fails closed (404) rather than fanning out against an unverified integration.

Throttling

Concurrency is bounded by an in-process SemaphoreSlim sized to maxConcurrent (clamped to [1, 64], default 8). The batch lifecycle and the in-flight gauge are tracked in an in-memory BatchStatusStore. A Valkey-backed distributed throttle is a future sprint - the Scanner test host strips the Valkey subscriber, so in-process is the correct default today.

CLI

Plugin Architecture

Each connector plugin implements IIntegrationConnectorPlugin (in StellaOps.Integrations.Contracts), which extends the shared IAvailabilityPlugin:

public interface IIntegrationConnectorPlugin : IAvailabilityPlugin
{
    IntegrationType Type { get; }
    IntegrationProvider Provider { get; }
    Task<TestConnectionResult> TestConnectionAsync(IntegrationConfig config, CancellationToken ct = default);
    Task<HealthCheckResult> CheckHealthAsync(IntegrationConfig config, CancellationToken ct = default);
}

(IAvailabilityPlugin supplies Name and an availability gate; there is no ProviderType string or ValidateConfigAsync method.) Discovery-capable connectors additionally implement IIntegrationDiscoveryPlugin (SupportedResourceTypes, DiscoverAsync). Connectors that gate operations declare IIntegrationConnectorCapabilityDeclaration.DeclaredCapabilities over the capability ids integration:test-connection, integration:health-check, integration:discovery, integration:discovery:<resourceType>, and integration:credential-materialization (IntegrationConnectorCapabilities).

IntegrationConfig (the value passed to connectors) carries IntegrationId, Type, Provider, Endpoint, ResolvedSecret (plaintext, resolved per-backend by IntegrationService.BuildConfigAsync), OrganizationId, ExtendedConfig, and SecretRefUri (set instead of ResolvedSecret for process-isolated connectors).

Image-resident connectors (the canonical set currently registered via IntegrationBuiltInPluginCatalog.GetAssemblies()):

Profile-bundled connectors:

Profile-bundled connectors are not WebService project references. They must be supplied as signed directory bundles before the production provider list exposes them.

The current generated bundle profiles are:

There is no base executable Integrations bundle in this slice. S3-compatible object storage and feed mirror providers stay host-owned until a future migration explicitly externalizes them.

InMemory is a deterministic test double for explicit test harnesses. It is NOT part of the built-in catalog and the default WebService runtime does not load it (GetSupportedProviders(includeTestOnly) filters it out unless explicitly requested).

External connector plugin admission

The production WebService does not treat a DLL in Integrations:PluginsDirectory as a valid connector by filename alone.

This keeps test-only connectors such as InMemory out of production discovery and prevents unsigned external connector binaries from becoming production-valid integration evidence.

Provider endpoint contracts

Secret Authority

The Integrations service now exposes a provider-agnostic Secret Authority boundary for onboarding flows that need to stage credential material before creating an integration.

Goals

API surface (/api/v1/secret-authority)

Current writer support

Security model

UI / CLI usage

Security Considerations

Public error-disclosure boundary

Integrations classifies failure text by origin; authentication alone does not make arbitrary provider text safe to return:

OriginClassificationPublic contract
ASP.NET request binding and JSON parsingUntrusted submitted-data diagnosticHTTP 400 returns stable host-authored detail plus traceId; raw parser/binding text is not projected.
Credential mutex, credential-kind validation, anonymous-registry policy, and Secret Authority 4xx validationClient-safe validationThe reviewed validation detail and stable error code are returned to the authenticated caller.
Secret Authority provider/credential/infrastructure 5xx failuresSensitive server failureThe API returns the stable errorCode plus a generic detail; Vault response bodies, authrefs, target names, and exception text remain server-side.
In-process connector exceptions and discovery failuresSensitive provider failureResults use stable codes such as connector_transport_failure and integration_discovery_failed; exception graphs are scrubbed where credential material may be reachable and logs record only exception type plus bounded operation identifiers.
External bundle admission, signature verification, availability probes, child-runner stderr, host paths, and process response failuresOperator-only diagnosticPlugin probe/results expose stable admission or sandbox codes. Raw exception messages, runner stderr, assembly paths, and arbitrary child response details are not projected.

Successful authenticated connector probes may retain bounded operational facts: HTTP status codes; normalized operation.success, operation.healthStatus, and operation.durationMs; typed runner.* lifecycle counters/states; boolean broker/materialization evidence; secret-reference counts/kinds; and explicitly redacted secret-leak proof fields. Arbitrary plugin detail keys and plugin-authored failure messages are not part of the public contract. Correlation uses the HTTP trace identifier and stable error code, not raw exception text.

The WebService enforces this boundary after every in-process or process-isolated test-connection and health result. Connector-authored messages, endpoint/path details, remote payload fragments, and write-probe prose are replaced or dropped before response, event, audit, or persisted probe-state projection; plugins cannot opt out of the host sanitizer. Top-level health status is restricted to defined HealthStatus values (unknown values become Unknown), and last_health_check_at is stamped from the host clock so a connector cannot poison monitor scheduling with a future timestamp.

Observability

Performance Characteristics

IDE Extensions (VS Code, JetBrains)

The Integrations module also owns the IDE extension plugins, located under src/Integrations/__Extensions/. These are non-.NET projects intended to provide developer-facing tooling consuming the Orchestrator/Router APIs.

Status: these extensions are early skeletons, not yet wired to live backend APIs. As shipped, the VS Code extension is a single src/extension.ts providing tree-view scaffolding and a configurable backend URL (default https://localhost:5001); it does not yet issue the /api/v1/releases/*, /api/v1/promotions/*, or /oauth/token calls described in “Data Flow (Extensions)” below — that flow is the target design. The JetBrains plugin ships StellaOpsPlugin.kt but does not include a committed Gradle wrapper. Treat the feature lists and data flow below as planned scope.

VS Code Extension (__Extensions/vscode-stella-ops/)

JetBrains Plugin (__Extensions/jetbrains-stella-ops/)

Design Principles (Extensions)

  1. Thin client - Extensions contain no business logic; all state and decisions live in backend services
  2. Consistent experience - Both plugins expose equivalent functionality despite different technology stacks
  3. Non-blocking - All API calls are asynchronous; the IDE remains responsive during network operations
  4. Offline-tolerant - Graceful degradation when the Stella Ops backend is unreachable

Data Flow (Extensions)

[Developer IDE] --> [Extension/Plugin]
                         |
                         +-- GET /api/v1/releases/* --------> [Orchestrator API]
                         +-- GET /api/v1/environments/* ----> [Orchestrator API]
                         +-- POST /api/v1/promotions/* -----> [Orchestrator API]
                         +-- POST /oauth/token -------------> [Authority]

Authentication uses OAuth tokens obtained from the Authority service, stored in the IDE’s secure credential store (VS Code SecretStorage, JetBrains PasswordSafe).

References