feat(web): integration-hub + wizard wiring for local-setup flow

Integration hub: extends integration.models with fields needed by the wizard
(capabilities, credentials, readiness), updates the shell and list components,
adds routing for the new hub flow, and broadens the integration-list spec.

Integration wizard: new integrations-hub.component, extended wizard with
capability/credential handling, updated template + type models, and broader
spec coverage.

Sprint docs: SPRINT_20260413_003 (UI-driven local setup rerun) updated with
wiring notes; SPRINT_20260410_001 (no-mocks) adjusted. ReleaseOrchestrator
architecture doc gets a minor clarification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-13 22:15:08 +03:00
parent 852c4d15fe
commit 44a253d485
13 changed files with 412 additions and 35 deletions

View File

@@ -74,22 +74,41 @@ Completion criteria:
- [ ] Targeted automated tests covering the changed runtime path pass or fail with concrete blockers recorded.
- [ ] Remaining cross-module runtime in-memory bindings are logged with next-action notes.
### NOMOCK-005 - Replace live script compatibility and Platform script alias stubs with the real scripts registry
Status: DONE
Dependency: NOMOCK-001
Owners: Developer
Task description:
- Remove the unconditional compatibility success stub from the owning Release Orchestrator `/api/v2/scripts/{id}/check-compatibility` endpoint and replace it with evaluation against persisted script metadata, requested target metadata, and declared secret availability.
- Replace the Platform direct `IScriptService` registration with the real Release Orchestrator scripts library when PostgreSQL is configured so the alias path is no longer backed by the local in-memory catalog.
- Carry declared script variables through create/update paths so compatibility checks operate on real persisted inputs instead of silently dropping UI-authored variable declarations.
Completion criteria:
- [x] Release Orchestrator script compatibility no longer returns unconditional success.
- [x] Platform binds `IScriptService` to the real scripts registry when PostgreSQL is available.
- [x] Script create/update paths persist declared variables through the owning backend.
- [x] Targeted backend tests cover the compatibility evaluator, variable persistence, and Platform alias mapping.
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2026-04-10 | Sprint created to remove live production-path stubs, mock providers, and in-memory runtime bindings starting with the active Angular app configuration and Concelier feed-mirror surfaces. | Developer |
| 2026-04-13 | Inventory extended beyond the initial Concelier slice. Confirmed `/ops/operations/feeds/mirror/*` now runs on persisted Concelier state, but found two still-live script-path fictions: Platform still registers `IScriptService` to `InMemoryScriptService` in `src/Platform/StellaOps.Platform.WebService/Program.cs`, and the owning Release Orchestrator `/api/v2/scripts/{id}/check-compatibility` endpoint still returns an unconditional stub success from `src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Endpoints/ScriptsEndpoints.cs`. Next implementation slice expands to `src/ReleaseOrchestrator/**` for script compatibility truthfulness and to the Platform alias only if still needed after the owning-service fix. | Developer |
| 2026-04-13 | Completed the scripts runtime slice. Release Orchestrator now evaluates `/api/v2/scripts/{id}/check-compatibility` against persisted script language, variables, dependencies, target metadata, and available secrets; create/update requests now persist declared variables instead of dropping them; and Platform now binds `IScriptService` to the real Release Orchestrator scripts registry when PostgreSQL is configured. Targeted tests passed: `StellaOps.ReleaseOrchestrator.Integration.Tests` `20/20`, `StellaOps.Platform.WebService.Tests` class-targeted `ReleaseOrchestratorScriptServiceTests` `2/2`, and `ReleaseOrchestrator.WebApi` rebuilt cleanly. Live frontdoor verification was blocked in this shell because the local stack was not running (`docker ps` empty, `curl.exe -k https://stella-ops.local/` connection failure). | Developer |
## Decisions & Risks
- Decision: this sprint prioritizes live runtime paths the browser can currently reach over test-only mock helpers.
- Decision: unsupported operations must return truthful empty/problem responses rather than seeded demo success/error payloads.
- Decision: after the feed-mirror cleanup, the next highest-value runtime slice is the scripts compatibility path because the browser uses the real `/api/v2/scripts` backend and its compatibility action still reports fabricated success.
- Decision: the Platform direct scripts alias now reuses the Release Orchestrator scripts library and schema instead of keeping a separate in-memory implementation when PostgreSQL is configured.
- Risk: several modules outside the initial slice still boot with runtime in-memory stores (`Notify`, `Graph`, `Policy`, `Platform`, `Scheduler`, `Scanner`, `BinaryIndex`, `Signals`, `SbomService`, `Signer`, `PacksRegistry`, `AdvisoryAI`). They will need follow-on slices unless a real persistence path already exists and can be wired safely.
- Risk: Platform still contains a production registration of `IScriptService -> InMemoryScriptService`, but the live frontdoor currently routes `/api/v2/scripts` to Release Orchestrator. Removing that alias safely requires checking any direct Platform callers so the owning-service fix lands first.
- Risk: Platform still falls back to `InMemoryScriptService` when no PostgreSQL connection string is configured. That preserves non-database test/dev paths, but it means the cleanup is not repo-wide complete yet.
- Risk: the live browser path for this slice could not be reverified from the shell because the local stack was down during close-out (`docker ps` returned no running containers). The code-level and project-level checks are green, but live redeploy verification still needs a running stack.
- Risk: some feed-mirror sub-features appear to have no real persisted backend contract yet, so removing fake data may temporarily surface explicit `501`/empty-state behavior in the UI until the owning backend is implemented.
## Next Checkpoints
- Remove the active Angular VEX Hub mock provider.
- Convert the Concelier feed-mirror endpoints from seeded data to real source/read-model state.
- Replace the Release Orchestrator script compatibility stub with persisted script-aware evaluation.
- Re-run the live scripts UI and compatibility panel once the local stack is back up.
- Re-test the live feed pages and record the next runtime cleanup slice.

View File

@@ -34,7 +34,7 @@ Completion criteria:
- [ ] `https://stella-ops.local` becomes reachable again for browser-driven setup.
### UISETUP-002 - Drive the operator setup through the browser UI
Status: TODO
Status: DOING
Dependency: UISETUP-001
Owners: Developer / QA
Task description:
@@ -66,6 +66,9 @@ Completion criteria:
| 2026-04-13 | Removed all `stellaops-*` containers, `compose_*` / `stellaops_*` volumes, the `stellaops` / `stellaops_frontdoor` networks, and all `stellaops/*:dev` images to return the machine to zero Stella runtime state before the rerun. | Developer |
| 2026-04-13 | Started the documented machine-level bootstrap with `scripts/setup.ps1 -QaIntegrationFixtures`; this restores the platform and fixture-backed frontdoor but not the full real-provider integrations compose lane. | Developer |
| 2026-04-13 | Code inspection ahead of the live browser run found two likely UI-path gaps to validate: the setup wizard backend persists much of its state only in-session, and the integrations onboarding wizard currently requires a non-empty `AuthRef URI` even though the backend API itself accepts null auth refs for local no-auth connectors. | Developer |
| 2026-04-13 | The initial `scripts/setup.ps1 -QaIntegrationFixtures` run failed in the repo-wide `.sln` preflight with `MSB4166` child-node exits on `src/Tools/StellaOps.Tools.sln`, `src/VexHub/StellaOps.VexHub.sln`, and `src/Zastava/StellaOps.Zastava.sln`; reran the documented setup entrypoint with `-SkipBuild` to continue the actual local stack bring-up. | Developer |
| 2026-04-13 | Patched the Integrations Hub UI to expose the missing local onboarding categories (`Secrets`, `Feed Mirrors`, `Object Storage`) and aligned wizard validation with the backend contract so optional-auth / optional-scope local connectors can be created from the browser instead of being blocked by frontend-only rules. | Developer |
| 2026-04-13 | Angular compile validation for the changed UI surfaces completed without new errors from the modified files; the remaining `ng test` failures are pre-existing audit-log and scheduler test breakages outside the Integrations working slice. | Developer |
## Decisions & Risks
- Decision: this rerun uses the real browser UI as the operator surface and treats CLI/bootstrap helpers only as fallback evidence if the product lacks a true UI path.

View File

@@ -4,7 +4,7 @@
**Status:** Active Development (backend substantially implemented; API surface layer in progress)
> **Implementation reality (updated 2026-04-10):** The backend is substantially complete with 140,000+ lines of production code across 49 projects. Core libraries (Release, Promotion, Deployment, Workflow, Evidence, PolicyGate, Progressive, Federation, Compliance) are implemented with comprehensive tests (283 test files, 37K lines). Six agent types are operational (Compose, Docker, SSH, WinRM, ECS, Nomad). Compatibility HTTP surfaces now exist across Platform, JobEngine, and Scanner for environment management, deployment monitoring, evidence inspection, dashboard promotion decisions, and registry search. The standalone WebApi now owns and auto-migrates the `scripts` PostgreSQL schema used by `/api/v2/scripts`, so fresh local installs no longer depend on Scheduler-owned SQL bootstrap for the scripts catalog. **Remaining gaps:** the dedicated Release Orchestrator WebApi host is still incomplete, and many compatibility surfaces still rely on in-memory storage outside the scripts catalog and audit/first-signal persistence.
> **Implementation reality (updated 2026-04-13):** The backend is substantially complete with 140,000+ lines of production code across 49 projects. Core libraries (Release, Promotion, Deployment, Workflow, Evidence, PolicyGate, Progressive, Federation, Compliance) are implemented with comprehensive tests (283 test files, 37K lines). Six agent types are operational (Compose, Docker, SSH, WinRM, ECS, Nomad). Compatibility HTTP surfaces now exist across Platform, JobEngine, and Scanner for environment management, deployment monitoring, evidence inspection, dashboard promotion decisions, and registry search. The standalone WebApi owns and auto-migrates the `scripts` PostgreSQL schema used by `/api/v2/scripts`, and the scripts compatibility endpoint now evaluates persisted script metadata, declared variables, dependency inventory, target metadata, and available secrets instead of returning a fabricated success response. **Remaining gaps:** the dedicated Release Orchestrator WebApi host is still incomplete, and many compatibility surfaces still rely on in-memory storage outside the scripts catalog and audit/first-signal persistence.
## Overview

View File

@@ -11,9 +11,10 @@
* scm - Source control managers
* ci - CI/CD systems
* runtime-hosts - Runtime and host connector inventory
* feeds - Advisory source connectors
* feeds - Feed mirror connectors
* vex-sources - VEX source connectors
* secrets - Secrets managers / vaults
* storage - Object storage connectors
* registry-admin - Registry plan management and audit (Sprint 023)
* :id - Integration detail (standard contract template)
*
@@ -76,6 +77,13 @@ export const integrationHubRoutes: Routes = [
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'feeds',
title: 'Feed Mirrors',
data: { breadcrumb: 'Feed Mirrors', type: 'FeedMirror' },
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'advisory-vex-sources',
title: 'Advisory & VEX Sources',
@@ -112,6 +120,13 @@ export const integrationHubRoutes: Routes = [
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'storage',
title: 'Object Storage',
data: { breadcrumb: 'Object Storage', type: 'ObjectStorage' },
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'notifications',

View File

@@ -111,6 +111,18 @@ describe('IntegrationListComponent', () => {
});
});
it('routes object-storage inventory to the storage onboarding flow', () => {
const navigateSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
(component as any).integrationType = IntegrationType.ObjectStorage;
component.typeLabel = 'Object Storage';
component.addIntegration();
expect(navigateSpy).toHaveBeenCalledWith(['/ops/integrations', 'onboarding', 'storage'], {
queryParamsHandling: 'merge',
});
});
it('shows inline success feedback instead of alert on successful test-connection', () => {
integrationService.testConnection.and.returnValue(of({
integrationId: 'int-1',

View File

@@ -405,6 +405,8 @@ export class IntegrationListComponent implements OnInit {
SecretsManager: 'Secrets Vault',
FeedMirror: 'Feed Mirror',
Feed: 'Feed Mirror',
ObjectStorage: 'Object Storage',
Storage: 'Object Storage',
SymbolSource: 'Symbol Source',
Marketplace: 'Marketplace',
Notification: 'Notification Provider',
@@ -616,6 +618,7 @@ export class IntegrationListComponent implements OnInit {
case 'FeedMirror': case 'Feed': return IntegrationType.FeedMirror;
case 'RepoSource': return IntegrationType.RepoSource;
case 'SecretsManager': case 'Secrets': return IntegrationType.SecretsManager;
case 'ObjectStorage': case 'Storage': return IntegrationType.ObjectStorage;
case 'SymbolSource': return IntegrationType.SymbolSource;
case 'Marketplace': return IntegrationType.Marketplace;
default: return undefined;
@@ -630,6 +633,7 @@ export class IntegrationListComponent implements OnInit {
case IntegrationType.FeedMirror: return 'feed';
case IntegrationType.RepoSource: return 'repo';
case IntegrationType.SecretsManager: return 'secrets';
case IntegrationType.ObjectStorage: return 'storage';
case IntegrationType.Registry:
default: return 'registry';
}
@@ -640,7 +644,10 @@ export class IntegrationListComponent implements OnInit {
this.integrationType === IntegrationType.Registry ||
this.integrationType === IntegrationType.Scm ||
this.integrationType === IntegrationType.CiCd ||
this.integrationType === IntegrationType.RuntimeHost
this.integrationType === IntegrationType.RuntimeHost ||
this.integrationType === IntegrationType.FeedMirror ||
this.integrationType === IntegrationType.SecretsManager ||
this.integrationType === IntegrationType.ObjectStorage
);
}

View File

@@ -7,10 +7,10 @@ import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/
import { StellaQuickLinksComponent, type StellaQuickLink } from '../../shared/components/stella-quick-links/stella-quick-links.component';
import { IntegrationHubComponent } from './integration-hub.component';
type TabType = 'registries' | 'scm' | 'ci' | 'runtime-hosts' | 'advisory-vex-sources' | 'secrets';
type TabType = 'registries' | 'scm' | 'ci' | 'runtime-hosts' | 'feeds' | 'advisory-vex-sources' | 'secrets' | 'storage';
const KNOWN_TAB_IDS: readonly string[] = [
'registries', 'scm', 'ci', 'runtime-hosts', 'advisory-vex-sources', 'secrets',
'registries', 'scm', 'ci', 'runtime-hosts', 'feeds', 'advisory-vex-sources', 'secrets', 'storage',
];
const PAGE_TABS: readonly StellaPageTab[] = [
@@ -18,12 +18,14 @@ const PAGE_TABS: readonly StellaPageTab[] = [
{ id: 'scm', label: 'SCM', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' },
{ id: 'ci', label: 'CI/CD', icon: 'M5 3l14 9-14 9V3z' },
{ id: 'runtime-hosts', label: 'Runtimes / Hosts', icon: 'M2 2h20v8H2z|||M2 14h20v8H2z|||M6 6h.01|||M6 18h.01' },
{ id: 'feeds', label: 'Feed Mirrors', icon: 'M4 6h16|||M4 12h16|||M4 18h10' },
{ id: 'advisory-vex-sources', label: 'Advisory & VEX', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'secrets', label: 'Secrets', icon: 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4' },
{ id: 'storage', label: 'Storage', icon: 'M3 6h18|||M5 6v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6|||M8 10h8|||M8 14h5' },
];
/** Priority order for auto-selecting the first populated tab */
const TAB_PRIORITY: readonly TabType[] = ['registries', 'scm', 'ci', 'advisory-vex-sources', 'runtime-hosts', 'secrets'];
const TAB_PRIORITY: readonly TabType[] = ['registries', 'scm', 'ci', 'feeds', 'advisory-vex-sources', 'runtime-hosts', 'secrets', 'storage'];
@Component({
selector: 'app-integration-shell',

View File

@@ -8,6 +8,7 @@ export enum IntegrationType {
SymbolSource = 7,
Marketplace = 8,
SecretsManager = 9,
ObjectStorage = 10,
}
export enum IntegrationProvider {
@@ -39,6 +40,7 @@ export enum IntegrationProvider {
NuGetOrg = 403,
CratesIo = 404,
GoProxy = 405,
S3Compatible = 450,
EbpfAgent = 500,
EtwAgent = 501,
DyldInterposer = 502,
@@ -168,6 +170,8 @@ export function getIntegrationTypeLabel(type: IntegrationType): string {
return 'Marketplace';
case IntegrationType.SecretsManager:
return 'Secrets Manager';
case IntegrationType.ObjectStorage:
return 'Object Storage';
default:
return 'Unknown';
}
@@ -293,6 +297,8 @@ export function getProviderLabel(provider: IntegrationProvider): string {
return 'crates.io';
case IntegrationProvider.GoProxy:
return 'Go Proxy';
case IntegrationProvider.S3Compatible:
return 'S3-Compatible Storage';
case IntegrationProvider.EbpfAgent:
return 'eBPF Agent';
case IntegrationProvider.EtwAgent:

View File

@@ -61,7 +61,7 @@
@if (currentStep() === 'auth') {
<div class="step-content">
<h2>Connection & Credentials</h2>
<p class="step-description">StellaOps stores only AuthRef URIs here. Keep the actual secret in your vault.</p>
<p class="step-description">Prefer AuthRef URIs for credentials. Local public probes can be created without one when the provider supports it.</p>
@if (selectedProvider(); as provider) {
<div class="auth-method-card selected">
@@ -83,7 +83,9 @@
<div class="form-field">
<label for="authRefUri">
AuthRef URI
<span class="required">*</span>
@if (isAuthRefRequired()) {
<span class="required">*</span>
}
</label>
<input
id="authRefUri"
@@ -139,10 +141,10 @@
@if (currentStep() === 'scope') {
<div class="step-content">
<h2>Discovery Scope</h2>
<p class="step-description">Define which repositories, namespaces, or tag patterns StellaOps should use for this connector.</p>
<p class="step-description">Define which repositories, namespaces, or tag patterns StellaOps should use for this connector when discovery scope matters.</p>
<div class="scope-form">
@if (integrationType() === 'registry' || integrationType() === 'scm') {
@if (hasScopeSelectors()) {
<div class="form-field">
<label for="repositories">Repositories</label>
<textarea
@@ -188,7 +190,17 @@
</div>
}
<span class="field-hint">At least one owner, repository, namespace, branch, or tag scope is required before creation.</span>
@if (!hasScopeSelectors()) {
<div class="check-item status-warning">
<span class="check-status">i</span>
<div class="check-info">
<span class="check-name">Optional scope</span>
<span class="check-message">This connector can be created without extra discovery filters and tightened later.</span>
</div>
</div>
}
<span class="field-hint">{{ getScopeGuidance() }}</span>
</div>
</div>
}

View File

@@ -2,18 +2,21 @@ import { TestBed } from '@angular/core/testing';
import { IntegrationProvider, IntegrationType } from '../integration-hub/integration.models';
import { IntegrationWizardComponent } from './integration-wizard.component';
import { resolveSupportedProviders } from './models/integration.models';
import { IntegrationOnboardingType, resolveSupportedProviders } from './models/integration.models';
describe('IntegrationWizardComponent', () => {
let component: IntegrationWizardComponent;
function createComponent() {
component = TestBed.runInInjectionContext(() => new IntegrationWizardComponent());
(component as any).integrationType = () => 'scm';
(component as any).supportedProviders = () => resolveSupportedProviders('scm', [
function createComponent(
type: IntegrationOnboardingType = 'scm',
catalog = [
{ name: 'github-app', type: IntegrationType.Scm, provider: IntegrationProvider.GitHubApp },
]);
component.draft.update((draft) => ({ ...draft, type: 'scm' }));
],
) {
component = TestBed.runInInjectionContext(() => new IntegrationWizardComponent());
(component as any).integrationType = () => type;
(component as any).supportedProviders = () => resolveSupportedProviders(type, catalog);
component.draft.update((draft) => ({ ...draft, type }));
}
it('selects a supported provider and seeds its default endpoint', () => {
@@ -25,20 +28,32 @@ describe('IntegrationWizardComponent', () => {
expect(component.draft().name).toContain('GitHub App');
});
it('requires AuthRef URI and provider metadata before leaving the connection step', () => {
it('requires provider metadata but allows optional AuthRef for local-friendly providers', () => {
createComponent();
component.selectProvider(IntegrationProvider.GitHubApp);
component.currentStep.set('auth');
expect(component.canGoNext()).toBeFalse();
component.updateAuthRefUri('authref://vault/github#app');
component.updateConfigField('appId', '12345');
component.updateConfigField('installationId', '67890');
expect(component.canGoNext()).toBeTrue();
});
it('allows no-auth and no-scope onboarding for local object storage connectors', () => {
createComponent('storage', [
{ name: 's3-compatible', type: IntegrationType.ObjectStorage, provider: IntegrationProvider.S3Compatible },
]);
component.selectProvider(IntegrationProvider.S3Compatible);
component.currentStep.set('auth');
expect(component.canGoNext()).toBeTrue();
component.currentStep.set('scope');
expect(component.canGoNext()).toBeTrue();
});
it('emits a canonical create request instead of a UI-only draft', () => {
createComponent();
component.selectProvider(IntegrationProvider.GitHubApp);
@@ -73,4 +88,26 @@ describe('IntegrationWizardComponent', () => {
}),
}));
});
it('emits null authRefUri for optional-auth object storage connectors', () => {
createComponent('storage', [
{ name: 's3-compatible', type: IntegrationType.ObjectStorage, provider: IntegrationProvider.S3Compatible },
]);
component.selectProvider(IntegrationProvider.S3Compatible);
const emitSpy = jasmine.createSpy('emit');
spyOn(component.create, 'emit').and.callFake(emitSpy);
component.updateName('Local MinIO');
component.currentStep.set('review');
component.onSubmit();
expect(emitSpy).toHaveBeenCalledWith(jasmine.objectContaining({
name: 'Local MinIO',
type: IntegrationType.ObjectStorage,
provider: IntegrationProvider.S3Compatible,
endpoint: 'http://minio.stella-ops.local:9000',
authRefUri: null,
}));
});
});

View File

@@ -67,7 +67,7 @@ export class IntegrationWizardComponent {
readonly selectedProvider = computed(() => resolveProviderDefinition(this.draft().provider));
readonly deploymentTemplate = computed(() => null);
readonly copySafetyGuidance = computed(() => [
'Only AuthRef URIs are stored. Keep the underlying secret in a vault.',
'Use AuthRef URIs whenever the connector requires credentials; local public probes can be created without one.',
'Non-secret provider fields such as App ID or Installation ID are stored as connector metadata.',
'Use environment-specific endpoints so prod, stage, and lab connectors remain explicit.',
]);
@@ -302,11 +302,36 @@ export class IntegrationWizardComponent {
return 'CI/CD';
case 'host':
return 'Host';
case 'feed':
return 'Feed Mirror';
case 'secrets':
return 'Secrets';
case 'storage':
return 'Object Storage';
default:
return 'Integration';
}
}
isAuthRefRequired(): boolean {
return this.selectedProvider()?.authRefRequired ?? true;
}
hasScopeSelectors(): boolean {
const type = this.integrationType();
return type === 'registry' || type === 'scm';
}
isScopeRequired(): boolean {
return this.selectedProvider()?.scopeRequired ?? this.hasScopeSelectors();
}
getScopeGuidance(): string {
return this.isScopeRequired()
? 'At least one owner, repository, namespace, branch, or tag scope is required before creation.'
: 'Scope is optional. Leave it blank to create the connector with endpoint-level health probing only.';
}
copyToClipboard(text: string): void {
void navigator.clipboard.writeText(text);
}
@@ -330,7 +355,11 @@ export class IntegrationWizardComponent {
return false;
}
if (draft.endpoint.trim().length === 0 || draft.authRefUri.trim().length === 0) {
if (draft.endpoint.trim().length === 0) {
return false;
}
if (this.isAuthRefRequired() && draft.authRefUri.trim().length === 0) {
return false;
}
@@ -341,6 +370,10 @@ export class IntegrationWizardComponent {
private isScopeValid(): boolean {
const draft = this.draft();
if (!this.isScopeRequired()) {
return true;
}
return (
draft.organizationId.trim().length > 0 ||
draft.repositories.length > 0 ||
@@ -372,9 +405,23 @@ export class IntegrationWizardComponent {
private getPreflightChecks(): PreflightCheck[] {
const provider = this.selectedProvider();
const checks: PreflightCheck[] = [
{ id: 'authref', name: 'Credential indirection', description: 'Verify the connector uses an AuthRef URI instead of raw secrets.', status: 'pending' },
{
id: 'authref',
name: 'Credential indirection',
description: this.isAuthRefRequired()
? 'Verify the connector uses an AuthRef URI instead of raw secrets.'
: 'Prefer AuthRef URI indirection when the connector needs credentials; allow empty auth for local public probes.',
status: 'pending',
},
{ id: 'endpoint', name: 'Endpoint contract', description: 'Confirm the endpoint matches the provider health/test contract.', status: 'pending' },
{ id: 'scope', name: 'Discovery scope', description: 'Ensure at least one repository, namespace, branch, or owner scope is set.', status: 'pending' },
{
id: 'scope',
name: 'Discovery scope',
description: this.isScopeRequired()
? 'Ensure at least one repository, namespace, branch, or owner scope is set.'
: 'Scope is optional for this connector; endpoint-only health checks are allowed.',
status: 'pending',
},
{ id: 'schedule', name: 'Probe schedule', description: 'Validate the deterministic check cadence for this connector.', status: 'pending' },
];
@@ -397,6 +444,12 @@ export class IntegrationWizardComponent {
switch (check.id) {
case 'authref':
if (draft.authRefUri.trim().length === 0) {
return this.isAuthRefRequired()
? { status: 'error', message: 'This connector requires an authref:// URI instead of embedding secrets in the connector.' }
: { status: 'warning', message: 'No AuthRef configured; the connector will rely on anonymous or endpoint-only health probing.' };
}
return draft.authRefUri.trim().startsWith('authref://')
? { status: 'success', message: 'Credential indirection is configured via AuthRef URI.' }
: { status: 'error', message: 'Use an authref:// URI instead of embedding secrets in the connector.' };
@@ -405,9 +458,13 @@ export class IntegrationWizardComponent {
? { status: 'success', message: `Endpoint ${draft.endpoint.trim()} will be used for connector probes.` }
: { status: 'error', message: 'A provider endpoint is required.' };
case 'scope':
return this.isScopeValid()
? { status: 'success', message: 'Connector scope is explicitly defined.' }
: { status: 'error', message: 'Set an owner, repository, namespace, branch, or tag scope before creating the connector.' };
if (this.hasExplicitScopeInputs()) {
return this.isScopeValid()
? { status: 'success', message: 'Connector scope is explicitly defined.' }
: { status: 'error', message: 'Set an owner, repository, namespace, branch, or tag scope before creating the connector.' };
}
return { status: 'warning', message: 'No explicit discovery scope is configured; the connector can still be created for endpoint-level checks.' };
case 'schedule':
return this.isScheduleValid()
? { status: 'success', message: `Connector checks will run in ${draft.schedule.type} mode.` }
@@ -426,4 +483,15 @@ export class IntegrationWizardComponent {
: { status: 'error', message: 'Select a supported provider first.' };
}
}
private hasExplicitScopeInputs(): boolean {
const draft = this.draft();
return (
draft.organizationId.trim().length > 0 ||
draft.repositories.length > 0 ||
draft.branches.length > 0 ||
draft.namespaces.length > 0 ||
draft.tagPatterns.length > 0
);
}
}

View File

@@ -120,6 +120,69 @@ import {
}
</section>
<section class="category-section">
<div class="category-header">
<div>
<h2>Secrets & Config Stores</h2>
<p class="category-desc">Connect Vault and Consul surfaces that back secret indirection and local config checks.</p>
</div>
<button class="btn btn-primary" type="button" (click)="openWizard('secrets')" [disabled]="secretProviders().length === 0">
+ Add Secrets
</button>
</div>
@if (secretProviders().length > 0) {
<div class="provider-pills">
@for (provider of secretProviders(); track provider.provider) {
<span class="provider-pill">{{ provider.name }}</span>
}
</div>
} @else {
<p class="category-empty">No secrets-manager connector plugins are installed in this environment.</p>
}
</section>
<section class="category-section">
<div class="category-header">
<div>
<h2>Feed Mirrors</h2>
<p class="category-desc">Register the StellaOps, NVD, and OSV mirror connectors used in the local advisory lane.</p>
</div>
<button class="btn btn-primary" type="button" (click)="openWizard('feed')" [disabled]="feedProviders().length === 0">
+ Add Feed Mirror
</button>
</div>
@if (feedProviders().length > 0) {
<div class="provider-pills">
@for (provider of feedProviders(); track provider.provider) {
<span class="provider-pill">{{ provider.name }}</span>
}
</div>
} @else {
<p class="category-empty">No feed-mirror connector plugins are installed in this environment.</p>
}
</section>
<section class="category-section">
<div class="category-header">
<div>
<h2>Object Storage</h2>
<p class="category-desc">Connect S3-compatible storage for air-gap bundles, exports, and mirrored artifacts.</p>
</div>
<button class="btn btn-primary" type="button" (click)="openWizard('storage')" [disabled]="storageProviders().length === 0">
+ Add Storage
</button>
</div>
@if (storageProviders().length > 0) {
<div class="provider-pills">
@for (provider of storageProviders(); track provider.provider) {
<span class="provider-pill">{{ provider.name }}</span>
}
</div>
} @else {
<p class="category-empty">No object-storage connector plugins are installed in this environment.</p>
}
</section>
<section class="category-section">
<div class="category-header">
<div>
@@ -278,6 +341,9 @@ export class IntegrationsHubComponent implements OnInit {
readonly scmProviders = computed(() => resolveSupportedProviders('scm', this.supportedCatalog()));
readonly ciProviders = computed(() => resolveSupportedProviders('ci', this.supportedCatalog()));
readonly hostProviders = computed(() => resolveSupportedProviders('host', this.supportedCatalog()));
readonly feedProviders = computed(() => resolveSupportedProviders('feed', this.supportedCatalog()));
readonly secretProviders = computed(() => resolveSupportedProviders('secrets', this.supportedCatalog()));
readonly storageProviders = computed(() => resolveSupportedProviders('storage', this.supportedCatalog()));
ngOnInit(): void {
this.route.paramMap.subscribe((params) => {
@@ -372,6 +438,12 @@ export class IntegrationsHubComponent implements OnInit {
return this.ciProviders();
case 'host':
return this.hostProviders();
case 'feed':
return this.feedProviders();
case 'secrets':
return this.secretProviders();
case 'storage':
return this.storageProviders();
default:
return [];
}
@@ -383,6 +455,9 @@ export class IntegrationsHubComponent implements OnInit {
case 'scm':
case 'ci':
case 'host':
case 'feed':
case 'secrets':
case 'storage':
return type;
default:
return null;

View File

@@ -5,7 +5,15 @@ import {
SupportedProviderInfo,
} from '../../integration-hub/integration.models';
export type IntegrationOnboardingType = 'registry' | 'scm' | 'ci' | 'host' | 'advisory-vex';
export type IntegrationOnboardingType =
| 'registry'
| 'scm'
| 'ci'
| 'host'
| 'feed'
| 'secrets'
| 'storage'
| 'advisory-vex';
export type WizardStep = 'provider' | 'auth' | 'scope' | 'schedule' | 'preflight' | 'review';
export interface ProviderField {
@@ -25,8 +33,10 @@ export interface IntegrationProviderDefinition {
defaultEndpoint: string;
endpointHint: string;
authRefHint: string;
authRefRequired?: boolean;
organizationLabel?: string;
organizationHint?: string;
scopeRequired?: boolean;
exposeInUi: boolean;
configFields: ProviderField[];
}
@@ -73,8 +83,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://harbor.local',
endpointHint: 'Use the Harbor base URL; StellaOps probes /api/v2.0/health.',
authRefHint: 'Reference a robot-account or username:password secret, for example authref://vault/harbor#robot-account.',
authRefRequired: false,
organizationLabel: 'Project / Namespace',
organizationHint: 'Optional Harbor project scope used for list and policy views.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -131,14 +143,16 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
{
provider: IntegrationProvider.DockerHub,
type: 'registry',
name: 'Docker Hub',
name: 'Docker Registry / Hub',
icon: 'DH',
description: 'Docker Hub registry with personal access token or username/password authentication.',
defaultEndpoint: 'https://registry.hub.docker.com',
endpointHint: 'Use the Docker Hub registry URL. Leave as default for Docker Hub Cloud.',
authRefHint: 'Reference a vault secret containing a Docker Hub access token or username:password.',
description: 'Open OCI registry or Docker Hub-compatible registry with optional credential-backed probes.',
defaultEndpoint: 'http://registry.stella-ops.local:5000',
endpointHint: 'Use the registry base URL. The local scratch lane uses registry.stella-ops.local:5000.',
authRefHint: 'Reference a vault secret containing username:password when the registry requires authentication.',
authRefRequired: false,
organizationLabel: 'Namespace / Organization',
organizationHint: 'Docker Hub namespace or organization to scope repository discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -179,8 +193,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://nexus.local',
endpointHint: 'Use the Nexus base URL with the Docker hosted or proxy repository port.',
authRefHint: 'Reference a vault secret containing a Nexus user token or username:password.',
authRefRequired: false,
organizationLabel: 'Repository Name',
organizationHint: 'Nexus Docker repository name to scope image discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -222,8 +238,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://github.com',
endpointHint: 'Use https://github.com for GitHub Cloud or your GitHub Enterprise Server base URL.',
authRefHint: 'Reference a vault secret that resolves to the app JWT or installation token.',
authRefRequired: false,
organizationLabel: 'Owner / Organization',
organizationHint: 'Optional owner used to scope repository discovery and policy views.',
scopeRequired: false,
exposeInUi: true,
configFields: [
{
@@ -277,8 +295,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://gitea.local',
endpointHint: 'Use your Gitea or Forgejo instance base URL.',
authRefHint: 'Reference a vault secret containing a Gitea API token.',
authRefRequired: false,
organizationLabel: 'Organization / Owner',
organizationHint: 'Gitea organization or owner to scope repository discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -336,8 +356,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://jenkins.local',
endpointHint: 'Use the Jenkins base URL (including context path if configured).',
authRefHint: 'Reference a vault secret containing a Jenkins API token or username:apiToken.',
authRefRequired: false,
organizationLabel: 'Folder / View',
organizationHint: 'Optional Jenkins folder or view to scope job discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -409,6 +431,8 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://agent.local:9443',
endpointHint: 'Use the eBPF agent gRPC or HTTP endpoint on the target host.',
authRefHint: 'Reference a vault secret containing the agent mTLS client certificate or bearer token.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -421,6 +445,8 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://agent.local:9443',
endpointHint: 'Use the ETW agent HTTP endpoint on the target Windows host.',
authRefHint: 'Reference a vault secret containing the agent bearer token.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -433,10 +459,96 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://agent.local:9443',
endpointHint: 'Use the dyld interposer agent HTTP endpoint on the target macOS host.',
authRefHint: 'Reference a vault secret containing the agent bearer token.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
// ── InMemory (testing only, not exposed) ────────────────────────
{
provider: IntegrationProvider.StellaOpsMirror,
type: 'feed',
name: 'StellaOps Mirror',
icon: 'SM',
description: 'Local or air-gap StellaOps advisory mirror served by Concelier.',
defaultEndpoint: 'http://concelier.stella-ops.local',
endpointHint: 'Use the Concelier base URL that serves the StellaOps mirror health surface.',
authRefHint: 'AuthRef is optional for local mirror health probes.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.NvdMirror,
type: 'feed',
name: 'NVD Mirror',
icon: 'NVD',
description: 'Local or mirrored NVD feed surface served by Concelier.',
defaultEndpoint: 'http://concelier.stella-ops.local',
endpointHint: 'Use the Concelier base URL that serves the NVD mirror health surface.',
authRefHint: 'AuthRef is optional for local mirror health probes.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.OsvMirror,
type: 'feed',
name: 'OSV Mirror',
icon: 'OSV',
description: 'Local or mirrored OSV feed surface served by Concelier.',
defaultEndpoint: 'http://concelier.stella-ops.local',
endpointHint: 'Use the Concelier base URL that serves the OSV mirror health surface.',
authRefHint: 'AuthRef is optional for local mirror health probes.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.Vault,
type: 'secrets',
name: 'HashiCorp Vault',
icon: 'V',
description: 'Vault KV and secret-reference provider for local and production credential indirection.',
defaultEndpoint: 'http://vault.stella-ops.local:8200',
endpointHint: 'Use the Vault base URL; StellaOps validates the Vault health surface.',
authRefHint: 'The Vault connector itself does not require an AuthRef for the local dev server.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.Consul,
type: 'secrets',
name: 'HashiCorp Consul',
icon: 'C',
description: 'Consul KV and settings integration for the optional local service-discovery lane.',
defaultEndpoint: 'http://consul.stella-ops.local:8500',
endpointHint: 'Use the Consul HTTP API base URL.',
authRefHint: 'The local single-node Consul profile does not require an AuthRef.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.S3Compatible,
type: 'storage',
name: 'S3-Compatible Storage',
icon: 'S3',
description: 'S3-compatible object storage used for exports, bundles, and mirrored artifacts.',
defaultEndpoint: 'http://minio.stella-ops.local:9000',
endpointHint: 'Use the storage base URL. The local MinIO lane probes /minio/health/live automatically.',
authRefHint: 'AuthRef is optional for the default local MinIO health probe.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.InMemory,
type: 'registry',
@@ -455,6 +567,9 @@ export const REGISTRY_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) =>
export const SCM_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'scm');
export const CI_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'ci');
export const HOST_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'host');
export const FEED_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'feed');
export const SECRETS_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'secrets');
export const STORAGE_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'storage');
export const scheduleOptions = [
{ value: 'manual', label: 'Manual', description: 'Create the connector now and trigger checks on demand.' },
@@ -499,6 +614,12 @@ export function toBackendIntegrationType(type: IntegrationOnboardingType | null)
return IntegrationType.CiCd;
case 'host':
return IntegrationType.RuntimeHost;
case 'feed':
return IntegrationType.FeedMirror;
case 'secrets':
return IntegrationType.SecretsManager;
case 'storage':
return IntegrationType.ObjectStorage;
default:
return null;
}