Setup Wizard - Doctor Integration Contract

Audience: developers working on Setup Wizard validation or Doctor checks who need to know which check IDs are live, which are design intent, and how the wizard reuses the shared Doctor remediation primitives. Read the status banner first — the wizard’s inline probes and the Doctor diagnostic engine are two separate surfaces.

This document defines how the Setup Wizard validates each step and provides actionable remediation guidance, drawing on the shared Doctor remediation primitives (runtime detection, placeholder resolution, verification execution).

Status (reconciled against src/, 2026-05-30). This document mixes the implemented contract with forward-looking design. Two distinct surfaces exist in code and must not be conflated:

  1. Live setup-wizard probes (Platform). src/Platform/StellaOps.Platform.WebService (Contracts/SetupWizardModels.cs, Services/PlatformSetupService.cs, Endpoints/SetupEndpoints.cs) runs its own inline probes per step and emits SetupCheckResult records carrying check.*-shaped string IDs. These probes do not route through the Doctor plugin engine — the check IDs they emit are wizard-local identifiers, not registered Doctor CheckIds. The live backend bootstrap contract supports only six steps: database, valkey, migrations, admin, crypto, sources (see ProbeStepCoreAsync / ApplyStepCoreAsync). All other steps return check.setup.unsupported.
  2. Doctor diagnostic engine (separate). src/Doctor/** and src/__Libraries/StellaOps.Doctor*/** implement a plugin-based diagnostic system with its own (larger, differently-named) catalog of CheckIds (e.g. check.db.connection, check.postgres.connectivity, check.crypto.fips). The wizard reuses Doctor’s remediation primitives (RemediationCommand, WizardRemediation, LikelyCause, IRuntimeDetector, IPlaceholderResolver, IVerificationExecutor — all under StellaOps.Doctor), but does not currently invoke Doctor checks through a wizard client.

Types described below that do not exist in source are flagged NOT IMPLEMENTED (design intent) inline. The canonical UX boundary is setup-wizard-ux.md.

1. Overview

The Setup Wizard aims to:

  1. Validate each configuration step (live: via Platform inline probes)
  2. Detect existing configuration (for resume/reconfigure)
  3. Generate runtime-specific fix commands (via shared Doctor remediation primitives)
  4. Verify that fixes were applied correctly

2. Step-to-Check Mapping

2.1 Implemented backend bootstrap steps

These are the only steps the live Platform setup contract supports (SetupStepDefinitions.All in Contracts/SetupWizardModels.cs; probes in Services/PlatformSetupService.cs). The “Check ID” column lists the wizard-local check IDs actually emitted as SetupCheckResult.CheckId, not Doctor plugin CheckIds. Step IDs are the SetupStepId enum values rendered in lowercase by the UX/API.

Step IDRequiredDepends onEmitted check IDs (wizard-local)
databaseYescheck.database.connectivity, check.database.version
valkeyYescheck.services.valkey.connectivity
migrationsYesdatabasecheck.database.migrations.module, check.database.migrations.applied
adminYesmigrationscheck.auth.admin.exists, check.auth.password.policy, check.authority.admin.exists
cryptoYesadmincheck.crypto.profile, check.crypto.fips, check.compliance.nis2.prerequisites, check.compliance.dora.prerequisites, check.compliance.cra.prerequisites
sourcesNoadmincheck.sources.feeds.configured, check.sources.feeds.connectivity

Notes (source-grounded):

2.2 Steps NOT in the backend bootstrap contract (design intent)

ProbeStepCoreAsync/ApplyStepCoreAsync reject any step outside §2.1 with check.setup.unsupported. The vault, scm, and registry steps exist only as local CLI client steps (src/Cli/StellaOps.Cli/Commands/Setup/Steps/Implementations/); they do not run server-side and emit no Platform SetupCheckResult. The check IDs below (check.integration.vault.*, check.integration.scm.*, check.integration.registry.*, check.notify.*, check.security.identity.*, check.orchestrator.*, check.feeds.*) are NOT IMPLEMENTED (design intent) — none are emitted by the wizard, and most are not registered as Doctor CheckIds either (see §2.3 for what the Doctor engine actually ships). Per setup-wizard-ux.md, post-bootstrap integration/notification/identity work is handled on the relevant Integrations / Notify / Identity & Access pages, not as wizard steps.

2.3 Relationship to the Doctor diagnostic catalog

The Doctor engine ships its own check IDs that are similarly named but distinct from the wizard-local IDs above. When cross-referencing, note (non-exhaustive, from src/Doctor/** and src/__Libraries/StellaOps.Doctor.Plugins.*/**):

Wizard-local IDNearest Doctor plugin CheckId(s)
check.database.connectivitycheck.db.connection, check.postgres.connectivity
check.database.versioncheck.db.schema.version
check.database.migrations.*check.db.migrations.pending, check.db.migrations.failed, check.postgres.migrations
check.services.valkey.connectivitycheck.servicegraph.valkey
check.auth.password.policycheck.users.password.policy, check.security.password.policy
check.authority.admin.existscheck.authority.bootstrap.exists, check.users.superuser.exists
check.crypto.fipscheck.crypto.fips (Doctor Crypto plugin — exact match)
check.sources.feeds.configuredcheck.sources.mode.configured

There is no check.database.migrations.checksums, check.services.valkey.ping, check.crypto.profile.valid, check.crypto.signing.test, or check.feeds.sync.enabled in either catalog.


3. Check Output Model

3.1 Result schemas

There is no single CheckResult type shared by the wizard and Doctor. Two distinct, source-accurate records apply:

Wizard-local result — what the Platform setup probes emit (Contracts/SetupWizardModels.cs):

public sealed record SetupCheckResult(
    string CheckId,
    SetupCheckStatus Status,
    string? Message,
    string? SuggestedFix);

public enum SetupCheckStatus { Pass, Fail, Warn, NotRun }

Doctor diagnostic result — what the Doctor engine produces (StellaOps.Doctor/Models/DoctorCheckResult.cs):

public sealed record DoctorCheckResult
{
    public required string CheckId { get; init; }
    public required string PluginId { get; init; }
    public required string Category { get; init; }
    public required DoctorSeverity Severity { get; init; }
    public required string Diagnosis { get; init; }
    public required Evidence Evidence { get; init; }
    public IReadOnlyList<string>? LikelyCauses { get; init; }   // string list, not LikelyCause records
    public Remediation? Remediation { get; init; }              // grouped steps, see §9
    public string? VerificationCommand { get; init; }
    public required TimeSpan Duration { get; init; }
    public required DateTimeOffset ExecutedAt { get; init; }
}

public enum DoctorSeverity { Pass = 0, Info = 1, Warn = 2, Fail = 3, Skip = 4 }

DoctorCheckResult.Evidence is the strongly-typed Evidence record (Description + IReadOnlyDictionary<string,string> Data + optional SensitiveKeys), not an ImmutableDictionary<string, object>. LikelyCauses on the result is a plain IReadOnlyList<string>; the structured LikelyCause record below is used by the wizard-remediation primitive (WizardRemediation), not by DoctorCheckResult.

Shared remediation primitives (StellaOps.Doctor/Models/, StellaOps.Doctor/Detection/) — these match source closely:

public sealed record LikelyCause
{
    public required int Priority { get; init; }       // 1 = most likely
    public required string Description { get; init; }
    public string? DocumentationUrl { get; init; }
}

public sealed record RemediationCommand
{
    public required RuntimeEnvironment Runtime { get; init; }
    public required string Command { get; init; }      // may contain {{PLACEHOLDER}}
    public required string Description { get; init; }
    public bool RequiresSudo { get; init; }
    public bool IsDangerous { get; init; }
    public IReadOnlyDictionary<string, string>? Placeholders { get; init; }
    public string? DangerWarning { get; init; }        // extra warning for dangerous commands
}

// WizardRemediation groups LikelyCauses + Commands + VerificationCommand and exposes
// GetCommandsForRuntime(runtime) (exact match then RuntimeEnvironment.Any fallback).

public enum RuntimeEnvironment
{
    DockerCompose,
    Kubernetes,
    Systemd,
    WindowsService,
    Bare,
    Any
}

3.2 Evidence content

DoctorCheckResult.Evidence.Data is IReadOnlyDictionary<string, string> with a human-readable Description; check authors populate it via the result builder (context.CreateResult(...).WithEvidence("…", e => e.Add(key, value))). The keys are check-specific and not contractually fixed. The table below is illustrative of typical keys, not a guaranteed schema:

Check CategoryTypical evidence keys
Databasehost, port, version
Valkeyhost, port
Migrationsmodule/convergence status
AuthUsername, Email, AutoBootstrap

Sensitive values are redacted before storage — checks use DoctorPluginContext.Redact(...) and may list redacted keys in Evidence.SensitiveKeys.


4. Remediation Command Generation

4.1 Runtime Detection

Runtime detection is provided by StellaOps.Doctor.Detection.IRuntimeDetector (default impl RuntimeDetector):

public interface IRuntimeDetector
{
    RuntimeEnvironment Detect();
    bool IsDockerAvailable();
    bool IsKubernetesContext();
    bool IsSystemdManaged(string serviceName);
    string? GetComposeProjectPath();    // nullable
    string? GetKubernetesNamespace();   // nullable; defaults to "stellaops"
    IReadOnlyDictionary<string, string> GetContextValues();  // placeholder source (§5.1 priority 3)
}

Detection logic (RuntimeDetector.DetectInternal):

  1. Check for /.dockerenv file → DockerCompose
  2. Check KUBERNETES_SERVICE_HOST env var → Kubernetes
  3. IsDockerAvailable() AND a compose file is found → DockerCompose
  4. On non-Windows, systemctl --version exits 0 → Systemd
  5. On Windows, Windows-Service heuristics → WindowsService
  6. Default → Bare

4.2 Command Templates

The YAML below is illustrative design intent, not a shipped configuration format. The live wizard probes (PlatformSetupService) return a single SuggestedFix string per SetupCheckResult; the richer WizardRemediation / RemediationCommand model is populated by Doctor checks (see the per-check WithRemediation(...) builders under src/Doctor/** and src/__Libraries/StellaOps.Doctor.Plugins.*/**). Treat the check IDs in these examples as logical labels — check.services.valkey.connectivity is wizard-local, check.integration.vault.auth is not implemented (see §2.2).

Database Connection Failure

check.database.connectivity:
  likelyCauses:
    - priority: 1
      description: "PostgreSQL is not running"
    - priority: 2
      description: "Firewall blocking port 5432"
    - priority: 3
      description: "Incorrect host or port"
    - priority: 4
      description: "Network connectivity issue"

  remediations:
    - runtime: DockerCompose
      description: "Start PostgreSQL container"
      command: "docker compose -f {{COMPOSE_FILE}} up -d postgres"
      placeholders:
        COMPOSE_FILE: "devops/compose/docker-compose.yml"

    - runtime: Kubernetes
      description: "Check PostgreSQL pod status"
      command: "kubectl get pods -n {{NAMESPACE}} -l app=postgres"
      placeholders:
        NAMESPACE: "stellaops"

    - runtime: Systemd
      description: "Start PostgreSQL service"
      command: "sudo systemctl start postgresql"
      requiresSudo: true

    - runtime: Any
      description: "Verify PostgreSQL is listening"
      command: "pg_isready -h {{HOST}} -p {{PORT}}"
      placeholders:
        HOST: "localhost"
        PORT: "5432"

  verificationCommand: "pg_isready -h {{HOST}} -p {{PORT}}"

Valkey Connection Failure

check.services.valkey.connectivity:
  likelyCauses:
    - priority: 1
      description: "Valkey/Redis is not running"
    - priority: 2
      description: "Firewall blocking port 6379"
    - priority: 3
      description: "Authentication required but not configured"

  remediations:
    - runtime: DockerCompose
      description: "Start Valkey container"
      command: "docker compose -f {{COMPOSE_FILE}} up -d valkey"
      placeholders:
        COMPOSE_FILE: "devops/compose/docker-compose.yml"

    - runtime: Kubernetes
      description: "Check Valkey pod status"
      command: "kubectl get pods -n {{NAMESPACE}} -l app=valkey"
      placeholders:
        NAMESPACE: "stellaops"

    - runtime: Systemd
      description: "Start Valkey service"
      command: "sudo systemctl start valkey"
      requiresSudo: true

    - runtime: Any
      description: "Test Valkey connection"
      command: "valkey-cli -h {{HOST}} -p {{PORT}} PING"
      placeholders:
        HOST: "localhost"
        PORT: "6379"

  verificationCommand: "valkey-cli -h {{HOST}} -p {{PORT}} PING"

Pending Migrations

check.database.migrations.applied:
  likelyCauses:
    - priority: 1
      description: "Pending release migrations require manual execution"
    - priority: 2
      description: "Startup migrations not yet applied"

  remediations:
    - runtime: Any
      description: "Run pending migrations (dry-run first)"
      command: "stella system migrations-run --module all --dry-run"

    - runtime: Any
      description: "Apply all pending migrations (release migrations require --force)"
      command: "stella system migrations-run --module all --force"
      isDangerous: true

    - runtime: DockerCompose
      description: "Run migrations in container"
      command: "docker compose exec api stella system migrations-run --module all --force"

    - runtime: Kubernetes
      description: "Run migrations job"
      command: "kubectl apply -f devops/k8s/jobs/migrations.yaml"

  verificationCommand: "stella system migrations-run --module all --dry-run"

Vault Authentication Failure

check.integration.vault.auth:
  likelyCauses:
    - priority: 1
      description: "Vault token expired or revoked"
    - priority: 2
      description: "AppRole credentials invalid"
    - priority: 3
      description: "Kubernetes service account not configured"
    - priority: 4
      description: "Vault server unreachable"

  remediations:
    - runtime: Any
      description: "Test Vault connectivity"
      command: "curl -s {{VAULT_ADDR}}/v1/sys/health"
      placeholders:
        VAULT_ADDR: "https://vault.example.com:8200"

    - runtime: Any
      description: "Verify token validity"
      command: "vault token lookup"

    - runtime: Kubernetes
      description: "Check Kubernetes auth configuration"
      command: "kubectl get serviceaccount -n {{NAMESPACE}} stellaops-vault-auth"
      placeholders:
        NAMESPACE: "stellaops"

  verificationCommand: "vault token lookup"

5. Placeholder Resolution

5.1 Placeholder Sources

Placeholders in commands are resolved from:

SourcePriorityExample
User input1 (highest){{HOST}} from form field
Environment2{{VAULT_ADDR}} from env
Detection3{{NAMESPACE}} from context
Default4 (lowest)Fallback value

5.2 Placeholder Syntax

{{PLACEHOLDER_NAME}}
{{PLACEHOLDER_NAME:-default_value}}

Examples:

5.3 Secret Redaction

PlaceholderResolver.Resolve(...) never substitutes sensitive placeholders — they are left in {{NAME}} form regardless of any available value, so the user must copy and manually fill them in. A placeholder is treated as sensitive (IsSensitivePlaceholder) when its name is in the built-in set or contains a sensitive keyword:

PlaceholderDisplayActual
{{PASSWORD}}{{PASSWORD}}Never substituted
{{TOKEN}}{{TOKEN}}Never substituted
{{SECRET_KEY}}{{SECRET_KEY}}Never substituted
{{ROLE_ID}}{{ROLE_ID}}Never substituted

6. Verification Flow

6.1 Post-Fix Verification

After the user applies a fix, the wizard:

  1. Wait - Pause for user confirmation (“I’ve run this command”)
  2. Verify - Run the verification command
  3. Re-check - Run the original Doctor check
  4. Report - Show success or next steps

6.2 Verification Command Execution

StellaOps.Doctor.Resolver.IVerificationExecutor (default impl VerificationExecutor):

namespace StellaOps.Doctor.Resolver;

public interface IVerificationExecutor
{
    Task<VerificationResult> ExecuteAsync(
        string command,
        TimeSpan timeout,
        CancellationToken ct = default);

    Task<VerificationResult> ExecuteWithPlaceholdersAsync(
        string command,
        IReadOnlyDictionary<string, string>? userValues,
        TimeSpan timeout,
        CancellationToken ct = default);
}

public sealed record VerificationResult
{
    public required bool Success { get; init; }
    public required int ExitCode { get; init; }
    public required string Output { get; init; }      // combined stdout + stderr
    public required TimeSpan Duration { get; init; }
    public bool TimedOut { get; init; }
    public string? Error { get; init; }               // set when the command failed to start
}

6.3 Re-Check Behavior

[FAIL] check.database.connectivity

Suggested fix applied. Verifying...

[RUN] pg_isready -h localhost -p 5432
      localhost:5432 - accepting connections

Re-running check...

[PASS] check.database.connectivity
       PostgreSQL connection successful

7. Check Aggregation

7.1 Step Completion Criteria

A step is complete when:

7.2 Aggregated Status

There is no StepValidationStatus type in source. The implemented status enums (Contracts/SetupWizardModels.cs) are:

// Per-step status within a session
public enum SetupStepStatus { Pending, Current, Passed, Failed, Skipped, Blocked }

// Overall session status
public enum SetupSessionStatus
{
    NotStarted, InProgress, Completed, CompletedPartial, Failed, Abandoned
}

SetupStepStatus.Blocked is set when a step’s dependency failed; there is no distinct “PassedWithWarns” — a step with only Warn checks (no Fail) still resolves to Passed.

7.3 Status Rollup for Thresholds

Illustrative only. The “Production-Ready” threshold and its check IDs (check.security.identity.configured, check.integration.vault.connected, check.integration.scm.connected, check.orchestrator.agent.healthy, check.feeds.sync.enabled) are NOT IMPLEMENTED — they are not emitted by the wizard and the corresponding optional steps are not in the backend bootstrap contract (§2.2). The “Operational” set maps to the six implemented required-step probes (§2.1).

Operational Threshold:
  [x] check.database.connectivity         PASS
  [x] check.database.permissions          PASS
  [x] check.database.migrations.applied   PASS
  [x] check.services.valkey.connectivity  PASS
  [x] check.auth.admin.exists             PASS
  [x] check.crypto.profile.valid          PASS

  Status: OPERATIONAL (6/6 required checks passed)

Production-Ready Threshold:
  [x] check.security.identity.configured  PASS
  [x] check.integration.vault.connected   PASS
  [x] check.integration.scm.connected     PASS
  [x] check.notify.channel.configured     PASS
  [ ] check.orchestrator.agent.healthy    SKIP
  [ ] check.feeds.sync.enabled            SKIP

  Status: NOT PRODUCTION-READY (4/6 recommended, 2 skipped)

8. Doctor Engine Integration

NOT IMPLEMENTED (design intent). WizardCheckContext and IWizardDoctorClient do not exist in source — there is no wizard→Doctor-engine client. In the live system the wizard runs inline probes in PlatformSetupService (ProbeStepCoreAsync / ApplyStepCoreAsync), driven by Endpoints/SetupEndpoints.cs. The Doctor engine (StellaOps.Doctor.Engine.DoctorEngine, CheckExecutor, CheckRegistry) is invoked separately (e.g. stella doctor run, the Doctor WebService) and is not part of the setup probe path. The records below sketch a possible future integration.

8.1 Wizard-Specific Check Context (design intent)

// NOT IMPLEMENTED — illustrative
public sealed record WizardCheckContext
{
    public required string StepId { get; init; }
    public required RuntimeEnvironment DetectedRuntime { get; init; }
    public required ImmutableDictionary<string, string> UserInputs { get; init; }
    public bool GenerateRemediations { get; init; } = true;
    public bool IncludePlaceholders { get; init; } = true;
}

The implemented context object passed to Doctor checks is StellaOps.Doctor.Plugins.DoctorPluginContext (services, configuration, time provider, logger, environment name, optional tenant, plugin config) — not a wizard-specific type.

8.2 Check Invocation (design intent)

// NOT IMPLEMENTED — illustrative
public interface IWizardDoctorClient
{
    Task<ImmutableArray<CheckResult>> RunStepChecksAsync(string stepId, WizardCheckContext context, CancellationToken ct);
    Task<CheckResult>                 RunSingleCheckAsync(string checkId, WizardCheckContext context, CancellationToken ct);
    Task<VerificationResult>          RunVerificationAsync(string command, WizardCheckContext context, CancellationToken ct);
}

Implemented entry points instead:

8.3 Check Timeout

Check CategoryDefault TimeoutMax Timeout
Connectivity10 seconds30 seconds
Authentication15 seconds60 seconds
Migrations60 seconds300 seconds
Full validation30 seconds120 seconds

9. Remediation Safety

9.1 Dangerous Commands

Commands marked isDangerous: true require user confirmation:

WARNING: This command will modify your database schema.

Command:
  stella system migrations-run --module all --force

This action:
  - Applies 5 pending migrations
  - Cannot be automatically rolled back
  - May take several minutes

Type 'apply' to confirm: _

9.2 Sudo Requirements

Commands requiring sudo show a notice:

This command requires administrator privileges.

Command:
  sudo systemctl start postgresql

[Copy Command]

Note: You may be prompted for your password.

9.3 Secret Substitution Notice

This command contains placeholders for sensitive values.

Command:
  vault write auth/approle/login role_id={{ROLE_ID}} secret_id={{SECRET_ID}}

Before running:
  1. Replace {{ROLE_ID}} with your AppRole Role ID
  2. Replace {{SECRET_ID}} with your AppRole Secret ID

[Copy Command]

10. Check Plugin Requirements

The original design called for several new Doctor checks. Current status against src/__Libraries/StellaOps.Doctor.Plugins.*/**:

Plugin (real)Check ID (real)PurposeStatus
Authoritycheck.users.password.policyPassword complexityImplemented
Authoritycheck.authority.bootstrap.exists, check.users.superuser.existsAdmin/bootstrap user existsImplemented
Notifycheck.notify.delivery.testTest notification deliveryImplemented
Databasecheck.db.migrations.failedFailed-migration detectionImplemented
check.crypto.signing.testTest signing operationNot implemented (closest: check.auth.signing-key, check.compliance.attestation-signing)
Databasecheck.database.migrations.checksumsMigration checksum integrityNot implemented
Integrationcheck.integration.vault.secrets.accessSecret retrieval testNot implemented (closest: check.integration.secrets.manager)

10.2 Check Implementation Contract

ISetupWizardAwareCheck does not exist in source. Doctor checks implement StellaOps.Doctor.Plugins.IDoctorCheck:

public interface IDoctorCheck
{
    string CheckId { get; }
    string Name { get; }
    string Description { get; }
    DoctorSeverity DefaultSeverity { get; }
    IReadOnlyList<string> Tags { get; }
    TimeSpan EstimatedDuration { get; }
    bool CanRun(DoctorPluginContext context);
    Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct);
}

Remediations and verification commands are not separate interface methods; checks attach them to the result via the builder (context.CreateResult(...).WithRemediation(r => r .AddStep(...).WithRunbookUrl(...)).WithVerification("stella doctor --check <id>")). The runtime-specific RemediationCommand / WizardRemediation primitives (§3.1, §4) are used where a check chooses to emit runtime-targeted commands.


11. Audit Trail

11.1 Setup Event Logging

Wizard endpoints are audited through the generic audit-emission framework (StellaOps.Audit.Emission), not via a bespoke SetupWizardEvent record or a direct Timeline write. In Endpoints/SetupEndpoints.cs each mutating endpoint chains .Audited(AuditModules.Platform, AuditActions.Platform.<action>, "setup"). There is no SetupWizardEvent type in source (the schema previously shown here is NOT IMPLEMENTED), and there is no per-check (check.passed/check.failed) or remediation.copied audit event in the live backend.

11.2 Audited actions (real)

The actual audited actions emitted by the setup endpoints (AuditActions.Platform.*):

Audit actionTriggering endpoint
CreateSetupSessionCreate / start a setup session
ExecuteSetupStepProbe / apply / reset a step
SkipSetupStepSkip an optional step
FinalizeSetupFinalize / complete the session

11.3 Doctor authorization scopes (reference)

The Doctor diagnostic surface (separate from the wizard) is gated by these scopes (StellaOpsScopes.cs, mirrored in DoctorScopes.cs): doctor:run, doctor:run:full, doctor:export, doctor:admin; the Doctor scheduler adds doctor-scheduler:read and doctor-scheduler:write. The first-run setup wizard itself is anonymous until the installation is bootstrapped, after which /setup-wizard/* continues as an authenticated reconfiguration session (see setup-wizard-ux.md §1.3).