checkId: check.core.plugins.loaded plugin: stellaops.doctor.core severity: fail tags: [quick, core, plugins]

Mounted plugins admitted

Doctor check check.core.plugins.loaded — closes the “rejected plugin → its checks disappear from the run → Doctor still reports all-green” hole (ADR-026 Decision 6). It compares the expected set of mounted Doctor runtime plugins against the set actually admitted, and reports any plugin that was rejected (whose checks would otherwise silently vanish) with the rejection reason.

What It Checks

This is a core check: it is contributed by the in-process Core plugin, so it always runs independent of mounted-plugin admission — the very property that lets it report a rejected mounted plugin, because the rejected plugin’s own checks have already disappeared from the run.

It reads the mounted-plugin admission state through the engine-lib seam IMountedPluginAdmissionStatus, which the Doctor WebService backs with the same mounted-bundle loader catalog that /readyz and the GET /internal/plugins/status probe report read. So a Doctor RUN and /readyz agree about a rejected/vanished plugin — there is no green run that hides one.

For each expected mounted plugin (a target the loader iterates — LoadFromMountedBundle:true in DoctorRuntimePluginTargets.MountedLoaderTargets):

OutcomeCondition
Admittedthe bundle cleared manifest/checksum/path-traversal/signature admission and registered
Rejectedadmission failed (missing/invalid trust-root, signature mismatch, unresolved transitive dependency, bundle not mounted) or the loader never recorded an admission status for the expected target
ResultCondition
Passevery expected mounted plugin was admitted
Failone or more expected mounted plugins were rejected (each named with its reason in the evidence)
Skipthere is no mounted-plugin host (the seam is not registered — e.g. the CLI). It does not Pass-by-default: “no host to evaluate” is not “all plugins admitted”

The evidence emits ExpectedCount, AdmittedCount, RejectedCount, and one rejected:<plugin-id> line per rejected plugin carrying the same reason the probe reporter / /readyz surface.

Why It Matters

Mounted Doctor runtime plugins (Release, Environment, Scanner, Compliance, BinaryAnalysis, Timestamping) load from signed bundles. If a bundle is rejected — most commonly because the trust-root (cosign.pub) is missing, the signature does not verify, or a transitive dependency is unresolved — the loader silently rejects the whole plugin and every check it contributes simply disappears from the run. Without this check, Doctor would then report all-green while entire families of checks were never executed: a green light that proves nothing. This check turns that silent disappearance into a visible Fail that names the rejected plugin and why.

Common Causes

How to Fix

For each rejected plugin in the evidence, read the reason, then probe the loader directly and restore the bundle/trust-root or fix the signature/dependency, then redeploy the Doctor service so admission succeeds.

Inspect the loader:

curl -s http://doctor-web:10261/internal/plugins/status | jq '.plugins[] | select(.admitted == false)'
curl -s -X POST http://doctor-web:10261/internal/plugins/probe

Docker Compose

The mounted bundles and trust-root are read-only mounts in devops/compose/docker-compose.plugins.doctor.yml. Confirm the bundle directory and cosign.pub are present, then recreate the service:

# Verify the mounts exist on the host, then:
docker compose -f devops/compose/docker-compose.plugins.doctor.yml up -d --force-recreate doctor-web

If a bundle was rebuilt, re-produce and re-sign it:

pwsh devops/build/package-runtime-plugins.ps1 -Module doctor -Profile base -SignDoctorBundles -UseOfflineDevSigner

Both switches are required. -SignDoctorBundles enables signing; -UseOfflineDevSigner selects the local RSA/SHA-256 signer. Without the first switch, the packager emits unsigned bundles and the fail-closed loader reports the missing <assembly>.dll.sig.

Bare Metal / systemd

Ensure the bundle directory (default /app/plugins/doctor/base/<plugin-id>) and the trust-root (default /app/etc/certificates/trust-roots/plugins/doctor/cosign.pub) exist and are readable by the service user, then restart:

sudo systemctl restart stellaops-doctor

Kubernetes / Helm

The bundles and trust-root are typically mounted from a ConfigMap/Secret. Confirm they are present and correctly keyed, then roll out:

kubectl rollout restart deployment/doctor

Verification

stella doctor run --check check.core.plugins.loaded

A Pass means every expected mounted plugin was admitted. A Fail lists each rejected plugin (rejected:<id> lines) with the reason; a Skip means this host has no mounted plugins to evaluate.

See the Doctor reference for the full check catalog, CLI usage, and export bundles.