elsa-workflows / elsa-workflows/elsa-core

/features/installed reports every discovered shell feature, including ones the shell disabled

Open
#7,910 0 comments 0 reactions 0 assignees View on GitHub
bug core triaged
Dominant language
C#
Stars
7.9k
Forks
1.5k
Avg merge
15h 22m
Merged PRs (30d)
114

Description

## Description

On a CShells-based host, `GET /features/installed` (and `GET /features/installed/{fullName}`) returns **every feature discovered in the runtime feature catalog**, including features the shell has explicitly disabled or simply never enabled. The endpoint reports installed-in-the-host packages rather than active-in-this-shell features.

This matters because the primary consumer treats the endpoint as an enablement check: Elsa Studio's `RemoteFeatureProvider.IsEnabledAsync(featureName)` calls `GET /features/installed/{fullName}` and returns `true` when the lookup succeeds. Modules decorated with `[RemoteFeature(...)]` therefore activate against backends that do not actually run the feature.

## Steps to Reproduce

Reproduction rate: every time.

1. Host an Elsa server on CShells that references the diagnostics packages, so they land in the runtime feature catalog:

```xml





```

2. Disable some of them in shell configuration, leaving one enabled as a control:

```json
{
"CShells": {
"Shells": {
"Default": {
"Features": {
"StructuredLogs": true,
"StructuredLogsDashboard": true,
"ConsoleLogs": false,
"ConsoleLogsDashboard": false,
"OpenTelemetry": false
}
}
}
}
}
```

3. Confirm the disable actually took effect — it did:

- `GET /elsa/api/diagnostics/console-logs/sources` → `404` (route not mapped)
- `GET /elsa/api/diagnostics/opentelemetry/storage` → `404`
- `GET /elsa/api/diagnostics/structured-logs/sources` → `200` (control, still enabled)
- `GET /elsa/api/dashboard/overview` → `diagnostics.consoleLogs.capability.status = "NotInstalled"` while `diagnostics.structuredLogs.capability.status = "Available"`

So the shell composition and the dashboard's own capability detection both agree the features are not running.

4. Now call `GET /elsa/api/features/installed`.

## Expected Behavior

The response lists the features the shell actually activated. `ConsoleLogs`, `ConsoleLogsDashboard` and `OpenTelemetry` should be absent; `StructuredLogs` and `StructuredLogsDashboard` should be present.

## Actual Behavior

All 60 discovered features are returned, including the three disabled ones:

```
ConsoleLogs listed=True
ConsoleLogsDashboard listed=True
OpenTelemetry listed=True
StructuredLogs listed=True
total 60
```

The same list is what CShells logs at startup as the discovered catalog:

```
CShells.Features.RuntimeFeatureCatalog[0]
Committed runtime feature catalog generation 1 with 60 feature(s): FastEndpoints, Elsa, ...,
ConsoleLogs, ConsoleLogsDashboard, OpenTelemetry, StructuredLogs, StructuredLogsDashboard, ...
```

A second, simpler symptom of the same cause: a host that references `Elsa.ExternalAuthentication` but enables it in no shell still reports `ExternalAuthentication` from `/features/installed`.

## Root Cause

[`ShellInstalledFeatureProvider`](https://github.com/elsa-workflows/elsa-core/blob/main/src/common/Elsa.Features/Services/ShellInstalledFeatureProvider.cs) injects `IEnumerable` and filters only on `StartupType != null`:

```csharp
public IEnumerable List()
{
return _shellFeatures
.Where(sf => sf.StartupType != null)
.Select(MapToElsaFeatureDescriptor);
}
```

In CShells (`CShells 0.0.28`, `src/CShells/Lifecycle/ShellProviderBuilder.cs`), that service is registered per shell from the **full catalog**, not the enabled subset:

```csharp
RegisterCoreServices(services, settings, holder, catalog.FeatureDescriptors);
// ...
var descriptorList = featureDescriptors.ToList().AsReadOnly();
services.AddSingleton>(descriptorList);
services.AddSingleton>(descriptorList);
```

The activated set lives on `ShellSettings.EnabledFeatures`, which the same method computes just above and writes back after dependency resolution, so it includes dependency-pulled features:

```csharp
var orderedFeatures = _dependencyResolver.GetOrderedFeatures(availableFeatures, catalog.FeatureMap);
settings.EnabledFeatures = [..orderedFeatures];
```

`ShellSettings` is registered as a shell singleton (`services.AddSingleton(settings)`), so the enabled set is directly injectable.

Worth noting: the legacy non-shell implementation had the opposite semantics. [`Module.cs`](https://github.com/elsa-workflows/elsa-core/blob/main/src/common/Elsa.Features/Implementations/Module.cs) builds the registry from the features actually added to the module, under the comment *"Add a registry of enabled features to the service collection for client applications to reflect on what features are installed"*. The shell-based provider changed that contract without the consumers changing with it.

## Impact

- Elsa Studio (`3.8.0-preview.1667`) gates modules on `[RemoteFeature(...)]`, resolved through `RemoteFeatureProvider.IsEnabledAsync` → `GET /features/installed/{fullName}`. A disabled feature still resolves, so Studio renders its pages, menu entries and dashboard widgets, and the resulting API calls fail against routes that were never mapped. Concretely: disabling `ConsoleLogs` on the server leaves the Console Logs page in Studio, and its requests fail.
- Operators lose the ability to switch a subsystem off for a deployment without also removing the package reference, which is not an option for a shared/prebuilt image where the same binaries serve every deployment.
- The API is internally inconsistent: `/dashboard/overview` reports diagnostics capability correctly as `NotInstalled` for the same feature that `/features/installed` claims is present.

## Recommendation

Intersect the descriptors with the shell's enabled features, in both `List()` and `Find()`:

```csharp
public class ShellInstalledFeatureProvider(
IEnumerable shellFeatures,
ShellSettings shellSettings) : IInstalledFeatureProvider
{
public IEnumerable List()
{
var enabled = new HashSet(shellSettings.EnabledFeatures, StringComparer.OrdinalIgnoreCase);

return shellFeatures
.Where(sf => sf.StartupType != null && enabled.Contains(sf.Id))
.Select(MapToElsaFeatureDescriptor);
}

// Find(): same predicate before the MapToFullName comparison.
}
```

Because `ShellSettings.EnabledFeatures` is rewritten from the dependency-resolved order during shell build, this reports effective enablement (a feature pulled in as another feature's dependency is correctly reported as present).

If the host-wide catalog semantics are intentional, then the alternative is to keep this endpoint as-is and make enablement explicit — for example an `isEnabled` field on `FeatureDescriptor`, or a separate `/features/enabled` route — and point `RemoteFeatureProvider` at that instead. Either way the current pairing is wrong, since the consumer-side API is literally named `IsEnabledAsync`. The filtering option looks preferable: it needs no client change and matches the pre-shell behaviour.

Happy to open a PR for whichever direction you prefer.

## Environment

- **Elsa Package Version**: `3.8.0-preview.5413`
- **Elsa Studio Version**: `3.8.0-preview.1667`
- **CShells Version**: `0.0.28`
- **.NET**: 10.0
- **Operating System**: macOS 15 (Darwin 25.5.0); not platform specific
- **Hosting**: CShells multi-shell host, single `Default` shell, FastEndpoints under the `elsa/api` prefix

## Related Issues

None found.

Contributor guide

Open the contributing guide

Research direction

Start with src/common/Elsa.Features/Services/ShellInstalledFeatureProvider.cs and trace its registration in CShells/Lifecycle/ShellProviderBuilder.cs. Compare the provider's catalog input with ShellSettings.EnabledFeatures, including dependency-resolved features, then verify both installed-feature endpoints omit disabled features while retaining enabled ones.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.